";
+ } else {
+ var packageName = args['_'][0];
+ var logDir = (process.env['ProgramData'] || 'C:\\ProgramData') + '\\MeshAgent';
+ try { if (!require('fs').existsSync(logDir)) { require('fs').mkdirSync(logDir); } } catch (e) { }
+ var logFile = logDir + '\\MeshAgent_StoreUninstall.log';
+ try {
+ var psPath = (process.env['SystemRoot'] || 'C:\\Windows') + '\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
+ var child_process = require('child_process');
+ var safePackageName = packageName.replace(/'/g, "''");
+ var completionMarker = '###DONE###' + Date.now();
+ var child = child_process.execFile(psPath, ['powershell', '-NoProfile', '-NoLogo', '-Command', '-'], {});
+ child.stdout.str = '';
+ child.stderr.str = '';
+ child._sessionid = sessionid;
+ child._packageName = packageName;
+ child._marker = completionMarker;
+ child._completed = false;
+ child._logFile = logFile;
+ child.stdout.on('data', function(c) {
+ this.str += c.toString();
+ if (!child._completed && this.str.indexOf(child._marker) >= 0) {
+ child._completed = true;
+ var result = this.str.split(child._marker)[0].trim();
+ if (result === 'NO_MATCH' || result === '') {
+ sendConsoleText(JSON.stringify({
+ success: false,
+ error: 'No matching packages found',
+ package: child._packageName
+ }), child._sessionid);
+ } else if (result.indexOf('REMOVED') >= 0 || result.indexOf('DEPROVISIONED') >= 0) {
+ var items = result.split('|').filter(function(x) { return x.length > 0; });
+ sendConsoleText(JSON.stringify({
+ success: true,
+ message: 'Store app removed',
+ results: items
+ }), child._sessionid);
+ } else if (result.indexOf('FAILED') >= 0) {
+ sendConsoleText(JSON.stringify({
+ success: false,
+ error: 'Removal failed',
+ details: result.split('|')
+ }), child._sessionid);
+ } else {
+ sendConsoleText(JSON.stringify({ output: result }), child._sessionid);
+ }
+ }
+ });
+ child.stderr.on('data', function(c) { this.str += c.toString(); });
+ var script = [
+ "$LogFile = '" + logFile + "'",
+ "function Write-Log { param([string]$Message); $LogLine = \"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - $Message\"; Add-Content -Path $LogFile -Value $LogLine -ErrorAction SilentlyContinue }",
+ "",
+ "Write-Log 'STORE UNINSTALL START: " + safePackageName + "'",
+ "Write-Log \"Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)\"",
+ "",
+ "$ErrorActionPreference = 'SilentlyContinue'",
+ "$results = @()",
+ "",
+ "# Packages suchen",
+ "$pkgs = Get-AppxPackage -AllUsers -Name '*" + safePackageName + "*'",
+ "Write-Log \"Found (AllUsers): $($pkgs.Count) packages\"",
+ "",
+ "if (-not $pkgs) {",
+ " $pkgs = Get-AppxPackage -Name '*" + safePackageName + "*'",
+ " Write-Log \"Found (CurrentUser): $($pkgs.Count) packages\"",
+ "}",
+ "",
+ "foreach ($p in $pkgs) {",
+ " Write-Log \"Removing: $($p.PackageFullName)\"",
+ " try {",
+ " Remove-AppxPackage -Package $p.PackageFullName -AllUsers -ErrorAction Stop",
+ " Write-Log 'SUCCESS: Removed with -AllUsers'",
+ " $results += 'REMOVED_ALLUSERS:' + $p.PackageFullName",
+ " } catch {",
+ " Write-Log \"FAILED -AllUsers: $($_.Exception.Message)\"",
+ " try {",
+ " Remove-AppxPackage -Package $p.PackageFullName -ErrorAction Stop",
+ " Write-Log 'SUCCESS: Removed'",
+ " $results += 'REMOVED:' + $p.PackageFullName",
+ " } catch {",
+ " Write-Log \"FAILED: $($_.Exception.Message)\"",
+ " $results += 'FAILED:' + $p.PackageFullName + ':' + $_.Exception.Message",
+ " }",
+ " }",
+ "}",
+ "",
+ "# Provisioned Packages entfernen",
+ "$prov = Get-AppxProvisionedPackage -Online 2>$null | Where-Object { $_.PackageName -like '*" + safePackageName + "*' }",
+ "Write-Log \"Found provisioned: $($prov.Count) packages\"",
+ "",
+ "foreach ($pr in $prov) {",
+ " Write-Log \"Deprovisioning: $($pr.DisplayName)\"",
+ " try {",
+ " Remove-AppxProvisionedPackage -Online -PackageName $pr.PackageName -ErrorAction Stop | Out-Null",
+ " Write-Log 'SUCCESS: Deprovisioned'",
+ " $results += 'DEPROVISIONED:' + $pr.DisplayName",
+ " } catch {",
+ " Write-Log \"FAILED: $($_.Exception.Message)\"",
+ " $results += 'DEPROV_FAILED:' + $pr.DisplayName",
+ " }",
+ "}",
+ "",
+ "if ($results.Count -eq 0) {",
+ " Write-Log 'NO_MATCH'",
+ " 'NO_MATCH'",
+ "} else {",
+ " $output = $results -join '|'",
+ " Write-Log \"RESULT: $output\"",
+ " $output",
+ "}",
+ "'" + completionMarker + "'",
+ "exit"
+ ].join("\r\n");
+ child.stdin.write(script + "\r\n");
+ setTimeout(function() {
+ if (!child._completed) {
+ sendConsoleText(JSON.stringify({ error: 'Timeout after 2 minutes' }), child._sessionid);
+ }
+ }, 120000);
+ response = JSON.stringify({ status: 'Store app removal started', package: packageName });
+ } catch (ex) {
+ var logErr = new Date().toISOString() + ' - STORE UNINSTALL ERROR: ' + ex.toString() + '\r\n';
+ try { require('fs').appendFileSync(logFile, logErr); } catch (e) { }
+ response = JSON.stringify({ error: ex.toString() });
+ }
}
break;
}
@@ -6248,9 +7398,6 @@ function handleServerConnection(state) {
sendAgentMessage("This is an old agent version, consider updating.", 3, 117);
}
- var oldNodeId = db.Get('OldNodeId');
- if (oldNodeId != null) { mesh.SendCommand({ action: 'mc1migration', oldnodeid: oldNodeId }); }
-
// Send SMBios tables if present
if (SMBiosTablesRaw != null) { mesh.SendCommand({ action: 'smbios', value: SMBiosTablesRaw }); }
@@ -6277,6 +7424,217 @@ function handleServerConnection(state) {
broadcastToRegisteredApps({ cmd: 'serverstate', value: meshServerConnectionState, url: require('MeshAgent').ConnectedServer });
}
+function getProbableMainLanIp() {
+ if (process.platform != 'win32') return null;
+
+ function str(v) {
+ if (v == null) return '';
+ try { return String(v); } catch (ex) { return ''; }
+ }
+
+ function intVal(v, d) {
+ var x;
+ try { x = parseInt(str(v)); } catch (ex) { x = NaN; }
+ if (isNaN(x)) return d;
+ return x;
+ }
+
+ function isIPv4(ip) {
+ var p, i, n;
+
+ ip = str(ip);
+ if (ip == '') return false;
+ if (ip == '0.0.0.0') return false;
+ if (ip.indexOf(':') >= 0) return false;
+ if (ip.indexOf('169.254.') == 0) return false;
+ if (ip.indexOf('127.') == 0) return false;
+
+ p = ip.split('.');
+ if (p.length != 4) return false;
+
+ for (i = 0; i < 4; i++) {
+ if (p[i] == '') return false;
+ n = parseInt(p[i]);
+ if (isNaN(n)) return false;
+ if ((n < 0) || (n > 255)) return false;
+ }
+
+ return true;
+ }
+
+ function hasAnyValue(v) {
+ var i;
+
+ if (v == null) return false;
+
+ if (typeof v == 'string') {
+ return (v != '');
+ }
+
+ if (typeof v == 'number') {
+ return true;
+ }
+
+ // Duktape/WMI may expose arrays or array-like objects.
+ try {
+ if (v.length != null) {
+ for (i = 0; i < v.length; i++) {
+ if (str(v[i]) != '') return true;
+ }
+ return false;
+ }
+ } catch (ex) { }
+
+ return (str(v) != '');
+ }
+
+ function firstIPv4(v) {
+ var i, s;
+
+ if (v == null) return null;
+
+ if (typeof v == 'string') {
+ return isIPv4(v) ? v : null;
+ }
+
+ try {
+ if (v.length != null) {
+ for (i = 0; i < v.length; i++) {
+ s = str(v[i]);
+ if (isIPv4(s)) return s;
+ }
+ }
+ } catch (ex) { }
+
+ s = str(v);
+ if (isIPv4(s)) return s;
+
+ return null;
+ }
+
+ function isUpAdapter(a) {
+ var op, media;
+
+ if (a == null) return true;
+
+ // InterfaceOperationalStatus commonly has 1 = Up.
+ // But do not hard-fail unknown values; WMI support varies.
+ op = intVal(a.InterfaceOperationalStatus, -1);
+ if ((op != -1) && (op != 1)) return false;
+
+ // MediaConnectState commonly has 1 = Connected.
+ media = intVal(a.MediaConnectState, -1);
+ if ((media != -1) && (media != 1)) return false;
+
+ return true;
+ }
+
+ function adapterPriority(a) {
+ var pm, medium, link, hardware, connector;
+
+ if (a == null) return 20;
+
+ pm = intVal(a.NdisPhysicalMedium, -1);
+ medium = intVal(a.NdisMedium, -1);
+ link = intVal(a.LinkTechnology, -1);
+
+ hardware = (str(a.HardwareInterface).toLowerCase() == 'true');
+ connector = (str(a.ConnectorPresent).toLowerCase() == 'true');
+
+ /*
+ MSFT_NetAdapter is documented in ROOT\StandardCimv2.
+ Useful physical media values include:
+ 14 = 802.3 / Ethernet
+ 1 = Wireless LAN
+ 9 = Native 802.11
+
+ These numeric values avoid matching adapter names such as
+ "Wi-Fi", "WLAN", or "Wireless".
+ */
+
+ if (pm == 14) return 0; // wired Ethernet
+ if ((medium == 0) && connector) return 1; // 802.3-style fallback
+ if ((link == 2) && hardware) return 2; // Ethernet-ish fallback
+
+ if ((pm == 1) || (pm == 9)) return 10; // Wi-Fi / WLAN
+ if (link == 11) return 11; // wireless fallback
+
+ return 20; // VPN, virtual, tunnel, unknown
+ }
+
+ try {
+ var wmi = require('win-wmi-fixed');
+ var configs;
+ var adapters;
+ var adapterByIndex = {};
+ var candidates = [];
+ var i, cfg, a, ip, gw, idx, metric;
+
+ configs = wmi.query(
+ 'ROOT\\CIMV2',
+ 'SELECT InterfaceIndex,IPAddress,DefaultIPGateway,IPConnectionMetric FROM Win32_NetworkAdapterConfiguration WHERE IPEnabled = True',
+ ['InterfaceIndex', 'IPAddress', 'DefaultIPGateway', 'IPConnectionMetric']
+ );
+
+ if ((configs == null) || (configs.length == null) || (configs.length == 0)) {
+ return null;
+ }
+
+ try {
+ adapters = wmi.query(
+ 'ROOT\\StandardCimv2',
+ 'SELECT InterfaceIndex,NdisPhysicalMedium,NdisMedium,LinkTechnology,HardwareInterface,ConnectorPresent,InterfaceOperationalStatus,MediaConnectState FROM MSFT_NetAdapter',
+ ['InterfaceIndex', 'NdisPhysicalMedium', 'NdisMedium', 'LinkTechnology', 'HardwareInterface', 'ConnectorPresent', 'InterfaceOperationalStatus', 'MediaConnectState']
+ );
+
+ if ((adapters != null) && (adapters.length != null)) {
+ for (i = 0; i < adapters.length; i++) {
+ idx = intVal(adapters[i].InterfaceIndex, -1);
+ if (idx >= 0) adapterByIndex['i' + idx] = adapters[i];
+ }
+ }
+ } catch (ex2) {
+ adapters = null;
+ }
+
+ for (i = 0; i < configs.length; i++) {
+ cfg = configs[i];
+
+ gw = cfg.DefaultIPGateway;
+ if (!hasAnyValue(gw)) continue;
+
+ ip = firstIPv4(cfg.IPAddress);
+ if (ip == null) continue;
+
+ idx = intVal(cfg.InterfaceIndex, -1);
+ a = adapterByIndex['i' + idx];
+
+ if (!isUpAdapter(a)) continue;
+
+ metric = intVal(cfg.IPConnectionMetric, 999999);
+
+ candidates[candidates.length] = {
+ ip: ip,
+ idx: idx,
+ pri: adapterPriority(a),
+ metric: metric
+ };
+ }
+
+ if (candidates.length == 0) return null;
+
+ candidates.sort(function (x, y) {
+ if (x.pri != y.pri) return x.pri - y.pri;
+ if (x.metric != y.metric) return x.metric - y.metric;
+ return x.idx - y.idx;
+ });
+
+ return candidates[0].ip;
+ } catch (ex) {
+ return null;
+ }
+}
+
// Update the server with the latest network interface information
var sendNetworkUpdateNagleTimer = null;
function sendNetworkUpdateNagle() { if (sendNetworkUpdateNagleTimer != null) { clearTimeout(sendNetworkUpdateNagleTimer); sendNetworkUpdateNagleTimer = null; } sendNetworkUpdateNagleTimer = setTimeout(sendNetworkUpdate, 5000); }
@@ -6288,7 +7646,7 @@ function sendNetworkUpdate(force) {
var netInfo = { netif2: require('os').networkInterfaces() };
if (process.platform == 'win32') {
try {
- var ret = require('win-wmi').query('ROOT\\CIMV2', 'SELECT InterfaceIndex,NetConnectionID,Speed FROM Win32_NetworkAdapter', ['InterfaceIndex','NetConnectionID','Speed']);
+ var ret = require('win-wmi-fixed').query('ROOT\\CIMV2', 'SELECT InterfaceIndex,NetConnectionID,Speed FROM Win32_NetworkAdapter', ['InterfaceIndex','NetConnectionID','Speed']);
if (ret[0]) {
var speedMap = {};
for (var i = 0; i < ret.length; i++) speedMap[ret[i].InterfaceIndex] = ret[i].Speed;
@@ -6302,6 +7660,13 @@ function sendNetworkUpdate(force) {
}
}
} catch(ex) { }
+
+ try {
+ var probableMainLanIp = getProbableMainLanIp();
+ if (probableMainLanIp != null) {
+ netInfo.probable_main_lan_ip = probableMainLanIp;
+ }
+ } catch (ex) { }
} else if (process.platform == 'linux') {
var adapterNames = Object.keys(netInfo.netif2);
for (var i = 0; i < adapterNames.length; i++) {
@@ -6327,67 +7692,107 @@ function sendNetworkUpdate(force) {
}
// Called periodically to check if we need to send updates to the server
+var sendPeriodicServerUpdateInProgress = false;
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)) {
- delete meshCoreObj.intelamt;
- amt.getMeiState(9, function (meinfo) {
- meshCoreObj.intelamt = meinfo;
- meshCoreObj.intelamt.microlms = amt.lmsstate;
- meshCoreObjChanged();
- });
- }
- // Update network information
- if (flags & 2) { sendNetworkUpdateNagle(false); }
+ // Defensive guard against re-entrancy. The info collectors below are currently
+ // non-blocking (Linux av()/firewall() are async; Windows uses native WMI), so
+ // nothing here pumps the event loop and this guard never actually fires. It's
+ // kept as a safety net: if a future collector re-introduces a blocking
+ // child.waitExit() (which runs a nested event loop), a timer firing inside it
+ // could re-enter and throw 'waitExit() already in progress'. Skipping the
+ // overlapping run avoids that.
+ if (sendPeriodicServerUpdateInProgress) return;
+ sendPeriodicServerUpdateInProgress = true;
+ try {
+ 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)) {
+ delete meshCoreObj.intelamt;
+ amt.getMeiState(9, function (meinfo) {
+ meshCoreObj.intelamt = meinfo;
+ meshCoreObj.intelamt.microlms = amt.lmsstate;
+ meshCoreObjChanged();
+ });
+ }
- // Update anti-virus information
- if ((flags & 4) && (process.platform == 'win32')) {
- // Windows Command: "wmic /Namespace:\\root\SecurityCenter2 Path AntiVirusProduct get /FORMAT:CSV"
- try { meshCoreObj.av = require('win-info').av(); meshCoreObjChanged(); } catch (ex) { av = null; } // Antivirus
- //if (process.platform == 'win32') { try { meshCoreObj.pr = require('win-info').pendingReboot(); meshCoreObjChanged(); } catch (ex) { meshCoreObj.pr = null; } } // Pending reboot
- }
- if (process.platform == 'win32') {
- if (require('MeshAgent')._securitycenter == null) {
+ // Update network information
+ if (flags & 2) { sendNetworkUpdateNagle(false); }
+
+ // Update anti-virus information
+ if ((flags & 4) && (process.platform == 'win32')) {
+ // Windows Command: "wmic /Namespace:\\root\SecurityCenter2 Path AntiVirusProduct get /FORMAT:CSV"
+ try { meshCoreObj.av = require('win-info').av(); meshCoreObjChanged(); } catch (ex) { av = null; } // Antivirus
+ }
+ // Update Linux AV/Firewall information. av()/firewall() are asynchronous (they
+ // spawn child processes), so they don't block this periodic update with a nested
+ // waitExit() loop. Results are applied in the callbacks and flushed via
+ // meshCoreObjChanged().
+ if ((flags & 4) && (process.platform == 'linux')) {
try {
- require('MeshAgent')._securitycenter = require('win-securitycenter').status();
- meshCoreObj['wsc'] = require('MeshAgent')._securitycenter; // Windows Security Central (WSC)
- require('win-securitycenter').on('changed', function () {
- require('MeshAgent')._securitycenter = require('win-securitycenter').status();
- meshCoreObj['wsc'] = require('MeshAgent')._securitycenter; // Windows Security Central (WSC)
- require('MeshAgent').SendCommand({ action: 'coreinfo', wsc: require('MeshAgent')._securitycenter });
+ require('linux-info').av(function (avResult) {
+ try {
+ var lsc = {};
+ if (avResult && avResult.length > 0) { meshCoreObj.av = avResult; lsc.antiVirus = 'OK'; }
+ require('linux-info').firewall(function (fwResult) {
+ try {
+ if (fwResult && fwResult.installed) { lsc.firewall = fwResult.enabled ? 'OK' : 'BAD'; }
+ if (Object.keys(lsc).length > 0) { meshCoreObj.lsc = lsc; meshCoreObjChanged(); }
+ } catch (ex) { }
+ });
+ } catch (ex) { }
});
} catch (ex) { }
}
- // Get Defender Information
- try {
- meshCoreObj.defender = require('win-info').defender();
- meshCoreObjChanged();
- } catch (ex) { }
+ if (process.platform == 'win32') {
+ if (require('MeshAgent')._securitycenter == null) {
+ try {
+ require('MeshAgent')._securitycenter = require('win-securitycenter').status();
+ meshCoreObj['wsc'] = require('MeshAgent')._securitycenter; // Windows Security Central (WSC)
+ require('win-securitycenter').on('changed', function () {
+ require('MeshAgent')._securitycenter = require('win-securitycenter').status();
+ meshCoreObj['wsc'] = require('MeshAgent')._securitycenter; // Windows Security Central (WSC)
+ require('MeshAgent').SendCommand({ action: 'coreinfo', wsc: require('MeshAgent')._securitycenter });
+ });
+ } catch (ex) { }
+ }
- // Calculate Windows Idle Time
- try {
- require('win-deskutils').idle.getSecondsAllSessions().then(function (seconds) {
- meshCoreObj.idletime = seconds;
+ // Pending reboot
+ try {
+ meshCoreObj.pr = require('win-info').pendingReboot();
meshCoreObjChanged();
- });
- } catch (ex) { sendConsoleText('Error getting idle time: ' + ex.toString());}
- }
+ } catch (ex) { }
+
+
+ // Get Defender Information
+ try {
+ meshCoreObj.defender = require('win-info').defender();
+ meshCoreObjChanged();
+ } catch (ex) { }
- // Send available data right now
- if (force) {
- meshCoreObj = sortObjRec(meshCoreObj);
- var x = JSON.stringify(meshCoreObj);
- if (x != LastPeriodicServerUpdate) {
- LastPeriodicServerUpdate = x;
- mesh.SendCommand(meshCoreObj);
+ // Calculate Windows Idle Time
+ try {
+ require('win-deskutils').idle.getSecondsAllSessions().then(function (seconds) {
+ meshCoreObj.idletime = seconds;
+ meshCoreObjChanged();
+ });
+ } catch (ex) { sendConsoleText('Error getting idle time: ' + ex.toString());}
}
- }
+
+ // Send available data right now
+ if (force) {
+ meshCoreObj = sortObjRec(meshCoreObj);
+ var x = JSON.stringify(meshCoreObj);
+ if (x != LastPeriodicServerUpdate) {
+ LastPeriodicServerUpdate = x;
+ mesh.SendCommand(meshCoreObj);
+ }
+ }
+ } finally { sendPeriodicServerUpdateInProgress = false; }
}
// Sort the names in an object
@@ -6395,17 +7800,17 @@ function sortObject(obj) { return Object.keys(obj).sort().reduce(function(a, v)
// Fix the incoming data and cut down how much data we use
function cleanGetBitLockerVolumeInfo(volumes) {
+ // Keep the raw codes info and let the view convert to strings
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; }
+ if (v.volumeStatus === 0) { delete v.volumeStatus; }
+ if (v.encryptionMethod === 0) { delete v.encryptionMethod; }
+ if (v.protectionStatus === 0) { delete v.protectionStatus; }
}
return sortObject(volumes);
}
@@ -6444,4 +7849,3 @@ function onWebSocketUpgrade(response, s, head) {
mesh.AddCommandHandler(handleServerCommand);
mesh.AddConnectHandler(handleServerConnection);
-
diff --git a/agents/meshinstall-linux.sh b/agents/meshinstall-linux.sh
index eeb7799d50..cbe08034a6 100644
--- a/agents/meshinstall-linux.sh
+++ b/agents/meshinstall-linux.sh
@@ -6,6 +6,12 @@ CheckStartupType() {
# 3 = init.d
# 5 = BSD
+ # Add freebsd usr service path
+ plattype=`uname | awk '{ tst=tolower($0);a=split(tst, res, "freebsd"); print a }'`
+ if [[ $plattype == 2 ]]
+ then mkdir -p /usr/local/etc/rc.d
+ fi
+
# echo "Checking if Linux or BSD Platform"
plattype=`uname | awk '{ tst=tolower($0);a=split(tst, res, "bsd"); if(a==1) { print "LINUX"; } else { print "BSD"; }}'`
if [[ $plattype == 'BSD' ]]
diff --git a/agents/modules_meshcmd/sysinfo.js b/agents/modules_meshcmd/sysinfo.js
index 611a7b1af6..27c825c5e0 100644
--- a/agents/modules_meshcmd/sysinfo.js
+++ b/agents/modules_meshcmd/sysinfo.js
@@ -226,7 +226,7 @@ function windows_thermals()
{
var ret = [];
try {
- ret = require('win-wmi').query('ROOT\\WMI', 'SELECT CurrentTemperature,InstanceName FROM MSAcpi_ThermalZoneTemperature',['CurrentTemperature','InstanceName']);
+ ret = require('win-wmi-fixed').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);
diff --git a/agents/modules_meshcore/computer-identifiers.js b/agents/modules_meshcore/computer-identifiers.js
index cfee592437..f168a2c6e0 100644
--- a/agents/modules_meshcore/computer-identifiers.js
+++ b/agents/modules_meshcore/computer-identifiers.js
@@ -70,55 +70,58 @@ function linux_identifiers()
var ret = {};
var values = {};
var child = null;
-
- 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')) {
- 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();
- const memorySlots = [];
- 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) { }
+ try {
+ identifiers['bios_vendor'] = 'Unknown';
+ if (!require('fs').existsSync('/sys/class/dmi/id')) {
+ if (require('fs').existsSync('/etc/wsl.conf')) {
+ identifiers['bios_vendor'] = 'Microsoft';
+ identifiers['bios_version'] = 'WSL';
} else {
- throw('Unknown board');
+ 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();
+ const memorySlots = [];
+ 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 ('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 {
- 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]]; }
+ 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 {
+ 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]]; }
+ }
}
+ entries = null;
+
+ identifiers['bios_date'] = ret['bios_date'];
+ identifiers['bios_vendor'] = ret['bios_vendor'];
+ identifiers['bios_version'] = ret['bios_version'];
+ identifiers['bios_serial'] = ret['product_serial'];
+ identifiers['board_name'] = ret['board_name'];
+ identifiers['board_serial'] = ret['board_serial'];
+ identifiers['board_vendor'] = ret['board_vendor'];
+ identifiers['board_version'] = ret['board_version'];
+ identifiers['product_uuid'] = ret['product_uuid'];
+ identifiers['product_name'] = ret['product_name'];
}
- entries = null;
-
- identifiers['bios_date'] = ret['bios_date'];
- identifiers['bios_vendor'] = ret['bios_vendor'];
- identifiers['bios_version'] = ret['bios_version'];
- identifiers['bios_serial'] = ret['product_serial'];
- identifiers['board_name'] = ret['board_name'];
- identifiers['board_serial'] = ret['board_serial'];
- identifiers['board_vendor'] = ret['board_vendor'];
- identifiers['board_version'] = ret['board_version'];
- identifiers['product_uuid'] = ret['product_uuid'];
- identifiers['product_name'] = ret['product_name'];
- }
+ } catch (e) { console.log(e && e.message ? e.message : e); }
// BIOS Mode
try {
@@ -161,6 +164,26 @@ function linux_identifiers()
}
} catch (xx) { }
+ // Kernel info
+ child = require('child_process').execFile('/bin/sh', ['sh']);
+ child.stdout.str = ''; child.stdout.on('data', dataHandler);
+ child.stdin.write('uname -r\nexit\n');
+ child.waitExit();
+ try { ret['kernel_release'] = child.stdout.str.trim(); } catch (xx) { }
+ if (ret['kernel_release'].indexOf('-WSL2') !== -1) { identifiers['bios_version'] = 'WSL2'; }
+
+ child = require('child_process').execFile('/bin/sh', ['sh']);
+ child.stdout.str = ''; child.stdout.on('data', dataHandler);
+ child.stdin.write('uname -v\nexit\n');
+ child.waitExit();
+ try { ret['kernel_build'] = child.stdout.str.trim(); } catch (xx) { }
+
+ child = require('child_process').execFile('/bin/sh', ['sh']);
+ child.stdout.str = ''; child.stdout.on('data', dataHandler);
+ child.stdin.write('uname -m\nexit\n');
+ child.waitExit();
+ try { ret['arch'] = child.stdout.str.trim(); } catch (xx) { }
+
// Fetch GPU info
child = require('child_process').execFile('/bin/sh', ['sh']);
child.stdout.str = ''; child.stdout.on('data', dataHandler);
@@ -422,9 +445,12 @@ function linux_identifiers()
];
const thedata = {};
for (var x in filesToRead) {
- try {
- const content = require('fs').readFileSync('/sys/class/power_supply/' + batteries[i] + '/' + filesToRead[x]).toString().trim();
- thedata[filesToRead[x]] = /^\d+$/.test(content) ? parseInt(content, 10) : content;
+ try {
+ const filePath = '/sys/class/power_supply/' + batteries[i] + '/' + filesToRead[x];
+ if(require('fs').existsSync(filePath)) {
+ const content = require('fs').readFileSync(filePath).toString().trim();
+ thedata[filesToRead[x]] = /^\d+$/.test(content) ? parseInt(content, 10) : content;
+ }
} catch (err) { }
}
if (Object.keys(thedata).length === 0) continue; // No data read, skip
@@ -497,7 +523,7 @@ function windows_identifiers()
ret['identifiers'] = {};
- var values = require('win-wmi').query('ROOT\\CIMV2', "SELECT * FROM Win32_Bios", ['ReleaseDate', 'Manufacturer', 'SMBIOSBIOSVersion', 'SerialNumber']);
+ var values = require('win-wmi-fixed').query('ROOT\\CIMV2', "SELECT * FROM Win32_Bios", ['ReleaseDate', 'Manufacturer', 'SMBIOSBIOSVersion', 'SerialNumber']);
if(values[0]){
ret['identifiers']['bios_date'] = values[0]['ReleaseDate'];
ret['identifiers']['bios_vendor'] = values[0]['Manufacturer'];
@@ -506,7 +532,7 @@ function windows_identifiers()
}
ret['identifiers']['bios_mode'] = 'Legacy';
- values = require('win-wmi').query('ROOT\\CIMV2', "SELECT * FROM Win32_BaseBoard", ['Product', 'SerialNumber', 'Manufacturer', 'Version']);
+ values = require('win-wmi-fixed').query('ROOT\\CIMV2', "SELECT * FROM Win32_BaseBoard", ['Product', 'SerialNumber', 'Manufacturer', 'Version']);
if(values[0]){
ret['identifiers']['board_name'] = values[0]['Product'];
ret['identifiers']['board_serial'] = values[0]['SerialNumber'];
@@ -514,13 +540,13 @@ function windows_identifiers()
ret['identifiers']['board_version'] = values[0]['Version'];
}
- values = require('win-wmi').query('ROOT\\CIMV2', "SELECT * FROM Win32_ComputerSystemProduct", ['UUID', 'Name']);
+ values = require('win-wmi-fixed').query('ROOT\\CIMV2', "SELECT * FROM Win32_ComputerSystemProduct", ['UUID', 'Name']);
if(values[0]){
ret['identifiers']['product_uuid'] = values[0]['UUID'];
ret['identifiers']['product_name'] = values[0]['Name'];
}
- values = require('win-wmi').query('ROOT\\CIMV2', "SELECT * FROM Win32_SystemEnclosure", ['SerialNumber', 'SMBIOSAssetTag', 'Manufacturer']);
+ values = require('win-wmi-fixed').query('ROOT\\CIMV2', "SELECT * FROM Win32_SystemEnclosure", ['SerialNumber', 'SMBIOSAssetTag', 'Manufacturer']);
if(values[0]){
ret['identifiers']['chassis_serial'] = values[0]['SerialNumber'];
ret['identifiers']['chassis_assettag'] = values[0]['SMBIOSAssetTag'];
@@ -529,13 +555,13 @@ function windows_identifiers()
trimIdentifiers(ret.identifiers);
- values = require('win-wmi').query('ROOT\\CIMV2', "SELECT * FROM Win32_PhysicalMemory");
+ values = require('win-wmi-fixed').query('ROOT\\CIMV2', "SELECT * FROM Win32_PhysicalMemory");
if(values[0]){
trimResults(values);
ret.windows.memory = values;
}
- values = require('win-wmi').query('ROOT\\CIMV2', "SELECT * FROM Win32_OperatingSystem");
+ values = require('win-wmi-fixed').query('ROOT\\CIMV2', "SELECT * FROM Win32_OperatingSystem");
if(values[0]){
trimResults(values);
ret.windows.osinfo = values[0];
@@ -560,17 +586,17 @@ function windows_identifiers()
}
}
- values = require('win-wmi').query('ROOT\\CIMV2', "SELECT * FROM Win32_Processor", ['Caption', 'DeviceID', 'Manufacturer', 'MaxClockSpeed', 'Name', 'SocketDesignation']);
+ values = require('win-wmi-fixed').query('ROOT\\CIMV2', "SELECT * FROM Win32_Processor", ['Caption', 'DeviceID', 'Manufacturer', 'MaxClockSpeed', 'Name', 'SocketDesignation']);
if(values[0]){
ret.windows.cpu = values;
}
- values = require('win-wmi').query('ROOT\\CIMV2', "SELECT * FROM Win32_VideoController", ['Name', 'CurrentHorizontalResolution', 'CurrentVerticalResolution']);
+ values = require('win-wmi-fixed').query('ROOT\\CIMV2', "SELECT * FROM Win32_VideoController", ['Name', 'CurrentHorizontalResolution', 'CurrentVerticalResolution']);
if(values[0]){
ret.windows.gpu = values;
}
- values = require('win-wmi').query('ROOT\\CIMV2', "SELECT * FROM Win32_DiskDrive", ['Caption', 'DeviceID', 'Model', 'Partitions', 'Size', 'Status']);
+ values = require('win-wmi-fixed').query('ROOT\\CIMV2', "SELECT * FROM Win32_DiskDrive", ['Caption', 'DeviceID', 'Model', 'Partitions', 'Size', 'Status']);
if(values[0]){
ret.windows.drives = values;
}
@@ -594,7 +620,7 @@ function windows_identifiers()
// 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']);
+ values = require('win-wmi-fixed').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],
@@ -643,15 +669,15 @@ function windows_identifiers()
}
return result;
}
- values = require('win-wmi').query('ROOT\\WMI', "SELECT * FROM BatteryCycleCount",['InstanceName','CycleCount']);
- var values2 = require('win-wmi').query('ROOT\\WMI', "SELECT * FROM BatteryFullChargedCapacity",['InstanceName','FullChargedCapacity']);
- var values3 = require('win-wmi').query('ROOT\\WMI', "SELECT * FROM BatteryRuntime",['InstanceName','EstimatedRuntime']);
- var values4 = require('win-wmi').query('ROOT\\WMI', "SELECT * FROM BatteryStaticData",['InstanceName','Chemistry','DesignedCapacity','DeviceName','ManufactureDate','ManufactureName','SerialNumber']);
+ values = require('win-wmi-fixed').query('ROOT\\WMI', "SELECT * FROM BatteryCycleCount",['InstanceName','CycleCount']);
+ var values2 = require('win-wmi-fixed').query('ROOT\\WMI', "SELECT * FROM BatteryFullChargedCapacity",['InstanceName','FullChargedCapacity']);
+ var values3 = require('win-wmi-fixed').query('ROOT\\WMI', "SELECT * FROM BatteryRuntime",['InstanceName','EstimatedRuntime']);
+ var values4 = require('win-wmi-fixed').query('ROOT\\WMI', "SELECT * FROM BatteryStaticData",['InstanceName','Chemistry','DesignedCapacity','DeviceName','ManufactureDate','ManufactureName','SerialNumber']);
for (i = 0; i < values4.length; ++i) {
if (values4[i].Chemistry) { values4[i].Chemistry = IntToStrLE(parseInt(values4[i].Chemistry)); }
if (values4[i].ManufactureDate) { if (values4[i].ManufactureDate.indexOf('*****') != -1) delete values4[i].ManufactureDate; }
}
- var values5 = require('win-wmi').query('ROOT\\WMI', "SELECT * FROM BatteryStatus",['InstanceName','ChargeRate','Charging','DischargeRate','Discharging','RemainingCapacity','Voltage']);
+ var values5 = require('win-wmi-fixed').query('ROOT\\WMI', "SELECT * FROM BatteryStatus",['InstanceName','ChargeRate','Charging','DischargeRate','Discharging','RemainingCapacity','Voltage']);
var values6 = [];
if (values2.length > 0 && values4.length > 0) {
for (i = 0; i < values2.length; ++i) {
@@ -885,7 +911,7 @@ function win_chassisType()
function win_systemType()
{
try {
- var tokens = require('win-wmi').query('ROOT\\CIMV2', 'SELECT PCSystemType FROM Win32_ComputerSystem', ['PCSystemType']);
+ var tokens = require('win-wmi-fixed').query('ROOT\\CIMV2', 'SELECT PCSystemType FROM Win32_ComputerSystem', ['PCSystemType']);
if (tokens[0]) {
return (parseInt(tokens[0]['PCSystemType']));
} else {
@@ -1052,6 +1078,7 @@ module.exports.isVM = function isVM()
}
}
+ if (id.identifiers.bios_vendor == 'Microsoft' && id.identifiers.bios_version.slice(0,3) === 'WSL') { ret = true; }
if (!ret) { ret = this.isDocker(); }
return (ret);
diff --git a/agents/modules_meshcore/linux-info.js b/agents/modules_meshcore/linux-info.js
new file mode 100644
index 0000000000..eae4e037dd
--- /dev/null
+++ b/agents/modules_meshcore/linux-info.js
@@ -0,0 +1,288 @@
+
+var promise = require('promise');
+
+function dataHandler(data) { this.str += data.toString(); }
+
+// Shell preamble that sets $TO to a 'timeout N' prefix when the 'timeout' utility
+// is present (busybox/coreutils), or empty otherwise. Killing /bin/sh does not
+// stop a reparented grandchild (e.g. a hung clamscan); 'timeout' does.
+function shellTimeout(seconds) { return 'TO=""; command -v timeout >/dev/null 2>&1 && TO="timeout ' + seconds + '"; '; }
+
+// Run a short shell script WITHOUT blocking the event loop: cb(trimmedStdout) is
+// called when the child exits or the timeout elapses. This never calls waitExit()
+// (whose nested event loop can re-enter from another timer as 'waitExit() already
+// in progress' when a command hangs). The child is killed on overrun, and
+// shellTimeout()'s 'timeout' prefix stops a hung grandchild.
+function runShellAsync(script, timeout, cb) {
+ var done = false;
+ function finish(child, out) {
+ if (done) { return; }
+ done = true;
+ if (child) {
+ if (child._to) { try { clearTimeout(child._to); } catch (ex) { } }
+ try { child.kill(); } catch (ex) { }
+ }
+ cb((out || '').trim());
+ }
+ try {
+ var child = require('child_process').execFile('/bin/sh', ['sh']);
+ child.stdout.str = ''; child.stdout.on('data', dataHandler);
+ child.stderr.on('data', function () { });
+ child.on('exit', function () { finish(this, this.stdout.str); });
+ child._to = setTimeout(function () { finish(child, child.stdout.str); }, timeout);
+ child.stdin.write(script + '\nexit\n');
+ } catch (ex) { finish(null, ''); }
+}
+
+// Asynchronous so it never blocks the caller with a waitExit() nested loop. Calls
+// callback(arrayOrEmpty).
+function av(callback)
+{
+ var fs = require('fs');
+
+ // Find the clamscan binary and use its absolute path (don't depend on PATH,
+ // the agent environment may not include the binary's directory).
+ var clamBinaries = ['/usr/bin/clamscan', '/usr/local/bin/clamscan', '/bin/clamscan'];
+ var clamPath = null;
+ for (var i = 0; i < clamBinaries.length; ++i) {
+ try { if (fs.existsSync(clamBinaries[i])) { clamPath = clamBinaries[i]; break; } } catch (ex) { }
+ }
+ if (clamPath == null) { callback([]); return; }
+
+ var status = { product: 'ClamAV', enabled: false, updated: false };
+
+ // Virus database date: newest mtime of the ClamAV signature files. Pure JS, so
+ // it needs no subprocess and can't hang on busybox-vs-coreutils tool differences.
+ try {
+ var newest = 0;
+ var dbFiles = ['/var/lib/clamav/main.cvd', '/var/lib/clamav/main.cld'];
+ for (var k = 0; k < dbFiles.length; ++k) {
+ try { var st = fs.statSync(dbFiles[k]); var t = (st && st.mtime) ? st.mtime.getTime() : 0; if (t > newest) { newest = t; } } catch (ex) { }
+ }
+ if (newest > 0) {
+ var d = new Date(newest);
+ status.definitionDate = d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2);
+ status.updated = true;
+ }
+ } catch (ex) { }
+
+ // Get the ClamAV version, then whether clamd is running, then return. Both run as
+ // non-blocking children, so a hung command can't stall the agent or nest waitExit().
+ runShellAsync(shellTimeout(4) + '$TO "' + clamPath + '" --version 2>/dev/null', 4000, function (verOut) {
+ var v = verOut.split('\n')[0].trim();
+ if (v) { status.product = v; }
+ // systemd via 'is-active'; otherwise a direct process check (pgrep). The old
+ // 'rc-service clamd status' path could block for seconds on Alpine/OpenRC.
+ runShellAsync(
+ shellTimeout(4) +
+ 'if command -v systemctl >/dev/null 2>&1; then ' +
+ 'if $TO systemctl is-active --quiet clamav-daemon 2>/dev/null || $TO systemctl is-active --quiet clamd 2>/dev/null; then echo active; else echo inactive; fi; ' +
+ 'else ' +
+ 'pgrep -x clamd >/dev/null 2>&1 && echo active || echo inactive; ' +
+ 'fi', 4000, function (svcOut) {
+ status.enabled = (svcOut.split('\n')[0].trim() === 'active');
+ callback([status]);
+ });
+ });
+}
+
+// Asynchronous so it never blocks the caller with a waitExit() nested loop. Calls
+// callback(objectOrNull).
+function firewall(callback)
+{
+ var fs = require('fs');
+
+ // Find the ufw binary and use its absolute path (don't depend on PATH).
+ var ufwBinaries = ['/usr/sbin/ufw', '/sbin/ufw', '/usr/bin/ufw'];
+ var ufwPath = null;
+ for (var i = 0; i < ufwBinaries.length; ++i) {
+ try { if (fs.existsSync(ufwBinaries[i])) { ufwPath = ufwBinaries[i]; break; } } catch (ex) { }
+ }
+ if (ufwPath == null) { callback(null); return; }
+
+ runShellAsync(shellTimeout(4) + '$TO "' + ufwPath + '" status 2>/dev/null', 4000, function (out) {
+ callback({
+ product: 'UFW',
+ installed: true,
+ enabled: /^status:\s+active$/i.test(out.split('\n')[0])
+ });
+ });
+}
+
+function packages() {
+ var ret = new promise(function (res, rej) { this._res = res; this._rej = rej; });
+ var fs = require('fs');
+
+ // Run a shell script without blocking the event loop, capped at 30s; cb(stdout).
+ function runSh(script, cb) { runShellAsync(shellTimeout(25) + '$TO ' + script, 30000, cb); }
+
+ function dirExists(path) {
+ try { return fs.existsSync(path); } catch (ex) { return false; }
+ }
+
+ function binExists(paths) {
+ for (var i = 0; i < paths.length; ++i) {
+ try { if (fs.existsSync(paths[i])) return true; } catch (ex) { }
+ }
+ return false;
+ }
+
+ // ---------- dpkg (Debian/Ubuntu) ----------
+ function collectDpkg(cb) {
+ if (!binExists(['/usr/bin/dpkg-query', '/bin/dpkg-query']) || !dirExists('/var/lib/dpkg/info')) { cb([]); return; }
+
+ runSh('dpkg-query -W -f=\'${binary:Package}\\t${Version}\\t${Architecture}\\t${db:Status-Abbrev}\\t${Maintainer}\\n\' 2>/dev/null', function (out) {
+ if (!out) { cb([]); return; }
+ runSh('find /var/lib/dpkg/info -name \'*.list\' -printf \'%f\\t%TY-%Tm-%Td\\n\' 2>/dev/null', function (dateOut) {
+ var dateMap = {};
+ if (dateOut) {
+ dateOut.split('\n').forEach(function (line) {
+ var p = line.split('\t');
+ if (p.length >= 2) { dateMap[p[0].replace(/\.list$/, '')] = p[1]; }
+ });
+ }
+
+ var results = [];
+ out.split('\n').forEach(function (line) {
+ var p = line.split('\t');
+ if (p.length < 4) return;
+ if (p[3].indexOf('ii') !== 0) return;
+ results.push({ name: p[0], version: p[1], arch: p[2], publisher: p[4] || '', date: dateMap[p[0]] || '', location: 'dpkg' });
+ });
+ cb(results);
+ });
+ });
+ }
+
+ // ---------- rpm (RHEL/Fedora/CentOS/SUSE) ----------
+ function collectRpm(cb) {
+ if (!binExists(['/usr/bin/rpm', '/bin/rpm']) || (!dirExists('/var/lib/rpm') && !dirExists('/usr/lib/sysimage/rpm'))) { cb([]); return; }
+
+ runSh('rpm -qa --qf \'%{NAME}\\t%{VERSION}\\t%{RELEASE}\\t%{ARCH}\\t%{VENDOR}\\t%{INSTALLTIME:date}\\n\' 2>/dev/null', function (out) {
+ if (!out) { cb([]); return; }
+ var results = [];
+ out.split('\n').forEach(function (line) {
+ var p = line.split('\t');
+ if (p.length < 5 || !p[0]) return;
+ results.push({ name: p[0], version: p[1] + (p[2] ? '-' + p[2] : ''), arch: p[3], publisher: p[4] || '', date: p[5] || '', location: 'rpm' });
+ });
+ cb(results);
+ });
+ }
+
+ // ---------- pacman (Arch/Manjaro) ----------
+ function collectPacman(cb) {
+ if (!binExists(['/usr/bin/pacman', '/bin/pacman']) || !dirExists('/var/lib/pacman/local')) { cb([]); return; }
+
+ runSh('pacman -Qi 2>/dev/null', function (out) {
+ if (!out) { cb([]); return; }
+ var results = [];
+ var current = {};
+ out.split('\n').forEach(function (line) {
+ var m = line.match(/^([A-Za-z ]+?)\s*:\s(.+)$/);
+ if (m) {
+ current[m[1].trim()] = m[2].trim();
+ } else if (!line.trim() && current['Name']) {
+ results.push({ name: current['Name'], version: current['Version'] || '', arch: current['Architecture'] || '', publisher: current['Packager'] || '', date: current['Install Date'] || '', location: 'pacman' });
+ current = {};
+ }
+ });
+ if (current['Name']) {
+ results.push({ name: current['Name'], version: current['Version'] || '', arch: current['Architecture'] || '', publisher: current['Packager'] || '', date: current['Install Date'] || '', location: 'pacman' });
+ }
+ cb(results);
+ });
+ }
+
+ // ---------- apk (Alpine) ----------
+ function collectApk(cb) {
+ if (!binExists(['/sbin/apk', '/usr/bin/apk']) || (!dirExists('/lib/apk/db') && !dirExists('/var/lib/apk/db'))) { cb([]); return; }
+
+ runSh('apk info -v 2>/dev/null', function (out) {
+ if (!out) { cb([]); return; }
+ var results = [];
+ out.split('\n').forEach(function (line) {
+ line = line.trim();
+ if (!line) return;
+ var match = line.match(/^(.+)-(\d[^-]*)(-r\d+)?$/);
+ if (match) {
+ results.push({ name: match[1], version: match[2] + (match[3] || ''), arch: '', publisher: '', location: 'apk' });
+ } else {
+ results.push({ name: line, version: '', arch: '', publisher: '', location: 'apk' });
+ }
+ });
+ cb(results);
+ });
+ }
+
+ // ---------- flatpak ----------
+ function collectFlatpak(cb) {
+ if (!binExists(['/usr/bin/flatpak', '/bin/flatpak']) || !dirExists('/var/lib/flatpak')) { cb([]); return; }
+
+ var results = [];
+
+ runSh('flatpak list --system --columns=application,version,origin 2>/dev/null', function (sys) {
+ sys.split('\n').forEach(function (line) {
+ var p = line.split('\t');
+ if (!p[0] || !p[0].trim()) return;
+ results.push({ name: p[0].trim(), version: (p[1] || '').trim(), arch: '', publisher: (p[2] || '').trim(), location: 'flatpak', scope: 'system' });
+ });
+
+ runSh('flatpak list --user --columns=application,version,origin 2>/dev/null', function (usr) {
+ usr.split('\n').forEach(function (line) {
+ var p = line.split('\t');
+ if (!p[0] || !p[0].trim()) return;
+ results.push({ name: p[0].trim(), version: (p[1] || '').trim(), arch: '', publisher: (p[2] || '').trim(), location: 'flatpak', scope: 'user' });
+ });
+
+ cb(results);
+ });
+ });
+ }
+
+ // ---------- snap ----------
+ function collectSnap(cb) {
+ if (!binExists(['/usr/bin/snap', '/bin/snap']) || !dirExists('/var/lib/snapd/snaps')) { cb([]); return; }
+
+ runSh('snap list --all 2>/dev/null', function (out) {
+ if (!out) { cb([]); return; }
+ var results = [];
+ out.split('\n').forEach(function (line) {
+ if (!line || line.indexOf('Name') === 0) return;
+ var p = line.trim().split(/\s+/);
+ if (p.length < 5 || !p[0]) return;
+ results.push({ name: p[0], version: p[1], arch: '', publisher: p[4] || '', location: 'snap' });
+ });
+ cb(results);
+ });
+ }
+
+ // Run every collector concurrently (non-blocking), keep results in source order,
+ // and resolve once all have reported. Each collector calls its callback exactly
+ // once; the bucket guard makes a stray double-callback harmless.
+ var collectors = [collectDpkg, collectRpm, collectPacman, collectApk, collectFlatpak, collectSnap];
+ var buckets = new Array(collectors.length);
+ var pending = collectors.length;
+ function onCollected(i, r) {
+ if (buckets[i] !== undefined) { return; }
+ buckets[i] = (r && r.length) ? r : [];
+ if (--pending === 0) {
+ try { ret._res([].concat.apply([], buckets)); } catch (e) { ret._rej(e); }
+ }
+ }
+ collectors.forEach(function (collector, i) {
+ try { collector(function (r) { onCollected(i, r); }); }
+ catch (e) { onCollected(i, []); }
+ });
+
+ return (ret);
+}
+
+if (process.platform == 'linux') {
+ module.exports = { av: av, firewall: firewall, packages: packages };
+}
+else
+{
+ var not_supported = function () { throw (process.platform + ' not supported'); };
+ module.exports = { av: not_supported, firewall: not_supported, packages: not_supported };
+}
\ No newline at end of file
diff --git a/agents/modules_meshcore/mac-info.js b/agents/modules_meshcore/mac-info.js
new file mode 100644
index 0000000000..2d042819eb
--- /dev/null
+++ b/agents/modules_meshcore/mac-info.js
@@ -0,0 +1,92 @@
+
+var promise = require('promise');
+
+function apps() {
+ var ret = new promise(function (res, rej) { this._res = res; this._rej = rej; });
+
+ function runSh(script) {
+ try {
+ var child = require('child_process').execFile('/bin/sh', ['sh']);
+ child.stdout.str = '';
+ child.stdout.on('data', function (d) { child.stdout.str += d; });
+ child.stdin.write(script + '\nexit\n');
+ child.waitExit();
+ return child.stdout.str.trim();
+ } catch (ex) { return ''; }
+ }
+
+ // Single shell process: PlistBuddy handles both XML and binary plists.
+ // Outputs tab-separated: name, version, publisher, date, scope — one app per line.
+ var script = [
+ 'PB=/usr/libexec/PlistBuddy',
+ 'process_app() {',
+ ' app="$1" scope="$2"',
+ ' plist="$app/Contents/Info.plist"',
+ ' [ -f "$plist" ] || return',
+ ' name=$(basename "$app" .app)',
+ ' version=$("$PB" -c "Print :CFBundleShortVersionString" "$plist" 2>/dev/null)',
+ ' publisher=$("$PB" -c "Print :NSHumanReadableCopyright" "$plist" 2>/dev/null)',
+ ' if [ -z "$publisher" ]; then',
+ ' bid=$("$PB" -c "Print :CFBundleIdentifier" "$plist" 2>/dev/null)',
+ ' publisher=$(echo "$bid" | awk -F. \'{if(NF>=2){s=$2; print toupper(substr(s,1,1)) substr(s,2)}}\')',
+ ' fi',
+ ' date=$(stat -f "%Sm" -t "%Y-%m-%d" "$app" 2>/dev/null)',
+ ' arch=""',
+ ' exe=$("$PB" -c "Print :CFBundleExecutable" "$plist" 2>/dev/null)',
+ ' if [ -n "$exe" ]; then',
+ ' lipoout=$(lipo -info "$app/Contents/MacOS/$exe" 2>/dev/null)',
+ ' if echo "$lipoout" | grep -q "x86_64" && echo "$lipoout" | grep -q "arm64"; then',
+ ' arch="universal"',
+ ' elif echo "$lipoout" | grep -q "arm64"; then',
+ ' arch="arm64"',
+ ' elif echo "$lipoout" | grep -q "x86_64"; then',
+ ' arch="x86_64"',
+ ' fi',
+ ' fi',
+ ' printf "%s\\t%s\\t%s\\t%s\\t%s\\t%s\\n" \\',
+ ' "$(printf "%s" "$name" | tr "\\t\\n" " ")" \\',
+ ' "$(printf "%s" "$version" | tr "\\t\\n" " ")" \\',
+ ' "$(printf "%s" "$publisher" | tr "\\t\\n" " ")" \\',
+ ' "$date" "$arch" "$scope"',
+ '}',
+ 'for sysdir in /Applications /System/Applications /System/Applications/Utilities; do',
+ ' [ -d "$sysdir" ] || continue',
+ ' find "$sysdir" -maxdepth 1 -name "*.app" -type d 2>/dev/null | while IFS= read -r app; do',
+ ' process_app "$app" system',
+ ' done',
+ 'done',
+ 'for userdir in /Users/*/; do',
+ ' user=$(basename "$userdir")',
+ ' [ "$user" = Shared ] && continue',
+ ' case "$user" in .*) continue;; esac',
+ ' appsdir="${userdir}Applications"',
+ ' [ -d "$appsdir" ] || continue',
+ ' find "$appsdir" -maxdepth 1 -name "*.app" -type d 2>/dev/null | while IFS= read -r app; do',
+ ' process_app "$app" user',
+ ' done',
+ 'done'
+ ].join('\n');
+
+ try {
+ var out = runSh(script);
+ var results = [];
+ if (out) {
+ out.split('\n').forEach(function (line) {
+ var p = line.split('\t');
+ if (p.length < 6 || !p[0]) return;
+ results.push({ name: p[0], version: p[1] || '', publisher: p[2] || '', date: p[3] || '', arch: p[4] || '', location: p[5] || 'system' });
+ });
+ }
+ ret._res(results);
+ } catch (e) {
+ ret._rej(e);
+ }
+ return (ret);
+}
+
+if (process.platform == 'darwin') {
+ module.exports = { apps: apps };
+} else {
+ var not_supported = function () { throw (process.platform + ' not supported'); };
+ module.exports = { apps: not_supported };
+}
diff --git a/agents/modules_meshcore/sysinfo.js b/agents/modules_meshcore/sysinfo.js
index cc13574dde..014d629b46 100644
--- a/agents/modules_meshcore/sysinfo.js
+++ b/agents/modules_meshcore/sysinfo.js
@@ -230,7 +230,7 @@ function windows_thermals()
{
var ret = [];
try {
- ret = require('win-wmi').query('ROOT\\WMI', 'SELECT CurrentTemperature,InstanceName FROM MSAcpi_ThermalZoneTemperature',['CurrentTemperature','InstanceName']);
+ ret = require('win-wmi-fixed').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);
diff --git a/agents/modules_meshcore/win-info.js b/agents/modules_meshcore/win-info.js
index 69377cc981..2473b93292 100644
--- a/agents/modules_meshcore/win-info.js
+++ b/agents/modules_meshcore/win-info.js
@@ -16,10 +16,13 @@ limitations under the License.
var promise = require('promise');
+// We use the environment variable directly or the standard Windows path
+var psPath = (process.env['SystemRoot'] ? process.env['SystemRoot'] : 'C:\\Windows') + '\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
+
function qfe()
{
try {
- var tokens = require('win-wmi').query('ROOT\\CIMV2', 'SELECT * FROM Win32_QuickFixEngineering');
+ var tokens = require('win-wmi-fixed').query('ROOT\\CIMV2', 'SELECT * FROM Win32_QuickFixEngineering');
if (tokens[0]){
for (var index = 0; index < tokens.length; index++) {
for (var key in tokens[index]) {
@@ -157,105 +160,153 @@ function regQuery(H, Path, Key)
}
function pendingReboot()
{
+ var ret = [];
var tmp = null;
- var ret = null;
var HKEY = require('win-registry').HKEY;
if(regQuery(HKEY.LocalMachine, 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing', 'RebootPending') !=null)
{
- ret = 'Component Based Servicing';
+ ret.push('Component Based Servicing');
}
- else if(regQuery(HKEY.LocalMachine, 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate', 'RebootRequired'))
+ if(regQuery(HKEY.LocalMachine, 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate', 'RebootRequired'))
{
- ret = 'Windows Update';
+ ret.push('Windows Update');
}
- else if ((tmp=regQuery(HKEY.LocalMachine, 'SYSTEM\\CurrentControlSet\\Control\\Session Manager', 'PendingFileRenameOperations'))!=null && tmp != 0 && tmp != '')
+ try {
+ var keyInfo = regQuery(HKEY.LocalMachine, 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate\\Auto Update');
+ if (keyInfo && keyInfo.subkeys){
+ if (keyInfo.subkeys.indexOf('RebootRequired') !== -1) {
+ ret.push('Windows Update');
+ }
+ }
+ } catch(ex) { }
+ if ((tmp=regQuery(HKEY.LocalMachine, 'SYSTEM\\CurrentControlSet\\Control\\Session Manager', 'PendingFileRenameOperations'))!=null && tmp != 0 && tmp != '')
{
- ret = 'File Rename';
+ ret.push('File Rename');
}
- else if (regQuery(HKEY.LocalMachine, 'SYSTEM\\CurrentControlSet\\Control\\ComputerName\\ActiveComputerName', 'ComputerName') != regQuery(HKEY.LocalMachine, 'SYSTEM\\CurrentControlSet\\Control\\ComputerName\\ComputerName', 'ComputerName'))
+ if (regQuery(HKEY.LocalMachine, 'SYSTEM\\CurrentControlSet\\Control\\ComputerName\\ActiveComputerName', 'ComputerName') != regQuery(HKEY.LocalMachine, 'SYSTEM\\CurrentControlSet\\Control\\ComputerName\\ComputerName', 'ComputerName'))
{
- ret = 'System Rename';
+ ret.push('System Rename');
}
return (ret);
}
-function installedApps()
-{
- var promise = require('promise');
- var ret = new promise(function (a, r) { this._resolve = a; this._reject = r; });
-
- var code = "\
- var reg = require('win-registry');\
- var result = [];\
- var val, tmp;\
- var items = reg.QueryKey(reg.HKEY.LocalMachine, 'SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall');\
- for (var key in items.subkeys)\
- {\
- val = {};\
- try\
- {\
- val.name = reg.QueryKey(reg.HKEY.LocalMachine, 'SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\' + items.subkeys[key], 'DisplayName');\
- }\
- catch(e)\
- {\
- continue;\
- }\
- try\
- {\
- val.version = reg.QueryKey(reg.HKEY.LocalMachine, 'SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\' + items.subkeys[key], 'DisplayVersion');\
- if (val.version == '') { delete val.version; }\
- }\
- catch(e)\
- {\
- }\
- try\
- {\
- val.location = reg.QueryKey(reg.HKEY.LocalMachine, 'SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\' + items.subkeys[key], 'InstallLocation');\
- if (val.location == '') { delete val.location; }\
- }\
- 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();";
-
- ret.child = require('child_process').execFile(process.execPath, [process.execPath.split('\\').pop().split('.exe')[0], '-exec "' + code + '"']);
- ret.child.promise = ret;
- ret.child.stdout.str = ''; ret.child.stdout.on('data', function (c) { this.str += c.toString(); });
- ret.child.on('exit', function (c) { this.promise._resolve(JSON.parse(this.stdout.str.trim())); });
+function installedApps() {
+ var ret = new promise(function (res, rej) { this._res = res; this._rej = rej; });
+ var registry = require('win-registry');
+ var HKEY = registry.HKEY;
+ var results = [];
+ var registryPaths = [
+ 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
+ 'SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall'
+ ];
+ for (var i in registryPaths) {
+ try {
+ var path = registryPaths[i];
+ var keyInfo = registry.QueryKey(HKEY.LocalMachine, path);
+ if (!keyInfo || !keyInfo.subkeys) continue;
+ for (var j = 0; j < keyInfo.subkeys.length; j++) {
+ var subPath = path + '\\' + keyInfo.subkeys[j];
+ var name = regQuery(HKEY.LocalMachine, subPath, 'DisplayName');
+ if (name && name != '') {
+ results.push({
+ name: name,
+ version: regQuery(HKEY.LocalMachine, subPath, 'DisplayVersion') || '',
+ publisher: regQuery(HKEY.LocalMachine, subPath, 'Publisher') || '',
+ uninstall: regQuery(HKEY.LocalMachine, subPath, 'QuietUninstallString') || regQuery(HKEY.LocalMachine, subPath, 'UninstallString') || '',
+ location: regQuery(HKEY.LocalMachine, subPath, 'InstallLocation') || '',
+ date: regQuery(HKEY.LocalMachine, subPath, 'InstallDate') || ''
+ });
+ }
+ }
+ } catch (e) { }
+ }
+ ret._res(results);
return (ret);
}
-function installedStoreApps(){
+function installedStoreApps() {
+ var ret = new promise(function (a, r) { this._resolve = a; this._reject = r; });
+
+ // Basierend auf deiner funktionierenden Version + Scope-Erkennung
+ var psCommand = [
+ "$ErrorActionPreference = 'SilentlyContinue'",
+ "$allUsersApps = @(Get-AppxPackage -AllUsers)",
+ "$allUsersPkgNames = @($allUsersApps | Select-Object -ExpandProperty PackageFullName)",
+ "$userOnlyApps = @(Get-AppxPackage | Where-Object { $allUsersPkgNames -notcontains $_.PackageFullName })",
+ "$provPkgs = @(Get-AppxProvisionedPackage -Online | Select-Object -ExpandProperty DisplayName)",
+ "$results = @()",
+ "foreach ($app in $allUsersApps) {",
+ " if ($app.Name -and $app.Name -notlike 'Microsoft.Windows.*' -and $app.Name -notlike 'windows.*' -and $app.Name -notlike '*_neutral_*') {",
+ " $scope = 'System'",
+ " if ($provPkgs -contains $app.Name) { $scope = 'System+Prov' }",
+ " $results += [PSCustomObject]@{ Name=$app.Name; Version=[string]$app.Version; PackageFullName=$app.PackageFullName; Publisher=$app.Publisher; Scope=$scope }",
+ " }",
+ "}",
+ "foreach ($app in $userOnlyApps) {",
+ " if ($app.Name -and $app.Name -notlike 'Microsoft.Windows.*' -and $app.Name -notlike 'windows.*' -and $app.Name -notlike '*_neutral_*') {",
+ " $scope = 'User'",
+ " if ($provPkgs -contains $app.Name) { $scope = 'User+Prov' }",
+ " $results += [PSCustomObject]@{ Name=$app.Name; Version=[string]$app.Version; PackageFullName=$app.PackageFullName; Publisher=$app.Publisher; Scope=$scope }",
+ " }",
+ "}",
+ "$results | Sort-Object Name -Unique | ConvertTo-Json -Compress"
+ ].join("; ");
+
try {
- var tokens = require('win-wmi-fixed').query('ROOT\\CIMV2', 'SELECT * FROM Win32_InstalledStoreProgram');
- if (tokens[0]){
- for (var index = 0; index < tokens.length; index++) {
- for (var key in tokens[index]) {
- if (key.startsWith('__')) delete tokens[index][key];
+ var psPath = (process.env['SystemRoot'] || 'C:\\Windows') + '\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
+
+ ret.child = require('child_process').execFile(
+ psPath,
+ ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', psCommand],
+ { timeout: 60000, maxBuffer: 10 * 1024 * 1024 }
+ );
+
+ ret.child.promise = ret;
+ ret.child.stdout.str = '';
+ ret.child.stdout.on('data', function (chunk) { this.str += chunk.toString(); });
+
+ ret.child.on('exit', function (code) {
+ try {
+ var output = this.stdout.str.trim();
+
+ if (output === '' || output === 'null' || output === '[]') {
+ this.promise._resolve([]);
+ return;
}
+
+ var data = JSON.parse(output);
+ if (!Array.isArray(data)) { data = [data]; }
+
+ var apps = data.map(function(app) {
+ return {
+ name: app.Name || '',
+ version: app.Version || '',
+ publisher: app.Publisher || '',
+ packageFullName: app.PackageFullName || '',
+ scope: app.Scope || '',
+ uninstall: 'Remove-AppxPackage -Package "' + (app.PackageFullName || '') + '" -AllUsers'
+ };
+ });
+
+ this.promise._resolve(apps);
+ } catch (e) {
+ this.promise._resolve([]);
}
- return (tokens);
- } else {
- return ([]);
- };
+ });
+
+ ret.child.on('error', function (err) {
+ this.promise._resolve([]);
+ });
} catch (ex) {
- return ([]);
+ ret._resolve([]);
}
+
+ return (ret);
}
function defender(){
try {
- var tokens = require('win-wmi').query('ROOT\\Microsoft\\Windows\\Defender', 'SELECT * FROM MSFT_MpComputerStatus', ['RealTimeProtectionEnabled','IsTamperProtected','AntivirusSignatureVersion','AntivirusSignatureLastUpdated']);
+ var tokens = require('win-wmi-fixed').query('ROOT\\Microsoft\\Windows\\Defender', 'SELECT * FROM MSFT_MpComputerStatus', ['RealTimeProtectionEnabled','IsTamperProtected','AntivirusSignatureVersion','AntivirusSignatureLastUpdated']);
if (tokens[0]){
var info = { RealTimeProtection: tokens[0].RealTimeProtectionEnabled, TamperProtected: tokens[0].IsTamperProtected };
if (tokens[0].AntivirusSignatureVersion) { info.AntivirusSignatureVersion = tokens[0].AntivirusSignatureVersion; }
@@ -268,13 +319,146 @@ function defender(){
return ({});
}
}
+function printers() {
+ var wmi = require('win-wmi-fixed');
+ var reg = require('win-registry');
+ var HKLM = reg.HKEY.LocalMachine;
+ var portMap = {};
+ var printers = wmi.query('ROOT\\CIMV2', 'SELECT * FROM Win32_Printer');
+ var tcpPorts = wmi.query('ROOT\\CIMV2', 'SELECT Name, HostAddress, PortNumber FROM Win32_TCPIPPrinterPort');
+ for (var j = 0; j < tcpPorts.length; ++j) { portMap[tcpPorts[j].Name] = tcpPorts[j].HostAddress + ':' + tcpPorts[j].PortNumber; }
+ try {
+ var monitorsKey = 'SYSTEM\\CurrentControlSet\\Control\\Print\\Monitors';
+ var monitors = reg.QueryKey(HKLM, monitorsKey);
+ if (monitors && monitors.keys) {
+ for (var m = 0; m < monitors.keys.length; ++m) {
+ var portsKey = monitorsKey + '\\' + monitors.keys[m] + '\\Ports';
+ try {
+ var portsNode = reg.QueryKey(HKLM, portsKey);
+ if (portsNode && portsNode.keys) {
+ for (var p = 0; p < portsNode.keys.length; ++p) {
+ var portName = portsNode.keys[p];
+ if (portMap[portName]) continue;
+ var portKey = portsKey + '\\' + portName;
+ var ip = null;
+ try { ip = reg.QueryKey(HKLM, portKey, 'IPAddress'); } catch (e) {}
+ if (!ip) { try { ip = reg.QueryKey(HKLM, portKey, 'HostName'); } catch (e) {} }
+ if (ip) { portMap[portName] = ip; }
+ }
+ }
+ } catch (e) {}
+ }
+ }
+ } catch (e) {}
+ try {
+ var msftPorts = wmi.query('ROOT\\StandardCimv2', 'SELECT Name, Description FROM MSFT_PrinterPort');
+ for (var j = 0; j < msftPorts.length; ++j) {
+ if (!portMap[msftPorts[j].Name] && msftPorts[j].Description) {
+ portMap[msftPorts[j].Name] = msftPorts[j].Description;
+ }
+ }
+ } catch (e) {}
+ var printJobs = wmi.query('ROOT\\CIMV2', 'SELECT Name FROM Win32_PrintJob');
+ var jobCount = {};
+ for (var j = 0; j < printJobs.length; ++j) {
+ var jobPrinter = printJobs[j].Name.split(',')[0];
+ jobCount[jobPrinter] = (jobCount[jobPrinter] || 0) + 1;
+ }
+ var printerStatusMap = { 1: 'Other', 2: 'Unknown', 3: 'Idle', 4: 'Printing', 5: 'Warmup', 6: 'Stopped', 7: 'Offline' };
+ var errorStateMap = { 0: 'Unknown', 1: 'Other', 2: 'No Error', 3: 'Low Paper', 4: 'No Paper', 5: 'Low Toner', 6: 'No Toner', 7: 'Door Open', 8: 'Jammed', 9: 'Offline', 10: 'Service Requested', 11: 'Output Bin Full' };
+ var result = [];
+ for (var i = 0; i < printers.length; ++i) {
+ var portDesc = portMap[printers[i].PortName];
+ var jobs = jobCount[printers[i].Name] || 0;
+ var status = printerStatusMap[printers[i].PrinterStatus] || 'Unknown';
+ var errors = [];
+ var err = parseInt(printers[i].DetectedErrorState) || 0;
+ if (err > 2) { errors.push(errorStateMap[err] || ('Error ' + err)); }
+ result.push({ type: 'system', name: printers[i].Name, port: printers[i].PortName, portDesc: portDesc, status: status, errors: errors, jobCount: jobs });
+ }
+ // AD/GPO user-level printers from HKU\Printers\Connections
+ var HKU = reg.HKEY.Users;
+ var HKLM2 = reg.HKEY.LocalMachine;
+ var userPrinters = [];
+ function collectUserPrinters(hiveRef, sid) {
+ try {
+ var label = sid;
+ try {
+ var profileListPath = 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\' + sid;
+ var profilePath = reg.QueryKey(HKLM2, profileListPath, 'ProfileImagePath');
+ if (profilePath) {
+ var parts = profilePath.split('\\');
+ if (parts.length > 0) { label = parts[parts.length - 1]; }
+ }
+ } catch (e) {}
+ var hiveLabel = label;
+ var fullPath = sid + '\\Printers\\Connections';
+ var node = reg.QueryKey(hiveRef, fullPath);
+ if (node && node.subkeys) {
+ for (var ki = 0; ki < node.subkeys.length; ++ki) {
+ var connKey = node.subkeys[ki];
+ if (connKey === '' || connKey === ',') { continue; }
+ var parts = connKey.split(',');
+ if (parts.length >= 4) {
+ var server = parts[2];
+ var printerName = parts[3];
+ var port = '\\\\' + server + '\\' + printerName;
+ var dup = false;
+ for (var di = 0; di < userPrinters.length; ++di) {
+ if (userPrinters[di].name === printerName) { dup = true; break; }
+ }
+ if (!dup) { userPrinters.push({ name: printerName, port: port, label: hiveLabel }); }
+ }
+ }
+ }
+ } catch (e) {}
+ }
+ try {
+ var loadedHivesResult = reg.QueryKey(HKU, '');
+ var loadedHives = (loadedHivesResult && loadedHivesResult.subkeys) ? loadedHivesResult.subkeys : [];
+ for (var u = 0; u < loadedHives.length; ++u) {
+ var sid = loadedHives[u];
+ if (sid === '.DEFAULT' || sid === 'S-1-5-18' || sid === 'S-1-5-19' || sid === 'S-1-5-20' || sid.indexOf('_Classes') > 0) { continue; }
+ collectUserPrinters(HKU, sid);
+ }
+ } catch (e) {}
+ try {
+ var printConnKey = 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Print\\Connections';
+ var printConn = reg.QueryKey(HKLM2, printConnKey);
+ if (printConn && printConn.keys) {
+ for (var k = 0; k < printConn.keys.length; ++k) {
+ var connPortName = printConn.keys[k];
+ var connPortKey = printConnKey + '\\' + connPortName;
+ var ip = null;
+ try { ip = reg.QueryKey(HKLM2, connPortKey, 'IPAddress'); } catch (e) {}
+ if (!ip) { try { ip = reg.QueryKey(HKLM2, connPortKey, 'HostName'); } catch (e) {} }
+ userPrinters.push({ name: connPortName, port: ip || '', label: 'HKLM' });
+ }
+ }
+ } catch (e) {}
+ for (var i = 0; i < userPrinters.length; ++i) {
+ var up = userPrinters[i];
+ var portSpec = up.port;
+ var portDesc = '';
+ if (portSpec) {
+ var colonIdx = portSpec.indexOf(',');
+ var basePort = (colonIdx > 0) ? portSpec.substring(0, colonIdx) : portSpec;
+ portDesc = portMap[basePort] || portMap[portSpec];
+ if (!portDesc && basePort.indexOf('IP_') === 0) { portDesc = basePort.substring(3); }
+ if (!portDesc && basePort.indexOf('\\\\') === 0) { portDesc = basePort; }
+ if (!portDesc) { portDesc = portSpec; }
+ }
+ result.push({ type: 'adgpo', name: up.name, port: up.port, portDesc: portDesc, label: up.label });
+ }
+ return result;
+}
if (process.platform == 'win32')
{
- module.exports = { qfe: qfe, av: av, defrag: defrag, pendingReboot: pendingReboot, installedApps: installedApps, installedStoreApps: installedStoreApps, defender: defender };
+ module.exports = { qfe: qfe, av: av, defrag: defrag, pendingReboot: pendingReboot, installedApps: installedApps, installedStoreApps: installedStoreApps, defender: defender, printers: printers };
}
else
{
var not_supported = function () { throw (process.platform + ' not supported'); };
- module.exports = { qfe: not_supported, av: not_supported, defrag: not_supported, pendingReboot: not_supported, installedApps: not_supported, installedStoreApps: not_supported, defender: not_supported };
+ module.exports = { qfe: not_supported, av: not_supported, defrag: not_supported, pendingReboot: not_supported, installedApps: not_supported, installedStoreApps: not_supported, defender: not_supported, printers: not_supported };
}
\ No newline at end of file
diff --git a/agents/modules_meshcore/win-updates.js b/agents/modules_meshcore/win-updates.js
new file mode 100644
index 0000000000..1fb1882fb8
--- /dev/null
+++ b/agents/modules_meshcore/win-updates.js
@@ -0,0 +1,474 @@
+// Windows Update available updates lister via Windows Update Agent COM API.
+// Uses IUpdateSession -> IUpdateSearcher (async) to find updates not yet installed.
+
+var promise = require('promise');
+
+var CLSID_UpdateSession = '{4CB43D7F-7EEE-4906-8698-60DA1C38F2FE}';
+// IID observed from QueryInterface wire call for IUpdateSearchCompletedCallback
+var IID_IUSCC = '58E0AE88B0D42547A2F1814A67AE964C';
+var E_NOINTERFACE = 0x80004002;
+
+var _GM = require('_GenericMarshal');
+var OleAut32 = _GM.CreateNativeProxy('OleAut32.dll');
+OleAut32.CreateMethod('SysAllocString');
+OleAut32.CreateMethod('SysFreeString');
+
+function makeBSTR(str) {
+ return OleAut32.SysAllocString(_GM.CreateVariable(str, { wide: true }));
+}
+
+var UpdateSessionFunctions = [
+ 'QueryInterface', 'AddRef', 'Release',
+ 'GetTypeInfoCount', 'GetTypeInfo', 'GetIDsOfNames', 'Invoke',
+ 'get_ClientApplicationID', 'put_ClientApplicationID', 'get_ReadOnly',
+ 'get_WebProxy', 'put_WebProxy',
+ 'CreateUpdateSearcher', 'CreateUpdateDownloader', 'CreateUpdateInstaller'
+];
+
+var UpdateSearcherFunctions = [
+ 'QueryInterface', 'AddRef', 'Release',
+ 'GetTypeInfoCount', 'GetTypeInfo', 'GetIDsOfNames', 'Invoke',
+ 'get_CanAutomaticallyUpgradeService', 'put_CanAutomaticallyUpgradeService',
+ 'get_ClientApplicationID', 'put_ClientApplicationID',
+ 'get_IncludePotentiallySupersededUpdates', 'put_IncludePotentiallySupersededUpdates',
+ 'get_ServerSelection', 'put_ServerSelection',
+ 'BeginSearch', 'EndSearch', 'EscapeString', 'QueryHistory', 'Search',
+ 'get_Online', 'put_Online', 'GetTotalHistoryCount', 'get_ServiceID', 'put_ServiceID'
+];
+
+var SearchResultFunctions = [
+ 'QueryInterface', 'AddRef', 'Release',
+ 'GetTypeInfoCount', 'GetTypeInfo', 'GetIDsOfNames', 'Invoke',
+ 'get_ResultCode', 'get_RootCategories', 'get_Updates', 'get_Warnings'
+];
+
+var UpdateCollectionFunctions = [
+ 'QueryInterface', 'AddRef', 'Release',
+ 'GetTypeInfoCount', 'GetTypeInfo', 'GetIDsOfNames', 'Invoke',
+ 'get_Item', 'put_Item', 'get__NewEnum', 'get_Count', 'get_ReadOnly',
+ 'Add', 'Clear', 'Copy', 'Insert', 'RemoveAt'
+];
+
+var UpdateFunctions = [
+ 'QueryInterface', 'AddRef', 'Release',
+ 'GetTypeInfoCount', 'GetTypeInfo', 'GetIDsOfNames', 'Invoke',
+ 'get_Title', // 7
+ 'get_AutoSelectOnWebSites', // 8
+ 'get_BundledUpdates', // 9
+ 'get_CanRequireSource', // 10
+ 'get_Categories', // 11
+ 'get_Deadline', // 12
+ 'get_DeltaCompressedContentAvailable', // 13
+ 'get_DeltaCompressedContentPreferred', // 14
+ 'get_Description', // 15
+ 'get_EulaAccepted', // 16
+ 'get_EulaText', // 17
+ 'get_HandlerID', // 18
+ 'get_Identity', // 19
+ 'get_Image', // 20
+ 'get_InstallationBehavior', // 21
+ 'get_IsBeta', // 22
+ 'get_IsDownloaded', // 23
+ 'get_IsHidden', // 24
+ 'put_IsHidden', // 25
+ 'get_IsInstalled', // 26
+ 'get_IsMandatory', // 27
+ 'get_IsUninstallable', // 28
+ 'get_Languages', // 29
+ 'get_LastDeploymentChangeTime', // 30
+ 'get_MaxDownloadSize', // 31
+ 'get_MinDownloadSize', // 32
+ 'get_MoreInfoUrls', // 33
+ 'get_MsrcSeverity', // 34
+ 'get_RecommendedCpuSpeed', // 35
+ 'get_RecommendedHardDiskSpace', // 36
+ 'get_RecommendedMemory', // 37
+ 'get_ReleaseNotes', // 38
+ 'get_SecurityBulletinIDs', // 39
+ 'get_SupersededUpdateIDs', // 40
+ 'get_SupportUrl', // 41
+ 'get_Type', // 42
+ 'get_UninstallationNotes', // 43
+ 'get_UninstallationBehavior', // 44
+ 'get_UninstallationSteps', // 45
+ 'AcceptEula', // 46
+ 'get_KBArticleIDs', // 47
+ 'get_DeploymentAction', // 48
+ 'CopyFromCache', // 49
+ 'get_DownloadPriority', // 50
+ 'get_DownloadContents' // 51
+];
+
+var UpdateIdentityFunctions = [
+ 'QueryInterface', 'AddRef', 'Release',
+ 'GetTypeInfoCount', 'GetTypeInfo', 'GetIDsOfNames', 'Invoke',
+ 'get_RevisionNumber', 'get_UpdateID'
+];
+
+// IUpdateHistoryEntryCollection
+var UpdateHistoryCollectionFunctions = [
+ 'QueryInterface', 'AddRef', 'Release',
+ 'GetTypeInfoCount', 'GetTypeInfo', 'GetIDsOfNames', 'Invoke',
+ 'get_Item', // 7
+ 'get__NewEnum', // 8
+ 'get_Count' // 9
+];
+
+// IUpdateHistoryEntry
+var UpdateHistoryEntryFunctions = [
+ 'QueryInterface', 'AddRef', 'Release',
+ 'GetTypeInfoCount', 'GetTypeInfo', 'GetIDsOfNames', 'Invoke',
+ 'get_Operation', // 7
+ 'get_ResultCode', // 8
+ 'get_Date', // 9
+ 'get_UpdateIdentity', // 10
+ 'get_HResult', // 11 (extra method between identity and title)
+ 'get_Title', // 12
+ 'get_Description', // 13
+ 'get_UnmappedResultCode', // 14
+ 'get_ClientApplicationID', // 15
+ 'get_ServerSelection', // 16
+ 'get_ServiceID', // 17
+ 'get_UninstallationSteps', // 18
+ 'get_UninstallationNotes', // 19
+ 'get_SupportUrl' // 20
+];
+
+var active_handlers = {};
+var _cx_counter = 30;
+
+
+function releaseCOM(obj) {
+ if (obj && obj.funcs) { try { obj.funcs.Release(obj); } catch (e) {} }
+}
+
+function readBSTR(ptr) {
+ try { return ptr.Deref().Wide2UTF8; } catch (e) { return ''; }
+}
+
+function readVarBool(v) {
+ try { return v.toBuffer().readInt16LE() !== 0; } catch (e) { return false; }
+}
+
+function readUpdates(searchResultPtr, COM, GM) {
+ var result = [];
+ var searchResult = searchResultPtr.Deref();
+ searchResult.funcs = COM.marshalFunctions(searchResult, SearchResultFunctions);
+
+ var rcV = GM.CreateVariable(4);
+ var resultCode = searchResult.funcs.get_ResultCode(searchResult, rcV).Val === 0 ? rcV.toBuffer().readInt32LE() : -1;
+ if (resultCode !== 2 && resultCode !== 3) { releaseCOM(searchResult); return []; }
+
+ var updatesPtr = GM.CreatePointer();
+ if (searchResult.funcs.get_Updates(searchResult, updatesPtr).Val !== 0) { releaseCOM(searchResult); return []; }
+
+ var updates = updatesPtr.Deref();
+ updates.funcs = COM.marshalFunctions(updates, UpdateCollectionFunctions);
+
+ var countV = GM.CreateVariable(4);
+ updates.funcs.get_Count(updates, countV);
+ var count = countV.toBuffer().readInt32LE();
+
+ for (var i = 0; i < count; i++) {
+ var updatePtr = GM.CreatePointer();
+ if (updates.funcs.get_Item(updates, i, updatePtr).Val !== 0) continue;
+
+ var update = updatePtr.Deref();
+ update.funcs = COM.marshalFunctions(update, UpdateFunctions);
+
+ var entry = {};
+ try {
+ var titlePtr = GM.CreatePointer();
+ entry.title = update.funcs.get_Title(update, titlePtr).Val === 0 ? readBSTR(titlePtr) : '';
+
+ var sevPtr = GM.CreatePointer();
+ entry.severity = update.funcs.get_MsrcSeverity(update, sevPtr).Val === 0 ? readBSTR(sevPtr) : '';
+
+ var dlV = GM.CreateVariable(2);
+ entry.isDownloaded = update.funcs.get_IsDownloaded(update, dlV).Val === 0 ? readVarBool(dlV) : false;
+
+ var mandV = GM.CreateVariable(2);
+ entry.isMandatory = update.funcs.get_IsMandatory(update, mandV).Val === 0 ? readVarBool(mandV) : false;
+
+ var identPtr = GM.CreatePointer();
+ entry.updateID = '';
+ if (update.funcs.get_Identity(update, identPtr).Val === 0) {
+ var identFuncs = COM.marshalFunctions(identPtr.Deref(), UpdateIdentityFunctions);
+ var uidPtr = GM.CreatePointer();
+ if (identFuncs.get_UpdateID(identPtr.Deref(), uidPtr).Val === 0) { entry.updateID = readBSTR(uidPtr); }
+ }
+
+ // KB article IDs extracted from title e.g. "(KB1234567)"
+ entry.kbArticleIDs = [];
+ var kbMatches = entry.title.match(/KB\d+/gi);
+ if (kbMatches) {
+ for (var k = 0; k < kbMatches.length; k++) {
+ entry.kbArticleIDs.push(kbMatches[k].toUpperCase());
+ }
+ }
+ } catch (e) {}
+
+ releaseCOM(update);
+ result.push(entry);
+ }
+
+ releaseCOM(updates);
+ releaseCOM(searchResult);
+ return result;
+}
+
+function makeSearchCallback(GM, COM, p, searcher) {
+ var cx = _cx_counter;
+ _cx_counter += 4;
+ var handler = COM.marshalInterface([
+ {
+ cx: cx, parms: 3, name: 'QueryInterface', func: function (j, riid, ppv)
+ {
+ var hex = riid.Deref(0, 16).toBuffer().toString('hex').toUpperCase();
+ var ret = GM.CreateVariable(4);
+ if (hex === '0000000000000000C000000000000046' || hex === IID_IUSCC) {
+ j.pointerBuffer().copy(ppv.Deref(0, GM.PointerSize).toBuffer());
+ ret.increment(++this.refcount, true);
+ } else {
+ ret.increment(E_NOINTERFACE, true);
+ }
+ return ret;
+ }
+ },
+ { cx: cx+1, parms: 1, name: 'AddRef', func: function () { return GM.CreateVariable(4).increment(++this.refcount, true); } },
+ { cx: cx+2, parms: 1, name: 'Release', func: function () {
+ if (--this.refcount === 0) { cleanup(this); }
+ return GM.CreateVariable(4).increment(this.refcount >>> 0, true);
+ }},
+ {
+ cx: cx+3, parms: 3, name: 'Invoke', func: function (j, searchJob, callbackArgs)
+ {
+ var searchResultPtr = GM.CreatePointer();
+ var hr = this.searcher.funcs.EndSearch(this.searcher, searchJob, searchResultPtr).Val;
+ if (hr !== 0) {
+ this.p._rej(new Error('EndSearch failed: 0x' + (hr >>> 0).toString(16)));
+ } else {
+ try { this.p._res(readUpdates(searchResultPtr, COM, GM)); }
+ catch (e) { this.p._rej(e); }
+ }
+ var self = this;
+ setImmediate(function () { if (--self.refcount === 0) { cleanup(self); } });
+ return GM.CreateVariable(4).increment(0, true);
+ }
+ }
+ ]);
+
+ handler.refcount = 1;
+ handler.p = p;
+ handler.searcher = searcher;
+ active_handlers[handler._hashCode()] = handler;
+ return handler;
+}
+
+function cleanup(h) {
+ if (h.cleanup) { h.cleanup(); }
+ delete active_handlers[h._hashCode()];
+}
+
+function getAvailableUpdates() {
+ var ret = new promise(function (res, rej) { this._res = res; this._rej = rej; });
+
+ try {
+ var GM = require('_GenericMarshal');
+ var COM = require('win-com');
+
+ var session = COM.createInstance(COM.CLSIDFromString(CLSID_UpdateSession), COM.IID_IUnknown);
+ session.funcs = COM.marshalFunctions(session, UpdateSessionFunctions);
+
+ var searcherPtr = GM.CreatePointer();
+ var hr = session.funcs.CreateUpdateSearcher(session, searcherPtr).Val;
+ if (hr !== 0) { throw new Error('CreateUpdateSearcher failed: 0x' + (hr >>> 0).toString(16)); }
+
+ var searcher = searcherPtr.Deref();
+ searcher.funcs = COM.marshalFunctions(searcher, UpdateSearcherFunctions);
+
+ var criteriaBSTR = makeBSTR('IsInstalled=0 and IsHidden=0');
+ var searchJobPtr = GM.CreatePointer();
+ var stateV = GM.CreateVariable(16); // VT_EMPTY VARIANT
+ var callback = makeSearchCallback(GM, COM, ret, searcher);
+
+ hr = searcher.funcs.BeginSearch(searcher, criteriaBSTR, callback, stateV, searchJobPtr).Val;
+ OleAut32.SysFreeString(criteriaBSTR);
+
+ if (hr !== 0) {
+ cleanup(callback);
+ releaseCOM(searcher);
+ releaseCOM(session);
+ throw new Error('BeginSearch failed: 0x' + (hr >>> 0).toString(16));
+ }
+
+ } catch (ex) {
+ ret._rej(ex);
+ }
+
+ return ret;
+}
+
+function getInstalledUpdates() {
+ var ret = new promise(function (res, rej) { this._res = res; this._rej = rej; });
+
+ try {
+ var GM = require('_GenericMarshal');
+ var COM = require('win-com');
+
+ var session = COM.createInstance(COM.CLSIDFromString(CLSID_UpdateSession), COM.IID_IUnknown);
+ session.funcs = COM.marshalFunctions(session, UpdateSessionFunctions);
+
+ var searcherPtr = GM.CreatePointer();
+ var hr = session.funcs.CreateUpdateSearcher(session, searcherPtr).Val;
+ if (hr !== 0) { throw new Error('CreateUpdateSearcher failed: 0x' + (hr >>> 0).toString(16)); }
+
+ var searcher = searcherPtr.Deref();
+ searcher.funcs = COM.marshalFunctions(searcher, UpdateSearcherFunctions);
+
+ var criteriaBSTR = makeBSTR('IsInstalled=1');
+ var searchJobPtr = GM.CreatePointer();
+ var stateV = GM.CreateVariable(16);
+ var callback = makeSearchCallback(GM, COM, ret, searcher);
+
+ hr = searcher.funcs.BeginSearch(searcher, criteriaBSTR, callback, stateV, searchJobPtr).Val;
+ OleAut32.SysFreeString(criteriaBSTR);
+
+ if (hr !== 0) {
+ cleanup(callback);
+ releaseCOM(searcher);
+ releaseCOM(session);
+ throw new Error('BeginSearch failed: 0x' + (hr >>> 0).toString(16));
+ }
+
+ } catch (ex) {
+ ret._rej(ex);
+ }
+
+ return ret;
+}
+
+function getInstalledUpdateHistory() {
+ var ret = new promise(function (res, rej) { this._res = res; this._rej = rej; });
+
+ var GM, COM, session, searcher, history;
+ try {
+ GM = require('_GenericMarshal');
+ COM = require('win-com');
+
+ session = COM.createInstance(COM.CLSIDFromString(CLSID_UpdateSession), COM.IID_IUnknown);
+ session.funcs = COM.marshalFunctions(session, UpdateSessionFunctions);
+
+ var searcherPtr = GM.CreatePointer();
+ var hr = session.funcs.CreateUpdateSearcher(session, searcherPtr).Val;
+ if (hr !== 0) { throw new Error('CreateUpdateSearcher failed: 0x' + (hr >>> 0).toString(16)); }
+
+ searcher = searcherPtr.Deref();
+ searcher.funcs = COM.marshalFunctions(searcher, UpdateSearcherFunctions);
+
+ var totalV = GM.CreateVariable(4);
+ hr = searcher.funcs.GetTotalHistoryCount(searcher, totalV).Val;
+ if (hr !== 0) { throw new Error('GetTotalHistoryCount failed: 0x' + (hr >>> 0).toString(16)); }
+ var total = totalV.toBuffer().readInt32LE();
+
+ var historyPtr = GM.CreatePointer();
+ hr = searcher.funcs.QueryHistory(searcher, 0, total, historyPtr).Val;
+ if (hr !== 0) { throw new Error('QueryHistory failed: 0x' + (hr >>> 0).toString(16)); }
+
+ history = historyPtr.Deref();
+ history.funcs = COM.marshalFunctions(history, UpdateHistoryCollectionFunctions);
+
+ var countV = GM.CreateVariable(4);
+ history.funcs.get_Count(history, countV);
+ var count = countV.toBuffer().readInt32LE();
+
+ var result = [];
+ var i = 0;
+ var BATCH = 100;
+
+ function processBatch() {
+ try {
+ var end = Math.min(i + BATCH, count);
+ while (i < end) {
+ var entryPtr2 = GM.CreatePointer();
+ if (history.funcs.get_Item(history, i, entryPtr2).Val === 0) {
+ var ec = entryPtr2.Deref();
+ ec.funcs = COM.marshalFunctions(ec, UpdateHistoryEntryFunctions);
+ try {
+ var opV = GM.CreateVariable(4);
+ var op = ec.funcs.get_Operation(ec, opV).Val === 0 ? opV.toBuffer().readInt32LE() : -1;
+ var rcV = GM.CreateVariable(4);
+ var rc = ec.funcs.get_ResultCode(ec, rcV).Val === 0 ? rcV.toBuffer().readInt32LE() : -1;
+ if (op === 1 && rc === 2) {
+ var entry = {};
+ var tPtr = GM.CreatePointer();
+ entry.title = ec.funcs.get_Title(ec, tPtr).Val === 0 ? readBSTR(tPtr) : '';
+ entry.kbArticleIDs = [];
+ var kbM = entry.title.match(/KB\d+/gi);
+ if (kbM) { for (var k = 0; k < kbM.length; k++) { entry.kbArticleIDs.push(kbM[k].toUpperCase()); } }
+ result.push(entry);
+ }
+ } catch (e) { }
+ releaseCOM(ec);
+ }
+ i++;
+ }
+ if (i < count) {
+ ret._batchTimer = setTimeout(processBatch, 0);
+ } else {
+ releaseCOM(history);
+ releaseCOM(searcher);
+ releaseCOM(session);
+ ret._res(result);
+ }
+ } catch (ex) {
+ releaseCOM(history);
+ releaseCOM(searcher);
+ releaseCOM(session);
+ ret._rej(ex);
+ }
+ }
+ processBatch();
+ } catch (ex) {
+ if (history) { releaseCOM(history); }
+ if (searcher) { releaseCOM(searcher); }
+ if (session) { releaseCOM(session); }
+ ret._rej(ex);
+ }
+ return ret;
+}
+
+function getInstalledUpdatesDeDuplicated() {
+ var ret = new promise(function (res, rej) { this._res = res; this._rej = rej; });
+ getInstalledUpdateHistory().then(function (all) {
+ // Deduplicate by KB number, keeping most recent (history is newest-first so first seen wins)
+ // For entries with no KB number, deduplicate by full title
+ var seen = {};
+ var result = [];
+ for (var i = 0; i < all.length; i++) {
+ var key = all[i].kbArticleIDs.length > 0 ? all[i].kbArticleIDs.join(',') : all[i].title;
+ if (!seen[key]) {
+ seen[key] = true;
+ result.push(all[i]);
+ }
+ }
+ ret._res(result);
+ }).catch(function (e) { ret._rej(e); });
+ return ret;
+}
+
+if (process.platform === 'win32') {
+ module.exports = { getAvailableUpdates: getAvailableUpdates, getInstalledUpdates: getInstalledUpdates, getInstalledUpdateHistory: getInstalledUpdateHistory, getInstalledUpdatesDeDuplicated: getInstalledUpdatesDeDuplicated };
+} else {
+ var _notSupported = function () {
+ var ret = new promise(function (res, rej) { this._res = res; this._rej = rej; });
+ ret._rej(new Error(process.platform + ' not supported'));
+ return ret;
+ };
+ module.exports = {
+ getAvailableUpdates: _notSupported,
+ getInstalledUpdates: _notSupported,
+ getInstalledUpdateHistory: _notSupported,
+ getInstalledUpdatesDeDuplicated: _notSupported
+ };
+}
diff --git a/agents/modules_meshcore/win-volumes.js b/agents/modules_meshcore/win-volumes.js
index 59e87c1a2e..0666b8c7d2 100644
--- a/agents/modules_meshcore/win-volumes.js
+++ b/agents/modules_meshcore/win-volumes.js
@@ -30,7 +30,7 @@ function trimObject(j)
function getVolumes()
{
- var v = require('win-wmi').query('ROOT\\CIMV2', 'SELECT * FROM Win32_Volume');
+ var v = require('win-wmi-fixed').query('ROOT\\CIMV2', 'SELECT * FROM Win32_Volume');
var i;
var ret = {};
@@ -40,7 +40,7 @@ function getVolumes()
ret[v[i].DeviceID] = trimObject(v[i]);
}
try {
- v = require('win-wmi').query('ROOT\\CIMV2\\Security\\MicrosoftVolumeEncryption', 'SELECT * FROM Win32_EncryptableVolume');
+ v = require('win-wmi-fixed').query('ROOT\\CIMV2\\Security\\MicrosoftVolumeEncryption', 'SELECT * FROM Win32_EncryptableVolume');
for (i in v)
{
var tmp = trimObject(v[i]);
@@ -55,74 +55,158 @@ function getVolumes()
function windows_volumes()
{
+ var drives = {};
+ var error = null;
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)
- };
+ // RegExps for the specific patterns in the manage-bde output, case-insensitive multiline
+ var reID = new RegExp("{[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}}", "mi");
+ var rePass = new RegExp("[0-9]{6}-[0-9]{6}-[0-9]{6}-[0-9]{6}-[0-9]{6}-[0-9]{6}-[0-9]{6}-[0-9]{6}", "mi");
+
+ var pending = 0, done = false;
+ function resolver() { if (done && (pending === 0)) { p1._res({ error: error, drives: drives }); } }
+
+ try {
+ var wmi = require('win-wmi-fixed');
+
+ wmi.query('ROOT\\CIMV2', 'SELECT DeviceID,VolumeName,FileSystem,Size,FreeSpace,DriveType FROM Win32_LogicalDisk', ['DeviceID', 'VolumeName', 'FileSystem', 'Size', 'FreeSpace', 'DriveType'], false, 5000)
+ .forEach(function (disk) {
+ if (!disk || !disk['DeviceID']) { return; } // skip rows without a DeviceID. Shouldn't be possible, but could in case of wmi funkyness
+ var drive = disk['DeviceID'].slice(0, -1);
+ drives[drive] = {
+ name: disk['VolumeName'] || '',
+ type: disk['FileSystem'] || 'Unknown',
+ size: parseInt(disk['Size']) || 0,
+ sizeremaining: parseInt(disk['FreeSpace']) || 0,
+ dType: disk['DriveType'] || 0
+ };
+ });
+
+ // The MicrosoftVolumeEncryption namespace is admin-only and manage-bde needs elevation; skip both entirely when not elevated, saves waiting unneccessary on time-out of wmi-query if run as user
+ if (require('user-sessions').isRoot()) {
+ var child_process = require('child_process');
+ wmi.query('ROOT\\CIMV2\\Security\\MicrosoftVolumeEncryption', 'SELECT DriveLetter,ConversionStatus,ProtectionStatus,EncryptionMethod FROM Win32_EncryptableVolume', ['DriveLetter', 'ConversionStatus', 'ProtectionStatus', 'EncryptionMethod'], false, 5000)
+ .forEach(function (vol) {
+ if (!vol || !vol['DriveLetter']) { return; } // there can be volumes without a DriveLetter(=null), which errors the slice
+ var drive = vol['DriveLetter'].slice(0, -1);
+ if (!drives[drive]) { return; } // no matching logical disk, skip for now.
+ drives[drive].volumeStatus = vol['ConversionStatus'];
+ drives[drive].encryptionMethod = vol['EncryptionMethod'];
+ drives[drive].protectionStatus = vol['ProtectionStatus'];
+ // Only run manage-bde on encrypted drives (ConversionStatus/volumeStatus 0 = FullyDecrypted, otherwise some sort of encryption)
+ if (drives[drive].volumeStatus != 0) {
+ pending++;
+ var keychild = child_process.execFile(process.env['windir'] + '\\system32\\manage-bde.exe', ['manage-bde', '-protectors', '-get', drive + ':', '-Type', 'recoverypassword'], {});
+ keychild.stdout.str = '';
+ keychild.stdout.on('data', function (c) { this.str += c.toString(); });
+ keychild.target = drive;
+ // Guard against a hung manage-bde
+ keychild.timeout = setTimeout(function () { try { keychild.kill(); } catch (ex) { } }, 4000);
+ keychild.on('exit', function () {
+ clearTimeout(this.timeout);
+ var id = this.stdout.str.match(reID);
+ var rp = this.stdout.str.match(rePass);
+ // a recovery password protector should always have an identifier
+ if (id) { drives[this.target].identifier = id[0]; }
+ // recoveryPW can be empty if volume is locked
+ if (rp) { drives[this.target].recoveryPassword = rp[0]; }
+ pending--;
+ resolver();
+ });
+ }
+ });
}
+ } catch (e) {
+ console.log('windows_volumes error: ' + (e && e.message ? e.message : e));
+ error = e; // add error, fall through to resolve below
}
+ done = true;
+ resolver(); // if not root, resolve with partial info, otherwise the last child process resolves
+ return (p1);
+}
+
+// Same as windows_volumes() but get the BitLocker recovery password through wmi. And win7 added
+function windows_volumes_wmi()
+{
+ var promise = require('promise');
+ var wmi = require('win-wmi-fixed');
+ const NS_VE = 'ROOT\\CIMV2\\Security\\MicrosoftVolumeEncryption';
+ var p1 = new promise(function (res, rej) { this._res = res; this._rej = rej; });
+ var drives = {};
+
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'
+ // use win32_volume, for future property expansion and link through DeviceID
+ wmi.query('ROOT\\CIMV2', 'SELECT DriveLetter,Label,FileSystem,Capacity,FreeSpace,DriveType FROM Win32_Volume', ['DriveLetter', 'Label', 'FileSystem', 'Capacity', 'FreeSpace', 'DriveType'], false, 5000)
+ .forEach(function (vol) {
+ if (!vol || vol['DriveLetter'] == null) { return; } // skip letterless volumes for now
+ var drive = vol['DriveLetter'].slice(0, -1);
+ drives[drive] = {
+ name: vol['Label'] || '',
+ type: vol['FileSystem'] || 'Unknown',
+ size: parseInt(vol['Capacity']) || 0,
+ sizeremaining: parseInt(vol['FreeSpace']) || 0,
+ dType: vol['DriveType'] || 0
};
- 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;
+ });
+
+ // The MicrosoftVolumeEncryption namespace is admin-only; skip entirely when not elevated, saves waiting on the wmi-query time-out if run as user
+ if (require('user-sessions').isRoot()) {
+ // Check win version: Win7=7600/7601, Win8+>=9200.
+ // Default to win8+ if unreadable.
+ var win8plus = true;
+ try {
+ var reg = require('win-registry');
+ var build = parseInt(reg.QueryKey(reg.HKEY.LocalMachine, 'Software\\Microsoft\\Windows NT\\CurrentVersion', 'CurrentBuildNumber'), 10);
+ win8plus = !(build > 0 && build < 9200);
+ } catch (ex) { }
+ // Win7 lacks the ConversionStatus & EncryptionMethod property, so only select it on win8+
+ var q = win8plus ? 'SELECT DeviceID,DriveLetter,ConversionStatus,ProtectionStatus,EncryptionMethod FROM Win32_EncryptableVolume' : 'SELECT DeviceID,DriveLetter,ProtectionStatus FROM Win32_EncryptableVolume';
+ var fields = win8plus ? ['DeviceID', 'DriveLetter', 'ConversionStatus', 'ProtectionStatus', 'EncryptionMethod'] : ['DeviceID', 'DriveLetter', 'ProtectionStatus'];
+ // re-use connection instead of creating a new one every time
+ var sess = wmi.connect(NS_VE);
+ try {
+ wmi.query(NS_VE, q, fields, false, 5000)
+ .forEach(function (vol) {
+ if (!vol || vol['DriveLetter'] == null) { return; } // shortcut letterless volumes
+ var drive = vol['DriveLetter'].slice(0, -1);
+ if (!drives[drive]) { return; } // no matching volume, skip for now. TODO match op DeviceID
+ try {
+ var v_id = { DeviceID: vol['DeviceID'] }; // DeviceID(=VolumeKeyProtectorID) is the key needed for the ExecMethod calls
+ drives[drive].protectionStatus = vol['ProtectionStatus'];
+ if (win8plus) {
+ drives[drive].encryptionMethod = vol['EncryptionMethod'];
+ drives[drive].volumeStatus = vol['ConversionStatus']; // property available from the query
+ } else {
+ var cs = sess.execMethod('Win32_EncryptableVolume', v_id, 'GetConversionStatus', null, 5000); // win7: no PrecisionFactor param, so no universal get
+ drives[drive].volumeStatus = (cs && cs.ReturnValue == 0) ? cs.ConversionStatus : 0;
+ cs = sess.execMethod('Win32_EncryptableVolume', v_id, 'GetEncryptionMethod', null, 5000);
+ drives[drive].encryptionMethod = (cs && cs.ReturnValue == 0) ? cs.EncryptionMethod : 0;
}
- }
- }
- 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
- }
+ // Only retrieve the recovery key on encrypted drives (conv 0 = FullyDecrypted, otherwise some sort of encryption)
+ if (drives[drive].volumeStatus != 0) {
+ // Get protectorKey (= identifier). For now only { KeyProtectorType: 3 } (=numerical password=recoverykey). Future possibilities: get all id's and statuses (TPM , Certificate etc)
+ // https://learn.microsoft.com/en-us/windows/win32/secprov/getkeyprotectors-win32-encryptablevolume
+ var protKeys = sess.execMethod('Win32_EncryptableVolume', v_id, 'GetKeyProtectors', { KeyProtectorType: 3 }, 5000);
+ if (protKeys && protKeys.ReturnValue == 0 && Array.isArray(protKeys.VolumeKeyProtectorID) && protKeys.VolumeKeyProtectorID.length > 0) {
+ drives[drive].identifier = protKeys.VolumeKeyProtectorID[0];
+ // Got an id, use it to retrieve the recoverykey
+ // https://learn.microsoft.com/en-us/windows/win32/secprov/getkeyprotectornumericalpassword-win32-encryptablevolume
+ var numPass = sess.execMethod('Win32_EncryptableVolume', v_id, 'GetKeyProtectorNumericalPassword', { VolumeKeyProtectorID: protKeys.VolumeKeyProtectorID[0] }, 5000);
+ // NumericalPassword is null if the volume is locked
+ if (numPass && numPass.ReturnValue == 0 && numPass.NumericalPassword) { drives[drive].recoveryPassword = numPass.NumericalPassword; }
+ }
+ }
+ } catch (ex) { console.log('win-volumes bitlocker error for ' + drive + ': ' + (ex && ex.message ? ex.message : ex)); } // skip this volume, carry on
+ });
+ } finally { sess.release(); }
}
- p1._res(ret);
- } catch (ex) { p1._res(ret); } // just return volumes as cant get encryption/bitlocker
+ p1._res({ error: null, drives: drives });
+ } catch (e) { console.log('windows_volumes error: ' + (e && e.message ? e.message : e)); p1._res({ error: e, drives: drives }); } // just return what we got
+
return (p1);
}
-module.exports = {
+module.exports = {
getVolumes: function () { try { return (getVolumes()); } catch (x) { return ({}); } },
- volumes_promise: windows_volumes
+ volumes_promise: windows_volumes_wmi
};
\ No newline at end of file
diff --git a/agents/modules_meshcore/win-wmi-fixed.js b/agents/modules_meshcore/win-wmi-fixed.js
index 60fc18ca00..5dd9592c75 100644
--- a/agents/modules_meshcore/win-wmi-fixed.js
+++ b/agents/modules_meshcore/win-wmi-fixed.js
@@ -16,15 +16,44 @@ limitations under the License.
var promise = require('promise');
var GM = require('_GenericMarshal');
+var COM = require('win-com');
+var sm = require('service-manager');
const CLSID_WbemAdministrativeLocator = '{CB8555CC-9128-11D1-AD9B-00C04FD8FDFF}';
const IID_WbemLocator = '{dc12a687-737f-11cf-884d-00aa004b2e24}';
+const WMI_FIELD = /^[A-Za-z_][A-Za-z0-9_]*$/;
const WBEM_FLAG_BIDIRECTIONAL = 0;
+const WBEM_FLAG_RETURN_IMMEDIATELY = 0x10;
+const WBEM_FLAG_FORWARD_ONLY = 0x20;
+const WBEM_FLAG_NONSYSTEM_ONLY = 0x40;
+const WBEM_FLAG_CONNECT_USE_MAX_WAIT = 0x80;
const WBEM_INFINITE = -1;
const WBEM_FLAG_ALWAYS = 0;
const E_NOINTERFACE = 0x80004002;
+const WBEM_S_NO_ERROR = 0;
+const WBEM_S_FALSE = 1;
+const WBEM_S_TIMEDOUT = 0x40004;
+const WBEM_STATUS_COMPLETE = 0;
+const WBEM_ERRORS = {
+ 0x80041001: 'Generic failure',
+ 0x80041002: 'Object not found',
+ 0x80041003: 'Access denied',
+ 0x80041004: 'Provider failure',
+ 0x80041006: 'Out of memory',
+ 0x80041008: 'Invalid parameter',
+ 0x80041009: 'Resource not available',
+ 0x8004100E: 'Invalid namespace',
+ 0x80041010: 'Invalid class',
+ 0x80041017: 'Invalid query',
+ 0x80041013: 'Provider not found',
+ 0x80041021: 'Invalid syntax',
+ 0x80070422: 'Service unavailable'
+};
+
var OleAut32 = GM.CreateNativeProxy('OleAut32.dll');
OleAut32.CreateMethod('SafeArrayAccessData');
-
+OleAut32.CreateMethod('SafeArrayUnaccessData');
+OleAut32.CreateMethod('SafeArrayDestroy');
+OleAut32.CreateMethod('VariantClear');
var wmi_handlers = {};
const LocatorFunctions = ['QueryInterface', 'AddRef', 'Release', 'ConnectToServer'];
@@ -77,6 +106,21 @@ const ResultsFunctions = [
'Skip'
];
+//
+// Reference to IWbemCallResult can be found at:
+// https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nn-wbemcli-iwbemcallresult
+// Used for semisynchronous calls (WBEM_FLAG_RETURN_IMMEDIATELY): poll GetResultObject() with a timeout.
+//
+const CallResultFunctions = [
+ 'QueryInterface',
+ 'AddRef',
+ 'Release',
+ 'GetResultObject',
+ 'GetResultString',
+ 'GetResultServices',
+ 'GetCallStatus'
+];
+
//
// Reference to IWbemClassObject can be found at:
// https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nn-wbemcli-iwbemclassobject
@@ -111,173 +155,272 @@ const ResultFunctions = [
'GetMethodOrigin'
];
-//
-// Reference to IWbemObjectSink can be found at:
// https://learn.microsoft.com/en-us/windows/win32/wmisdk/iwbemobjectsink
-//
+// A COM interface consists at least of QueryInterface, Addref and Release, which are called by the windows system to manage the COM object
+// Indicate gets resultsets pushed and SetStatus gets called when finished
+// When refCount hits 0, which normally happens after the SetStatus call through the system calling the Release function, clean everything up
const QueryAsyncHandler =
[
{
- cx: 10, parms: 3, name: 'QueryInterface', func: function (j, riid, ppv)
+ cx: 10, parms: 3, name: 'QueryInterface', func: function (sink, riid, ppv)
{
var ret = GM.CreateVariable(4);
- console.info1('QueryInterface', riid.Deref(0, 16).toBuffer().toString('hex'));
+ // console.log('QueryInterface', riid.Deref(0, 16).toBuffer().toString('hex'));
switch (riid.Deref(0, 16).toBuffer().toString('hex'))
{
case '0000000000000000C000000000000046': // IID_IUnknown
- j.pointerBuffer().copy(ppv.Deref(0, GM.PointerSize).toBuffer());
- ret.increment(0, true);
- //++this.p.refcount;
- console.info1('QueryInterface (IID_IUnknown)', this.refcount);
- break;
case '0178857C8173CF11884D00AA004B2E24': // IID_IWmiObjectSink
- j.pointerBuffer().copy(ppv.Deref(0, GM.PointerSize).toBuffer());
- ret.increment(0, true);
- //++this.p.refcount;
- console.info1('QueryInterface (IID_IWmiObjectSink)', this.refcount);
+ sink.pointerBuffer().copy(ppv.Deref(0, GM.PointerSize).toBuffer());
+ ret.increment(0, true); //return WBEM_S_NO_ERROR=0
+ ++this.refcount;
break;
default:
ret.increment(E_NOINTERFACE, true);
- console.info1(riid.Deref(0, 16).toBuffer().toString('hex'), 'returning E_NOINTERFACE');
break;
}
- return (ret);
+ return ret;
}
},
{
cx: 11, parms: 1, name: 'AddRef', func: function ()
{
- ++this.refcount;
- console.info1('AddRef', this.refcount);
- return (GM.CreateVariable(4));
+ // console.log('AddRef: ' + this.refcount);
+ return (GM.CreateVariable(4).increment(++this.refcount, true)); // return new refCount
}
},
{
cx: 12, parms: 1, name: 'Release', func: function ()
{
- --this.refcount;
- console.info1('Release', this.refcount);
- if (this.refcount == 0)
- {
- console.info1('No More References');
-
- this.cleanup();
- this.services.funcs.Release(this.services.Deref());
-
- this.services = null;
- this.p = null;
- if (this.callbackDispatched)
- {
- setImmediate(function (j) { j.locator = null; }, this);
- }
- else
- {
- this.locator = null;
- }
-
- console.info1('No More References [END]');
- }
- return (GM.CreateVariable(4));
+ // console.log('Release: ' + this.refcount);
+ if (--this.refcount === 0) { // console.log('Released');
+ destroy(this); }
+ return GM.CreateVariable(4).increment(this.refcount >>> 0, true); // return new refCount
}
},
{
- cx: 13, parms: 3, name: 'Indicate', func: function (j, count, arr)
+ cx: 13, parms: 3, name: 'Indicate', func: function (sink, count, arr)
{
- console.info1('Indicate', count.Val);
- var j, nme, len, nn;
-
+ // console.log('Indicate: ' + count.Val);
+ if (this._abandoned) { return (GM.CreateVariable(4).increment(0, true)); } // discard
+ if (this.sessionid) {
+ this.progress += count.Val;
+ var now = Date.now();
+ if ((now - this.lastProg) > 2000) { //wait at least 2 seconds per msg, otherwise it gets throttled and possibly lose the result msg
+ this.lastProg = now;
+ this.MA.SendCommand({ action: 'msg', type: 'console', value: 'Queryprogress: ' + this.progress + ' results', sessionid: this.sessionid }); }
+ }
for (var i = 0; i < count.Val; ++i)
{
- j = arr.Deref((i * GM.PointerSize) + 0, GM.PointerSize);
- this.results.push(enumerateProperties(j, this.fields));
+ var wmiResultObj = arr.Deref((i * GM.PointerSize) + 0, GM.PointerSize);
+ try {
+ if (this.reqFields != null) {
+ this.results.push(enumerateProperties(wmiResultObj, this.reqFields, this.fixedNameVars));
+ } else {
+ var c = this.cache.forRow(wmiResultObj);
+ this.results.push(enumerateProperties(wmiResultObj, c.propNames, c.propNameVars));
+ }
+ } catch (e) {
+ // only skip, not throw
+ console.log('win-wmi Indicate row error: ' + (e && e.message ? e.message : e));
+ }
}
-
- var ret = GM.CreateVariable(4);
- ret.increment(0, true);
- return (ret);
+ return GM.CreateVariable(4).increment(0, true); //Indicate must always return WBEM_S_NO_ERROR=0
}
},
{
- cx: 14, parms: 5, name: 'SetStatus', func: function (j, lFlags, hResult, strParam, pObjParam)
+ cx: 14, parms: 5, name: 'SetStatus', func: function (sink, lFlags, hResult, strParam, pObjParam)
{
- console.info1('SetStatus', hResult.Val);
-
- var ret = GM.CreateVariable(4);
- ret.increment(0, true);
-
- if (hResult.Val == 0)
- {
- this.p.resolve(this.results);
- }
- else
- {
- this.p.reject(hResult.Val);
+ // console.log('SetStatus ' + lFlags.Val);
+ var ret = GM.CreateVariable(4).increment(0, true); // SetStatus must always return WBEM_S_NO_ERROR=0
+ if (lFlags.Val !== WBEM_STATUS_COMPLETE) { return (ret); } // SetStatus can receive other messages, skip those
+
+ if (this._timer) { clearTimeout(this._timer); this._timer = null; } // Finished before the settimeout, remove timer
+ var self = this;
+ setImmediate(function () { if (--self.refcount === 0) { destroy(self); } }); // decrement self set refCount of 1 at init
+ if (this._abandoned) { return (ret); } // timeout settled the promise
+ this._abandoned = true; // Signal timeout function
+ if (hResult.Val == 0) { // Everything ok, msg & resolve
+ if (this.sessionid) {
+ this.MA.SendCommand({ action: 'msg', type: 'console', value: 'Querytotal: ' + this.results.length + ' results, time take: ' + (Date.now()-this.progStart)/1000 + ' seconds', sessionid: this.sessionid });
+ }
+ this.p.resolve(this.results); }
+ else {
+ var e = new Error('WMI async query error: ' + (WBEM_ERRORS[hResult.Val>>>0] || 'unknown') + ' (0x' + (hResult.Val>>>0).toString(16) + ')');
+ if (this.results.length > 0) { e.results = this.results; }
+ this.p.reject(e);
}
return (ret);
}
}
];
+function destroy(h) {
+ // console.log('Destroy oh boy!');
+ if (h.cleanup) { h.cleanup(); }
+ h.p = null;
+ delete wmi_handlers[h._hashCode()];
+ h.services = releaseCOM(h.services, true);
+
+ if (h.callbackDispatched) {
+ setImmediate(function () { h.locator = releaseCOM(h.locator, false); });
+ } else {
+ h.locator = releaseCOM(h.locator, false);
+ }
+}
+
+function releaseCOM(obj, deref) {
+ if (obj && obj.funcs) {
+ try { obj.funcs.Release(deref ? obj.Deref() : obj); }
+ catch (e) { console.log('WMI: releaseCOM error = ' + (e && e.message ? e.message : e)); }
+ }
+ return null;
+}
-function enumerateProperties(j, fields)
-{
- //
- // Reference to SafeArrayAccessData() can be found at:
- // https://learn.microsoft.com/en-us/windows/win32/api/oleauto/nf-oleauto-safearrayaccessdata
- //
+function extractFields(queryString) {
+ if (typeof queryString !== 'string') { return null; }
- var nme, len, nn;
- var properties = [];
- var values = {};
+ // get between 'select' and 'from'
+ var props = /^\s*SELECT\s+(.+?)\s+FROM\s/i.exec(queryString);
+ if (props == null) { return null; }
- j.funcs = require('win-com').marshalFunctions(j.Deref(), ResultFunctions);
+ var list = props[1].trim();
+ if (list === '*') { return null; }
- // First we need to enumerate the COM Array
- if (fields != null && Array.isArray(fields))
- {
- properties = fields;
+ var parts = list.split(',');
+ var fields = [];
+ for (var i = 0; i < parts.length; ++i) {
+ var name = parts[i].trim();
+ // check if wmi field, otherwise exit, fallback to GetNames in enum
+ if (!WMI_FIELD.test(name)) { return null; }
+ fields.push(name);
}
- else
- {
- nme = GM.CreatePointer();
- j.funcs.GetNames(j.Deref(), 0, WBEM_FLAG_ALWAYS, 0, nme);
- len = nme.Deref().Deref(GM.PointerSize == 8 ? 24 : 16, 4).toBuffer().readUInt32LE();
- nn = GM.CreatePointer();
- OleAut32.SafeArrayAccessData(nme.Deref(), nn);
+ return (fields.length > 0 ? fields : null);
+}
+
+// Optimze query: If a 'select * from' query has the 'fields' argument, rewrite query to replace the * with fields argument
+// Except for event/notification queries (WITHIN or FROM __)
+// This gives a significant perfomance boost as enumerateProperties is very costly if it needs to get all properties every row
+// Always try to do: queryString='select [field1][,field2][...] from [class]', fields=['field1','field2',...]
+// Keeping the fields argument filled helps with the enumerateProperties function not getting all the names every row.
+// And the reverse, if there are fields between 'select' and 'from', extract them and return it as the fields argument
+function prepareQuery (queryString, fields) {
+ if (typeof(queryString) !=='string' || queryString.trim().length === 0) { throw new Error('No querystring'); }
+ // Always check if wmi service is running
+ var s = sm.manager.getService('winmgmt');
+ if (!s.isRunning()) { throw new Error('WMI service not running'); }
+ if (!Array.isArray(fields) || fields.length === 0) {
+ fields = extractFields(queryString); }
+ else {
+ for (var i = 0; i < fields.length; ++i) {
+ if (typeof fields[i] !== 'string' || !WMI_FIELD.test(fields[i])) {
+ throw new Error('win-wmi: invalid field name: ' + fields[i]);
+ }
+ }
+ //skip 'within' clause and system event queries
+ if ( !(/\bWITHIN\b/i.test(queryString) || /\bFROM\s+__/i.test(queryString))) {
+ queryString = queryString.replace(/^(\s*SELECT\s+)\*(\s+FROM\s)/i, '$1' + fields.join(',') + '$2');
+ }
+ }
+ // console.info1('WMI: prepared query = ' + queryString + ' fields: ' + fields);
+ return { q: queryString, f: fields };
+}
+// get all property names for a wmi result
+function getAllNames(wmiResultObj, includeSysProp)
+{
+ if (!wmiResultObj.funcs) { wmiResultObj.funcs = COM.marshalFunctions(wmiResultObj.Deref(), ResultFunctions); }
+
+ var saNames = GM.CreatePointer();
+ // https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemclassobject-getnames
+ var res = (wmiResultObj.funcs.GetNames(wmiResultObj.Deref(), 0, (includeSysProp ? WBEM_FLAG_ALWAYS : WBEM_FLAG_NONSYSTEM_ONLY), 0, saNames));
+ if (res.Val != 0) { return []; }
+ var len = saNames.Deref().Deref(GM.PointerSize == 8 ? 24 : 16, 4).toBuffer().readUInt32LE();
+ var pNamesArray = GM.CreatePointer();
+ OleAut32.SafeArrayAccessData(saNames.Deref(), pNamesArray);
+
+ var propNames = [];
+ for (var i = 0; i < len; ++i) {
+ var propName = pNamesArray.Deref().increment(i * GM.PointerSize).Deref().Wide2UTF8;
+ if (propName.length === 0) { continue; }
+ propNames.push(propName);
+ }
+ OleAut32.SafeArrayUnaccessData(saNames.Deref());
+ OleAut32.SafeArrayDestroy(saNames.Deref()); // saNames is caller-owned
+ // console.info1('WMI: getAllNames(propNames) = ' + JSON.stringify(propNames));
+ return propNames;
+}
- for (var i = 0; i < len; ++i)
- {
- var propName = nn.Deref().increment(i * GM.PointerSize).Deref().Wide2UTF8;
- if (propName.length === 0) { continue; }
- properties.push(propName);
+function NameCache(includeSysProp)
+{
+ this.includeSysProp = (typeof(includeSysProp) == 'boolean') ? includeSysProp : false;
+ this.byClass = {};
+ this._classNameVar = GM.CreateVariable('__CLASS', { wide: true }); // reused every row
+}
+
+NameCache.prototype.forRow = function (wmiResultObj)
+{
+ if (!wmiResultObj.funcs) { wmiResultObj.funcs = COM.marshalFunctions(wmiResultObj.Deref(), ResultFunctions); }
+
+ // Read __CLASS (system property, always a BSTR). Falls back to '' if absent.
+ var key = '';
+ var val = GM.CreateVariable(24);
+ try {
+ if (wmiResultObj.funcs.Get(wmiResultObj.Deref(), this._classNameVar, 0, val, 0, 0).Val == 0 && val.toBuffer().readUInt16LE() == 0x0008) {
+ key = val.Deref(8, GM.PointerSize).Deref().Wide2UTF8;
}
+ } finally {
+ OleAut32.VariantClear(val);
}
+
+ var entry = this.byClass[key];
+ if (!entry) {
+ var propNames = getAllNames(wmiResultObj, this.includeSysProp);
+ var propNameVars = [];
+ for (var n = 0; n < propNames.length; ++n) {
+ propNameVars.push(GM.CreateVariable(propNames[n], { wide: true }));
+ }
+ entry = this.byClass[key] = { propNames: propNames, propNameVars: propNameVars };
+ }
+ return entry;
+};
+
+function enumerateProperties(wmiResultObj, propNames, propNameVars)
+{
+ //
+ // Reference to SafeArrayAccessData() can be found at:
+ // https://learn.microsoft.com/en-us/windows/win32/api/oleauto/nf-oleauto-safearrayaccessdata
+ //
+
+ var values = {};
+ if (!wmiResultObj.funcs) { wmiResultObj.funcs = COM.marshalFunctions(wmiResultObj.Deref(), ResultFunctions); }
// Now we need to introspect the Array Fields
- for (var i = 0; i < properties.length; ++i)
+ var propVal = GM.CreateVariable(24);
+ for (var i = 0; i < propNames.length; ++i)
{
- var tmp1 = GM.CreateVariable(24);
- if (j.funcs.Get(j.Deref(), GM.CreateVariable(properties[i], { wide: true }), 0, tmp1, 0, 0).Val == 0)
+ if (wmiResultObj.funcs.Get(wmiResultObj.Deref(), propNameVars[i], 0, propVal, 0, 0).Val == 0)
{
//
// Reference for IWbemClassObject::Get() can be found at:
// https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemclassobject-get
//
- var vartype = tmp1.toBuffer().readUInt16LE();
+ var vartype = propVal.toBuffer().readUInt16LE();
var isArray = (vartype & 0x2000) != 0; // VT_ARRAY flag
var baseType = vartype & 0x0FFF;
if (isArray)
{
// Handle array types (VT_ARRAY | base type)
- var safeArray = tmp1.Deref(8, GM.PointerSize).Deref();
+ var safeArray = propVal.Deref(8, GM.PointerSize).Deref();
var arrayLength = safeArray.Deref(GM.PointerSize == 8 ? 24 : 16, 4).toBuffer().readUInt32LE();
var arrayData = GM.CreatePointer();
OleAut32.SafeArrayAccessData(safeArray, arrayData);
+ // VT_I8 & VT_UI8 (64-bit types) aren't used (yet?) in wmi, added for completeness
+ // added float type VT_R4 and VT_R8, uncommon but used
+ // VT_DECIMAL not mapped in wmi
var arrayValues = [];
for (var k = 0; k < arrayLength; ++k)
{
@@ -290,9 +433,20 @@ function enumerateProperties(j, fields)
case 0x0016: // VT_INT
arrayValues.push(arrayData.Deref().Deref(k * 4, 4).toBuffer().readInt32LE());
break;
+ case 0x0004: // VT_R4
+ arrayValues.push(arrayData.Deref().Deref(k * 4, 4).toBuffer().readFloatLE(0));
+ break;
+ case 0x0005: // VT_R8
+ arrayValues.push(arrayData.Deref().Deref(k * 8, 8).toBuffer().readDoubleLE(0));
+ break;
+ case 0x0008: // VT_BSTR
+ arrayValues.push(arrayData.Deref().Deref(k * GM.PointerSize, GM.PointerSize).Deref().Wide2UTF8);
+ break;
case 0x000B: // VT_BOOL
arrayValues.push(arrayData.Deref().Deref(k * 2, 2).toBuffer().readInt16LE() != 0);
break;
+ // case 0x000E: //VT_DECIMAL
+ // break;
case 0x0010: // VT_I1
arrayValues.push(arrayData.Deref().Deref(k, 1).toBuffer().readInt8());
break;
@@ -306,12 +460,24 @@ function enumerateProperties(j, fields)
case 0x0017: // VT_UINT
arrayValues.push(arrayData.Deref().Deref(k * 4, 4).toBuffer().readUInt32LE());
break;
- case 0x0008: // VT_BSTR
- arrayValues.push(arrayData.Deref().Deref(k * GM.PointerSize, GM.PointerSize).Deref().Wide2UTF8);
+/*
+ case 0x0014: // VT_I8 (signed 64-bit)
+ var ai8 = arrayData.Deref().Deref(k * 8, 8).toBuffer();
+ // recombine the two 32-bit halves; exact up to 2^53 (high word signed)
+ arrayValues.push(ai8.readInt32LE(4) * 0x100000000 + ai8.readUInt32LE(0));
+ break;
+ case 0x0015: // VT_UI8 (unsigned 64-bit)
+ var aui8 = arrayData.Deref().Deref(k * 8, 8).toBuffer();
+ arrayValues.push(aui8.readUInt32LE(4) * 0x100000000 + aui8.readUInt32LE(0));
+ break;
+*/
+ default:
+ console.info1('VARTYPE (array element): 0x' + baseType.toString(16));
break;
}
}
- values[properties[i]] = arrayValues;
+ OleAut32.SafeArrayUnaccessData(safeArray);
+ values[propNames[i]] = arrayValues;
}
else
{
@@ -320,116 +486,461 @@ function enumerateProperties(j, fields)
{
case 0x0000: // VT_EMPTY
case 0x0001: // VT_NULL
- values[properties[i]] = null;
+ values[propNames[i]] = null;
break;
case 0x0002: // VT_I2
- values[properties[i]] = tmp1.Deref(8, GM.PointerSize).toBuffer().readInt16LE();
+ values[propNames[i]] = propVal.Deref(8, GM.PointerSize).toBuffer().readInt16LE();
break;
case 0x0003: // VT_I4
case 0x0016: // VT_INT
- values[properties[i]] = tmp1.Deref(8, GM.PointerSize).toBuffer().readInt32LE();
+ values[propNames[i]] = propVal.Deref(8, GM.PointerSize).toBuffer().readInt32LE();
break;
- case 0x000B: // VT_BOOL
- values[properties[i]] = tmp1.Deref(8, GM.PointerSize).toBuffer().readInt32LE() != 0;
+ case 0x0004: // VT_R4
+ values[propNames[i]] = propVal.Deref(8, 4).toBuffer().readFloatLE(0);
break;
- case 0x000E: // VT_DECIMAL
+ case 0x0005: // VT_R8
+ values[propNames[i]] = propVal.Deref(8, 8).toBuffer().readDoubleLE(0);
+ break;
+ case 0x0008: // VT_BSTR
+ values[propNames[i]] = propVal.Deref(8, GM.PointerSize).Deref().Wide2UTF8;
+ break;
+ case 0x000B: // VT_BOOL
+ values[propNames[i]] = propVal.Deref(8, GM.PointerSize).toBuffer().readInt32LE() != 0;
break;
+ // case 0x000E: // VT_DECIMAL
+ // break;
case 0x0010: // VT_I1
- values[properties[i]] = tmp1.Deref(8, GM.PointerSize).toBuffer().readInt8();
+ values[propNames[i]] = propVal.Deref(8, GM.PointerSize).toBuffer().readInt8();
break;
case 0x0011: // VT_UI1
- values[properties[i]] = tmp1.Deref(8, GM.PointerSize).toBuffer().readUInt8();
+ values[propNames[i]] = propVal.Deref(8, GM.PointerSize).toBuffer().readUInt8();
break;
case 0x0012: // VT_UI2
- values[properties[i]] = tmp1.Deref(8, GM.PointerSize).toBuffer().readUInt16LE();
+ values[propNames[i]] = propVal.Deref(8, GM.PointerSize).toBuffer().readUInt16LE();
break;
case 0x0013: // VT_UI4
case 0x0017: // VT_UINT
- values[properties[i]] = tmp1.Deref(8, GM.PointerSize).toBuffer().readUInt32LE();
+ values[propNames[i]] = propVal.Deref(8, GM.PointerSize).toBuffer().readUInt32LE();
break;
- //case 0x0014: // VT_I8
- // break;
- //case 0x0015: // VT_UI8
- // break;
- case 0x0008: // VT_BSTR
- values[properties[i]] = tmp1.Deref(8, GM.PointerSize).Deref().Wide2UTF8;
+/*
+ case 0x0014: // VT_I8
+ var i8 = propVal.Deref(8, 8).toBuffer();
+ // recombine the two 32-bit halves; exact up to 2^53 (high word is signed)
+ values[propNames[i]] = i8.readInt32LE(4) * 0x100000000 + i8.readUInt32LE(0);
+ break;
+ case 0x0015: // VT_UI8
+ var ui8 = propVal.Deref(8, 8).toBuffer();
+ values[propNames[i]] = ui8.readUInt32LE(4) * 0x100000000 + ui8.readUInt32LE(0);
break;
+*/
default:
- console.info1('VARTYPE: ' + vartype);
+ console.info1('VARTYPE: 0x' + vartype.toString(16));
break;
}
}
}
+ OleAut32.VariantClear(propVal);
}
-
return (values);
}
-function queryAsync(resourceString, queryString, fields)
+
+// (optional) includeSysProp, default=false. Include system properties (__CLASS, etc.)
+// (optional) timeout, default=120 seconds. Query duration timeout in ms
+// (optional) sessionid, default=null. Report progress back to the given sessionid.
+function queryAsync(resourceString, queryString, fields, includeSysProp, timeout, sessionid)
{
- var p = new promise(require('promise').defaultInit);
- var resource = GM.CreateVariable(resourceString, { wide: true });
- var language = GM.CreateVariable("WQL", { wide: true });
- var query = GM.CreateVariable(queryString, { wide: true });
- var results = GM.CreatePointer();
-
- // Setup the Async COM handler for QueryAsync()
- var handlers = require('win-com').marshalInterface(QueryAsyncHandler);
- handlers.refcount = 1;
- handlers.results = [];
- handlers.fields = fields;
- handlers.locator = require('win-com').createInstance(require('win-com').CLSIDFromString(CLSID_WbemAdministrativeLocator), require('win-com').IID_IUnknown);
- handlers.locator.funcs = require('win-com').marshalFunctions(handlers.locator, LocatorFunctions);
-
- handlers.services = require('_GenericMarshal').CreatePointer();
- if (handlers.locator.funcs.ConnectToServer(handlers.locator, resource, 0, 0, 0, 0, 0, 0, handlers.services).Val != 0) { throw ('Error calling ConnectToService'); }
-
- handlers.services.funcs = require('win-com').marshalFunctions(handlers.services.Deref(), ServiceFunctions);
- handlers.p = p;
-
- // Make the COM call
- if (handlers.services.funcs.ExecQueryAsync(handlers.services.Deref(), language, query, WBEM_FLAG_BIDIRECTIONAL, 0, handlers).Val != 0)
- {
- throw ('Error in Query');
+ var queryStarted = false;
+ var handlers = null;
+ try {
+ var p = new promise(promise.defaultInit);
+ //32-bit windows cannot do more than 1 async query at a time because of the hardcoded vtable for the cx pre-compiled __stdcall custom handlers in iLibDuktape_GenericMarshal.c
+ if (GM.PointerSize == 4 && Object.keys(wmi_handlers).length != 0) {
+ setImmediate(function(){ p.reject(new Error('Another AsyncQuery is already running, only one AsyncQuery possible at a time on 32-bit Windows')); }); return (p); }
+ if (!timeout || typeof timeout !== 'number') { timeout = 120 * 1000 }; //Default timeout set to 2m
+ var pq = prepareQuery(queryString, fields);
+ queryString = pq.q;
+ var reqFields = pq.f;
+ var resource = GM.CreateVariable(resourceString, { wide: true });
+ var language = GM.CreateVariable("WQL", { wide: true });
+ var query = GM.CreateVariable(queryString, { wide: true });
+
+ // Setup the Async COM handler for QueryAsync()
+ handlers = COM.marshalInterface(QueryAsyncHandler);
+ handlers.refcount = 1;
+ handlers.results = [];
+ handlers.reqFields = reqFields;
+ handlers.query = resourceString + ':' + queryString;
+ handlers._timer = null;
+ handlers._abandoned = false;
+ if (reqFields != null && Array.isArray(reqFields)) {
+ handlers.fixedNameVars = reqFields.map(function (f) { return GM.CreateVariable(f, { wide: true }); });
+ } else {
+ handlers.cache = new NameCache(includeSysProp);
+ }
+ if (sessionid) { handlers.sessionid = sessionid; handlers.progStart = Date.now(); handlers.progress = 0; handlers.lastProg = 0; handlers.MA = require('MeshAgent'); }
+ handlers.locator = COM.createInstance(COM.CLSIDFromString(CLSID_WbemAdministrativeLocator), COM.IID_IUnknown);
+ handlers.locator.funcs = COM.marshalFunctions(handlers.locator, LocatorFunctions);
+
+ handlers.services = GM.CreatePointer();
+
+ // https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemlocator-connectserver
+ // WBEM_FLAG_CONNECT_USE_MAX_WAIT=wait max 2 minutes instead of infinite. Prevents blocking.
+ var hr = handlers.locator.funcs.ConnectToServer(handlers.locator, resource, 0, 0, 0, WBEM_FLAG_CONNECT_USE_MAX_WAIT, 0, 0, handlers.services).Val >>> 0;
+ if (hr != 0) {
+ throw (new Error('ConnectToServer(' + resourceString + ') failed: ' + (WBEM_ERRORS[hr] || 'unknown') + ' (0x' + hr.toString(16) + ')')); }
+
+ handlers.services.funcs = COM.marshalFunctions(handlers.services.Deref(), ServiceFunctions);
+ handlers.p = p;
+
+ // Make the COM call
+ var execRes = handlers.services.funcs.ExecQueryAsync(handlers.services.Deref(), language, query, WBEM_FLAG_BIDIRECTIONAL, 0, handlers).Val >>> 0;
+ if (execRes !== 0) {
+ throw new Error('ExecQueryAsync(' + handlers.query + ') failed: ' + (WBEM_ERRORS[execRes] || 'unknown') + ' (0x' + execRes.toString(16) + ')'); }
+ queryStarted = true;
+ // Hold a reference to the callback object
+ wmi_handlers[handlers._hashCode()] = handlers;
+ handlers._timer = setTimeout(function () {
+ handlers._timer = null;
+ if (handlers._abandoned) { return; } //Signal from SetStatus, it settled
+ // WMI CancelAsyncCall causes a fatal exception. Only way to cancel is through abandonment.
+ // Reject the promise and clean that part up, hoping the wmi query resolves eventually and cleans itself up through SetStatus
+ // There seems to be not other way to stop a hung/long query
+ handlers._abandoned = true; // Signal SetStatus we'll reject
+ var e = new Error('WMI query timed out after ' + (timeout/1000) + ' seconds');
+ if (handlers.results.length > 0) { e.results = handlers.results; }
+ handlers.p.reject(e);
+ }, timeout);
+ } catch (e) {
+ console.log('win-wmi queryAsync error: ' + e.message);
+ if (!queryStarted && handlers) {
+ handlers.refcount = 0;
+ destroy(handlers);
+ }
+ p.reject(e);
}
-
- // Hold a reference to the callback object
- wmi_handlers[handlers._hashCode()] = handlers;
return (p);
}
-function query(resourceString, queryString, fields)
+
+// (optional) includeSysProp, default=false. Include system properties (__CLASS, etc.)
+// (optional) timeout, default=120 seconds. Query duration timeout in ms
+// (optional) sessionid, default=null. Report progress back to the given sessionid.
+function query(resourceString, queryString, fields, includeSysProp, timeout, sessionid)
{
- var resource = GM.CreateVariable(resourceString, { wide: true });
- var language = GM.CreateVariable("WQL", { wide: true });
- var query = GM.CreateVariable(queryString, { wide: true });
- var results = GM.CreatePointer();
-
- // Connect the locator connection for WMI
- var locator = require('win-com').createInstance(require('win-com').CLSIDFromString(CLSID_WbemAdministrativeLocator), require('win-com').IID_IUnknown);
- locator.funcs = require('win-com').marshalFunctions(locator, LocatorFunctions);
- var services = require('_GenericMarshal').CreatePointer();
- if (locator.funcs.ConnectToServer(locator, resource, 0, 0, 0, 0, 0, 0, services).Val != 0) { throw ('Error calling ConnectToService'); }
-
- // Execute the Query
- services.funcs = require('win-com').marshalFunctions(services.Deref(), ServiceFunctions);
- if (services.funcs.ExecQuery(services.Deref(), language, query, WBEM_FLAG_BIDIRECTIONAL, 0, results).Val != 0) { throw ('Error in Query'); }
-
- results.funcs = require('win-com').marshalFunctions(results.Deref(), ResultsFunctions);
- var returnedCount = GM.CreateVariable(8);
- var result = GM.CreatePointer();
+ const uCount = 32; // number of rows to retrieve per round
var ret = [];
+ try {
+ if (!timeout || typeof timeout !== 'number') { timeout = 120 * 1000 }; //Default timeout set to 2m
+ var pq = prepareQuery(queryString, fields);
+ queryString = pq.q;
+ var reqFields = pq.f;
+ var fixedNameVars = null;
+ var cache = null;
+ if (reqFields != null && Array.isArray(reqFields)) {
+ fixedNameVars = reqFields.map(function (f) { return GM.CreateVariable(f, { wide: true }); });
+ } else {
+ cache = new NameCache(includeSysProp);
+ }
+ var resource = GM.CreateVariable(resourceString, { wide: true });
+ var language = GM.CreateVariable("WQL", { wide: true });
+ var query = GM.CreateVariable(queryString, { wide: true });
+ var results = GM.CreatePointer();
+ if (sessionid) { var MA = require('MeshAgent'), progress = 0, lastProg = 0 }
+ // Connect the locator connection for WMI
+ var locator = COM.createInstance(COM.CLSIDFromString(CLSID_WbemAdministrativeLocator), COM.IID_IUnknown);
+ locator.funcs = COM.marshalFunctions(locator, LocatorFunctions);
+ var services = GM.CreatePointer();
+
+ // https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemlocator-connectserver
+ // WBEM_FLAG_CONNECT_USE_MAX_WAIT=wait max 2 minutes instead of infinite. Prevents blocking.
+ var hr = locator.funcs.ConnectToServer(locator, resource, 0, 0, 0, WBEM_FLAG_CONNECT_USE_MAX_WAIT, 0, 0, services).Val >>> 0;
+ if (hr != 0) {
+ throw new Error('ConnectToServer(' + resourceString + ') failed: ' + (WBEM_ERRORS[hr] || 'unknown') + ' (0x' + hr.toString(16) + ')'); }
+
+ // Execute the Query
+ // FORWARD_ONLY & RETURN_IMMEDIATELY instead of BIDIRECTIONAL, faster and less memory. https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemservices-execquery
+ services.funcs = COM.marshalFunctions(services.Deref(), ServiceFunctions);
+ if ((hr = services.funcs.ExecQuery(services.Deref(), language, query, WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, 0, results).Val >>> 0) != 0) {
+ throw new Error('ExecQuery() failed: ' + (WBEM_ERRORS[hr] || 'unknown') + ' (0x' + hr.toString(16) + ')'); }
+
+ // https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-ienumwbemclassobject-next
+ // Batch retrieve per round instead of the original 1. Improves speed on larger sets as it lowers COM calls, like the asyncHandler does
+ // nextRes=WBEM_S_FALSE: signals the last set. Check returnedCount if rows left to process
+ // nextRes=WBEM_S_NO_ERROR: returnedCount equal to requested number
+ results.funcs = COM.marshalFunctions(results.Deref(), ResultsFunctions);
+ var apObjects = GM.CreateVariable(uCount * GM.PointerSize);
+ var returnedCount = GM.CreateVariable(4); // ULONG puReturned
+ var retries = 0, rowTimeout = 10 * 1000; // rowTimeout was WBEM_INFINITE. in ms. Prevents blocking. Generates WBEM_S_TIMEDOUT on timeout
+ var progStart = Date.now();
+ while (true) {
+ if ((Date.now()-progStart) > timeout) { throw new Error('WMI query timed out after ' + (timeout/1000) + ' seconds'); }
+ var nextRes = results.funcs.Next(results.Deref(), rowTimeout, uCount, apObjects, returnedCount).Val >>> 0;
+ if (nextRes >= 0x80000000) { throw new Error('Next() failed: ' + (WBEM_ERRORS[nextRes] || 'unknown') + ' (0x' + nextRes.toString(16) + ')'); }
+ var returnedRows = (nextRes === WBEM_S_NO_ERROR) ? uCount : returnedCount.toBuffer().readUInt32LE(); // Only use returnedCount on last set, as WBEM_S_NO_ERROR implies a full batch
+
+ if (returnedRows === 0) { // handle special case, no rows returned
+ if (nextRes === WBEM_S_FALSE) { break; } // Done. Exit.
+ if (++retries > 6) { throw new Error('Next() errored too many times: ' + (WBEM_ERRORS[nextRes] || 'unknown') + ' (0x' + nextRes.toString(16) + ')'); }
+ if (sessionid) { MA.SendCommand({ action: 'msg', type: 'console', value: 'Queryprogress: issue ' + (WBEM_ERRORS[nextRes] || 'unknown') + ' (0x' + nextRes.toString(16) + ') after ' + progress + ' results', sessionid: sessionid }); }
+ continue; // timed out, retry
+ }
+ retries = 0;
+
+ for (var row = 0; row < returnedRows; ++row) {
+ var rowObj = apObjects.Deref(row * GM.PointerSize, GM.PointerSize);
+ rowObj.funcs = COM.marshalFunctions(rowObj.Deref(), ResultFunctions);
+ try {
+ if (reqFields != null) { ret.push(enumerateProperties(rowObj, reqFields, fixedNameVars)); }
+ else { var c = cache.forRow(rowObj); ret.push(enumerateProperties(rowObj, c.propNames, c.propNameVars)); }
+ } catch (rowErr) {
+ console.log('win-wmi query row error: ' + (rowErr && rowErr.message ? rowErr.message : rowErr));
+ } finally {
+ rowObj.funcs.Release(rowObj.Deref());
+ }
+ }
- // Enumerate the results
- while (results.funcs.Next(results.Deref(), WBEM_INFINITE, 1, result, returnedCount).Val == 0)
- {
- ret.push(enumerateProperties(result, fields));
+ if (sessionid) {
+ progress += returnedRows;
+ var now = Date.now();
+ if ((now - lastProg) > 2000) {
+ lastProg = now;
+ MA.SendCommand({ action: 'msg', type: 'console', value: 'Queryprogress: ' + progress + ' results', sessionid: sessionid });
+ }
+ }
+
+ if (nextRes === WBEM_S_FALSE) { break; } // last (partial) batch done. Exit
+ }
+ } catch (e) {
+ console.log('win-wmi query error: ' + e.message);
+ if (ret.length > 0) { e.results = ret; }
+ throw (e);
+ } finally {
+ results = releaseCOM(results, true);
+ services = releaseCOM(services, true);
+ locator = releaseCOM(locator, false);
+ }
+ // console.log(JSON.stringify(ret).substring(0,200));
+ if (sessionid) {
+ MA.SendCommand({ action: 'msg', type: 'console', value: 'Querytotal: ' + ret.length + ' results, time take: ' + (Date.now()-progStart)/1000 + ' seconds', sessionid: sessionid }); }
+ return (ret);
+}
+
+// Helperfunction to avoid annoying backslash/double quote escapes
+function buildEscapePath(className, keys) {
+ if (!keys || Object.keys(keys).length === 0) { return className; }
+ var parts = [];
+ for (var k in keys) {
+ var val = keys[k];
+ // Numeric keys are written as is, the rest gets an escape
+ if (typeof val === 'number') { parts.push(k + '=' + val); }
+ else { parts.push(k + '="' + ('' + val).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"'); }
}
+ return className + '.' + parts.join(',');
+}
- results.funcs.Release(results.Deref());
- services.funcs.Release(services.Deref());
- locator.funcs.Release(locator);
+// Write a single input value into a VARIANT buffer (vbuf = the variant's toBuffer() view).
+// !!! Returns a GM variable that must be KEPT ALIVE until AFTER the COM call (for VT_BSTR only, whose pointer lives in the variant),
+// This is because the VARIANT does not contain the string bytes. It contains an 8-byte pointer to bstr's separately-allocated native memory.
+// The VARIANT and the string buffer are two different allocations, so the caller must keep a reference until after the COM call.
+// This prevents the GC from cleaning up the buffer allocation until we're done.
+function writeInParamVariant(vbuf, val) {
+ var type = null, value = val;
+ if (val !== null && typeof val === 'object' && val.type != null) { type = ('' + val.type).toLowerCase(); value = val.value; }
+ if (type === null) {
+ if (val === null || typeof val === 'undefined') { type = 'null'; }
+ else if (typeof val === 'boolean') { type = 'bool'; }
+ else if (typeof val === 'string') { type = 'bstr'; }
+ else if (typeof val === 'number') { type = (Math.floor(value) === value) ? 'i4' : 'r8'; } // integer -> VT_I4, fractional -> VT_R8
+ else { throw new Error('win-wmi: unsupported inParam value: ' + JSON.stringify(val)); }
+ }
+ switch (type) {
+ case 'empty': vbuf.writeUInt16LE(0x0000, 0); break; // VT_EMPTY
+ case 'null': vbuf.writeUInt16LE(0x0001, 0); break; // VT_NULL
+ case 'i2': case 'int16': case 'short':vbuf.writeUInt16LE(0x0002, 0); vbuf.writeInt16LE(value | 0, 8); break; // VT_I2
+ case 'i4': case 'int32': case 'int': vbuf.writeUInt16LE(0x0003, 0); vbuf.writeInt32LE(value | 0, 8); break; // VT_I4
+ case 'r4': case 'float': vbuf.writeUInt16LE(0x0004, 0); vbuf.writeFloatLE(+value, 8); break; // VT_R4
+ case 'r8': case 'double': vbuf.writeUInt16LE(0x0005, 0); vbuf.writeDoubleLE(+value, 8); break; // VT_R8
+ case 'bool': case 'boolean': vbuf.writeUInt16LE(0x000B, 0); vbuf.writeInt16LE(value ? -1 : 0, 8); break; // VT_BOOL (-1 = true)
+ case 'i1': case 'int8': case 'sbyte': vbuf.writeUInt16LE(0x0010, 0); vbuf.writeInt8(value | 0, 8); break; // VT_I1
+ case 'ui1': case 'uint8': case 'byte': vbuf.writeUInt16LE(0x0011, 0); vbuf.writeUInt8(value & 0xFF, 8);break; // VT_UI1
+ case 'ui2': case 'uint16': vbuf.writeUInt16LE(0x0012, 0); vbuf.writeUInt16LE(value & 0xFFFF, 8); break; // VT_UI2
+ case 'ui4': case 'uint32': case 'uint': vbuf.writeUInt16LE(0x0013, 0); vbuf.writeUInt32LE(value >>> 0, 8); break; // VT_UI4
+ case 'bstr': case 'string': // VT_BSTR
+ var bstr = GM.CreateVariable('' + value, { wide: true });
+ vbuf.writeUInt16LE(0x0008, 0);
+ bstr.pointerBuffer().copy(vbuf, 8); // pointer to wide string at union offset 8
+ return bstr; // caller must keep this alive so the GC won't reclaim
+ default: throw new Error('win-wmi: unsupported inParam VARIANT type: ' + type);
+ }
+ return null; // directly written, no need to keep alive
+}
- return (ret);
+// Open a reusable connection to a WMI namespace, so multiple ExecMethod calls can share a single
+// IWbemServices and skip the per-call ConnectToServer (the expensive DCOM/RPC handshake). Caller MUST
+// call release() when done.
+// var sess = wmi.connect(ns);
+// try { sess.execMethod(className, keys, method, inParams, timeout); ... } finally { sess.release(); }
+function wmiConnect(resourceString)
+{
+ var s = sm.manager.getService('winmgmt');
+ if (!s.isRunning()) { throw new Error('WMI service not running'); }
+
+ var locator = COM.createInstance(COM.CLSIDFromString(CLSID_WbemAdministrativeLocator), COM.IID_IUnknown);
+ locator.funcs = COM.marshalFunctions(locator, LocatorFunctions);
+ var services = GM.CreatePointer();
+ var resource = GM.CreateVariable(resourceString, { wide: true });
+ var hr = locator.funcs.ConnectToServer(locator, resource, 0, 0, 0, WBEM_FLAG_CONNECT_USE_MAX_WAIT, 0, 0, services).Val >>> 0;
+ if (hr != 0) {
+ releaseCOM(locator, false);
+ throw new Error('ConnectToServer(' + resourceString + ') failed: ' + (WBEM_ERRORS[hr] || 'unknown') + ' (0x' + hr.toString(16) + ')');
+ }
+ services.funcs = COM.marshalFunctions(services.Deref(), ServiceFunctions);
+
+ return {
+ ns: resourceString,
+ services: services,
+ locator: locator,
+ _sigCache: {}, // 'Class.Method' -> in-params signature (IWbemClassObject), reused across calls
+ // Same signature as execMethod() minus the leading namespace (it's bound to this connection)
+ execMethod: function (className, keys, methodName, inParams, timeout) {
+ return execMethodOn(this.services, this.ns, className, keys, methodName, inParams, timeout, this._sigCache);
+ },
+ release: function () {
+ for (var k in this._sigCache) { this._sigCache[k] = releaseCOM(this._sigCache[k], true); }
+ this.services = releaseCOM(this.services, true);
+ this.locator = releaseCOM(this.locator, false);
+ }
+ };
+}
+
+// Call a WMI method via IWbemServices::ExecMethod
+// resourceString : namespace, e.g. 'root\\cimv2\\Security\\MicrosoftVolumeEncryption'
+// className : the class the method lives on, e.g. 'Win32_EncryptableVolume'
+// keys : (optional) plain object of key property/properties identifying the
+// instance, e.g. { DeviceID: vols[0].DeviceID }. Values are escaped
+// automatically - pass the RAW value, do NOT pre-escape. Pass null/{}
+// for static (class-level) methods.
+// methodName : e.g. 'GetConversionStatus'
+// inParams : (optional) plain object of { paramName: value } input arguments
+// timeout : (optional) max ms to wait for the method to complete, default 60s.
+//
+// One-shot: connects, runs, releases. For several calls to the same namespace use wmi.connect(ns)
+// and reuse sess.execMethod(...) to avoid reconnecting each time.
+// Reference: https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemservices-execmethod
+function execMethod(resourceString, className, keys, methodName, inParams, timeout)
+{
+ var sess = wmiConnect(resourceString);
+ try { return execMethodOn(sess.services, resourceString, className, keys, methodName, inParams, timeout); }
+ finally { sess.release(); }
+}
+
+// Core ExecMethod against an already-connected IWbemServices. Does NOT release `services`/locator -
+// the caller (execMethod wrapper or a wmiConnect() session) owns the connection lifetime.
+// sigCache (optional): a session-owned { 'Class.Method': inSignature } map. When passed, the in-params
+// signature is fetched once (GetObject+GetMethod) and reused on subsequent calls; the session releases it.
+function execMethodOn(services, ns, className, keys, methodName, inParams, timeout, sigCache)
+{
+ if (!timeout || typeof timeout !== 'number') { timeout = 60 * 1000; } // default 60s ceiling
+ var debug = false;
+ function DBG(msg) { if (debug) { console.log('win-wmiii execMethod: ' + msg); } }
+ var objectPath = buildEscapePath(className, keys);
+ DBG('ns=' + ns + ' path=' + objectPath + ' method=' + methodName + ' in=' + JSON.stringify(inParams || {}));
+ var classObj = null, inSig = null, inSigCached = false, inInst = null, outParams = null, callRes = null;
+ try {
+ var hr;
+ var pathVar = GM.CreateVariable(objectPath, { wide: true });
+ var methodVar = GM.CreateVariable(methodName, { wide: true });
+
+ // Get the in-params signature (cached per Class.Method when a session is used) > spawn instance > put params in
+ var pInParams = 0;
+ if (inParams && Object.keys(inParams).length > 0) {
+ var sigKey = className + '.' + methodName;
+ if (sigCache && sigCache[sigKey]) {
+ inSig = sigCache[sigKey]; // reuse the cached signature, skip GetObject/GetMethod
+ inSigCached = true;
+ } else {
+ // Get the class definition, then the method's in-signature
+ // https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemservices-getobject
+ var classNameVar = GM.CreateVariable(className, { wide: true });
+ classObj = GM.CreatePointer();
+ if ((hr = services.funcs.GetObject(services.Deref(), classNameVar, 0, 0, classObj, 0).Val >>> 0) != 0) {
+ throw new Error('GetObject(' + className + ') failed: ' + (WBEM_ERRORS[hr] || 'unknown') + ' (0x' + hr.toString(16) + ')'); }
+ classObj.funcs = COM.marshalFunctions(classObj.Deref(), ResultFunctions);
+
+ // GetMethod -> in-param signature (ppOutSignature is optional, pass 0 - we don't use it)
+ // https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemclassobject-getmethod
+ inSig = GM.CreatePointer();
+ if ((hr = classObj.funcs.GetMethod(classObj.Deref(), methodVar, 0, inSig, 0).Val >>> 0) != 0) {
+ throw new Error('GetMethod(' + methodName + ') failed: ' + (WBEM_ERRORS[hr] || 'unknown') + ' (0x' + hr.toString(16) + ')'); }
+ inSig.funcs = COM.marshalFunctions(inSig.Deref(), ResultFunctions);
+ if (sigCache) { sigCache[sigKey] = inSig; inSigCached = true; } // hand ownership to the session cache
+ }
+
+ // create class instance to put params in
+ // https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemclassobject-spawninstance
+ inInst = GM.CreatePointer();
+ if ((hr = inSig.funcs.SpawnInstance(inSig.Deref(), 0, inInst).Val >>> 0) != 0) {
+ throw new Error('SpawnInstance failed: ' + (WBEM_ERRORS[hr] || 'unknown') + ' (0x' + hr.toString(16) + ')'); }
+ inInst.funcs = COM.marshalFunctions(inInst.Deref(), ResultFunctions);
+
+ // Set the named properties in the instance
+ // https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemclassobject-put
+ var keepAlive = [];
+ for (var name in inParams) {
+ var nameVar = GM.CreateVariable(name, { wide: true });
+ var v = GM.CreateVariable(24); // VARIANT: 16 bytes on x86 / 24 on x64; same offset, so doesn't have to be 32/64 bit specific, use largest
+ var vbuf = v.toBuffer();
+ var val = inParams[name];
+ var keep = writeInParamVariant(vbuf, val);
+ if (keep != null) { keepAlive.push(keep); } // Keep the B_STR reference alive until at least after the COM call, now releases after function exit
+ DBG('Put ' + name + '=' + JSON.stringify(val) + ' variant[0:12]=' + vbuf.slice(0, 12).toString('hex'));
+ if ((hr = inInst.funcs.Put(inInst.Deref(), nameVar, 0, v, 0).Val >>> 0) != 0) {
+ throw new Error('Put(' + name + ') failed: ' + (WBEM_ERRORS[hr] || 'unknown') + ' (0x' + hr.toString(16) + ')'); }
+ }
+ pInParams = inInst.Deref();
+ }
+
+ // Run execMethod semi-synchronous to prevent blocking
+ // https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemservices-execmethod
+ DBG('ExecMethod calling, pInParams=' + (pInParams ? 'set' : 'NULL'));
+ callRes = GM.CreatePointer();
+ hr = services.funcs.ExecMethod(services.Deref(), pathVar, methodVar, WBEM_FLAG_RETURN_IMMEDIATELY, 0, pInParams, 0, callRes).Val >>> 0;
+ DBG('ExecMethod returned 0x' + hr.toString(16) + (hr ? ' (' + (WBEM_ERRORS[hr] || 'unknown') + ')' : ''));
+ if (hr != 0) {
+ throw new Error('ExecMethod(' + methodName + ') failed: ' + (WBEM_ERRORS[hr] || 'unknown') + ' (0x' + hr.toString(16) + ')'); }
+ callRes.funcs = COM.marshalFunctions(callRes.Deref(), CallResultFunctions);
+
+ // GetResultObject returns WBEM_S_TIMEDOUT if the method hasn't finished within the chunk.
+ // https://learn.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemcallresult-getresultobject
+ outParams = GM.CreatePointer(); // results
+ var deadline = Date.now() + timeout;
+ var chunk = 2000; // ms per GetResultObject() call
+ while (true) {
+ if (Date.now() > deadline) { throw new Error('ExecMethod(' + methodName + ') timed out after ' + (timeout/1000) + ' seconds'); }
+ var gr = callRes.funcs.GetResultObject(callRes.Deref(), chunk, outParams).Val >>> 0;
+ if (gr === WBEM_S_NO_ERROR) { break; } // ready
+ if (gr === WBEM_S_TIMEDOUT) { continue; } //continue until deadline
+ throw new Error('GetResultObject(' + methodName + ') failed: ' + (WBEM_ERRORS[gr] || 'unknown') + ' (0x' + gr.toString(16) + ')');
+ }
+
+ if (outParams.Deref().Val == 0) { DBG('no out-params returned'); return {}; } // method returned no out-params
+ outParams.funcs = COM.marshalFunctions(outParams.Deref(), ResultFunctions);
+ // Single result object: read its property names directly (no need for NameCache's per-class caching / __CLASS read)
+ var propNames = getAllNames(outParams, false);
+ var propNameVars = [];
+ for (var pi = 0; pi < propNames.length; ++pi) { propNameVars.push(GM.CreateVariable(propNames[pi], { wide: true })); }
+ var out = enumerateProperties(outParams, propNames, propNameVars);
+ DBG('out=' + JSON.stringify(out) + '\r\n');
+ return out;
+ } finally {
+ // Release only the per-call COM objects. services/locator are owned by the caller (wrapper or session).
+ // inSig is released here only when NOT cached - a cached signature is owned by the session and freed in release().
+ outParams = releaseCOM(outParams, true);
+ callRes = releaseCOM(callRes, true);
+ inInst = releaseCOM(inInst, true);
+ if (!inSigCached) { inSig = releaseCOM(inSig, true); }
+ classObj = releaseCOM(classObj, true);
+ }
}
-module.exports = { query: query, queryAsync: queryAsync };
+module.exports = { query: query, queryAsync: queryAsync, execMethod: execMethod, connect: wmiConnect };
\ No newline at end of file
diff --git a/amtmanager.js b/amtmanager.js
index 24e76e0298..a67068f8af 100644
--- a/amtmanager.js
+++ b/amtmanager.js
@@ -2892,7 +2892,7 @@ module.exports.CreateAmtManager = function (parent) {
// See what admin password to use
dev.aquired.user = 'admin';
- dev.aquired.pass = dev.temp.password;
+ dev.aquired.pass = dev.temp.pass;
// Set the account password
if (typeof dev.temp.mebxpass == 'string') {
diff --git a/amtprovisioningserver.js b/amtprovisioningserver.js
index d31b7b8387..ae9bd71c9c 100644
--- a/amtprovisioningserver.js
+++ b/amtprovisioningserver.js
@@ -479,7 +479,22 @@ module.exports.CreateAmtProvisioningServer = function (parent, config) {
// Save activation data to amtactivation.log
var domain = parent.config.domains[dev.domainid];
- obj.logAmtActivation(domain, { time: new Date(), action: 'acmactivate-bare-metal', domain: dev.domainid, amtUuid: dev.guid, newmebx: config.newmebxpassword, mesh: dev.meshid, amtRealm: dev.aquired.realm, amtver: dev.aquired.version, host: dev.aquired.host, ip: dev.addr, user: dev.aquired.user, pass: dev.aquired.pass, tls: dev.aquired.tls, tlshash: dev.aquired.hash });
+ obj.logAmtActivation(domain, {
+ time: new Date(),
+ action: 'acmactivate-bare-metal',
+ domain: dev.domainid,
+ amtUuid: dev.guid,
+ newmebx: config.newmebxpassword,
+ mesh: dev.meshid,
+ amtRealm: dev.aquired.realm,
+ amtver: dev.aquired.version,
+ host: dev.aquired.host,
+ ip: dev.addr,
+ user: dev.aquired.user,
+ password: dev.aquired.pass,
+ tls: dev.aquired.tls,
+ tlshash: dev.aquired.hash
+ });
// Update device in the database
if (UpdateDevice(dev) == false) return;
diff --git a/amtscanner.js b/amtscanner.js
index ae558f8af2..f376172d08 100644
--- a/amtscanner.js
+++ b/amtscanner.js
@@ -169,7 +169,7 @@ module.exports.CreateAmtScanner = function (parent) {
var scaninfo = obj.scanTable[doc._id];
if (scaninfo == null) {
var tag = obj.nextTag++;
- obj.scanTableTags[tag] = obj.scanTable[doc._id] = scaninfo = { nodeinfo: doc, present: true, tag: tag, state: 0 };
+ obj.scanTableTags[tag] = obj.scanTable[doc._id] = scaninfo = { nodeinfo: doc, present: true, tag: tag, state: 0, lastpong: 0 };
//console.log('Scan ' + host + ', state=' + scaninfo.state + ', delta=' + delta);
} else {
scaninfo.present = true;
@@ -208,6 +208,15 @@ module.exports.CreateAmtScanner = function (parent) {
delete obj.scanTable[i];
}
}
+ // Close any UDP servers whose serverid is no longer referenced by any active tag
+ var activeServerIds = {};
+ for (var t in obj.scanTableTags) { activeServerIds[Math.floor(parseInt(t) / 255)] = true; }
+ for (var sid in obj.servers) {
+ if (activeServerIds[parseInt(sid)] !== true) {
+ try { obj.servers[sid].close(); } catch (e) { }
+ delete obj.servers[sid];
+ }
+ }
});
return true;
};
@@ -252,18 +261,19 @@ module.exports.CreateAmtScanner = function (parent) {
}
};
- // Send a pending RMCP packet
+ // Send pending RMCP packets - process a batch per tick to avoid queue buildup with large device counts
obj.sendPendingPacket = function () {
- try {
+ var batchSize = 10;
+ var count = 0;
+ while (count++ < batchSize) {
var p = obj.pendingSends.shift();
- if (p != undefined) {
- p[0].send(p[1], 623, p[2]);
- p[0].send(p[1], 623, p[2]);
- } else {
+ if (p == undefined) {
clearInterval(obj.pendingSendTimer);
obj.pendingSendTimer = null;
+ break;
}
- } catch (e) { }
+ try { p[0].send(p[1], 623, p[2]); p[0].send(p[1], 623, p[2]); } catch (e) { }
+ }
};
// Parse RMCP packet
diff --git a/apprelays.js b/apprelays.js
index 03ea8058e6..e1939eb544 100644
--- a/apprelays.js
+++ b/apprelays.js
@@ -57,6 +57,7 @@ const MESHRIGHT_RESETOFF = 0x00040000; // 262144
const MESHRIGHT_GUESTSHARING = 0x00080000; // 524288
const MESHRIGHT_DEVICEDETAILS = 0x00100000; // 1048576
const MESHRIGHT_RELAY = 0x00200000; // 2097152
+const MESHRIGHT_NOREGISTRY = 0x00400000; // 4194304
const MESHRIGHT_ADMIN = 0xFFFFFFFF;
// SerialTunnel object is used to embed TLS within another connection.
@@ -531,6 +532,11 @@ module.exports.CreateWebRelay = function (parent, db, args, domain, mtype) {
obj.socketContentLengthRemaining = 0;
function processHttpData(data) {
//console.log('processHttpData', data.length);
+ // If already in streaming mode, forward directly without accumulating to avoid memory leaks. This is used for streaming responses like SSE (Server-Sent Events).
+ if (obj.isStreaming) {
+ try { obj.res.write(data, 'binary'); } catch (ex) { }
+ return;
+ }
obj.socketAccumulator += data;
while (true) {
//console.log('ACC(' + obj.socketAccumulator + '): ' + obj.socketAccumulator);
@@ -556,8 +562,15 @@ module.exports.CreateWebRelay = function (parent, db, args, domain, mtype) {
// Check if this is a streaming response
if ((obj.socketXHeader['content-type'] != null) && (obj.socketXHeader['content-type'].toLowerCase().indexOf('text/event-stream') >= 0)) {
- obj.isStreaming = true; // This tunnel is now a streaming tunnel and will not close anytime soon.
- if (obj.onNextRequest != null) obj.onNextRequest(); // Call this so that any HTTP requests that are waitting for this one to finish get handled by a new tunnel.
+ obj.isStreaming = true;
+ if (obj.onNextRequest != null) obj.onNextRequest();
+ // Forward headers, then clear accumulator and switch to direct forward mode
+ processHttpResponse(obj.socketXHeader, null, false);
+ if (obj.socketAccumulator.length > 0) {
+ try { obj.res.write(obj.socketAccumulator, 'binary'); } catch (ex) { }
+ obj.socketAccumulator = ''; // Free the memory to avoid memory leaks
+ }
+ return; // Exit the while loop, future data handled directly
}
// Check if this HTTP request has a body
diff --git a/backup.js b/backup.js
new file mode 100644
index 0000000000..d18afc7253
--- /dev/null
+++ b/backup.js
@@ -0,0 +1,107 @@
+// backup to zipfile
+module.exports.zipDirectory = async function (dirPath, outputZip, password = '', compression = 5) {
+ try {
+ const { ZipWriterStream } = require("@zip.js/zip.js");
+ const { readdir, stat } = require("fs/promises");
+ const path = require("path");
+ const { createReadStream, createWriteStream } = require("fs");
+ const { Readable } = require("stream");
+ const { pipeline } = require("stream/promises");
+
+ const zipStream = new ZipWriterStream({
+ ...(password ? { password, encryptionStrength: 3 } : {}), // 3 = AES-256
+ level: compression
+ // msDosCompatible: true // to write EntryMetaData#externalFileAttributes in MS-DOS format for folder entries. Fixes Dir entries issue
+ });
+
+ async function addFiles() {
+ const entries = await readdir(dirPath, { recursive: true, withFileTypes: true });
+ for (const entry of entries) {
+ const fullPath = path.join(entry.parentPath, entry.name);
+ const relPath = path.relative(dirPath, fullPath).replaceAll(path.sep, path.posix.sep);
+
+ if (entry.isDirectory()) {
+ await zipStream.zipWriter.add(relPath + '/', null);
+ } else {
+ await Readable.toWeb(createReadStream(fullPath)).pipeTo(zipStream.writable(relPath));
+ }
+ // console.log ("zip addfile: " + entry);
+ }
+ }
+
+ await Promise.all([
+ addFiles().then(() => zipStream.close()),
+ pipeline(Readable.fromWeb(zipStream.readable), createWriteStream(outputZip)),
+ ]);
+ return { res: true, mes: 'Zip created succesfully' };
+ } catch (e) {
+ // console.error(e);
+ return { res: false, mes: e.message };
+ }
+}
+
+// restore zipfile
+// removePath: optional path to remove from destination path
+// First check archive on illegal paths (updirs and absolute paths), then extract
+module.exports.zipExtract = async function (zipPath, destPath, removePath = '', password = "") {
+ try {
+ const { ZipReader, Uint8ArrayReader } = require("@zip.js/zip.js");
+ const { createWriteStream } = require("fs");
+ const { readFile, mkdir } = require("fs/promises");
+ const path = require("path");
+ const { Readable } = require("stream");
+ const { pipeline } = require("stream/promises");
+
+ const zipReader = new ZipReader(new Uint8ArrayReader(await readFile(zipPath)));
+ const entries = await zipReader.getEntries();
+ const resolveDestPath = path.resolve(destPath);
+
+ // Archive validation: Abort the restore on any bad path before extraction
+ const targets = []; // { entry, fullPath } cache the paths while we're at it
+ for (const entry of entries) {
+ // skip symlinks in unix-mode zips (upper 16 bits hold st_mode); not present in dos/win zips
+ if (((entry.externalFileAttributes >>> 16) & 0xF000) === 0xA000) { continue; }
+
+ // anchored prefix strip — must match what the extraction pass writes
+ let name = entry.filename;
+ if (removePath && name.startsWith(removePath)) name = name.slice(removePath.length);
+
+ // reject any parent-dir token outright (split on both separators; segment match, not substring)
+ if (name.split(/[/\\]/).includes('..')) {
+ throw new Error('Aborting, nothing restored. Illegal path (parent traversal) in zip, entry: ' + entry.filename);
+ }
+ // reject absolute paths (posix root, or Windows drive / UNC)
+ if (path.isAbsolute(name) || /^[a-zA-Z]:/.test(name) || name.startsWith('\\')) {
+ throw new Error('Aborting, nothing restored. Illegal absolute path in zip, entry: ' + entry.filename);
+ }
+
+ // backstop: resolved target must stay inside destPath
+ const fullPath = path.resolve(destPath, name);
+ if (fullPath !== resolveDestPath && !fullPath.startsWith(resolveDestPath + path.sep)) {
+ throw new Error('Illegal path in zip entry: ' + entry.filename);
+ }
+
+ targets.push({ entry, fullPath });
+ }
+
+ // Validated, extract!
+ for (const { entry, fullPath } of targets) {
+ if (entry.directory) {
+ await mkdir(fullPath, { recursive: true });
+ } else {
+ await mkdir(path.dirname(fullPath), { recursive: true });
+ const { writable, readable } = new TransformStream();
+ await Promise.all([
+ entry.getData(writable, { password }),
+ pipeline(Readable.fromWeb(readable), createWriteStream(fullPath)),
+ ]);
+ }
+ }
+
+ await zipReader.close();
+ return { res: true, mes: 'Extraction successful' };
+ } catch (e) {
+ // console.error(e.message);
+ return { res: false, mes: e.message };
+ }
+}
\ No newline at end of file
diff --git a/certoperations.js b/certoperations.js
index bc3234f2b4..6c8da72412 100644
--- a/certoperations.js
+++ b/certoperations.js
@@ -518,16 +518,23 @@ module.exports.CertificateOperations = function (parent) {
// Return a text file from a remote HTTPS server
obj.loadTextFile = function (url, tag, func) {
- const u = require('url').parse(url);
+ const u = new URL(url);
if (u.protocol == 'https:') {
// Read from HTTPS
const https = require('https');
- https.get(url, function(resp) {
+ const options = { timeout: 10000 };
+ if (process.env['HTTPS_PROXY'] || process.env['HTTP_PROXY'] || process.env['https_proxy'] || process.env['http_proxy']) {
+ options.agent = new (require('https-proxy-agent').HttpsProxyAgent)(process.env['HTTPS_PROXY'] || process.env['HTTP_PROXY'] || process.env['https_proxy'] || process.env['http_proxy']);
+ }
+ const req = https.get(url, options, function(resp) {
+ if (resp.statusCode < 200 || resp.statusCode >= 300) { resp.resume(); func(url, null, tag); return; }
var data = '';
resp.on('data', function(chunk) { data += chunk; });
- resp.on('end', function () { func(url, data, tag); });
- resp.on('error', function (chunk) { func(url, null, tag); });
- }).on('error', function (err) { func(url, null, tag); });
+ resp.on('end', function() { func(url, data, tag); });
+ resp.on('error', function() { func(url, null, tag); });
+ });
+ req.on('error', function() { func(url, null, tag); });
+ req.on('timeout', function() { req.destroy(); func(url, null, tag); });
} else if (u.protocol == 'file:') {
// Read a file
obj.fs.readFile(url.substring(7), 'utf8', function (err, data) {
@@ -538,7 +545,7 @@ module.exports.CertificateOperations = function (parent) {
// Return the certificate of the remote HTTPS server
obj.loadCertificate = function (url, hostname, tag, func) {
- const u = require('url').parse(url);
+ const u = new URL(url);
if (u.protocol == 'https:') {
// Read the certificate from HTTPS
if (hostname == null) { hostname = u.hostname; }
diff --git a/db.js b/db.js
index 2d6eb35db3..1af81a9695 100644
--- a/db.js
+++ b/db.js
@@ -42,9 +42,9 @@ module.exports.CreateDB = function (parent, func) {
const SQLITE_AUTOVACUUM = ['none', 'full', 'incremental'];
const SQLITE_SYNCHRONOUS = ['off', 'normal', 'full', 'extra'];
obj.sqliteConfig = {
- maintenance: '',
+ maintenance: 'PRAGMA optimize;',
startupVacuum: false,
- autoVacuum: 'full',
+ autoVacuum: 'incremental',
incrementalVacuum: 100,
journalMode: 'delete',
journalSize: 4096000,
@@ -241,7 +241,7 @@ module.exports.CreateDB = function (parent, func) {
obj.Remove('si' + node._id); // Remove system information
obj.Remove('al' + node._id); // Remove error log last time
if (obj.RemoveSMBIOS) { obj.RemoveSMBIOS(node._id); } // Remove SMBios data
- obj.RemoveAllNodeEvents(node._id); // Remove all events for this node
+ obj.RemoveAllNodeEvents(node.domain, node._id); // Remove all events for this node
obj.removeAllPowerEventsForNode(node._id); // Remove all power events for this node
if (typeof node.pmt == 'string') { obj.Remove('pmt_' + node.pmt); } // Remove Push Messaging Token
obj.Get('ra' + node._id, function (err, nodes) {
@@ -267,7 +267,7 @@ module.exports.CreateDB = function (parent, func) {
parent.DispatchEvent(targets, obj, event);
}
} else if (i.startsWith('ugrp/')) {
- var cusergroup = parent.userGroups[i];
+ var cusergroup = parent.webserver.userGroups[i];
if ((cusergroup != null) && (cusergroup.links != null) && (cusergroup.links[node._id] != null)) {
// Remove the user link & save the user
delete cusergroup.links[node._id];
@@ -454,7 +454,7 @@ module.exports.CreateDB = function (parent, func) {
} 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) { });
+ sqlDbQuery('DELETE FROM main WHERE extra LIKE \'mesh/%\' AND extra <> ALL ($1)', [meshlist], function (err, response) { });
} 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) { });
@@ -768,6 +768,7 @@ module.exports.CreateDB = function (parent, func) {
sqlDbExec('CREATE INDEX ndxeventsusername ON events(domain, userid, time)', null, function (err, response) { });
sqlDbExec('CREATE INDEX ndxeventsdomainnodeidtime ON events(domain, nodeid, time)', null, function (err, response) { });
sqlDbExec('CREATE INDEX ndxeventids ON eventids(target)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxeventidsfkid ON eventids(fkid)', null, function (err, response) { });
sqlDbExec('CREATE INDEX ndxserverstattime ON serverstats (time)', null, function (err, response) { });
sqlDbExec('CREATE INDEX ndxserverstatexpire ON serverstats (expire)', null, function (err, response) { });
sqlDbExec('CREATE INDEX ndxpowernodeidtime ON power (nodeid, time)', null, function (err, response) { });
@@ -783,23 +784,26 @@ module.exports.CreateDB = function (parent, func) {
// SQLite3 database setup
obj.databaseType = DB_SQLITE;
const sqlite3 = require('sqlite3');
- 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';
+ let configParams = parent.config.settings.sqlite3 || {};
+ databaseName = (typeof configParams == 'string') ? configParams : (configParams.name || 'meshcentral');
+ obj.sqliteConfig.startupVacuum = (typeof configParams.startupvacuum == 'boolean') ? configParams.startupvacuum : false;
+ obj.sqliteConfig.autoVacuum = (typeof configParams.autovacuum == 'string') ? configParams.autovacuum.toLowerCase() : 'incremental';
+ if (!(['none','full','incremental'].includes(obj.sqliteConfig.autoVacuum))) { obj.sqliteConfig.autoVacuum = 'incremental'; }
+ const incrementalVacuum = Number(configParams.incrementalvacuum);
+ obj.sqliteConfig.incrementalVacuum = (Number.isInteger(incrementalVacuum) && incrementalVacuum >= 0) ? incrementalVacuum : 100;
+
//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;
+ obj.sqliteConfig.journalMode = (typeof configParams.journalmode == 'string' && ['delete', 'truncate', 'persist', 'memory', 'wal'].includes(configParams.journalmode.toLowerCase())) ? configParams.journalmode.toLowerCase() : 'delete';
+ const journalSize = Number(configParams.journalsize);
+ obj.sqliteConfig.journalSize = Number.isInteger(journalSize) ? journalSize : 4096000;
+
//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;';
-
+ 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 + ');'}
+
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') };
+ 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')) {
@@ -829,24 +833,20 @@ module.exports.CreateDB = function (parent, func) {
CREATE INDEX ndxsmbiosexpire ON smbios (expire);
`, function (err) {
// Completed DB creation of SQLite3
- sqliteSetOptions(func);
- //setupFunctions could be put in the sqliteSetupOptions, but left after it for clarity
- setupFunctions(func);
+ sqliteSetOptions(function () { setupFunctions(func); });
}
);
});
return;
- } else if (err) { console.log("SQLite Error: " + err); process.exit(0); }
+ } else if (err) { console.log("SQLite Error: " + err); process.exit(1); }
//for existing db's
- sqliteSetOptions();
// Create any missing tables (e.g., pluginpermissions added in updates)
obj.file.exec(`
CREATE TABLE IF NOT EXISTS pluginpermissions (id VARCHAR(255) PRIMARY KEY, doc JSON)
`, function (err) {
if (err) { console.log("SQLite Error creating pluginpermissions table: " + err); }
- //setupFunctions could be put in the sqliteSetupOptions, but left after it for clarity
- setupFunctions(func);
+ sqliteSetOptions(function () { setupFunctions(func); });
});
});
} else if (parent.args.acebase) {
@@ -1360,41 +1360,42 @@ module.exports.CreateDB = function (parent, func) {
}
function sqliteSetOptions(func) {
- //get current auto_vacuum mode for comparison
+ // Read current auto_vacuum mode; switching into or out of 'none' requires a VACUUM to take effect. See https://www.sqlite.org/pragma.html#pragma_auto_vacuum
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;';
- };
+ if (err || !current) { parent.debug('db', 'Could not read auto_vacuum: ' + (err ? err.message : 'no row')); current = { auto_vacuum: 0 }; }
+ 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;PRAGMA foreign_keys = ON;`;
+ // VACUUM needed only when crossing the 'none' boundary in either direction
+ if (obj.sqliteConfig.startupVacuum || ((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);
- });
+ // check if debug db is enabled to prevent unneccessary calls
+ if ((parent.debugSources != null) && ((parent.debugSources == '*') || (parent.debugSources.indexOf('db') >= 0))) {
+ sqliteGetPragmas(['journal_mode', 'journal_size_limit', 'freelist_count', 'auto_vacuum', 'page_size', 'wal_autocheckpoint', 'synchronous'], function (pragma, pragmaValue) {
+ parent.debug('db', 'PRAGMA: ' + pragma + '=' + pragmaValue);
+ });
+ }
+ if (func) { func(); }
});
});
- //setupFunctions(func);
}
function sqliteGetPragmas (pragmas, func){
- //pragmas can only be gotting one by one
+ //pragmas can only be gotten 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]); }
+ if (err || !res) { return; }
+ let value = res[pragma];
+ if (pragma == 'auto_vacuum') { value = SQLITE_AUTOVACUUM[value]; }
+ else if (pragma == 'synchronous') { value = SQLITE_SYNCHRONOUS[value]; }
+ if (func) { func (pragma, value); }
});
});
}
@@ -1618,18 +1619,18 @@ module.exports.CreateDB = function (parent, func) {
obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
if (limit == 0) { limit = -1; } // In SQLite, no limit is -1
if (id && (id != '')) {
- sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra IN (' + dbMergeSqlArray(meshes) + ')) LIMIT $4 OFFSET $5', [id, type, domain, limit, skip], function (err, docs) {
+ sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra IN (' + dbMergeSqlArray(meshes) + ')) ORDER BY LOWER(json_extract(doc, \'$.name\')) LIMIT $4 OFFSET $5', [id, type, domain, limit, skip], function (err, docs) {
if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
func(err, performTypedRecordDecrypt(docs));
});
} else {
if (extrasids == null) {
- sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra IN (' + dbMergeSqlArray(meshes) + ')) LIMIT $3 OFFSET $4', [type, domain, limit, skip], function (err, docs) {
+ sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra IN (' + dbMergeSqlArray(meshes) + ')) ORDER BY LOWER(json_extract(doc, \'$.name\')) LIMIT $3 OFFSET $4', [type, domain, limit, skip], function (err, docs) {
if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
func(err, performTypedRecordDecrypt(docs));
});
} else {
- sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND ((extra IN (' + dbMergeSqlArray(meshes) + ')) OR (id IN (' + dbMergeSqlArray(extrasids) + '))) LIMIT $3 OFFSET $4', [type, domain, limit, skip], function (err, docs) {
+ sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND ((extra IN (' + dbMergeSqlArray(meshes) + ')) OR (id IN (' + dbMergeSqlArray(extrasids) + '))) ORDER BY LOWER(json_extract(doc, \'$.name\')) LIMIT $3 OFFSET $4', [type, domain, limit, skip], function (err, docs) {
if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
func(err, performTypedRecordDecrypt(docs));
});
@@ -1877,9 +1878,6 @@ module.exports.CreateDB = function (parent, func) {
// Write a configuration file to the database
obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
- // List all configuration files
- obj.listConfigFiles = function (func) { sqlDbQuery('SELECT doc FROM main WHERE type = "cfile" ORDER BY id', func); }
-
// Get database information (TODO: Complete this)
obj.getDbStats = function (func) {
obj.stats = { c: 4 };
@@ -1934,7 +1932,7 @@ module.exports.CreateDB = function (parent, func) {
}
obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
if (meshes.length == 0) { func(null, []); return; }
- var query = obj.file.query('meshcentral').skip(skip).take(limit).filter('type', '==', type).filter('domain', '==', domain);
+ var query = obj.file.query('meshcentral').sort('name', true).skip(skip).take(limit).filter('type', '==', type).filter('domain', '==', domain);
if (id) { query = query.filter('_id', '==', id); }
if (extrasids == null) {
query = query.filter('meshid', 'in', meshes);
@@ -2160,13 +2158,6 @@ module.exports.CreateDB = function (parent, func) {
// Write a configuration file to the database
obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
- // List all configuration files
- obj.listConfigFiles = function (func) {
- obj.file.query('meshcentral').filter('type', '==', 'cfile').sort('_id').get(function (snapshots) {
- const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs);
- });
- }
-
// Get database information
obj.getDbStats = function (func) {
obj.stats = { c: 5 };
@@ -2220,12 +2211,12 @@ module.exports.CreateDB = function (parent, func) {
obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
if (limit == 0) { limit = 0xFFFFFFFF; }
if (id && (id != '')) {
- sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra = ANY ($4)) LIMIT $5 OFFSET $6', [id, type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
+ sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra = ANY ($4)) ORDER BY LOWER(doc->>\'name\') LIMIT $5 OFFSET $6', [id, type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
} else {
if (extrasids == null) {
- sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra = ANY ($3)) LIMIT $4 OFFSET $5', [type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }, true);
+ sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra = ANY ($3)) ORDER BY LOWER(doc->>\'name\') LIMIT $4 OFFSET $5', [type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }, true);
} else {
- sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND ((extra = ANY ($3)) OR (id = ANY ($4))) LIMIT $5 OFFSET $6', [type, domain, meshes, extrasids, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
+ sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND ((extra = ANY ($3)) OR (id = ANY ($4))) ORDER BY LOWER(doc->>\'name\') LIMIT $5 OFFSET $6', [type, domain, meshes, extrasids, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
}
}
};
@@ -2433,9 +2424,6 @@ module.exports.CreateDB = function (parent, func) {
// Write a configuration file to the database
obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
- // List all configuration files
- obj.listConfigFiles = function (func) { sqlDbQuery('SELECT doc FROM main WHERE type = "cfile" ORDER BY id', func); }
-
// Get database information (TODO: Complete this)
obj.getDbStats = function (func) {
obj.stats = { c: 4 };
@@ -2484,9 +2472,9 @@ module.exports.CreateDB = function (parent, func) {
if ((meshes == null) || (meshes.length == 0)) { meshes = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
if ((extrasids == null) || (extrasids.length == 0)) { extrasids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
if (id && (id != '')) {
- sqlDbQuery('SELECT doc FROM main WHERE id = ? AND type = ? AND domain = ? AND extra IN (?) LIMIT ? OFFSET ?', [id, type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
+ sqlDbQuery('SELECT doc FROM main WHERE id = ? AND type = ? AND domain = ? AND extra IN (?) ORDER BY LOWER(JSON_UNQUOTE(JSON_EXTRACT(doc, \'$.name\'))) LIMIT ? OFFSET ?', [id, type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
} else {
- sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ? AND (extra IN (?) OR id IN (?)) LIMIT ? OFFSET ?', [type, domain, meshes, extrasids, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
+ sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ? AND (extra IN (?) OR id IN (?)) ORDER BY LOWER(JSON_UNQUOTE(JSON_EXTRACT(doc, \'$.name\'))) LIMIT ? OFFSET ?', [type, domain, meshes, extrasids, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
}
};
obj.CountAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
@@ -2685,9 +2673,6 @@ module.exports.CreateDB = function (parent, func) {
// Write a configuration file to the database
obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
- // List all configuration files
- obj.listConfigFiles = function (func) { sqlDbQuery('SELECT doc FROM main WHERE type = "cfile" ORDER BY id', func); }
-
// Get database information (TODO: Complete this)
obj.getDbStats = function (func) {
obj.stats = { c: 4 };
@@ -2789,14 +2774,14 @@ module.exports.CreateDB = function (parent, func) {
if (extrasids == null) {
const x = { type: type, domain: domain, meshid: { $in: meshes } };
if (id) { x._id = id; }
- var f = obj.file.find(x, { type: 0 });
+ var f = obj.file.find(x, { type: 0 }).collation({ locale: 'en', strength: 2 }).sort({ name: 1 });
if (skip > 0) f = f.skip(skip); // Skip records
if (limit > 0) f = f.limit(limit); // Limit records
f.toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
} else {
const x = { type: type, domain: domain, $or: [ { meshid: { $in: meshes } }, { _id: { $in: extrasids } } ] };
if (id) { x._id = id; }
- var f = obj.file.find(x, { type: 0 });
+ var f = obj.file.find(x, { type: 0 }).collation({ locale: 'en', strength: 2 }).sort({ name: 1 });
if (skip > 0) f = f.skip(skip); // Skip records
if (limit > 0) f = f.limit(limit); // Limit records
f.toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
@@ -2987,9 +2972,6 @@ module.exports.CreateDB = function (parent, func) {
// Write a configuration file to the database
obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
- // List all configuration files
- obj.listConfigFiles = function (func) { obj.file.find({ type: 'cfile' }).sort({ _id: 1 }).toArray(func); }
-
// Get database information
obj.getDbStats = function (func) {
obj.stats = { c: 6 };
@@ -3076,11 +3058,11 @@ module.exports.CreateDB = function (parent, func) {
if (extrasids == null) {
const x = { type: type, domain: domain, meshid: { $in: meshes } };
if (id) { x._id = id; }
- obj.file.find(x).skip(skip).limit(limit).exec(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
+ obj.file.find(x).sort({ name: 1 }).skip(skip).limit(limit).exec(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
} else {
const x = { type: type, domain: domain, $or: [{ meshid: { $in: meshes } }, { _id: { $in: extrasids } }] };
if (id) { x._id = id; }
- obj.file.find(x).skip(skip).limit(limit).exec(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
+ obj.file.find(x).sort({ name: 1 }).skip(skip).limit(limit).exec(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
}
};
obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
@@ -3206,9 +3188,6 @@ module.exports.CreateDB = function (parent, func) {
// Write a configuration file to the database
obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
- // List all configuration files
- obj.listConfigFiles = function (func) { obj.file.find({ type: 'cfile' }).sort({ _id: 1 }).exec(func); }
-
// Get database information
obj.getDbStats = function (func) {
obj.stats = { c: 5 };
@@ -3463,6 +3442,13 @@ module.exports.CreateDB = function (parent, func) {
child_process.exec(cmd, { cwd: backupPath }, function (error, stdout, stderr) {
if ((error != null) && (error != '')) {
func(1, "Mongodump error, backup will not be performed. Check path or use mongodumppath & mongodumpargs");
+
+ let processedError = error.message;
+ if (typeof parent?.config?.settings?.postgres?.password === "string" && parent.config.settings.postgres.password.length > 0) {
+ processedError = processedError.replaceAll(encodeURIComponent(parent.config.settings.postgres.password), "****");
+ }
+ parent.debug('backup', 'MongoDB/MongoJS DumpTool: ' + processedError);
+
return;
} else {parent.config.settings.autobackup.backupintervalhours = backupInterval;}
});
@@ -3474,6 +3460,13 @@ module.exports.CreateDB = function (parent, func) {
child_process.exec(cmd, { cwd: backupPath, timeout: 1000*30 }, function(error, stdout, stdin) {
if ((error != null) && (error != '')) {
func(1, "mysqldump error, backup will not be performed. Check path or use mysqldumppath");
+
+ let processedError = error.message;
+ if (typeof parent?.config?.settings?.postgres?.password === "string" && parent.config.settings.postgres.password.length > 0) {
+ processedError = processedError.replaceAll(encodeURIComponent(parent.config.settings.postgres.password), "****");
+ }
+ parent.debug('backup', 'MariaDB/MySQL DumpTool: ' + processedError);
+
return;
} else {parent.config.settings.autobackup.backupintervalhours = backupInterval;}
@@ -3489,6 +3482,13 @@ module.exports.CreateDB = function (parent, func) {
child_process.exec(cmd, { cwd: backupPath }, function(error, stdout, stdin) {
if ((error != null) && (error != '')) {
func(1, "pg_dump error, backup will not be performed. Check path or use pgdumppath.");
+
+ let processedError = error.message;
+ if (typeof parent?.config?.settings?.postgres?.password === "string" && parent.config.settings.postgres.password.length > 0) {
+ processedError = processedError.replaceAll(encodeURIComponent(parent.config.settings.postgres.password), "****");
+ }
+ parent.debug('backup', 'PostgreSQL DumpTool: ' + processedError);
+
return;
} else {parent.config.settings.autobackup.backupintervalhours = backupInterval;}
});
@@ -3610,8 +3610,8 @@ module.exports.CreateDB = function (parent, func) {
obj.performBackup = function (func) {
parent.debug('backup','Entering performBackup');
try {
- if (obj.performingBackup) return 'Backup alreay in progress.';
- if (parent.config.settings.autobackup.backupintervalhours == -1) { if (func) { func('Backup disabled.'); return 'Backup disabled.' }};
+ if (obj.performingBackup) { return 'Backup already in progress.' };
+ if (parent.config.settings.autobackup.backupintervalhours == -1) { return 'Backup disabled.' };
obj.performingBackup = true;
let backupPath = parent.backuppath;
let dataPath = parent.datapath;
@@ -3707,12 +3707,13 @@ module.exports.CreateDB = function (parent, func) {
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')) {
+ //if password defined, or a password entered for the manual backup, create encrypted zip
+ if ((parent.config.settings.autobackup.zippasswordrequest != '') && ((typeof parent.config.settings.autobackup.zippassword == 'string') || (typeof parent.config.settings.autobackup.zippasswordrequest == '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 });
+ archive = archiver.create('zip-encrypted', { zlib: { level: zipLevel }, encryptionMethod: 'aes256',
+ password: (typeof parent.config.settings.autobackup.zippasswordrequest == 'string')?parent.config.settings.autobackup.zippasswordrequest: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;
@@ -3723,6 +3724,7 @@ module.exports.CreateDB = function (parent, func) {
if (func) { func('Creating a NON-ENCRYPTED ZIP'); }
archive = archiver('zip', { zlib: { level: zipLevel } });
}
+ delete parent.config.settings.autobackup.zippasswordrequest;
//original behavior, just a filebackup if dbdump fails : (obj.backupStatus == 0 || obj.backupStatus == BACKUPFAIL_DBDUMP)
if (obj.backupStatus == 0) {
diff --git a/dependencies.txt b/dependencies.txt
index dfc26a2780..22c03358df 100644
--- a/dependencies.txt
+++ b/dependencies.txt
@@ -1,18 +1,18 @@
"@seald-io/nedb": "4.1.2",
+ "@zip.js/zip.js": "2.8.26",
"archiver": "7.0.1",
- "body-parser": "1.20.4",
"cbor": "5.2.0",
"compression": "1.8.1",
"cookie-session": "2.1.1",
- "express": "4.22.1",
+ "express": "4.22.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.2",
- "otplib": "12.0.1",
+ "multiparty": "4.3.0",
+ "node-forge": "1.4.0",
+ "otplib": "13.4.1",
"ua-client-hints-js": "0.1.2",
"ua-parser-js": "1.0.40",
- "ws": "8.18.3",
+ "ws": "8.21.1",
"yauzl": "2.10.0"
\ No newline at end of file
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 554bce379e..1ce402df52 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,5 +1,10 @@
+FROM alpine:3.24 AS node-base
+
+RUN apk add --no-cache --update \
+ nodejs npm jq tzdata
+
### STAGE 1 BUILDING.
-FROM alpine:3.23 AS builder
+FROM node-base AS mesh-compiler
# Any value inside one of the disable ARGs will be accepted.
ARG DISABLE_EXTRACT="yes"
@@ -8,9 +13,7 @@ ARG DISABLE_TRANSLATE="yes"
# NODE_OPTIONS="--max_old_space_size=4096"
# If your process gets OOM killed, perhaps the above will help.
-RUN apk add --no-cache --update \
- nodejs npm; \
- mkdir -p /opt/meshcentral/meshcentral
+RUN mkdir -p /opt/meshcentral/meshcentral
WORKDIR /opt/meshcentral
COPY ./ /opt/meshcentral/meshcentral/
@@ -49,13 +52,13 @@ RUN rm -rf /opt/meshcentral/meshcentral/docker /opt/meshcentral/meshcentral/node
### STAGE 2 PRECOMPILE DEPS MODULE
-FROM alpine:3.23 AS dep-compiler
+FROM node-base AS dep-compiler
RUN echo -e "----------\nINSTALLING ALPINE PACKAGES...\n----------"; \
apk add --no-cache --update \
- bash gcc g++ jq make nodejs npm python3 tzdata
+ bash gcc g++ make python3
-COPY --from=builder /opt/meshcentral/meshcentral /opt/meshcentral/meshcentral
+COPY --from=mesh-compiler /opt/meshcentral/meshcentral /opt/meshcentral/meshcentral
WORKDIR /opt/meshcentral/meshcentral
RUN jq '.dependencies += {"modern-syslog": "1.2.0", "telegram": "2.26.22"}' package.json > temp.json && mv temp.json package.json \
@@ -65,7 +68,7 @@ RUN jq '.dependencies += {"modern-syslog": "1.2.0", "telegram": "2.26.22"}' pack
### STAGE 3 BUILDING.
-FROM alpine:3.23 AS finalizer
+FROM node-base AS finalizer
# copy files from previous layer
COPY --from=dep-compiler /opt/meshcentral/meshcentral /opt/meshcentral/meshcentral
@@ -131,15 +134,14 @@ ENV MARIADB_HOST="" \
RUN echo -e "----------\nINSTALLING ALPINE PACKAGES...\n----------"; \
mkdir -p /opt/meshcentral/meshcentral; \
apk add --no-cache --update \
- bash curl jq nodejs npm tzdata && \
+ bash curl && \
rm -rf /var/cache/* \
/tmp/* \
/usr/share/man/ \
/usr/share/doc/ \
/var/log/* \
/var/spool/* \
- /usr/lib/debug/ && \
- npm install -g npm@latest
+ /usr/lib/debug/
WORKDIR /opt/meshcentral
@@ -147,7 +149,7 @@ RUN case "$PREINSTALL_LIBS" in \
true|yes|TRUE|YES) \
cd meshcentral && \
echo -e "----------\nPREINSTALLING LIBRARIES...\n----------"; \
- npm install ssh2@1.16.0 nodemailer@6.10.1 image-size@2.0.2 wildleek@2.0.0 otplib@12.0.1 yub@0.11.1;; \
+ npm install ssh2@1.17.0 nodemailer@6.10.1 image-size@2.0.2 wildleek@2.0.0 yub@0.11.1;; \
false|no|FALSE|NO) \
echo "Not pre-installing libraries.";; \
*) \
@@ -174,7 +176,7 @@ RUN case "$INCLUDE_POSTGRESQL_TOOLS" in \
apk add --no-cache postgresql-client && \
cd meshcentral && \
echo -e "----------\nPREINSTALLING POSTGRESQL LIBRARIES...\n----------"; \
- npm install pg@8.14.1;; \
+ npm install pg@8.16.3;; \
false|no|FALSE|NO) \
echo "Not including PostgreSQL Tools.";; \
*) \
@@ -187,7 +189,7 @@ RUN case "$INCLUDE_MARIADB_TOOLS" in \
apk add --no-cache mariadb-client && \
cd meshcentral && \
echo -e "----------\nPREINSTALLING MARIADB/MYSQL LIBRARIES...\n----------"; \
- npm install mariadb@3.4.0 mysql2@3.11.4;; \
+ npm install mariadb@3.4.5 mysql2@3.15.1;; \
false|no|FALSE|NO) \
echo "Not including MariaDB/MySQL Tools.";; \
*) \
diff --git a/docker/Dockerfile-debian b/docker/Dockerfile-debian
index 6382ef3c44..900ed81588 100644
--- a/docker/Dockerfile-debian
+++ b/docker/Dockerfile-debian
@@ -1,5 +1,22 @@
-### STAGE 1 BUILDING.
-FROM debian:trixie-slim AS builder
+### STAGE 1 NODEJS BASE IMAGE
+
+FROM debian:trixie-slim AS node-base
+
+ARG NODE_MAJOR=24
+
+# Install general dependencies and nodesource's NodeJS
+RUN apt-get update && \
+ apt-get install -y --no-install-recommends --no-install-suggests \
+ ca-certificates curl gnupg2 jq tzdata wget && \
+ mkdir -p /etc/apt/keyrings/ && \
+ curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
+ echo "Types: deb \nURIs: https://deb.nodesource.com/node_${NODE_MAJOR}.x/ \nSuites: nodistro \nComponents: main \nSigned-By: /etc/apt/keyrings/nodesource.gpg" > /etc/apt/sources.list.d/nodesource.sources && cat /etc/apt/sources.list.d/nodesource.sources && \
+ apt-get update && \
+ apt-get install -y --no-install-recommends --no-install-suggests \
+ nodejs
+
+### STAGE 2 BUILDING MESHCENTRAL BASICS
+FROM node-base AS mesh-compiler
# Any value inside one of the disable ARGs will be accepted.
ARG DISABLE_EXTRACT="yes"
@@ -8,39 +25,34 @@ ARG DISABLE_TRANSLATE="yes"
# NODE_OPTIONS="--max_old_space_size=4096"
# If your process gets OOM killed, perhaps the above will help.
-# Install nodesource's NodeJS
-RUN apt-get update && \
- apt-get install -y --no-install-recommends --no-install-suggests \
- nodejs npm && \
- npm install -g npm@latest && \
- mkdir -p /opt/meshcentral/meshcentral
+RUN mkdir -p /opt/meshcentral/meshcentral
WORKDIR /opt/meshcentral
COPY ./ /opt/meshcentral/meshcentral/
# Check the Docker build arguments and if they are empty do the task.
RUN if [ -n "$DISABLE_EXTRACT" ] || [ -n "$DISABLE_MINIFY" ] || [ -n "$DISABLE_TRANSLATE" ]; then \
- echo -e "----------\nPREPARING ENVIRONMENT...\n----------"; \
+ echo "----------\nPREPARING ENVIRONMENT...\n----------"; \
cd meshcentral && \
npm install html-minifier-terser@7.2.0 jsdom@26.0.0 esprima@4.0.1 && \
cd translate && \
case "$DISABLE_EXTRACT" in \
false|no|FALSE|NO) \
- echo -e "----------\nSTARTING THE EXTRACTING PROCESS...\n----------"; \
+ echo "----------\nSTARTING THE EXTRACTING PROCESS...\n----------"; \
node translate.js extractall;; \
*) \
echo "Setting EXTRACT as disabled.";; \
esac && \
case "$DISABLE_MINIFY" in \
false|no|FALSE|NO) \
- echo -e "----------\nSTARTING THE MINIFYING PROCESS...\n----------"; \
+ echo "----------\nSTARTING THE MINIFYING PROCESS...\n----------"; \
node translate.js minifyall;; \
*) \
echo "Setting MINIFY as disabled.";; \
esac && \
case "$DISABLE_TRANSLATE" in \
false|no|FALSE|NO) \
- echo -e "----------\nSTARTING THE TRANSLATING PROCESS...\n----------"; \
+ echo "----------\nSTARTING THE TRANSLATING PROCESS...\n----------"; \
node translate.js translateall;; \
*) \
echo "Setting TRANSLATE as disabled.";; \
@@ -51,36 +63,34 @@ RUN if [ -n "$DISABLE_EXTRACT" ] || [ -n "$DISABLE_MINIFY" ] || [ -n "$DISABLE_T
RUN rm -rf /opt/meshcentral/meshcentral/docker /opt/meshcentral/meshcentral/node_modules /opt/meshcentral/meshcentral/docs
-### STAGE 2 PRECOMPILE DEPS MODULE
+### STAGE 3 PRECOMPILE NODEJS DEPENDENCIES
-FROM debian:trixie-slim AS dep-compiler
+FROM node-base AS dep-compiler
ENV NODE_ENV="production"
-# Install general dependencies and nodesource's NodeJS
RUN apt-get update && \
- echo -e "----------\nINSTALLING DEBIAN PACKAGES...\n----------"; \
apt-get install -y --no-install-recommends --no-install-suggests \
- bash gcc g++ jq make nodejs npm python3 tzdata && \
- npm install -g npm@latest
+ gcc g++ make python3
-COPY --from=builder /opt/meshcentral/meshcentral /opt/meshcentral/meshcentral
+COPY --from=mesh-compiler /opt/meshcentral/meshcentral /opt/meshcentral/meshcentral
WORKDIR /opt/meshcentral/meshcentral
RUN jq '.dependencies += {"modern-syslog": "1.2.0", "telegram": "2.26.22"}' package.json > temp.json && mv temp.json package.json \
- && npm i --package-lock-only \
+ && npm install --package-lock-only \
&& npm ci --omit=dev \
&& npm cache clean --force
-### STAGE 3 fun. building from source...
+### STAGE 4 COMPILING MONGODB TOOLS AND DEPENDENCIES
-FROM golang:trixie AS mongo-tools-compiler
+FROM golang:trixie AS mongo-compiler
ARG INCLUDE_MONGODB_TOOLS="false"
-RUN apt-get update && \
- apt-get install -y --no-install-recommends --no-install-suggests \
- git lsb-release
+# where build deps are installed for mongo-tools compilation
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ build-essential git lsb-release pkg-config libkrb5-dev \
+ && rm -rf /var/lib/apt/lists/*
RUN case "$INCLUDE_MONGODB_TOOLS" in \
true|yes|TRUE|YES) \
@@ -95,15 +105,13 @@ RUN case "$INCLUDE_MONGODB_TOOLS" in \
exit 1;; \
esac
-### STAGE 4 BUILDING.
-
-FROM debian:trixie-slim AS finalizer
+### STAGE 5 COMPILING LAYERS AND FORGING MESHCENTRAL STATE
-ARG NODEJS_DOWNLOAD="https://nodejs.org/dist/v24.12.0/node-v24.12.0-linux-x64.tar.xz"
+FROM node-base AS finalizer
# Copy files from previous layers
COPY --from=dep-compiler /opt/meshcentral/meshcentral /opt/meshcentral/meshcentral
-COPY --from=mongo-tools-compiler /mongo-tools/bin/ /tmp/bin/
+COPY --from=mongo-compiler /mongo-tools/bin/ /tmp/bin/
# environment variables
ENV NODE_ENV="production" \
@@ -165,12 +173,7 @@ ENV MARIADB_HOST="" \
WORKDIR /opt/meshcentral
-RUN apt-get update && \
- echo -e "----------\nINSTALLING DEBIAN PACKAGES...\n----------"; \
- apt-get install -y --no-install-recommends --no-install-suggests \
- bash ca-certificates curl jq nodejs npm tzdata && \
- npm install -g npm@latest && \
- rm -rfv \
+RUN rm -rfv \
/var/cache/* \
/usr/share/man/ \
/usr/share/doc/ \
@@ -182,12 +185,12 @@ RUN apt-get update && \
RUN case "$PREINSTALL_LIBS" in \
true|yes|TRUE|YES) \
cd meshcentral && \
- echo -e "----------\nPREINSTALLING LIBRARIES...\n----------"; \
- npm install ssh2@1.16.0 nodemailer@6.10.1 image-size@2.0.2 wildleek@2.0.0 otplib@12.0.1 yub@0.11.1;; \
+ echo "----------\nPREINSTALLING LIBRARIES...\n----------"; \
+ npm install ssh2@1.17.0 nodemailer@6.10.1 image-size@2.0.2 wildleek@2.0.0 yub@0.11.1;; \
false|no|FALSE|NO) \
echo "Not pre-installing libraries.";; \
*) \
- echo -e "Invalid value for build argument INCLUDE_POSTGRESQL_TOOLS, possible values: 'yes', 'true', 'no' or 'false'"; \
+ echo "Invalid value for build argument INCLUDE_POSTGRESQL_TOOLS, possible values: 'yes', 'true', 'no' or 'false'"; \
exit 1;; \
esac
@@ -196,7 +199,7 @@ RUN case "$INCLUDE_MONGODB_TOOLS" in \
true|yes|TRUE|YES) \
mv /tmp/bin/* /usr/bin; \
cd meshcentral && \
- echo -e "----------\nPREINSTALLING MONGODB LIBRARIES...\n----------"; \
+ echo "----------\nPREINSTALLING MONGODB LIBRARIES...\n----------"; \
npm install mongodb@4.17.2 @mongodb-js/saslprep@1.3.1;; \
false|no|FALSE|NO) \
echo "Not including MongoDB Tools.";; \
@@ -209,12 +212,12 @@ RUN case "$INCLUDE_POSTGRESQL_TOOLS" in \
true|yes|TRUE|YES) \
apt-get install -y --no-install-recommends --no-install-suggests postgresql-client-17; \
cd meshcentral && \
- echo -e "----------\nPREINSTALLING POSTGRESQL LIBRARIES...\n----------"; \
- npm install pg@8.14.1;; \
+ echo "----------\nPREINSTALLING POSTGRESQL LIBRARIES...\n----------"; \
+ npm install pg@8.16.3;; \
false|no|FALSE|NO) \
echo "Not including PostgreSQL Tools.";; \
*) \
- echo -e "Invalid value for build argument INCLUDE_POSTGRESQL_TOOLS, possible values: 'yes', 'true', 'no' or 'false'"; \
+ echo "Invalid value for build argument INCLUDE_POSTGRESQL_TOOLS, possible values: 'yes', 'true', 'no' or 'false'"; \
exit 1;; \
esac
@@ -222,12 +225,12 @@ RUN case "$INCLUDE_MARIADB_TOOLS" in \
true|yes|TRUE|YES) \
apt-get install -y --no-install-recommends --no-install-suggests default-mysql-client mariadb-client; \
cd meshcentral && \
- echo -e "----------\nPREINSTALLING MARIADB/MYSQL LIBRARIES...\n----------"; \
- npm install mariadb@3.4.0 mysql2@3.11.4;; \
+ echo "----------\nPREINSTALLING MARIADB/MYSQL LIBRARIES...\n----------"; \
+ npm install mariadb@3.4.5 mysql2@3.15.1;; \
false|no|FALSE|NO) \
echo "Not including MariaDB/MySQL Tools.";; \
*) \
- echo -e "Invalid value for build argument INCLUDE_MARIADB_TOOLS, possible values: 'yes', 'true', 'no' or 'false'"; \
+ echo "Invalid value for build argument INCLUDE_MARIADB_TOOLS, possible values: 'yes', 'true', 'no' or 'false'"; \
exit 1;; \
esac
diff --git a/docker/README.md b/docker/README.md
index be04ebd479..a072bbc370 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -61,6 +61,8 @@ Below is a breakdown of environment variables used in this setup.
| REVERSE_PROXY | "" | Configures reverse proxy support through `certUrl`. |
| REVERSE_PROXY_TLS_PORT | "443" | Configures reverse proxy TLS port, will be combined with: `REVERSE_PROXY`. |
| WEBRTC | false | Enables/disables WebRTC support. |
+| TRUSTED_PROXY | "" | Trust forwarded headers from these IPs or domains. |
+| TLS_OFFLOAD | false | When set to true, indicate that TLS is being performed by a device in front of MeshCentral. |
### Database Configuration
diff --git a/docker/compose.yaml b/docker/compose.yaml
index d0745f0326..a2040cee14 100644
--- a/docker/compose.yaml
+++ b/docker/compose.yaml
@@ -15,8 +15,8 @@ services:
- meshcentral-web:/opt/meshcentral/meshcentral-web
- meshcentral-backups:/opt/meshcentral/meshcentral-backups
ports:
- - ${PORT}:${PORT}
- - ${REDIR_PORT}:${REDIR_PORT}
+ - 443:443
+ - 80:80
volumes:
meshcentral-data:
meshcentral-files:
diff --git a/docker/config.json.template b/docker/config.json.template
index 588d2184b6..dfff6bdab4 100644
--- a/docker/config.json.template
+++ b/docker/config.json.template
@@ -13,7 +13,8 @@
"redirPort": 80,
"_redirAliasPort": 80,
"AgentPong": 300,
- "TLSOffload": false,
+ "tlsOffload": false,
+ "_trustedProxy": "",
"SelfUpdate": false,
"AllowFraming": false,
"WebRTC": false,
@@ -51,4 +52,4 @@
"_names": "myserver.mydomain.com",
"production": false
}
-}
\ No newline at end of file
+}
diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh
index f535b9d435..82afe304bc 100644
--- a/docker/entrypoint.sh
+++ b/docker/entrypoint.sh
@@ -1,12 +1,15 @@
#!/bin/bash
+echo "NodeJS: $(node -v)"
+echo "NPM: $(npm -v)"
+
# Origin: https://github.com/Melo-Professional/MeshCentral-Stylish-UI
stylishui_base_url="https://github.com/Melo-Professional/MeshCentral-Stylish-UI/archive/refs"
stylishui_compat="https://raw.githubusercontent.com/Melo-Professional/MeshCentral-Stylish-UI/refs/heads/main/metadata/compat.json"
function graceful_shutdown() {
echo "Received SIGTERM from the container host. Cleaning up..."
- kill -SIGINT $meshcentral_pid
+ kill -SIGINT "$meshcentral_pid"
echo "MeshCentral process stopped. Exiting..."
exit 0
@@ -14,8 +17,7 @@ function graceful_shutdown() {
trap graceful_shutdown SIGTERM
function test_url() {
- wget --spider $1 &> /dev/null
- if [[ $? -eq 0 ]]; then
+ if wget --spider "$1" &> /dev/null; then
echo "is ok."
return 0
else
@@ -24,10 +26,11 @@ function test_url() {
fi
}
+
function dynamic_config() {
# BEGIN DATABASE CONFIGURATION FIELDS
USE_MONGODB=${USE_MONGODB,,}
- if [[ $USE_MONGODB =~ ^(true|yes)$ ]]; then
+ if [[ "$USE_MONGODB" =~ ^(true|yes)$ ]]; then
echo "Enabling MongoDB-connector..."
if [[ -n "$MONGO_URL" ]]; then
@@ -47,7 +50,7 @@ function dynamic_config() {
fi
USE_POSTGRESQL=${USE_POSTGRESQL,,}
- if [[ $USE_POSTGRESQL =~ ^(true|yes)$ ]]; then
+ if [[ "$USE_POSTGRESQL" =~ ^(true|yes)$ ]]; then
echo "Enabling PostgreSQL-connector..."
sed -i 's/"_postgres"/"postgres"/' "$CONFIG_FILE"
@@ -68,7 +71,7 @@ function dynamic_config() {
fi
USE_MARIADB=${USE_MARIADB,,}
- if [[ $USE_MARIADB =~ ^(true|yes)$ ]]; then
+ if [[ "$USE_MARIADB" =~ ^(true|yes)$ ]]; then
echo "Enabling MariaDB-connector..."
sed -i 's/"_mariaDB"/"mariaDB"/' "$CONFIG_FILE"
jq --arg mariadb_host "$MARIADB_HOST" \
@@ -96,7 +99,7 @@ function dynamic_config() {
echo "If defaults are going to get applied, refer to: https://raw.githubusercontent.com/Ylianst/MeshCentral/master/meshcentral-config-schema.json"
# SESSIONKEY
- if [[ ${REGEN_SESSIONKEY,,} =~ ^(true|yes)$ ]]; then
+ if [[ "${REGEN_SESSIONKEY,,}" =~ ^(true|yes)$ ]]; then
echo "Regenerating Session-Key because REGENSESSIONKEY is 'true' or 'yes'"
SESSION_KEY=$(tr -dc 'A-Z0-9' < /dev/urandom | fold -w 96 | head -n 1)
@@ -109,50 +112,50 @@ function dynamic_config() {
fi
# HOSTNAME
- if [[ -n $HOSTNAME ]]; then
+ if [[ -n "$HOSTNAME" ]]; then
echo "Setting hostname (cert)... $HOSTNAME"
jq --arg hostname "$HOSTNAME" \
'.settings.cert = $hostname' \
"$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE"
else
- echo "Invalid or no hostname, defaulting to 'localhost', value given: $HOSTNAME"
+ echo "Invalid or no hostname, defaulting to 'localhost', value(s) given: ${HOSTNAME:-empty}"
jq --arg hostname "localhost" \
'.settings.cert = $hostname' \
"$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE"
fi
# PORT
- if [[ -n $PORT ]]; then
+ if [[ -n "$PORT" ]]; then
echo "Setting port... $PORT"
jq --arg port "$PORT" \
- '.settings.port = $port' \
+ '.settings.port = ($port|tonumber)' \
"$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE"
else
- echo "Invalid or no port, defaulting to '443', value given: $PORT"
+ echo "Invalid or no port, defaulting to '443', Value(s) given: ${PORT:-empty}"
jq --arg port "443" \
- '.settings.port = $port' \
+ '.settings.port = ($port|tonumber)' \
"$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE"
fi
# REDIR_PORT
- if [[ -n $REDIR_PORT ]]; then
+ if [[ -n "$REDIR_PORT" ]]; then
echo "Setting redirport... $REDIR_PORT"
jq --arg redirport "$REDIR_PORT" \
- '.settings.redirPort = $redirport' \
+ '.settings.redirPort = ($redirport|tonumber)' \
"$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE"
else
- echo "Invalid or no redirport, defaulting to '80', value given: $REDIR_PORT"
+ echo "Invalid or no redirport, defaulting to '80', Value(s) given: ${REDIR_PORT:-empty}"
jq --arg redirport "80" \
- '.settings.redirPort = $redirport' \
+ '.settings.redirPort = ($redirport|tonumber)' \
"$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE"
fi
# ALLOWPLUGINS
ALLOW_PLUGINS=${ALLOW_PLUGINS,,}
- if [[ $ALLOW_PLUGINS =~ ^(true|false)$ ]]; then
+ if [[ "$ALLOW_PLUGINS" =~ ^(true|false)$ ]]; then
echo "Setting plugins... $ALLOW_PLUGINS"
sed -i 's/"_plugins"/"plugins"/' "$CONFIG_FILE"
@@ -160,13 +163,13 @@ function dynamic_config() {
'.settings.plugins.enabled = $allow_plugins' \
"$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE"
else
- echo "Invalid or no ALLOWPLUGINS value given, commenting out so default applies... Value given: $ALLOW_PLUGINS"
+ echo "Invalid or no ALLOWPLUGINS value given, commenting out so default applies... Value(s) given: ${ALLOW_PLUGINS:-empty}"
sed -i 's/"plugins":/"_plugins":/g' "$CONFIG_FILE"
fi
# WEBRTC
WEBRTC=${WEBRTC,,}
- if [[ $WEBRTC =~ ^(true|false)$ ]]; then
+ if [[ "$WEBRTC" =~ ^(true|false)$ ]]; then
echo "Setting WebRTC... $WEBRTC"
sed -i 's/"_WebRTC"/"WebRTC"/' "$CONFIG_FILE"
@@ -175,13 +178,13 @@ function dynamic_config() {
"$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE"
#sed -i "s/\"WebRTC\": *[a-z]*/\"WebRTC\": $WEBRTC/" "$CONFIG_FILE"
else
- echo "Invalid or no WEBRTC value given, commenting out so default applies... Value given: $WEBRTC"
+ echo "Invalid or no WEBRTC value given, commenting out so default applies... Value(s) given: ${WEBRTC:-empty}"
sed -i 's/"WebRTC":/"_WebRTC":/g' "$CONFIG_FILE"
fi
# IFRAME
IFRAME=${IFRAME,,}
- if [[ $IFRAME =~ ^(true|false)$ ]]; then
+ if [[ "$IFRAME" =~ ^(true|false)$ ]]; then
echo "Setting AllowFraming... $IFRAME"
sed -i 's/"_AllowFraming"/"AllowFraming"/' "$CONFIG_FILE"
@@ -189,15 +192,44 @@ function dynamic_config() {
'.settings.AllowFraming = $allow_framing' \
"$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE"
else
- echo "Invalid or no IFRAME value given, commenting out so default applies... Value given: $IFRAME"
+ echo "Invalid or no IFRAME value given, commenting out so default applies... Value(s) given: ${IFRAME:-empty}"
sed -i 's/"AllowFraming":/"_AllowFraming":/g' "$CONFIG_FILE"
fi
+ # ALLOWED_ORIGIN
+ ALLOWED_ORIGIN=${ALLOWED_ORIGIN,,}
+ if [[ "$ALLOWED_ORIGIN" =~ ^(true|false)$ ]]; then
+ echo "Setting allowedOrigin... $ALLOWED_ORIGIN"
+
+ sed -i 's/"_allowedOrigin"/"allowedOrigin"/' "$CONFIG_FILE"
+ jq --argjson allowed_origin "$ALLOWED_ORIGIN" \
+ '.domains[""].allowedOrigin = $allowed_origin' \
+ "$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE"
+ else
+ echo "Invalid or no ALLOWED_ORIGIN value given, commenting out so default applies... Value(s) given: ${ALLOWED_ORIGIN:-empty}"
+ sed -i 's/"allowedOrigin":/"_allowedOrigin":/g' "$CONFIG_FILE"
+ fi
+
+ # certUrl || reverseProxy
+ if [[ -n "$REVERSE_PROXY" ]] && [[ -n "$REVERSE_PROXY_TLS_PORT" ]]; then
+ REVERSE_PROXY_STRING="${REVERSE_PROXY}:${REVERSE_PROXY_TLS_PORT}"
+
+ echo "Setting certUrl... - $REVERSE_PROXY_STRING"
+ sed -i 's/"_certUrl"/"certUrl"/' "$CONFIG_FILE"
+ jq --arg cert_url "$REVERSE_PROXY_STRING" \
+ '.domains[""].certUrl = $cert_url' \
+ "$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE"
+ #sed -i "s/\"certUrl\": *[a-z]*/\"certUrl\": $REVERSE_PROXY_STRING/" "$CONFIG_FILE"
+ else
+ echo "Invalid or no REVERSE_PROXY and/or REVERSE_PROXY_TLS_PORT value given, commenting out so default applies... Value(s) given: ${REVERSE_PROXY_STRING:-empty}"
+ sed -i 's/"certUrl":/"_certUrl":/g' "$CONFIG_FILE"
+ fi
+
# trustedProxy
- if [[ -n $TRUSTED_PROXY ]]; then
+ if [[ -n "$TRUSTED_PROXY" ]]; then
echo "Setting trustedProxy... - $TRUSTED_PROXY"
- if [[ $TRUSTED_PROXY == "all" ]] || [[ $TRUSTED_PROXY == "true" ]]; then
+ if [[ "$TRUSTED_PROXY" == "all" ]] || [[ "$TRUSTED_PROXY" == "true" ]]; then
sed -i 's/"_trustedProxy"/"trustedProxy"/' "$CONFIG_FILE"
jq --argjson trusted_proxy "true" \
'.settings.trustedProxy = $trusted_proxy' \
@@ -209,13 +241,26 @@ function dynamic_config() {
"$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE"
fi
else
- echo "Invalid or no REVERSE_PROXY and/or REVERSE_PROXY_TLS_PORT value given, commenting out so default applies... Value(s) given: $REVERSE_PROXY_STRING"
- sed -i 's/"certUrl":/"_certUrl":/g' "$CONFIG_FILE"
+ echo "Invalid or no TRUSTED_PROXY value given, commenting out so default applies... Value(s) given: ${TRUSTED_PROXY:-empty}"
+ sed -i 's/"trustedProxy":/"_trustedProxy":/g' "$CONFIG_FILE"
+ fi
+
+ # tlsOffload
+ if [[ -n "$TLS_OFFLOAD" ]]; then
+ echo "Setting tlsOffload... - $TLS_OFFLOAD"
+
+ sed -i 's/"_tlsOffload"/"tlsOffload"/' "$CONFIG_FILE"
+ jq --arg tls_offload "$TLS_OFFLOAD" \
+ '.settings.tlsOffload = $tls_offload' \
+ "$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE"
+ else
+ echo "Invalid or no TLS_OFFLOAD value given, commenting out so default applies... Value(s) given: ${TLS_OFFLOAD:-empty}"
+ sed -i 's/"tlsOffload":/"_tlsOffload":/g' "$CONFIG_FILE"
fi
# ALLOW_NEW_ACCOUNTS
ALLOW_NEW_ACCOUNTS=${ALLOW_NEW_ACCOUNTS,,}
- if [[ $ALLOW_NEW_ACCOUNTS =~ ^(true|false)$ ]]; then
+ if [[ "$ALLOW_NEW_ACCOUNTS" =~ ^(true|false)$ ]]; then
echo "Setting NewAccounts... $ALLOW_NEW_ACCOUNTS"
sed -i 's/"_NewAccounts"/"NewAccounts"/' "$CONFIG_FILE"
@@ -223,13 +268,13 @@ function dynamic_config() {
'.domains[""].NewAccounts = $new_accounts' \
"$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE"
else
- echo "Invalid or no ALLOW_NEW_ACCOUNTS value given, commenting out so default applies... Value given: $ALLOW_NEW_ACCOUNTS"
+ echo "Invalid or no ALLOW_NEW_ACCOUNTS value given, commenting out so default applies... Value(s) given: ${ALLOW_NEW_ACCOUNTS:-empty}"
sed -i 's/"NewAccounts":/"_NewAccounts":/g' "$CONFIG_FILE"
fi
# LOCALSESSIONRECORDING
LOCAL_SESSION_RECORDING=${LOCAL_SESSION_RECORDING,,}
- if [[ $LOCAL_SESSION_RECORDING =~ ^(true|false)$ ]]; then
+ if [[ "$LOCAL_SESSION_RECORDING" =~ ^(true|false)$ ]]; then
echo "Setting localSessionRecording... $LOCAL_SESSION_RECORDING"
sed -i 's/"_localSessionRecording"/"localSessionRecording"/' "$CONFIG_FILE"
@@ -237,13 +282,13 @@ function dynamic_config() {
'.domains[""].localSessionRecording = $session_recording' \
"$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE"
else
- echo "Invalid or no LOCALSESSIONRECORDING value given, commenting out so default applies... Value given: $LOCAL_SESSION_RECORDING"
+ echo "Invalid or no LOCALSESSIONRECORDING value given, commenting out so default applies... Value(s) given: ${LOCAL_SESSION_RECORDING:-empty}"
sed -i 's/"localSessionRecording":/"_localSessionRecording":/g' "$CONFIG_FILE"
fi
# MINIFY
MINIFY=${MINIFY,,}
- if [[ $MINIFY =~ ^(true|false)$ ]]; then
+ if [[ "$MINIFY" =~ ^(true|false)$ ]]; then
echo "Setting minify... $MINIFY"
sed -i 's/"_minify"/"minify"/' "$CONFIG_FILE"
@@ -252,46 +297,17 @@ function dynamic_config() {
"$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE"
#sed -i "s/\"minify\": *[a-z]*/\"minify\": $MINIFY/" "$CONFIG_FILE"
else
- echo "Invalid or no MINIFY value given, commenting out so default applies... Value given: $MINIFY"
+ echo "Invalid or no MINIFY value given, commenting out so default applies... Value(s) given: ${MINIFY:-empty}"
sed -i 's/"minify":/"_minify":/g' "$CONFIG_FILE"
fi
- # ALLOWED_ORIGIN
- ALLOWED_ORIGIN=${ALLOWED_ORIGIN,,}
- if [[ $ALLOWED_ORIGIN =~ ^(true|false)$ ]]; then
- echo "Setting allowedOrigin... $ALLOWED_ORIGIN"
-
- sed -i 's/"_allowedOrigin"/"allowedOrigin"/' "$CONFIG_FILE"
- jq --argjson allowed_origin "$ALLOWED_ORIGIN" \
- '.domains[""].allowedOrigin = $allowed_origin' \
- "$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE"
- else
- echo "Invalid or no ALLOWED_ORIGIN value given, commenting out so default applies... Value given: $ALLOWED_ORIGIN"
- sed -i 's/"allowedOrigin":/"_allowedOrigin":/g' "$CONFIG_FILE"
- fi
-
- # certUrl
- if [[ -n $REVERSE_PROXY ]] && [[ -n $REVERSE_PROXY_TLS_PORT ]]; then
- REVERSE_PROXY_STRING="${REVERSE_PROXY}:${REVERSE_PROXY_TLS_PORT}"
-
- echo "Setting certUrl... - $REVERSE_PROXY_STRING"
- sed -i 's/"_certUrl"/"certUrl"/' "$CONFIG_FILE"
- jq --arg cert_url "$REVERSE_PROXY_STRING" \
- '.domains[""].certUrl = $cert_url' \
- "$CONFIG_FILE" > temp_config.json && mv temp_config.json "$CONFIG_FILE"
- #sed -i "s/\"certUrl\": *[a-z]*/\"certUrl\": $REVERSE_PROXY_STRING/" "$CONFIG_FILE"
- else
- echo "Invalid or no REVERSE_PROXY and/or REVERSE_PROXY_TLS_PORT value given, commenting out so default applies... Value(s) given: $REVERSE_PROXY_STRING"
- sed -i 's/"certUrl":/"_certUrl":/g' "$CONFIG_FILE"
- fi
-
cat "$CONFIG_FILE"
}
function install_stylishui() {
# Start by testing if we can determine compatibility
printf "Testing compatibility schema URL..."
- if ! test_url $stylishui_compat; then
+ if ! test_url "$stylishui_compat"; then
echo "Compat URL failed."
return 1
fi
@@ -301,7 +317,7 @@ function install_stylishui() {
full_url="${stylishui_base_url}/heads/main.tar.gz"
else
# Retrieve the values we need to determine compatibility
- compat_data=$(curl -fsSL $stylishui_compat)
+ compat_data=$(curl -fsSL "$stylishui_compat")
meshcentral_version=$(jq -r '.version' /opt/meshcentral/meshcentral/package.json)
# Target the StylishUI version we need for our present Meshcentral version
compat_version=$(echo "$compat_data" | jq -r --arg mcv "$meshcentral_version" \
@@ -314,18 +330,18 @@ function install_stylishui() {
# Test if we can reach the data/content URL on github
printf "Testing content URL..."
- if ! test_url $full_url; then
+ if ! test_url "$full_url"; then
echo "StylishUI URL failed."
return 1
fi
# Lets download and install the UI
- wget -O /tmp/stylishui.tar.gz $full_url > /dev/null
+ wget -O /tmp/stylishui.tar.gz "$full_url" > /dev/null
tar -xzf /tmp/stylishui.tar.gz -C /tmp
web_folder=$(find /tmp -name meshcentral-web)
# Check if we have some integrity
- if [[ -z $web_folder ]]; then
+ if [[ -z "$web_folder" ]]; then
echo "Installation failed, cleaning..."
rm /tmp/stylishui*
return 1
@@ -365,12 +381,12 @@ fi
if [ -f "${CONFIG_FILE}" ]; then
echo "Pre-existing config found, not recreating..."
else
- if [ ! -d $(dirname "$CONFIG_FILE") ]; then
+ if [ ! -d "$(dirname "$CONFIG_FILE")" ]; then
echo "Creating meshcentral-data directory..."
mkdir -p /opt/meshcentral/meshcentral-data
fi
- echo "Placing template into the relevant directory: $(dirname $CONFIG_FILE)"
+ echo "Placing template into the relevant directory: $(dirname "$CONFIG_FILE")"
cp /opt/meshcentral/config.json.template "${CONFIG_FILE}"
fi
diff --git a/emails/translations/account-check_bs.html b/emails/translations/account-check_bs.html
deleted file mode 100644
index b568f6e2b9..0000000000
--- a/emails/translations/account-check_bs.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Potvrda e-pošte
-
\ No newline at end of file
diff --git a/emails/translations/account-check_bs.txt b/emails/translations/account-check_bs.txt
deleted file mode 100644
index 5b4ddb0fda..0000000000
--- a/emails/translations/account-check_bs.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Potvrda e-pošte
-Zdravo [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) vrši verifikaciju e-pošte. Idite na sljedeću vezu da dovršite proces:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Ako niste vi inicirali ovaj zahtjev, zanemarite ovu poruku.
\ No newline at end of file
diff --git a/emails/translations/account-check_cs.html b/emails/translations/account-check_cs.html
deleted file mode 100644
index 64112c51a1..0000000000
--- a/emails/translations/account-check_cs.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Ověření e-mailem
-
\ No newline at end of file
diff --git a/emails/translations/account-check_cs.txt b/emails/translations/account-check_cs.txt
deleted file mode 100644
index 6bf7299f7d..0000000000
--- a/emails/translations/account-check_cs.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Ověření e-mailem
-Ahoj [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) provádí ověření e-mailem. Chcete-li proces dokončit, přejděte na následující odkaz:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Pokud jste tento požadavek nezačali, ignorujte tento e-mail.
\ No newline at end of file
diff --git a/emails/translations/account-check_da.html b/emails/translations/account-check_da.html
deleted file mode 100644
index feb0437a0f..0000000000
--- a/emails/translations/account-check_da.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Email verificering
-
\ No newline at end of file
diff --git a/emails/translations/account-check_da.txt b/emails/translations/account-check_da.txt
deleted file mode 100644
index 3ee23dc77e..0000000000
--- a/emails/translations/account-check_da.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Email verificering
-Hej [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) udfører en e-mailbekræftelse. Gå til følgende link for at fuldføre processen:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Hvis du ikke startede denne anmodning, bedes du ignorere denne mail.
\ No newline at end of file
diff --git a/emails/translations/account-check_de.html b/emails/translations/account-check_de.html
deleted file mode 100644
index ec9a7d2e30..0000000000
--- a/emails/translations/account-check_de.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - E-Mail-Überprüfung
-
\ No newline at end of file
diff --git a/emails/translations/account-check_de.txt b/emails/translations/account-check_de.txt
deleted file mode 100644
index df7bc55ffc..0000000000
--- a/emails/translations/account-check_de.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - E-Mail-Überprüfung
-Hallo [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) führt eine E-Mail-Überprüfung durch. Klicken Sie auf den folgenden Link, um den Vorgang abzuschließen:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Wenn Sie diese Anfrage nicht initiiert haben, ignorieren Sie diese Mail bitte.
\ No newline at end of file
diff --git a/emails/translations/account-check_es.html b/emails/translations/account-check_es.html
deleted file mode 100644
index 709eba822a..0000000000
--- a/emails/translations/account-check_es.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Verificación de Correo Electrónico
-
\ No newline at end of file
diff --git a/emails/translations/account-check_es.txt b/emails/translations/account-check_es.txt
deleted file mode 100644
index 5be9acca5a..0000000000
--- a/emails/translations/account-check_es.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Verificación de Correo Electrónico
-Hola [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) esta realizando una comprobación del correo electrónico. Navega al siguiente enlace para completar el proceso:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Si tu no iniciaste este requerimiento, por favor ignora este correo.
\ No newline at end of file
diff --git a/emails/translations/account-check_fi.html b/emails/translations/account-check_fi.html
deleted file mode 100644
index 8f7caf7e88..0000000000
--- a/emails/translations/account-check_fi.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Sähköpostivarmistus
-
\ No newline at end of file
diff --git a/emails/translations/account-check_fi.txt b/emails/translations/account-check_fi.txt
deleted file mode 100644
index bde9a7a515..0000000000
--- a/emails/translations/account-check_fi.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Sähköpostivarmistus
-Hei [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) tarkistaa sähköpostiosoitetta. Seuraa linkkiä prosessin loppuun saattamiseksi:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Jos et suorittanut tätä pyyntöä, voit ohitaa tämän sähköpostin.
\ No newline at end of file
diff --git a/emails/translations/account-check_fr.html b/emails/translations/account-check_fr.html
deleted file mode 100644
index 82d7beff04..0000000000
--- a/emails/translations/account-check_fr.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Vérification E-mail
-
\ No newline at end of file
diff --git a/emails/translations/account-check_fr.txt b/emails/translations/account-check_fr.txt
deleted file mode 100644
index 1deeaafa15..0000000000
--- a/emails/translations/account-check_fr.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Vérification E-mail
-Bonjour [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) effectue une vérification par e-mail. Accédez au lien suivant pour terminer le processus:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Si vous n'avez pas initié cette demande, veuillez ignorer ce courrier.
\ No newline at end of file
diff --git a/emails/translations/account-check_hi.html b/emails/translations/account-check_hi.html
deleted file mode 100644
index e380c72144..0000000000
--- a/emails/translations/account-check_hi.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - ईमेल सत्यापन
-
\ No newline at end of file
diff --git a/emails/translations/account-check_hi.txt b/emails/translations/account-check_hi.txt
deleted file mode 100644
index b7a11ab342..0000000000
--- a/emails/translations/account-check_hi.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - ईमेल सत्यापन
-हाय [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) ई-मेल सत्यापन कर रहा है। प्रक्रिया को पूरा करने के लिए निम्नलिखित लिंक को बताएं:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-यदि आपने यह अनुरोध आरंभ नहीं किया है, तो कृपया इस मेल को अनदेखा करें।
\ No newline at end of file
diff --git a/emails/translations/account-check_hu.html b/emails/translations/account-check_hu.html
deleted file mode 100644
index ee47a1ca49..0000000000
--- a/emails/translations/account-check_hu.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Email megerősítés
-
\ No newline at end of file
diff --git a/emails/translations/account-check_hu.txt b/emails/translations/account-check_hu.txt
deleted file mode 100644
index 2e42035262..0000000000
--- a/emails/translations/account-check_hu.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Email megerősítés
-Tisztelt [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) e-mail ellenőrzést végez. A folyamat befejezéséhez lépjen a következő linkre:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Ha nem Ön kezdeményezte ezt a kérést, kérjük, hagyja figyelmen kívül ezt a levelet.
\ No newline at end of file
diff --git a/emails/translations/account-check_it.html b/emails/translations/account-check_it.html
deleted file mode 100644
index 065a7f7e73..0000000000
--- a/emails/translations/account-check_it.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Verifica email
-
\ No newline at end of file
diff --git a/emails/translations/account-check_it.txt b/emails/translations/account-check_it.txt
deleted file mode 100644
index b9184e525a..0000000000
--- a/emails/translations/account-check_it.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Verifica email
-Salve [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]] [[[URLARGS1]]]) sta eseguendo una verifica e-mail. Vai al seguente link per completare il processo:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Se non hai avviato questa richiesta, ignora questo messaggio.
\ No newline at end of file
diff --git a/emails/translations/account-check_ja.html b/emails/translations/account-check_ja.html
deleted file mode 100644
index 943e73e38a..0000000000
--- a/emails/translations/account-check_ja.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - メールの確認
-
\ No newline at end of file
diff --git a/emails/translations/account-check_ja.txt b/emails/translations/account-check_ja.txt
deleted file mode 100644
index 04db0e8ca0..0000000000
--- a/emails/translations/account-check_ja.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - メールの確認
-こんにちは[[[USERNAME]]]、[[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) は電子メールの検証を実行しています。プロセスを完了するには、次のリンクに移動します。
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-このリクエストを開始していない場合は、このメールを無視してください。
\ No newline at end of file
diff --git a/emails/translations/account-check_ko.html b/emails/translations/account-check_ko.html
deleted file mode 100644
index fe533bb53a..0000000000
--- a/emails/translations/account-check_ko.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Email 인증
-
\ No newline at end of file
diff --git a/emails/translations/account-check_ko.txt b/emails/translations/account-check_ko.txt
deleted file mode 100644
index f12607534f..0000000000
--- a/emails/translations/account-check_ko.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Email 인증
-안녕하세요, [[[USERNAME]]]님. [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]])은 이메일 검증을 위해 실행됩니다. 다음 링크로 이동하여 과정을 완료하십시오 :
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-이 요청을 시작하지 않은 경우, 이 메일을 무시하십시오.
\ No newline at end of file
diff --git a/emails/translations/account-check_nl.html b/emails/translations/account-check_nl.html
deleted file mode 100644
index 6b193429c3..0000000000
--- a/emails/translations/account-check_nl.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - E-mail Verificatie
-
-
-
-
- [[[SERVERNAME]]] - Verificatie
-
-
-
-
Hallo [[[USERNAME]]], [[[SERVERNAME]]] vraagt om e-mailverificatie, klik op de volgende link om het proces te voltooien.
-
- Klik hier om uw e-mailadres te verifiëren.
-
- Als u dit verzoek niet heeft ingediend, dan kunt u deze e-mail negeren.
-
\ No newline at end of file
diff --git a/emails/translations/account-check_nl.txt b/emails/translations/account-check_nl.txt
deleted file mode 100644
index c1f236ce7e..0000000000
--- a/emails/translations/account-check_nl.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - E-mail Verificatie
-Hallo [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) voert een e-mailverificatie uit. Ga naar de volgende link om het proces te voltooien:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Als u dit verzoek niet heeft ingediend, dan kunt u deze e-mail negeren.
\ No newline at end of file
diff --git a/emails/translations/account-check_pl.html b/emails/translations/account-check_pl.html
deleted file mode 100644
index 6d99d8746d..0000000000
--- a/emails/translations/account-check_pl.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Weryfikacja Email
-
\ No newline at end of file
diff --git a/emails/translations/account-check_pl.txt b/emails/translations/account-check_pl.txt
deleted file mode 100644
index 0cadc2891e..0000000000
--- a/emails/translations/account-check_pl.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Weryfikacja Email
-Cześć [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) przeprowadza weryfikację poczty elektronicznej. Kliknij na poniższy link, aby zakończyć proces:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Jeśli nie byłeś inicjatorem tej prośby, proszę zignorować ten email.
\ No newline at end of file
diff --git a/emails/translations/account-check_pt-br.html b/emails/translations/account-check_pt-br.html
deleted file mode 100644
index 17802e2e5b..0000000000
--- a/emails/translations/account-check_pt-br.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Verificação de e-mail
-
\ No newline at end of file
diff --git a/emails/translations/account-check_pt-br.txt b/emails/translations/account-check_pt-br.txt
deleted file mode 100644
index 7f0be27be1..0000000000
--- a/emails/translations/account-check_pt-br.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Verificação de e-mail
-Olá, [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]] [[[URLARGS1]]]) está realizando uma verificação de e-mail. Acesse o seguinte link para concluir o processo:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Se você não iniciou esta solicitação, ignore este e-mail.
\ No newline at end of file
diff --git a/emails/translations/account-check_pt.html b/emails/translations/account-check_pt.html
deleted file mode 100644
index 0caea85e5c..0000000000
--- a/emails/translations/account-check_pt.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Verificação de Email
-
\ No newline at end of file
diff --git a/emails/translations/account-check_pt.txt b/emails/translations/account-check_pt.txt
deleted file mode 100644
index 8371a6bf4d..0000000000
--- a/emails/translations/account-check_pt.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Verificação de Email
-Olá, [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) está realizando uma verificação de e-mail. Acesse o seguinte link para concluir o processo:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Se você não iniciou esta solicitação, ignore este e-mail.
\ No newline at end of file
diff --git a/emails/translations/account-check_ru.html b/emails/translations/account-check_ru.html
deleted file mode 100644
index 875dacac76..0000000000
--- a/emails/translations/account-check_ru.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - подтверждение по электронной почте
-
\ No newline at end of file
diff --git a/emails/translations/account-check_ru.txt b/emails/translations/account-check_ru.txt
deleted file mode 100644
index ab6dd187ea..0000000000
--- a/emails/translations/account-check_ru.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - подтверждение по электронной почте
-Здравствуйте, [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) выполняет проверку электронной почты. Для завершения процесса перейдите по следующей ссылке:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Если вы не инициировали этот запрос, игнорируйте это письмо.
\ No newline at end of file
diff --git a/emails/translations/account-check_sv.html b/emails/translations/account-check_sv.html
deleted file mode 100644
index dfa7812e74..0000000000
--- a/emails/translations/account-check_sv.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - E-postverifiering
-
\ No newline at end of file
diff --git a/emails/translations/account-check_sv.txt b/emails/translations/account-check_sv.txt
deleted file mode 100644
index e5f7be09c3..0000000000
--- a/emails/translations/account-check_sv.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - E-postverifiering
-Hej [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]] [[[[URLARGS1]]]) utför en e-postverifiering. Ta bort till följande länk för att slutföra processen:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Om du inte initierade denna begäran, ignorerar du det här meddelandet.
\ No newline at end of file
diff --git a/emails/translations/account-check_tr.html b/emails/translations/account-check_tr.html
deleted file mode 100644
index eb24fd9fb8..0000000000
--- a/emails/translations/account-check_tr.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - E-posta Doğrulaması
-
\ No newline at end of file
diff --git a/emails/translations/account-check_tr.txt b/emails/translations/account-check_tr.txt
deleted file mode 100644
index b8e675d924..0000000000
--- a/emails/translations/account-check_tr.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - E-posta Doğrulaması
-Merhaba [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) bir e-posta doğrulaması yapıyor. İşlemi tamamlamak için aşağıdaki bağlantıya gidin:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Bu isteği siz başlatmadıysanız, lütfen bu postayı dikkate almayın.
\ No newline at end of file
diff --git a/emails/translations/account-check_zh-chs.html b/emails/translations/account-check_zh-chs.html
deleted file mode 100644
index 49ebb48448..0000000000
--- a/emails/translations/account-check_zh-chs.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - 邮件验证
-
\ No newline at end of file
diff --git a/emails/translations/account-check_zh-chs.txt b/emails/translations/account-check_zh-chs.txt
deleted file mode 100644
index 38db4b5bc5..0000000000
--- a/emails/translations/account-check_zh-chs.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - 邮件验证
-嗨[[[USERNAME]]],[[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) 正在执行邮件验证。转到以下链接以完成该过程:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-如果您没有发起此请求,请忽略此邮件。
\ No newline at end of file
diff --git a/emails/translations/account-check_zh-cht.html b/emails/translations/account-check_zh-cht.html
deleted file mode 100644
index 4addc99d2c..0000000000
--- a/emails/translations/account-check_zh-cht.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - 電郵驗證
-
\ No newline at end of file
diff --git a/emails/translations/account-check_zh-cht.txt b/emails/translations/account-check_zh-cht.txt
deleted file mode 100644
index 9a7cb5f2b3..0000000000
--- a/emails/translations/account-check_zh-cht.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - 電郵驗證
-嗨[[[USERNAME]]],[[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) 正在執行電郵驗證。導航至以下鏈結以完成該過程:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-如果你沒有發起此請求,請不理此電郵。
\ No newline at end of file
diff --git a/emails/translations/account-invite_bs.html b/emails/translations/account-invite_bs.html
deleted file mode 100644
index 3ff65f3735..0000000000
--- a/emails/translations/account-invite_bs.html
+++ /dev/null
@@ -1,19 +0,0 @@
-[[[SERVERNAME]]] - Pozivnica za račun
-
-
-
-
- [[[SERVERNAME]]] - Pozivnica za račun
-
-
-
-
Za vas je kreiran nalog na serveru [[[SERVERNAME]]] , sada mu možete pristupiti sa:
-
- Korisničko ime: [[[ACCOUNTNAME]]]
- Lozinka: [[[PASSWORD]]]
-
- Srdačan pozdrav,
-
- [[[USERNAME]]]
-
-
\ No newline at end of file
diff --git a/emails/translations/account-invite_bs.txt b/emails/translations/account-invite_bs.txt
deleted file mode 100644
index d2fbb3dc64..0000000000
--- a/emails/translations/account-invite_bs.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-[[[SERVERNAME]]] - Pozivnica za račun
-Za vas je kreiran račun na serveru [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]), sada mu možete pristupiti sa korisničkim imenom "[[[ACCOUNTNAME]]]" i lozinka "[[[PASSWORD]]]".
-~
-Srdačan pozdrav,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/account-invite_cs.html b/emails/translations/account-invite_cs.html
deleted file mode 100644
index 056a73b665..0000000000
--- a/emails/translations/account-invite_cs.html
+++ /dev/null
@@ -1,19 +0,0 @@
-[[[SERVERNAME]]] - Pozvánka na účet
-
-
-
-
- [[[SERVERNAME]]] - Pozvánka na účet
-
-
-
-
Účet byl pro vás vytvořen na serveru [[[SERVERNAME]]] , nyní k němu máte přístup:
-
- Uživatelské jméno: [[[ACCOUNTNAME]]]
- Heslo: [[[PASSWORD]]]
-
- S pozdravem,
-
- [[[USERNAME]]]
-
-
\ No newline at end of file
diff --git a/emails/translations/account-invite_cs.txt b/emails/translations/account-invite_cs.txt
deleted file mode 100644
index 43c4db3267..0000000000
--- a/emails/translations/account-invite_cs.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-[[[SERVERNAME]]] - Pozvánka na účet
-Účet pro vás byl vytvořen na serveru [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]), nyní k němu můžete přistupovat pomocí uživatelského jména „[[[ACCOUNTNAME]]]“ “a hesla„ [[[PASSWORD]]] ".
-~
-S pozdravem,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/account-invite_da.html b/emails/translations/account-invite_da.html
deleted file mode 100644
index 867ae2c3db..0000000000
--- a/emails/translations/account-invite_da.html
+++ /dev/null
@@ -1,19 +0,0 @@
-[[[SERVERNAME]]] - Konto invitation
-
-
-
-
- [[[SERVERNAME]]] - Konto invitation
-
-
-
-
En konto blev oprettet til dig på serveren [[[SERVERNAME]]] du kan tilgå den nu med:
-
- Brugernavn: [[[ACCOUNTNAME]]]
- Adgangskode: [[[PASSWORD]]]
-
- Venlig hilsen,
-
- [[[USERNAME]]]
-
-
\ No newline at end of file
diff --git a/emails/translations/account-invite_da.txt b/emails/translations/account-invite_da.txt
deleted file mode 100644
index 0c31c92a27..0000000000
--- a/emails/translations/account-invite_da.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-[[[SERVERNAME]]] - Konto invitation
-En konto blev oprettet til dig på serveren [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]), du kan tilgå den nu med brugernavnet "[[[ACCOUNTNAME]]]" og adgangskode "[[[PASSWORD]]]".
-~
-Venlig hilsen,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/account-invite_de.html b/emails/translations/account-invite_de.html
deleted file mode 100644
index 96bfb7e7f8..0000000000
--- a/emails/translations/account-invite_de.html
+++ /dev/null
@@ -1,19 +0,0 @@
-[[[SERVERNAME]]] - Kontoeinladung
-
-
-
-
- [[[SERVERNAME]]] - Kontoeinladung
-
-
-
-
Auf dem Server wurde ein Konto für Sie erstellt [[[SERVERNAME]]] , Sie können jetzt darauf zugreifen mit:
-
- Benutzername: [[[ACCOUNTNAME]]]
- Passwort: [[[PASSWORD]]]
-
- Freundliche Grüße,
-
- [[[USERNAME]]]
-
-
\ No newline at end of file
diff --git a/emails/translations/account-invite_de.txt b/emails/translations/account-invite_de.txt
deleted file mode 100644
index eaed8c0667..0000000000
--- a/emails/translations/account-invite_de.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-[[[SERVERNAME]]] - Kontoeinladung
-Auf dem Server [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) wurde ein Konto für Sie erstellt, Sie können ab sofort mit dem Benutzernamen "[[[ACCOUNTNAME]]]" und dem Passwort "[[[PASSWORD]]]" darauf zugreifen.
-~
-Freundliche Grüße,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/account-invite_es.html b/emails/translations/account-invite_es.html
deleted file mode 100644
index b10e0a5668..0000000000
--- a/emails/translations/account-invite_es.html
+++ /dev/null
@@ -1,19 +0,0 @@
-[[[SERVERNAME]]] - Invitación de Cuenta
-
-
-
-
- [[[SERVERNAME]]] - Invitación de Cuenta
-
-
-
-
Una cuenta ha sido creada en tu servidor [[[SERVERNAME]]] , ahora puede acceder con:
-
- Nombre de Usuario: [[[ACCOUNTNAME]]]
- Contraseña: [[[PASSWORD]]]
-
- Atentamente,
-
- [[[USERNAME]]]
-
-
\ No newline at end of file
diff --git a/emails/translations/account-invite_es.txt b/emails/translations/account-invite_es.txt
deleted file mode 100644
index 69d6490037..0000000000
--- a/emails/translations/account-invite_es.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-[[[SERVERNAME]]] - Invitación de Cuenta
-Una cuenta ha sido creada en tu servidor [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]), ahora puedes acceder con el usuario "[[[ACCOUNTNAME]]]" y la contraseña "[[[PASSWORD]]]".
-~
-Atentamente,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/account-invite_fi.html b/emails/translations/account-invite_fi.html
deleted file mode 100644
index b779860625..0000000000
--- a/emails/translations/account-invite_fi.html
+++ /dev/null
@@ -1,19 +0,0 @@
-[[[SERVERNAME]]] - Tili kutsu
-
-
-
-
- [[[SERVERNAME]]] - Tili kutsu
-
-
-
-
Sinulle on luotu tili palvelimelle [[[SERVERNAME]]] , voit käyttää sitä nyt:
-
- Käyttäjätunnus: [[[ACCOUNTNAME]]]
- Salasana: [[[PASSWORD]]]
-
- Ystävällisin terveisin,
-
- [[[USERNAME]]]
-
-
\ No newline at end of file
diff --git a/emails/translations/account-invite_fi.txt b/emails/translations/account-invite_fi.txt
deleted file mode 100644
index bb9531207d..0000000000
--- a/emails/translations/account-invite_fi.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-[[[SERVERNAME]]] - Tili kutsu
-Sinulle on luotu tili palvelimelle [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]), voit käyttää sitä nyt käyttäjätunnuksella "[[[ACCOUNTNAME]]]" and salasanalla "[[[PASSWORD]]]".
-~
-Ystävällisin terveisin,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/account-invite_fr.html b/emails/translations/account-invite_fr.html
deleted file mode 100644
index 2eb85ab02d..0000000000
--- a/emails/translations/account-invite_fr.html
+++ /dev/null
@@ -1,19 +0,0 @@
-[[[SERVERNAME]]] - Invitation au compte
-
-
-
-
- [[[SERVERNAME]]] - Invitation au compte
-
-
-
-
Un compte a été créé pour vous sur le serveur [[[SERVERNAME]]] , vous pouvez y accéder maintenant avec :
-
- Nom d'utilisateur: [[[ACCOUNTNAME]]]
- Mot de passe: [[[PASSWORD]]]
-
- Meilleures salutations,
-
- [[[USERNAME]]]
-
-
\ No newline at end of file
diff --git a/emails/translations/account-invite_fr.txt b/emails/translations/account-invite_fr.txt
deleted file mode 100644
index 2b2b6a0932..0000000000
--- a/emails/translations/account-invite_fr.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-[[[SERVERNAME]]] - Invitation au compte
-Un compte a été créé pour vous sur le serveur [[[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]), vous pouvez y accéder maintenant avec le nom d'utilisateur "[[[ACCOUNTNAME]]]" et le mot de passe "[[[PASSWORD]]]".
-~
-Meilleures salutations,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/account-invite_hi.html b/emails/translations/account-invite_hi.html
deleted file mode 100644
index b1d529f718..0000000000
--- a/emails/translations/account-invite_hi.html
+++ /dev/null
@@ -1,19 +0,0 @@
-[[[SERVERNAME]]] - खाता निमंत्रण
-
-
-
-
- [[[SERVERNAME]]] - खाता निमंत्रण
-
-
-
-
सर्वर पर आपके लिए एक खाता बनाया गया था [[[SERVERNAME]]] , आप इसे अभी एक्सेस कर सकते हैं:
-
- उपयोगकर्ता नाम: [[[ACCOUNTNAME]]]
- कुंजिका: [[[PASSWORD]]]
-
- सादर,
-
- [[[USERNAME]]]
-
-
\ No newline at end of file
diff --git a/emails/translations/account-invite_hi.txt b/emails/translations/account-invite_hi.txt
deleted file mode 100644
index f1e09f0f0d..0000000000
--- a/emails/translations/account-invite_hi.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-[[[SERVERNAME]]] - खाता निमंत्रण
-सर्वर [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) पर आपके लिए एक खाता बनाया गया था, आप इसे अब उपयोगकर्ता नाम "[[[ACCOUNTNAME]]]]" और पासवर्ड "[[[PASSWORD]]]" के साथ एक्सेस कर सकते हैं।।
-~
-सादर,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/account-invite_hu.html b/emails/translations/account-invite_hu.html
deleted file mode 100644
index b615171009..0000000000
--- a/emails/translations/account-invite_hu.html
+++ /dev/null
@@ -1,19 +0,0 @@
-[[[SERVERNAME]]] - Fiók meghívó
-
-
-
-
- [[[SERVERNAME]]] - Fiók meghívó
-
-
-
-
Létrehozásra került Önnek egy fiók a kiszolgálón [[[SERVERNAME]]] , most a következőkkel érheti el:
-
- Felhasználó név: [[[ACCOUNTNAME]]]
- Jelszó: [[[PASSWORD]]]
-
- Üdvözlettel,
-
- [[[USERNAME]]]
-
-
\ No newline at end of file
diff --git a/emails/translations/account-invite_hu.txt b/emails/translations/account-invite_hu.txt
deleted file mode 100644
index 6b85d933ec..0000000000
--- a/emails/translations/account-invite_hu.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-[[[SERVERNAME]]] - Fiók meghívó
-A [[[SERVERNAME]]] szerveren ([[[SERVERURL]]]/[[[URLARGS1]]]) létrehozásra került egy fiókot az Ön számára, amelyhez most már hozzáférhet a "[[[[ACCOUNTNAME]]]" felhasználónévvel és a "[[[[PASSWORD]]]" jelszóval.
-~
-Üdvözlettel,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/account-invite_it.html b/emails/translations/account-invite_it.html
deleted file mode 100644
index 08fcdd876d..0000000000
--- a/emails/translations/account-invite_it.html
+++ /dev/null
@@ -1,19 +0,0 @@
-[[[SERVERNAME]]] - Invito per l'account
-
-
-
-
- [[[SERVERNAME]]] - Invito per l'account
-
-
-
-
È stato creato un account per te sul server [[[SERVERNAME]]] , puoi accedervi ora con
-
- Nome utente: [[[ACCOUNTNAME]]]
- Password: [[[PASSWORD]]]
-
- I migliori saluti,
-
- [[[USERNAME]]]
-
-
\ No newline at end of file
diff --git a/emails/translations/account-invite_it.txt b/emails/translations/account-invite_it.txt
deleted file mode 100644
index 0e9242cbf6..0000000000
--- a/emails/translations/account-invite_it.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-[[[SERVERNAME]]] - Invito per l'account
-È stato creato un account per te sul server [[[SERVERNAME]]] ([[[SERVERURL]]] / [[[URLARGS1]]]), puoi accedervi ora con il nome utente "[[[ACCOUNTNAME]]]" e password "[[[PASSWORD]]]".
-~
-I migliori saluti,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/account-invite_ja.html b/emails/translations/account-invite_ja.html
deleted file mode 100644
index 65af4f1941..0000000000
--- a/emails/translations/account-invite_ja.html
+++ /dev/null
@@ -1,19 +0,0 @@
-[[[SERVERNAME]]] - アカウントの招待
-
-
-
-
- [[[SERVERNAME]]] - アカウントの招待
-
-
-
-
サーバー上にアカウントが作成されました [[[SERVERNAME]]] 、あなたは今それを使ってそれにアクセスできます:
-
- ユーザー名: [[[ACCOUNTNAME]]]
- パスワード: [[[PASSWORD]]]
-
- 宜しくお願いします、
-
- [[[USERNAME]]]
-
-
\ No newline at end of file
diff --git a/emails/translations/account-invite_ja.txt b/emails/translations/account-invite_ja.txt
deleted file mode 100644
index b689fdb233..0000000000
--- a/emails/translations/account-invite_ja.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-[[[SERVERNAME]]] - アカウントの招待
-サーバー[[[SERVERNAME]]]([[[SERVERURL]]]/[[[URLARGS1]]])にアカウントが作成されました。ユーザー名 "[[[ACCOUNTNAME]]]"とパスワード "[[[PASSWORD] ]] "。
-~
-宜しくお願いします、
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/account-invite_ko.html b/emails/translations/account-invite_ko.html
deleted file mode 100644
index e04faafa0e..0000000000
--- a/emails/translations/account-invite_ko.html
+++ /dev/null
@@ -1,19 +0,0 @@
-[[[SERVERNAME]]] - 계정 초대
-
-
-
-
- [[[SERVERNAME]]] - 계정 초대
-
-
-
-
당신을 위해 서버에서 한 계정이 생성되었습니다. [[[SERVERNAME]]] 이제 당신은 다음으로 접근 가능합니다:
-
- 사용자 이름: [[[ACCOUNTNAME]]]
- 암호: [[[PASSWORD]]]
-
- 최고의 안부를 전합니다,
-
- [[[USERNAME]]]
-
-
\ No newline at end of file
diff --git a/emails/translations/account-invite_ko.txt b/emails/translations/account-invite_ko.txt
deleted file mode 100644
index 348270a5b9..0000000000
--- a/emails/translations/account-invite_ko.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-[[[SERVERNAME]]] - 계정 초대
-당신을 위해 서버에서 한 계정이 생성되었습니다 : [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]), 이제 당신은 다음 이름으로 접근 가능합니다 : "[[[ACCOUNTNAME]]]" 그리고 비밀번호는 다음과 같습니다 : "[[[PASSWORD]]]".
-~
-최고의 안부를 전합니다,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/account-invite_nl.html b/emails/translations/account-invite_nl.html
deleted file mode 100644
index 814f1532ce..0000000000
--- a/emails/translations/account-invite_nl.html
+++ /dev/null
@@ -1,19 +0,0 @@
-[[[SERVERNAME]]] - Account uitnodiging
-
-
-
-
- [[[SERVERNAME]]] - Account uitnodiging
-
-
-
-
Er is een account voor je aangemaakt op de server [[[SERVERNAME]]] , je hebt er nu toegang toe met:
-
- Gebruikersnaam: [[[ACCOUNTNAME]]]
- Wachtwoord: [[[PASSWORD]]]
-
- Vriendelijke groeten,
-
- [[[USERNAME]]]
-
-
\ No newline at end of file
diff --git a/emails/translations/account-invite_nl.txt b/emails/translations/account-invite_nl.txt
deleted file mode 100644
index 6de3dff1a1..0000000000
--- a/emails/translations/account-invite_nl.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-[[[SERVERNAME]]] - Account uitnodiging
-Er is een account jouw aangemaakt op de server [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]), Je kan inloggen met de gebruikersnaam "[[[ACCOUNTNAME]]]" en wachtwoord "[[[PASSWORD]]]".
-~
-Vriendelijke groeten,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/account-invite_pl.html b/emails/translations/account-invite_pl.html
deleted file mode 100644
index daa59a3144..0000000000
--- a/emails/translations/account-invite_pl.html
+++ /dev/null
@@ -1,19 +0,0 @@
-[[[SERVERNAME]]] - Zaproszenie Do Założenia Konta
-
-
-
-
- [[[SERVERNAME]]] - Zaproszenie Do Założenia Konta
-
-
-
-
Konto zostało utworzone dla Ciebie na serwerze [[[SERVERNAME]]] , możesz uzyskać dostęp przez:
-
- Nazwa użytkownika: [[[ACCOUNTNAME]]]
- Hasło: [[[PASSWORD]]]
-
- Z wyrazami szacunku,
-
- [[[USERNAME]]]
-
-
\ No newline at end of file
diff --git a/emails/translations/account-invite_pl.txt b/emails/translations/account-invite_pl.txt
deleted file mode 100644
index aab5ab4ed0..0000000000
--- a/emails/translations/account-invite_pl.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-[[[SERVERNAME]]] - Zaproszenie Do Założenia Konta
-Konto zostało utworzone dla Ciebie na serwerze [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]), możesz uzyskać do niego dostęp podając nazwę użytkownika "[[[ACCOUNTNAME]]]" i hasło "[[[PASSWORD]]]".
-~
-Z wyrazami szacunku,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/account-invite_pt-br.html b/emails/translations/account-invite_pt-br.html
deleted file mode 100644
index 302ada86b9..0000000000
--- a/emails/translations/account-invite_pt-br.html
+++ /dev/null
@@ -1,19 +0,0 @@
-[[[SERVERNAME]]] - Convite para conta
-
-
-
-
- [[[SERVERNAME]]] - Convite para conta
-
-
-
-
Uma conta foi criada para você no servidor [[[SERVERNAME]]] , você pode acessá-lo agora com:
-
- Nome do usuário: [[[ACCOUNTNAME]]]
- Senha: [[[PASSWORD]]]
-
- Atenciosamente,
-
- [[[USERNAME]]]
-
-
\ No newline at end of file
diff --git a/emails/translations/account-invite_pt-br.txt b/emails/translations/account-invite_pt-br.txt
deleted file mode 100644
index be1d1e3c1d..0000000000
--- a/emails/translations/account-invite_pt-br.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-[[[SERVERNAME]]] - Convite para conta
-Uma conta foi criada para você no servidor [[[SERVERNAME]]] ([[[SERVERURL]]] / [[[URLARGS1]]]), você pode acessá-la agora com o nome de usuário "[[[ACCOUNTNAME]]]" e senha "[[[PASSWORD]]]".
-~
-Atenciosamente,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/account-invite_pt.html b/emails/translations/account-invite_pt.html
deleted file mode 100644
index d804ac67a7..0000000000
--- a/emails/translations/account-invite_pt.html
+++ /dev/null
@@ -1,19 +0,0 @@
-[[[SERVERNAME]]] - Convite para conta
-
-
-
-
- [[[SERVERNAME]]] - Convite para conta
-
-
-
-
Uma conta foi criada para você no servidor [[[SERVERNAME]]] , pode aceder agora com:
-
- Nome de usuário: [[[ACCOUNTNAME]]]
- Senha: [[[PASSWORD]]]
-
- Cumprimentos,
-
- [[[USERNAME]]]
-
-
\ No newline at end of file
diff --git a/emails/translations/account-invite_pt.txt b/emails/translations/account-invite_pt.txt
deleted file mode 100644
index b9d0e618a0..0000000000
--- a/emails/translations/account-invite_pt.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-[[[SERVERNAME]]] - Convite para conta
-Uma conta foi criada para você no servidor [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]), você pode acessá-la agora com o nome de usuário "[[[ACCOUNTNAME]]]" e a senha "[[[PASSWORD] ]] ".
-~
-Cumprimentos,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/account-invite_ru.html b/emails/translations/account-invite_ru.html
deleted file mode 100644
index 308e6a82ea..0000000000
--- a/emails/translations/account-invite_ru.html
+++ /dev/null
@@ -1,19 +0,0 @@
-[[[SERVERNAME]]] - приглашение в аккаунт
-
-
-
-
- [[[SERVERNAME]]] - приглашение в аккаунт
-
-
-
-
Для вас была создана учетная запись на сервере [[[SERVERNAME]]] Вы можете получить к нему доступ сейчас:
-
- Имя пользователя: [[[ACCOUNTNAME]]]
- Пароль: [[[PASSWORD]]]
-
- С уважением,
-
- [[[USERNAME]]]
-
-
\ No newline at end of file
diff --git a/emails/translations/account-invite_ru.txt b/emails/translations/account-invite_ru.txt
deleted file mode 100644
index b9814a0289..0000000000
--- a/emails/translations/account-invite_ru.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-[[[SERVERNAME]]] - приглашение в аккаунт
-Для вас на сервере [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) была создана учетная запись, теперь вы можете получить к ней доступ с именем пользователя «[[[ACCOUNTNAME]]]» и паролем «[[[PASSWORD]]]".
-~
-С уважением,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/account-invite_sv.html b/emails/translations/account-invite_sv.html
deleted file mode 100644
index 3dd6c3e34a..0000000000
--- a/emails/translations/account-invite_sv.html
+++ /dev/null
@@ -1,19 +0,0 @@
-[[[SERVERNAME]]] - Kontoinbjudan
-
-
-
-
- [[[SERVERNAME]]] - Kontoinbjudan
-
-
-
-
Ett konto skapades åt dig på servern [[[SERVERNAME]]] kan du komma åt det nu med:
-
- Användarnamn: [[[ACCOUNTNAME]]]
- Lösenord: [[[PASSWORD]]]
-
- Vänliga hälsningar,
-
- [[[USERNAME]]]
-
-
\ No newline at end of file
diff --git a/emails/translations/account-invite_sv.txt b/emails/translations/account-invite_sv.txt
deleted file mode 100644
index e95330d9b1..0000000000
--- a/emails/translations/account-invite_sv.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-[[[SERVERNAME]]] - Kontoinbjudan
-Ett konto skapades åt dig på servern [[[SERVERNAME]]] ([[[SERVERURL]]] / [[[URLARGS1]]], du kan komma åt det nu med användarnamnet "[[[ACCOUNTNAME]]]" och lösenord "[[[PASSWORD]]]".
-~
-Vänliga hälsningar,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/account-invite_tr.html b/emails/translations/account-invite_tr.html
deleted file mode 100644
index 7ba1915f07..0000000000
--- a/emails/translations/account-invite_tr.html
+++ /dev/null
@@ -1,19 +0,0 @@
-[[[SERVERNAME]]] - Hesap Davetiyesi
-
-
-
-
- [[[SERVERNAME]]] - Hesap Davetiyesi
-
-
-
-
Sunucuda sizin için bir hesap oluşturuldu [[[SERVERNAME]]] , şimdi şununla erişebilirsiniz:
-
- Kullanıcı adı: [[[ACCOUNTNAME]]]
- Parola: [[[PASSWORD]]]
-
- Saygılarımla,
-
- [[[USERNAME]]]
-
-
\ No newline at end of file
diff --git a/emails/translations/account-invite_tr.txt b/emails/translations/account-invite_tr.txt
deleted file mode 100644
index 49e30567f9..0000000000
--- a/emails/translations/account-invite_tr.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-[[[SERVERNAME]]] - Hesap Davetiyesi
-[[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) sunucusunda sizin için bir hesap oluşturuldu, şimdi "[[[ACCOUNTNAME]]]" kullanıcı adı ve "[[[PASSWORD]]]" ile bu hesaba erişebilirsiniz.
-~
-Saygılarımla,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/account-invite_zh-chs.html b/emails/translations/account-invite_zh-chs.html
deleted file mode 100644
index 256c34e484..0000000000
--- a/emails/translations/account-invite_zh-chs.html
+++ /dev/null
@@ -1,19 +0,0 @@
-[[[SERVERNAME]]] - 帐户邀请
-
-
-
-
- [[[SERVERNAME]]] - 帐户邀请
-
-
-
-
在服务器上为您创建了一个帐户 [[[SERVERNAME]]] ,您现在可以通过以下方式访问它:
-
- 用户名: [[[ACCOUNTNAME]]]
- 密码: [[[PASSWORD]]]
-
- 最好的祝福,
-
- [[[USERNAME]]]
-
-
\ No newline at end of file
diff --git a/emails/translations/account-invite_zh-chs.txt b/emails/translations/account-invite_zh-chs.txt
deleted file mode 100644
index 89ea6e62e1..0000000000
--- a/emails/translations/account-invite_zh-chs.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-[[[SERVERNAME]]] - 帐户邀请
-已在服务器[[[SERVERNAME]]]([[[SERVERURL]]]/[[[URLARGS1]]])上为您创建了一个帐户,您现在可以使用用户名“ [[[ACCOUNTNAME]]]”和密码“ [[[PASSWORD]]]”。
-~
-最好的祝福,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/account-invite_zh-cht.html b/emails/translations/account-invite_zh-cht.html
deleted file mode 100644
index 3b7f71c670..0000000000
--- a/emails/translations/account-invite_zh-cht.html
+++ /dev/null
@@ -1,19 +0,0 @@
-[[[SERVERNAME]]] - 帳戶邀請
-
-
-
-
- [[[SERVERNAME]]] - 帳戶邀請
-
-
-
-
在伺服器上為你創建了一個帳戶 [[[SERVERNAME]]] ,你現在可以通過以下方式訪問它:
-
- 用戶名: [[[ACCOUNTNAME]]]
- 密碼: [[[PASSWORD]]]
-
- 最好的祝福,
-
- [[[USERNAME]]]
-
-
\ No newline at end of file
diff --git a/emails/translations/account-invite_zh-cht.txt b/emails/translations/account-invite_zh-cht.txt
deleted file mode 100644
index 5af1127485..0000000000
--- a/emails/translations/account-invite_zh-cht.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-[[[SERVERNAME]]] - 帳戶邀請
-在伺服器[[[SERVERNAME]]]([[[SERVERURL]]]/[[[URLARGS1]]])上為你創建了一個帳戶,你現在可以使用用戶名“ [[[ACCOUNTNAME]]]”和密碼“ [[[PASSWORD]]]”。
-~
-最好的祝福,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/account-login_bs.html b/emails/translations/account-login_bs.html
deleted file mode 100644
index 0a65c8e75f..0000000000
--- a/emails/translations/account-login_bs.html
+++ /dev/null
@@ -1,12 +0,0 @@
-[[[SERVERNAME]]] - Prijava na račun
-
-
-
-
- [[[SERVERNAME]]] - Prijava na račun
-
-
-
-
Vaš token za prijavu je: [[[TOKEN]]]
-
Ovaj token se može koristiti samo jednom i vrijedi 5 minuta.
-
\ No newline at end of file
diff --git a/emails/translations/account-login_bs.txt b/emails/translations/account-login_bs.txt
deleted file mode 100644
index 5d3ae8a86b..0000000000
--- a/emails/translations/account-login_bs.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-[[[SERVERNAME]]] - Prijava na račun
-Vaš token za prijavu je: [[[TOKEN]]]
-~
-Ovaj token se može koristiti samo jednom i vrijedi 5 minuta.
\ No newline at end of file
diff --git a/emails/translations/account-login_cs.html b/emails/translations/account-login_cs.html
deleted file mode 100644
index 00c8f5e4d1..0000000000
--- a/emails/translations/account-login_cs.html
+++ /dev/null
@@ -1,12 +0,0 @@
-[[[SERVERNAME]]] - Přihlášení k účtu
-
-
-
-
- [[[SERVERNAME]]] - Přihlášení k účtu
-
-
-
-
Váš přihlašovací token je: [[[TOKEN]]]
-
Tento token lze použít pouze jednou a je platný po dobu 5 minut.
-
\ No newline at end of file
diff --git a/emails/translations/account-login_cs.txt b/emails/translations/account-login_cs.txt
deleted file mode 100644
index 266ad4504c..0000000000
--- a/emails/translations/account-login_cs.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-[[[SERVERNAME]]] - Přihlášení k účtu
-Váš přihlašovací token je: [[[TOKEN]]]
-~
-Tento token lze použít pouze jednou a je platný po dobu 5 minut.
\ No newline at end of file
diff --git a/emails/translations/account-login_da.html b/emails/translations/account-login_da.html
deleted file mode 100644
index ea3efa099d..0000000000
--- a/emails/translations/account-login_da.html
+++ /dev/null
@@ -1,12 +0,0 @@
-[[[SERVERNAME]]] - Konto login
-
-
-
-
- [[[SERVERNAME]]] - Konto login
-
-
-
-
Dit login-token er: [[[TOKEN]]]
-
Denne token kan kun bruges én gang og er gyldig i 5 minutter.
-
\ No newline at end of file
diff --git a/emails/translations/account-login_da.txt b/emails/translations/account-login_da.txt
deleted file mode 100644
index f6a8b44f19..0000000000
--- a/emails/translations/account-login_da.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-[[[SERVERNAME]]] - Konto login
-Dit login-token er: [[[TOKEN]]]
-~
-Denne token kan kun bruges én gang og er gyldig i 5 minutter.
\ No newline at end of file
diff --git a/emails/translations/account-login_de.html b/emails/translations/account-login_de.html
deleted file mode 100644
index 9047666c00..0000000000
--- a/emails/translations/account-login_de.html
+++ /dev/null
@@ -1,12 +0,0 @@
-[[[SERVERNAME]]] - Konto-Login
-
-
-
-
- [[[SERVERNAME]]] - Konto-Login
-
-
-
-
Ihr Anmelde-Token lautet: [[[TOKEN]]]
-
Dieser Token kann nur einmal verwendet werden und ist 5 Minuten gültig.
-
\ No newline at end of file
diff --git a/emails/translations/account-login_de.txt b/emails/translations/account-login_de.txt
deleted file mode 100644
index 0f97eb947b..0000000000
--- a/emails/translations/account-login_de.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-[[[SERVERNAME]]] - Konto-Login
-Ihr Anmelde-Token lautet: [[[TOKEN]]]
-~
-Dieser Token kann nur einmal verwendet werden und ist 5 Minuten gültig.
\ No newline at end of file
diff --git a/emails/translations/account-login_es.html b/emails/translations/account-login_es.html
deleted file mode 100644
index 20f603a13a..0000000000
--- a/emails/translations/account-login_es.html
+++ /dev/null
@@ -1,12 +0,0 @@
-[[[SERVERNAME]]] - Inicio de Sesión
-
-
-
-
- [[[SERVERNAME]]] - Inicio de Sesión
-
-
-
-
Tu token de inicio de sesión es: [[[TOKEN]]]
-
Este token sólo se puede usar una vez y es válido durante 5 minutos.
-
\ No newline at end of file
diff --git a/emails/translations/account-login_es.txt b/emails/translations/account-login_es.txt
deleted file mode 100644
index 290ecb94f2..0000000000
--- a/emails/translations/account-login_es.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-[[[SERVERNAME]]] - Inicio de Sesión
-Tu token de inicio de sesión es: [[[TOKEN]]]
-~
-Este token sólo se puede usar una vez y es válido durante 5 minutos.
\ No newline at end of file
diff --git a/emails/translations/account-login_fi.html b/emails/translations/account-login_fi.html
deleted file mode 100644
index 3dc496ea89..0000000000
--- a/emails/translations/account-login_fi.html
+++ /dev/null
@@ -1,12 +0,0 @@
-[[[SERVERNAME]]] - Tilille Kirjautuminen
-
-
-
-
- [[[SERVERNAME]]] - Tilille Kirjautuminen
-
-
-
-
Kirjautumistunnuksesi on: [[[TOKEN]]]
-
Tätä tunnusta voidaan käyttää vain kerran ja se on voimassa 5 minuuttia.
-
\ No newline at end of file
diff --git a/emails/translations/account-login_fi.txt b/emails/translations/account-login_fi.txt
deleted file mode 100644
index 87ec2ff3ad..0000000000
--- a/emails/translations/account-login_fi.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-[[[SERVERNAME]]] - Tilille Kirjautuminen
-Kirjautumistunnuksesi on: [[[TOKEN]]]
-~
-Tätä tunnusta voidaan käyttää vain kerran ja se on voimassa 5 minuuttia.
\ No newline at end of file
diff --git a/emails/translations/account-login_fr.html b/emails/translations/account-login_fr.html
deleted file mode 100644
index 16c14f7df3..0000000000
--- a/emails/translations/account-login_fr.html
+++ /dev/null
@@ -1,12 +0,0 @@
-[[[SERVERNAME]]] - Connexion au compte
-
-
-
-
- [[[SERVERNAME]]] - Connexion au compte
-
-
-
-
Votre jeton de connexion est: [[[TOKEN]]]
-
Ce jeton ne peut être utilisé qu'une seule fois et est valide pendant 5 minutes.
-
\ No newline at end of file
diff --git a/emails/translations/account-login_fr.txt b/emails/translations/account-login_fr.txt
deleted file mode 100644
index c1813f23dc..0000000000
--- a/emails/translations/account-login_fr.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-[[[SERVERNAME]]] - Connexion au compte
-Votre jeton de connexion est: [[[TOKEN]]]
-~
-Ce jeton ne peut être utilisé qu'une seule fois et est valide pendant 5 minutes.
\ No newline at end of file
diff --git a/emails/translations/account-login_hi.html b/emails/translations/account-login_hi.html
deleted file mode 100644
index 2144013166..0000000000
--- a/emails/translations/account-login_hi.html
+++ /dev/null
@@ -1,12 +0,0 @@
-[[[SERVERNAME]]] - खाता लॉगिन
-
-
-
-
- [[[SERVERNAME]]] - खाता लॉगिन
-
-
-
-
आपका लॉगिन टोकन है: [[[TOKEN]]]
-
यह टोकन केवल एक बार उपयोग किया जा सकता है और 5 मिनट के लिए वैध है।
-
\ No newline at end of file
diff --git a/emails/translations/account-login_hi.txt b/emails/translations/account-login_hi.txt
deleted file mode 100644
index cac0e1899d..0000000000
--- a/emails/translations/account-login_hi.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-[[[SERVERNAME]]] - खाता लॉगिन
-आपका लॉगिन टोकन है: [[[TOKEN]]]
-~
-यह टोकन केवल एक बार उपयोग किया जा सकता है और 5 मिनट के लिए वैध है।
\ No newline at end of file
diff --git a/emails/translations/account-login_hu.html b/emails/translations/account-login_hu.html
deleted file mode 100644
index e8c93ae323..0000000000
--- a/emails/translations/account-login_hu.html
+++ /dev/null
@@ -1,12 +0,0 @@
-[[[SERVERNAME]]] - Bejelentkezés a fiókba
-
-
-
-
- [[[SERVERNAME]]] - Bejelentkezés a fiókba
-
-
-
-
Az Ön bejelentkezési tokenje: [[[TOKEN]]]
-
Ez a token csak egyszer használható, és 5 percig érvényes.
-
\ No newline at end of file
diff --git a/emails/translations/account-login_hu.txt b/emails/translations/account-login_hu.txt
deleted file mode 100644
index 0b104a0900..0000000000
--- a/emails/translations/account-login_hu.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-[[[SERVERNAME]]] - Bejelentkezés a fiókba
-Az Ön bejelentkezési tokenje: [[[TOKEN]]]
-~
-Ez a token csak egyszer használható, és 5 percig érvényes.
\ No newline at end of file
diff --git a/emails/translations/account-login_it.html b/emails/translations/account-login_it.html
deleted file mode 100644
index ccc5302210..0000000000
--- a/emails/translations/account-login_it.html
+++ /dev/null
@@ -1,12 +0,0 @@
-[[[SERVERNAME]]] - Accesso all'account
-
-
-
-
- [[[SERVERNAME]]] - Accesso all'account
-
-
-
-
Il tuo token di accesso è: [[[TOKEN]]]
-
Questo token può essere utilizzato solo una volta ed è valido per 5 minuti.
-
\ No newline at end of file
diff --git a/emails/translations/account-login_it.txt b/emails/translations/account-login_it.txt
deleted file mode 100644
index 22b4bf105e..0000000000
--- a/emails/translations/account-login_it.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-[[[SERVERNAME]]] - Accesso all'account
-Il tuo token di accesso è: [[[TOKEN]]]
-~
-Questo token può essere utilizzato solo una volta ed è valido per 5 minuti.
\ No newline at end of file
diff --git a/emails/translations/account-login_ja.html b/emails/translations/account-login_ja.html
deleted file mode 100644
index 2670cffe2f..0000000000
--- a/emails/translations/account-login_ja.html
+++ /dev/null
@@ -1,12 +0,0 @@
-[[[SERVERNAME]]] - アカウントログイン
-
-
-
-
- [[[SERVERNAME]]] - アカウントログイン
-
-
-
-
ログイントークンは次のとおりです:[[[TOKEN]]]
-
このトークンは1回だけ使用でき、5分間有効です。
-
\ No newline at end of file
diff --git a/emails/translations/account-login_ja.txt b/emails/translations/account-login_ja.txt
deleted file mode 100644
index aeda87ef4f..0000000000
--- a/emails/translations/account-login_ja.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-[[[SERVERNAME]]] - アカウントログイン
-ログイントークンは次のとおりです:[[[TOKEN]]]
-~
-このトークンは1回だけ使用でき、5分間有効です。
\ No newline at end of file
diff --git a/emails/translations/account-login_ko.html b/emails/translations/account-login_ko.html
deleted file mode 100644
index cec867ca8a..0000000000
--- a/emails/translations/account-login_ko.html
+++ /dev/null
@@ -1,12 +0,0 @@
-[[[SERVERNAME]]] - 계정 로그인
-
-
-
-
- [[[SERVERNAME]]] - 계정 로그인
-
-
-
-
당신이 로그인한 토큰은 다음과 같습니다 : [[[TOKEN]]]
-
이 토큰은 오직 한 번만 사용될 수 있으며, 5분 동안만 유효합니다.
-
\ No newline at end of file
diff --git a/emails/translations/account-login_ko.txt b/emails/translations/account-login_ko.txt
deleted file mode 100644
index 6540818142..0000000000
--- a/emails/translations/account-login_ko.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-[[[SERVERNAME]]] - 계정 로그인
-당신이 로그인한 토큰은 다음과 같습니다 : [[[TOKEN]]]
-~
-이 토큰은 오직 한 번만 사용될 수 있으며, 5분 동안만 유효합니다.
\ No newline at end of file
diff --git a/emails/translations/account-login_nl.html b/emails/translations/account-login_nl.html
deleted file mode 100644
index bc5d7e2a8d..0000000000
--- a/emails/translations/account-login_nl.html
+++ /dev/null
@@ -1,12 +0,0 @@
-[[[SERVERNAME]]] - Inloggen account
-
-
-
-
- [[[SERVERNAME]]] - Inloggen account
-
-
-
-
Uw login token is: [[[TOKEN]]]
-
Dit token kan maar één keer worden gebruikt en is 5 minuten geldig.
-
\ No newline at end of file
diff --git a/emails/translations/account-login_nl.txt b/emails/translations/account-login_nl.txt
deleted file mode 100644
index 2aeadd65a8..0000000000
--- a/emails/translations/account-login_nl.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-[[[SERVERNAME]]] - Inloggen account
-Uw login token is: [[[TOKEN]]]
-~
-Dit token kan maar één keer worden gebruikt en is 5 minuten geldig.
\ No newline at end of file
diff --git a/emails/translations/account-login_pl.html b/emails/translations/account-login_pl.html
deleted file mode 100644
index dc43fc0901..0000000000
--- a/emails/translations/account-login_pl.html
+++ /dev/null
@@ -1,12 +0,0 @@
-[[[SERVERNAME]]] - Logowanie Do Konta
-
-
-
-
- [[[SERVERNAME]]] - Logowanie Do Konta
-
-
-
-
Twój token logowania to: [[[TOKEN]]]
-
Ten token może być użyty tylko raz i jest ważny przez 5 minut.
-
\ No newline at end of file
diff --git a/emails/translations/account-login_pl.txt b/emails/translations/account-login_pl.txt
deleted file mode 100644
index 28cb3225e8..0000000000
--- a/emails/translations/account-login_pl.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-[[[SERVERNAME]]] - Logowanie Do Konta
-Twój token logowania to: [[[TOKEN]]]
-~
-Ten token może być użyty tylko raz i jest ważny przez 5 minut.
\ No newline at end of file
diff --git a/emails/translations/account-login_pt-br.html b/emails/translations/account-login_pt-br.html
deleted file mode 100644
index 7bb5973d13..0000000000
--- a/emails/translations/account-login_pt-br.html
+++ /dev/null
@@ -1,12 +0,0 @@
-[[[SERVERNAME]]] - Login da conta
-
-
-
-
- [[[SERVERNAME]]] - Login da conta
-
-
-
-
Seu token de login é: [[[TOKEN]]]
-
Este token só pode ser usado uma vez e é válido por 5 minutos.
-
\ No newline at end of file
diff --git a/emails/translations/account-login_pt-br.txt b/emails/translations/account-login_pt-br.txt
deleted file mode 100644
index ebfbc919e0..0000000000
--- a/emails/translations/account-login_pt-br.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-[[[SERVERNAME]]] - Login da conta
-Seu token de login é: [[[TOKEN]]]
-~
-Este token só pode ser usado uma vez e é válido por 5 minutos.
\ No newline at end of file
diff --git a/emails/translations/account-login_pt.html b/emails/translations/account-login_pt.html
deleted file mode 100644
index 7bb5973d13..0000000000
--- a/emails/translations/account-login_pt.html
+++ /dev/null
@@ -1,12 +0,0 @@
-[[[SERVERNAME]]] - Login da conta
-
-
-
-
- [[[SERVERNAME]]] - Login da conta
-
-
-
-
Seu token de login é: [[[TOKEN]]]
-
Este token só pode ser usado uma vez e é válido por 5 minutos.
-
\ No newline at end of file
diff --git a/emails/translations/account-login_pt.txt b/emails/translations/account-login_pt.txt
deleted file mode 100644
index ebfbc919e0..0000000000
--- a/emails/translations/account-login_pt.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-[[[SERVERNAME]]] - Login da conta
-Seu token de login é: [[[TOKEN]]]
-~
-Este token só pode ser usado uma vez e é válido por 5 minutos.
\ No newline at end of file
diff --git a/emails/translations/account-login_ru.html b/emails/translations/account-login_ru.html
deleted file mode 100644
index c1b2dd46b0..0000000000
--- a/emails/translations/account-login_ru.html
+++ /dev/null
@@ -1,12 +0,0 @@
-[[[SERVERNAME]]] - Вход в аккаунт
-
-
-
-
- [[[SERVERNAME]]] - Вход в аккаунт
-
-
-
-
Ваш токен для входа: [[[TOKEN]]]
-
Этот токен может быть использован только один раз и действителен в течение 5 минут.
-
\ No newline at end of file
diff --git a/emails/translations/account-login_ru.txt b/emails/translations/account-login_ru.txt
deleted file mode 100644
index 54fca89500..0000000000
--- a/emails/translations/account-login_ru.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-[[[SERVERNAME]]] - Вход в аккаунт
-Ваш токен для входа: [[[TOKEN]]]
-~
-Этот токен может быть использован только один раз и действителен в течение 5 минут.
\ No newline at end of file
diff --git a/emails/translations/account-login_sv.html b/emails/translations/account-login_sv.html
deleted file mode 100644
index 017416db05..0000000000
--- a/emails/translations/account-login_sv.html
+++ /dev/null
@@ -1,12 +0,0 @@
-[[[SERVERNAME]]] - Kontoinloggning
-
-
-
-
- [[[SERVERNAME]]] - Kontoinloggning
-
-
-
-
Din inloggningstoken är: [[[TOKEN]]]
-
Denna token kan bara användas en gång och är giltig i 5 minuter.
-
\ No newline at end of file
diff --git a/emails/translations/account-login_sv.txt b/emails/translations/account-login_sv.txt
deleted file mode 100644
index 4ca1ee6986..0000000000
--- a/emails/translations/account-login_sv.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-[[[SERVERNAME]]] - Kontoinloggning
-Din inloggningstoken är: [[[TOKEN]]]
-~
-Denna token kan bara användas en gång och är giltig i 5 minuter.
\ No newline at end of file
diff --git a/emails/translations/account-login_tr.html b/emails/translations/account-login_tr.html
deleted file mode 100644
index e3f19929e3..0000000000
--- a/emails/translations/account-login_tr.html
+++ /dev/null
@@ -1,12 +0,0 @@
-[[[SERVERNAME]]] - Hesap Girişi
-
-
-
-
- [[[SERVERNAME]]] - Hesap Girişi
-
-
-
-
Giriş anahtarınız: [[[TOKEN]]]
-
Bu simge yalnızca bir kez kullanılabilir ve 5 dakika geçerlidir.
-
\ No newline at end of file
diff --git a/emails/translations/account-login_tr.txt b/emails/translations/account-login_tr.txt
deleted file mode 100644
index b37d8b57a9..0000000000
--- a/emails/translations/account-login_tr.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-[[[SERVERNAME]]] - Hesap Girişi
-Giriş anahtarınız: [[[TOKEN]]]
-~
-Bu simge yalnızca bir kez kullanılabilir ve 5 dakika geçerlidir.
\ No newline at end of file
diff --git a/emails/translations/account-login_zh-chs.html b/emails/translations/account-login_zh-chs.html
deleted file mode 100644
index 82d896cef4..0000000000
--- a/emails/translations/account-login_zh-chs.html
+++ /dev/null
@@ -1,12 +0,0 @@
-[[[SERVERNAME]]] - 帐户登录
-
-
-
-
- [[[SERVERNAME]]] - 帐户登录
-
-
-
-
您的登录保安编码为:[[[TOKEN]]]
-
此保安编码只能使用一次,有效期为5分钟。
-
\ No newline at end of file
diff --git a/emails/translations/account-login_zh-chs.txt b/emails/translations/account-login_zh-chs.txt
deleted file mode 100644
index 2dc58cb421..0000000000
--- a/emails/translations/account-login_zh-chs.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-[[[SERVERNAME]]] - 帐户登录
-您的登录保安编码为:[[[TOKEN]]]
-~
-此保安编码只能使用一次,有效期为5分钟。
\ No newline at end of file
diff --git a/emails/translations/account-login_zh-cht.html b/emails/translations/account-login_zh-cht.html
deleted file mode 100644
index 876e41d4b6..0000000000
--- a/emails/translations/account-login_zh-cht.html
+++ /dev/null
@@ -1,12 +0,0 @@
-[[[SERVERNAME]]] - 帳戶登入
-
-
-
-
- [[[SERVERNAME]]] - 帳戶登入
-
-
-
-
你的登入保安編碼為:[[[TOKEN]]]
-
該保安編碼只能使用一次,有效期為5分鐘。
-
\ No newline at end of file
diff --git a/emails/translations/account-login_zh-cht.txt b/emails/translations/account-login_zh-cht.txt
deleted file mode 100644
index adeb97ddf0..0000000000
--- a/emails/translations/account-login_zh-cht.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-[[[SERVERNAME]]] - 帳戶登入
-你的登入保安編碼為:[[[TOKEN]]]
-~
-該保安編碼只能使用一次,有效期為5分鐘。
\ No newline at end of file
diff --git a/emails/translations/account-reset_bs.html b/emails/translations/account-reset_bs.html
deleted file mode 100644
index 36138d24f2..0000000000
--- a/emails/translations/account-reset_bs.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Resetiranje računa
-
\ No newline at end of file
diff --git a/emails/translations/account-reset_bs.txt b/emails/translations/account-reset_bs.txt
deleted file mode 100644
index fe99cfa6ad..0000000000
--- a/emails/translations/account-reset_bs.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Resetiranje računa
-Zdravo [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) traži reset lozinke naloga. Idite na sljedeću vezu da dovršite proces:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Ako niste vi inicirali ovaj zahtjev, zanemarite ovu poruku.
\ No newline at end of file
diff --git a/emails/translations/account-reset_cs.html b/emails/translations/account-reset_cs.html
deleted file mode 100644
index f5b5cae18a..0000000000
--- a/emails/translations/account-reset_cs.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Reset účtu
-
\ No newline at end of file
diff --git a/emails/translations/account-reset_cs.txt b/emails/translations/account-reset_cs.txt
deleted file mode 100644
index bdd89774e4..0000000000
--- a/emails/translations/account-reset_cs.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Reset účtu
-Ahoj [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) požaduje obnovení hesla k účtu. Chcete-li proces dokončit, přejděte na následující odkaz:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Pokud jste tento požadavek nezačali, ignorujte tento e-mail.
\ No newline at end of file
diff --git a/emails/translations/account-reset_da.html b/emails/translations/account-reset_da.html
deleted file mode 100644
index fbe49e3693..0000000000
--- a/emails/translations/account-reset_da.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Konto reset
-
\ No newline at end of file
diff --git a/emails/translations/account-reset_da.txt b/emails/translations/account-reset_da.txt
deleted file mode 100644
index 8acd2a3465..0000000000
--- a/emails/translations/account-reset_da.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Konto reset
-Hej [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) anmoder om en nulstilling af adgangskoden til kontoen. Gå til følgende link for at fuldføre processen:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Hvis du ikke startede denne anmodning, bedes du ignorere denne mail.
\ No newline at end of file
diff --git a/emails/translations/account-reset_de.html b/emails/translations/account-reset_de.html
deleted file mode 100644
index 63caa2e8c9..0000000000
--- a/emails/translations/account-reset_de.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Zurücksetzen des Kontos
-
-
-
-
- [[[SERVERNAME]]] - Überprüfung
-
-
-
-
Hallo [[[USERNAME]]], [[[SERVERNAME]]] Wenn Sie ein Zurücksetzen des Kontokennworts anfordern, klicken Sie auf den folgenden Link, um den Vorgang abzuschließen.
-
- Klicken Sie hier, um Ihr Kontopasswort zurückzusetzen.
-
- Wenn Sie diese Anfrage nicht initiiert haben, ignorieren Sie diese Mail bitte.
-
\ No newline at end of file
diff --git a/emails/translations/account-reset_de.txt b/emails/translations/account-reset_de.txt
deleted file mode 100644
index fe4feb8623..0000000000
--- a/emails/translations/account-reset_de.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Zurücksetzen des Kontos
-Hallo [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) fordert ein Zurücksetzen des Kontokennworts an. Klicken Sie auf den folgenden Link, um den Vorgang abzuschließen:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Wenn Sie diese Anfrage nicht initiiert haben, ignorieren Sie diese Mail bitte.
\ No newline at end of file
diff --git a/emails/translations/account-reset_es.html b/emails/translations/account-reset_es.html
deleted file mode 100644
index 688f27dbcd..0000000000
--- a/emails/translations/account-reset_es.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Restablecimiento de Cuenta
-
\ No newline at end of file
diff --git a/emails/translations/account-reset_es.txt b/emails/translations/account-reset_es.txt
deleted file mode 100644
index ca211dcab1..0000000000
--- a/emails/translations/account-reset_es.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Restablecimiento de Cuenta
-Hola [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) está solicitando un restablecimiento de contraseña de cuenta. Navega al siguiente enlace para completar el proceso:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Si tu no iniciaste este requerimiento, por favor ignora este correo.
\ No newline at end of file
diff --git a/emails/translations/account-reset_fi.html b/emails/translations/account-reset_fi.html
deleted file mode 100644
index 99bb08f22c..0000000000
--- a/emails/translations/account-reset_fi.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Tili nollattu
-
\ No newline at end of file
diff --git a/emails/translations/account-reset_fi.txt b/emails/translations/account-reset_fi.txt
deleted file mode 100644
index 99b6fe225b..0000000000
--- a/emails/translations/account-reset_fi.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Tili nollattu
-Hei [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) pyytää tilin salasanan palauttamista. Seuraa linkkiä prosessin loppuun saattamiseksi:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Jos et suorittanut tätä pyyntöä, voit ohitaa tämän sähköpostin.
\ No newline at end of file
diff --git a/emails/translations/account-reset_fr.html b/emails/translations/account-reset_fr.html
deleted file mode 100644
index 8cab5d350f..0000000000
--- a/emails/translations/account-reset_fr.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Réinitialisation du compte
-
\ No newline at end of file
diff --git a/emails/translations/account-reset_fr.txt b/emails/translations/account-reset_fr.txt
deleted file mode 100644
index 23467a20a9..0000000000
--- a/emails/translations/account-reset_fr.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Réinitialisation du compte
-Bonjour [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) demande une réinitialisation du mot de passe du compte. Accédez au lien suivant pour terminer le processus:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Si vous n'avez pas initié cette demande, veuillez ignorer ce courrier.
\ No newline at end of file
diff --git a/emails/translations/account-reset_hi.html b/emails/translations/account-reset_hi.html
deleted file mode 100644
index 85808d60f4..0000000000
--- a/emails/translations/account-reset_hi.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - खाता रीसेट
-
\ No newline at end of file
diff --git a/emails/translations/account-reset_hi.txt b/emails/translations/account-reset_hi.txt
deleted file mode 100644
index 9578c0d225..0000000000
--- a/emails/translations/account-reset_hi.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - खाता रीसेट
-हाय [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) से खाता पासवर्ड रीसेट करने का अनुरोध किया जा रहा है। प्रक्रिया को पूरा करने के लिए निम्नलिखित लिंक को बताएं:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-यदि आपने यह अनुरोध आरंभ नहीं किया है, तो कृपया इस मेल को अनदेखा करें।
\ No newline at end of file
diff --git a/emails/translations/account-reset_hu.html b/emails/translations/account-reset_hu.html
deleted file mode 100644
index 4d8247d978..0000000000
--- a/emails/translations/account-reset_hu.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Fiók visszaállítás
-
-
-
-
- [[[SERVERNAME]]] - Megerősítés
-
-
-
-
Tisztelt [[[USERNAME]]], [[[SERVERNAME]]] fiókhoz tartozó jelszó visszaállítási igényt kapott, kattintson a következő linkre a folyamat befejezéséhez.
-
- Kattintson ide fiókja jelszaváknak visszaállításához.
-
- Ha nem Ön kezdeményezte ezt a kérést, kérjük, hagyja figyelmen kívül ezt a levelet.
-
\ No newline at end of file
diff --git a/emails/translations/account-reset_hu.txt b/emails/translations/account-reset_hu.txt
deleted file mode 100644
index 39f76c7608..0000000000
--- a/emails/translations/account-reset_hu.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Fiók visszaállítás
-Tisztelt [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) a fiók jelszavának visszaállítását kéri. A folyamat befejezéséhez lépjen a következő linkre:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Ha nem Ön kezdeményezte ezt a kérést, kérjük, hagyja figyelmen kívül ezt a levelet.
\ No newline at end of file
diff --git a/emails/translations/account-reset_it.html b/emails/translations/account-reset_it.html
deleted file mode 100644
index e9c6ba934b..0000000000
--- a/emails/translations/account-reset_it.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Reimpostazione dell'account
-
\ No newline at end of file
diff --git a/emails/translations/account-reset_it.txt b/emails/translations/account-reset_it.txt
deleted file mode 100644
index de70be98ac..0000000000
--- a/emails/translations/account-reset_it.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Reimpostazione dell'account
-Salve [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]] [[[URLARGS1]]]) sta richiedendo la reimpostazione della password dell'account. Vai al seguente link per completare il processo:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Se non hai avviato questa richiesta, ignora questo messaggio.
\ No newline at end of file
diff --git a/emails/translations/account-reset_ja.html b/emails/translations/account-reset_ja.html
deleted file mode 100644
index bd2ec17481..0000000000
--- a/emails/translations/account-reset_ja.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - アカウントのリセット
-
\ No newline at end of file
diff --git a/emails/translations/account-reset_ja.txt b/emails/translations/account-reset_ja.txt
deleted file mode 100644
index 18e5701c65..0000000000
--- a/emails/translations/account-reset_ja.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - アカウントのリセット
-こんにちは[[[USERNAME]]]、([[[SERVERURL]]][[[URLARGS1]]]) はアカウントパスワードのリセットをリクエストしています。プロセスを完了するには、次のリンクに移動します。
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-このリクエストを開始していない場合は、このメールを無視してください。
\ No newline at end of file
diff --git a/emails/translations/account-reset_ko.html b/emails/translations/account-reset_ko.html
deleted file mode 100644
index db32681d18..0000000000
--- a/emails/translations/account-reset_ko.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - 계정 재설정
-
\ No newline at end of file
diff --git a/emails/translations/account-reset_ko.txt b/emails/translations/account-reset_ko.txt
deleted file mode 100644
index b5b03b0c71..0000000000
--- a/emails/translations/account-reset_ko.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - 계정 재설정
-안녕하세요, [[[USERNAME]]]님. [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]])은 계정 비밀번호 초기화를 위해 요구됩니다. 다음 링크로 이동하여 과정을 완료하십시오 :
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-이 요청을 시작하지 않은 경우, 이 메일을 무시하십시오.
\ No newline at end of file
diff --git a/emails/translations/account-reset_nl.html b/emails/translations/account-reset_nl.html
deleted file mode 100644
index cb4a4979f3..0000000000
--- a/emails/translations/account-reset_nl.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Account Reset
-
-
-
-
- [[[SERVERNAME]]] - Verificatie
-
-
-
-
Hallo [[[USERNAME]]], [[[SERVERNAME]]] vraagt om het opnieuw instellen van het wachtwoord van een account, klik op de volgende link om het proces te voltooien.
-
- Klik hier om je wachtwoord opnieuw in te stellen.
-
- Als u dit verzoek niet heeft ingediend, dan kunt u deze e-mail negeren.
-
\ No newline at end of file
diff --git a/emails/translations/account-reset_nl.txt b/emails/translations/account-reset_nl.txt
deleted file mode 100644
index 9480032c1f..0000000000
--- a/emails/translations/account-reset_nl.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Account Reset
-Hallo [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) vraagt om het opnieuw instellen van het account wachtwoord. Ga naar de volgende link om het proces te voltooien:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Als u dit verzoek niet heeft ingediend, dan kunt u deze e-mail negeren.
\ No newline at end of file
diff --git a/emails/translations/account-reset_pl.html b/emails/translations/account-reset_pl.html
deleted file mode 100644
index 2a68f61999..0000000000
--- a/emails/translations/account-reset_pl.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Resetowanie Konta
-
\ No newline at end of file
diff --git a/emails/translations/account-reset_pl.txt b/emails/translations/account-reset_pl.txt
deleted file mode 100644
index d72e4e4fa8..0000000000
--- a/emails/translations/account-reset_pl.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Resetowanie Konta
-Cześć [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) zwraca się z prośbą o zresetowanie hasła do konta. Kliknij poniższy link, aby zakończyć proces:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Jeśli nie byłeś inicjatorem tej prośby, proszę zignorować ten email.
\ No newline at end of file
diff --git a/emails/translations/account-reset_pt-br.html b/emails/translations/account-reset_pt-br.html
deleted file mode 100644
index 7840865358..0000000000
--- a/emails/translations/account-reset_pt-br.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Redefinição de conta
-
\ No newline at end of file
diff --git a/emails/translations/account-reset_pt-br.txt b/emails/translations/account-reset_pt-br.txt
deleted file mode 100644
index 5fb99df8d1..0000000000
--- a/emails/translations/account-reset_pt-br.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Redefinição de conta
-Olá, [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]] [[[URLARGS1]]]) está solicitando uma redefinição de senha de conta. Acesse o seguinte link para concluir o processo:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Se você não iniciou esta solicitação, ignore este e-mail.
\ No newline at end of file
diff --git a/emails/translations/account-reset_pt.html b/emails/translations/account-reset_pt.html
deleted file mode 100644
index 7840865358..0000000000
--- a/emails/translations/account-reset_pt.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Redefinição de conta
-
\ No newline at end of file
diff --git a/emails/translations/account-reset_pt.txt b/emails/translations/account-reset_pt.txt
deleted file mode 100644
index c4dc402e34..0000000000
--- a/emails/translations/account-reset_pt.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Redefinição de conta
-Olá, [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) está solicitando uma redefinição de senha de conta. Acesse o seguinte link para concluir o processo:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Se você não iniciou esta solicitação, ignore este e-mail.
\ No newline at end of file
diff --git a/emails/translations/account-reset_ru.html b/emails/translations/account-reset_ru.html
deleted file mode 100644
index e6cba832bd..0000000000
--- a/emails/translations/account-reset_ru.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Сброс учетной записи
-
\ No newline at end of file
diff --git a/emails/translations/account-reset_ru.txt b/emails/translations/account-reset_ru.txt
deleted file mode 100644
index c61f41783f..0000000000
--- a/emails/translations/account-reset_ru.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Сброс учетной записи
-Здравствуйте, [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) запрашивает сброс пароля учетной записи. Для завершения процесса перейдите по следующей ссылке:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Если вы не инициировали этот запрос, игнорируйте это письмо.
\ No newline at end of file
diff --git a/emails/translations/account-reset_sv.html b/emails/translations/account-reset_sv.html
deleted file mode 100644
index 594e6c0687..0000000000
--- a/emails/translations/account-reset_sv.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Återställ konto
-
\ No newline at end of file
diff --git a/emails/translations/account-reset_sv.txt b/emails/translations/account-reset_sv.txt
deleted file mode 100644
index ab9c1419e7..0000000000
--- a/emails/translations/account-reset_sv.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Återställ konto
-Hej [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]] [[[URLARGS1]]) begär återställning av kontolösenord. Ta bort till följande länk för att slutföra processen:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Om du inte initierade denna begäran, ignorerar du det här meddelandet.
\ No newline at end of file
diff --git a/emails/translations/account-reset_tr.html b/emails/translations/account-reset_tr.html
deleted file mode 100644
index 9faf6e795c..0000000000
--- a/emails/translations/account-reset_tr.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]] - Hesabı Sıfırlama
-
\ No newline at end of file
diff --git a/emails/translations/account-reset_tr.txt b/emails/translations/account-reset_tr.txt
deleted file mode 100644
index 1c30b082d9..0000000000
--- a/emails/translations/account-reset_tr.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]] - Hesabı Sıfırlama
-Merhaba [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) bir hesap şifresi sıfırlama istiyor. İşlemi tamamlamak için aşağıdaki bağlantıya gidin:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-Bu isteği siz başlatmadıysanız, lütfen bu postayı dikkate almayın.
\ No newline at end of file
diff --git a/emails/translations/account-reset_zh-chs.html b/emails/translations/account-reset_zh-chs.html
deleted file mode 100644
index 824250ddd2..0000000000
--- a/emails/translations/account-reset_zh-chs.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]]-帐户重置
-
\ No newline at end of file
diff --git a/emails/translations/account-reset_zh-chs.txt b/emails/translations/account-reset_zh-chs.txt
deleted file mode 100644
index 8f1e1cb940..0000000000
--- a/emails/translations/account-reset_zh-chs.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]]-帐户重置
-[[[USERNAME]],您好:[[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) 正在请求重置帐户密码。导航至以下连结以完成该过程:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-如果您没有发起此请求,请忽略此邮件。
\ No newline at end of file
diff --git a/emails/translations/account-reset_zh-cht.html b/emails/translations/account-reset_zh-cht.html
deleted file mode 100644
index d31e2ad3c3..0000000000
--- a/emails/translations/account-reset_zh-cht.html
+++ /dev/null
@@ -1,15 +0,0 @@
-[[[SERVERNAME]]]-帳戶重置
-
-
-
-
- [[[SERVERNAME]]] - 驗證
-
-
-
-
[[[USERNAME]],你好, [[[SERVERNAME]] 正在要求重置帳戶密碼,請單擊以下鏈結以完成該過程。
-
- 單擊此處重置你的帳戶密碼。
-
- 如果你沒有發起此請求,請不理此電郵。
-
\ No newline at end of file
diff --git a/emails/translations/account-reset_zh-cht.txt b/emails/translations/account-reset_zh-cht.txt
deleted file mode 100644
index 7356909ecb..0000000000
--- a/emails/translations/account-reset_zh-cht.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-[[[SERVERNAME]]]-帳戶重置
-[[[USERNAME]],你好,[[[SERVERNAME]]] ([[[SERVERURL]]][[[URLARGS1]]]) 正在請求重設帳戶密碼。導航至以下鏈結以完成該過程:
-~
-~[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]][[[URLARGS2]]]
-~
-如果你沒有發起此請求,請不理此電郵。
\ No newline at end of file
diff --git a/emails/translations/device-help_bs.html b/emails/translations/device-help_bs.html
deleted file mode 100644
index 3c4ee3887c..0000000000
--- a/emails/translations/device-help_bs.html
+++ /dev/null
@@ -1,20 +0,0 @@
-[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Zahtjev za pomoć
-
-
-
-
- [[[SERVERNAME]]] - Zahtjev za pomoć
-
-
-
-
- uređaj "[[[NAZIV UREĐAJA]]] “ zatražio pomoć.
-
-
- Korisnik: [[[IME POMOĆI]]]
- Zahtjev: [[[UPIT ZA POMOĆ]]]
-
-
- kliknite ovdje za navigaciju do ovog uređaja.
-
-
\ No newline at end of file
diff --git a/emails/translations/device-help_bs.txt b/emails/translations/device-help_bs.txt
deleted file mode 100644
index d060d62b4e..0000000000
--- a/emails/translations/device-help_bs.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-[[[SERVERNAME]]] - Zahtjev za pomoć uređaja
-Uređaj "[[[DEVICENAME]]]" je zatražio pomoć.
-
-Korisnik: "[[[IME POMOĆI]]]"
-Zahtjev: "[[[UPIT ZA POMOĆ]]]"
-
-[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]]
\ No newline at end of file
diff --git a/emails/translations/device-help_cs.html b/emails/translations/device-help_cs.html
deleted file mode 100644
index ca6f8ed6d9..0000000000
--- a/emails/translations/device-help_cs.html
+++ /dev/null
@@ -1,20 +0,0 @@
-[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
-
-
-
- [[[SERVERNAME]]] - Help Request
-
-
-
-
- Device "[[[DEVICENAME]]] " požádal o pomoc.
-
-
- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]]
-
-
- klikněte zde to navigate to this device.
-
-
\ No newline at end of file
diff --git a/emails/translations/device-help_cs.txt b/emails/translations/device-help_cs.txt
deleted file mode 100644
index c2b3c09f4b..0000000000
--- a/emails/translations/device-help_cs.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-[[[SERVERNAME]]] - Device Help Request
-Device "[[[DEVICENAME]]]" requested assistance.
-
-User: "[[[HELPUSERNAME]]]"
-Request: "[[[HELPREQUEST]]]"
-
-[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]]
\ No newline at end of file
diff --git a/emails/translations/device-help_da.html b/emails/translations/device-help_da.html
deleted file mode 100644
index d152a334ee..0000000000
--- a/emails/translations/device-help_da.html
+++ /dev/null
@@ -1,20 +0,0 @@
-[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
-
-
-
- [[[SERVERNAME]]] - Help Request
-
-
-
-
- Device "[[[DEVICENAME]]] " requested help.
-
-
- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]]
-
-
- Klik her to navigate to this device.
-
-
\ No newline at end of file
diff --git a/emails/translations/device-help_da.txt b/emails/translations/device-help_da.txt
deleted file mode 100644
index c2b3c09f4b..0000000000
--- a/emails/translations/device-help_da.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-[[[SERVERNAME]]] - Device Help Request
-Device "[[[DEVICENAME]]]" requested assistance.
-
-User: "[[[HELPUSERNAME]]]"
-Request: "[[[HELPREQUEST]]]"
-
-[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]]
\ No newline at end of file
diff --git a/emails/translations/device-help_de.html b/emails/translations/device-help_de.html
deleted file mode 100644
index 7e01917a02..0000000000
--- a/emails/translations/device-help_de.html
+++ /dev/null
@@ -1,20 +0,0 @@
-[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
-
-
-
- [[[SERVERNAME]]] - Help Request
-
-
-
-
- Gerät "[[[DEVICENAME]]] " requested help.
-
-
- Benutzer: [[[HELPUSERNAME]]]
- Anforderung: [[[HELPREQUEST]]]
-
-
- hier klicken um zu diesem Gerät zu navigieren.
-
-
\ No newline at end of file
diff --git a/emails/translations/device-help_de.txt b/emails/translations/device-help_de.txt
deleted file mode 100644
index 849df051c7..0000000000
--- a/emails/translations/device-help_de.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-[[[SERVERNAME]]] - Device Help Request
-Gerät "[[[DEVICENAME]]]" hat Unterstützung angefordert.
-
-Benutzer: "[[[HELPUSERNAME]]]"
-Anforderung: "[[[HELPREQUEST]]]"
-
-[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]]
\ No newline at end of file
diff --git a/emails/translations/device-help_es.html b/emails/translations/device-help_es.html
deleted file mode 100644
index beafbccfda..0000000000
--- a/emails/translations/device-help_es.html
+++ /dev/null
@@ -1,20 +0,0 @@
-[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Solicitud de Ayuda
-
-
-
-
- [[[SERVERNAME]]] - Solicitud de Ayuda
-
-
-
-
- Dispositivo "[[[DEVICENAME]]] " ha pedido ayuda.
-
-
- Usuario: [[[HELPUSERNAME]]]
- Solicitud: [[[HELPREQUEST]]]
-
-
- 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
deleted file mode 100644
index 3dc02cc3f1..0000000000
--- a/emails/translations/device-help_es.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-[[[SERVERNAME]]] - Solicitud de ayuda con el Dispositivo
-Dispositivo "[[[DEVICENAME]]]" solicito asistencia.
-
-Usuario: "[[[HELPUSERNAME]]]"
-Solicitud: "[[[HELPREQUEST]]]"
-
-[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]]
\ No newline at end of file
diff --git a/emails/translations/device-help_fi.html b/emails/translations/device-help_fi.html
deleted file mode 100644
index 1dd46aaf67..0000000000
--- a/emails/translations/device-help_fi.html
+++ /dev/null
@@ -1,20 +0,0 @@
-[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
-
-
-
- [[[SERVERNAME]]] - Help Request
-
-
-
-
- Device "[[[DEVICENAME]]] " requested help.
-
-
- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]]
-
-
- klikkaa tästä to navigate to this device.
-
-
\ No newline at end of file
diff --git a/emails/translations/device-help_fi.txt b/emails/translations/device-help_fi.txt
deleted file mode 100644
index c2b3c09f4b..0000000000
--- a/emails/translations/device-help_fi.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-[[[SERVERNAME]]] - Device Help Request
-Device "[[[DEVICENAME]]]" requested assistance.
-
-User: "[[[HELPUSERNAME]]]"
-Request: "[[[HELPREQUEST]]]"
-
-[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]]
\ No newline at end of file
diff --git a/emails/translations/device-help_fr.html b/emails/translations/device-help_fr.html
deleted file mode 100644
index 745639b396..0000000000
--- a/emails/translations/device-help_fr.html
+++ /dev/null
@@ -1,20 +0,0 @@
-[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Demande d'assistance
-
-
-
-
- [[[SERVERNAME]]] - Demande d'assistance
-
-
-
-
- Appareil "[[[DEVICENAME]]] " assistance demandée.
-
-
- Utilisateur : [[[HELPUSERNAME]]]
- Demande : [[[HELPREQUEST]]]
-
-
- cliquez ici pour naviguer vers cet appareil.
-
-
\ No newline at end of file
diff --git a/emails/translations/device-help_fr.txt b/emails/translations/device-help_fr.txt
deleted file mode 100644
index 8c08fe7a74..0000000000
--- a/emails/translations/device-help_fr.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-[[[SERVERNAME]]] - Demande d'assistance sur l'appareil
-Une assistance est demandée par l'appareil "[[[DEVICENAME]]]"
-
-Utilistateur : "[[[HELPUSERNAME]]]"
-Demande : "[[[HELPREQUEST]]]"
-
-[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]]
\ No newline at end of file
diff --git a/emails/translations/device-help_hi.html b/emails/translations/device-help_hi.html
deleted file mode 100644
index c5c0a18ea4..0000000000
--- a/emails/translations/device-help_hi.html
+++ /dev/null
@@ -1,20 +0,0 @@
-[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
-
-
-
- [[[SERVERNAME]]] - Help Request
-
-
-
-
- Device "[[[DEVICENAME]]] " requested help.
-
-
- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]]
-
-
- यहाँ क्लिक करें to navigate to this device.
-
-
\ No newline at end of file
diff --git a/emails/translations/device-help_hi.txt b/emails/translations/device-help_hi.txt
deleted file mode 100644
index c2b3c09f4b..0000000000
--- a/emails/translations/device-help_hi.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-[[[SERVERNAME]]] - Device Help Request
-Device "[[[DEVICENAME]]]" requested assistance.
-
-User: "[[[HELPUSERNAME]]]"
-Request: "[[[HELPREQUEST]]]"
-
-[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]]
\ No newline at end of file
diff --git a/emails/translations/device-help_hu.html b/emails/translations/device-help_hu.html
deleted file mode 100644
index 75f8cb3298..0000000000
--- a/emails/translations/device-help_hu.html
+++ /dev/null
@@ -1,20 +0,0 @@
-[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Segítség kérés
-
-
-
-
- [[[SERVERNAME]]] - Segítség kérés
-
-
-
-
- Eszköz "[[[DEVICENAME]]] " segítséget kért.
-
-
- Felhsználó: [[[HELPUSERNAME]]]
- Segítségkérés: [[[HELPREQUEST]]]
-
-
- Kattintson ide az eszközhöz való navigáláshoz.
-
-
\ No newline at end of file
diff --git a/emails/translations/device-help_hu.txt b/emails/translations/device-help_hu.txt
deleted file mode 100644
index 4dcc6a948f..0000000000
--- a/emails/translations/device-help_hu.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-[[[SERVERNAME]]] - Segítség kérés
-A "[[[DEVICENAME]]]" eszköz segítséget kért.
-
-Felhasználó: "[[[HELPUSERNAME]]]"
-Segítség kérés: "[[[HELPREQUEST]]]"
-
-[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]]
\ No newline at end of file
diff --git a/emails/translations/device-help_it.html b/emails/translations/device-help_it.html
deleted file mode 100644
index 812e9607d0..0000000000
--- a/emails/translations/device-help_it.html
+++ /dev/null
@@ -1,20 +0,0 @@
-[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
-
-
-
- [[[SERVERNAME]]] - Help Request
-
-
-
-
- Device "[[[DEVICENAME]]] " requested help.
-
-
- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]]
-
-
- clicca qui to navigate to this device.
-
-
\ No newline at end of file
diff --git a/emails/translations/device-help_it.txt b/emails/translations/device-help_it.txt
deleted file mode 100644
index c2b3c09f4b..0000000000
--- a/emails/translations/device-help_it.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-[[[SERVERNAME]]] - Device Help Request
-Device "[[[DEVICENAME]]]" requested assistance.
-
-User: "[[[HELPUSERNAME]]]"
-Request: "[[[HELPREQUEST]]]"
-
-[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]]
\ No newline at end of file
diff --git a/emails/translations/device-help_ja.html b/emails/translations/device-help_ja.html
deleted file mode 100644
index 42e39cbac3..0000000000
--- a/emails/translations/device-help_ja.html
+++ /dev/null
@@ -1,20 +0,0 @@
-[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
-
-
-
- [[[SERVERNAME]]] - Help Request
-
-
-
-
- Device "[[[DEVICENAME]]] " requested help.
-
-
- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]]
-
-
- ここをクリック to navigate to this device.
-
-
\ No newline at end of file
diff --git a/emails/translations/device-help_ja.txt b/emails/translations/device-help_ja.txt
deleted file mode 100644
index c2b3c09f4b..0000000000
--- a/emails/translations/device-help_ja.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-[[[SERVERNAME]]] - Device Help Request
-Device "[[[DEVICENAME]]]" requested assistance.
-
-User: "[[[HELPUSERNAME]]]"
-Request: "[[[HELPREQUEST]]]"
-
-[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]]
\ No newline at end of file
diff --git a/emails/translations/device-help_ko.html b/emails/translations/device-help_ko.html
deleted file mode 100644
index 37356e4389..0000000000
--- a/emails/translations/device-help_ko.html
+++ /dev/null
@@ -1,20 +0,0 @@
-[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
-
-
-
- [[[SERVERNAME]]] - Help Request
-
-
-
-
- Device "[[[DEVICENAME]]] " requested help.
-
-
- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]]
-
-
- 여기를 클릭하십시오. to navigate to this device.
-
-
\ No newline at end of file
diff --git a/emails/translations/device-help_ko.txt b/emails/translations/device-help_ko.txt
deleted file mode 100644
index c2b3c09f4b..0000000000
--- a/emails/translations/device-help_ko.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-[[[SERVERNAME]]] - Device Help Request
-Device "[[[DEVICENAME]]]" requested assistance.
-
-User: "[[[HELPUSERNAME]]]"
-Request: "[[[HELPREQUEST]]]"
-
-[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]]
\ No newline at end of file
diff --git a/emails/translations/device-help_nl.html b/emails/translations/device-help_nl.html
deleted file mode 100644
index 1f0f659683..0000000000
--- a/emails/translations/device-help_nl.html
+++ /dev/null
@@ -1,20 +0,0 @@
-[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Verzoek om hulp
-
-
-
-
- [[[SERVERNAME]]] - Verzoek om hulp
-
-
-
-
- Apparaat "[[[DEVICENAME]]] " vraagt om hulp.
-
-
- Gebruiker: [[[HELPUSERNAME]]]
- Verzoek: [[[HELPREQUEST]]]
-
-
- Klik hier naar dit apparaat navigeren.
-
-
\ No newline at end of file
diff --git a/emails/translations/device-help_nl.txt b/emails/translations/device-help_nl.txt
deleted file mode 100644
index 7a592968bb..0000000000
--- a/emails/translations/device-help_nl.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-[[[SERVERNAME]]] - Een verzoek om het apparaat te helpen
-Apparaat "[[[DEVICENAME]]]" heeft assistentie gevraagd
-
-Gebruiker: "[[[HELPUSERNAME]]]"
-Verzoek: "[[[HELPREQUEST]]]"
-
-[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]]
\ No newline at end of file
diff --git a/emails/translations/device-help_pl.html b/emails/translations/device-help_pl.html
deleted file mode 100644
index b94b193d5d..0000000000
--- a/emails/translations/device-help_pl.html
+++ /dev/null
@@ -1,20 +0,0 @@
-[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Prośba Pomocy
-
-
-
-
- [[[SERVERNAME]]] - Prośba Pomocy
-
-
-
-
- Urządzenie "[[[DEVICENAME]]] " prosi o pomoc.
-
-
- Użytkownik: [[[HELPUSERNAME]]]
- Prośba: [[[HELPREQUEST]]]
-
-
- kliknij tutaj by nawigować do tego urządzenia.
-
-
\ No newline at end of file
diff --git a/emails/translations/device-help_pl.txt b/emails/translations/device-help_pl.txt
deleted file mode 100644
index 1b6b5a979e..0000000000
--- a/emails/translations/device-help_pl.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-[[[SERVERNAME]]] - Prośba Pomocy Urządzenia
-Urządzenie "[[[DEVICENAME]]]" prosiło o pomoc.
-
-Użytkownik: "[[[HELPUSERNAME]]]"
-Prośba: "[[[HELPREQUEST]]]"
-
-[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]]
\ No newline at end of file
diff --git a/emails/translations/device-help_pt-br.html b/emails/translations/device-help_pt-br.html
deleted file mode 100644
index b77b579445..0000000000
--- a/emails/translations/device-help_pt-br.html
+++ /dev/null
@@ -1,20 +0,0 @@
-[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
-
-
-
- [[[SERVERNAME]]] - Help Request
-
-
-
-
- Device "[[[DEVICENAME]]] " requested help.
-
-
- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]]
-
-
- Clique aqui to navigate to this device.
-
-
\ No newline at end of file
diff --git a/emails/translations/device-help_pt-br.txt b/emails/translations/device-help_pt-br.txt
deleted file mode 100644
index c2b3c09f4b..0000000000
--- a/emails/translations/device-help_pt-br.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-[[[SERVERNAME]]] - Device Help Request
-Device "[[[DEVICENAME]]]" requested assistance.
-
-User: "[[[HELPUSERNAME]]]"
-Request: "[[[HELPREQUEST]]]"
-
-[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]]
\ No newline at end of file
diff --git a/emails/translations/device-help_pt.html b/emails/translations/device-help_pt.html
deleted file mode 100644
index b77b579445..0000000000
--- a/emails/translations/device-help_pt.html
+++ /dev/null
@@ -1,20 +0,0 @@
-[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
-
-
-
- [[[SERVERNAME]]] - Help Request
-
-
-
-
- Device "[[[DEVICENAME]]] " requested help.
-
-
- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]]
-
-
- Clique aqui to navigate to this device.
-
-
\ No newline at end of file
diff --git a/emails/translations/device-help_pt.txt b/emails/translations/device-help_pt.txt
deleted file mode 100644
index c2b3c09f4b..0000000000
--- a/emails/translations/device-help_pt.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-[[[SERVERNAME]]] - Device Help Request
-Device "[[[DEVICENAME]]]" requested assistance.
-
-User: "[[[HELPUSERNAME]]]"
-Request: "[[[HELPREQUEST]]]"
-
-[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]]
\ No newline at end of file
diff --git a/emails/translations/device-help_ru.html b/emails/translations/device-help_ru.html
deleted file mode 100644
index 19986fa873..0000000000
--- a/emails/translations/device-help_ru.html
+++ /dev/null
@@ -1,20 +0,0 @@
-[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
-
-
-
- [[[SERVERNAME]]] - Help Request
-
-
-
-
- Device "[[[DEVICENAME]]] " requested help.
-
-
- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]]
-
-
- кликните сюда to navigate to this device.
-
-
\ No newline at end of file
diff --git a/emails/translations/device-help_ru.txt b/emails/translations/device-help_ru.txt
deleted file mode 100644
index c2b3c09f4b..0000000000
--- a/emails/translations/device-help_ru.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-[[[SERVERNAME]]] - Device Help Request
-Device "[[[DEVICENAME]]]" requested assistance.
-
-User: "[[[HELPUSERNAME]]]"
-Request: "[[[HELPREQUEST]]]"
-
-[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]]
\ No newline at end of file
diff --git a/emails/translations/device-help_sv.html b/emails/translations/device-help_sv.html
deleted file mode 100644
index 8423ff37aa..0000000000
--- a/emails/translations/device-help_sv.html
+++ /dev/null
@@ -1,20 +0,0 @@
-[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
-
-
-
- [[[SERVERNAME]]] - Help Request
-
-
-
-
- Device "[[[DEVICENAME]]] " requested help.
-
-
- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]]
-
-
- Klicka här to navigate to this device.
-
-
\ No newline at end of file
diff --git a/emails/translations/device-help_sv.txt b/emails/translations/device-help_sv.txt
deleted file mode 100644
index c2b3c09f4b..0000000000
--- a/emails/translations/device-help_sv.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-[[[SERVERNAME]]] - Device Help Request
-Device "[[[DEVICENAME]]]" requested assistance.
-
-User: "[[[HELPUSERNAME]]]"
-Request: "[[[HELPREQUEST]]]"
-
-[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]]
\ No newline at end of file
diff --git a/emails/translations/device-help_tr.html b/emails/translations/device-help_tr.html
deleted file mode 100644
index 2844ba28ed..0000000000
--- a/emails/translations/device-help_tr.html
+++ /dev/null
@@ -1,20 +0,0 @@
-[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
-
-
-
- [[[SERVERNAME]]] - Help Request
-
-
-
-
- Device "[[[DEVICENAME]]] " requested help.
-
-
- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]]
-
-
- buraya Tıkla to navigate to this device.
-
-
\ No newline at end of file
diff --git a/emails/translations/device-help_tr.txt b/emails/translations/device-help_tr.txt
deleted file mode 100644
index c2b3c09f4b..0000000000
--- a/emails/translations/device-help_tr.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-[[[SERVERNAME]]] - Device Help Request
-Device "[[[DEVICENAME]]]" requested assistance.
-
-User: "[[[HELPUSERNAME]]]"
-Request: "[[[HELPREQUEST]]]"
-
-[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]]
\ No newline at end of file
diff --git a/emails/translations/device-help_zh-chs.html b/emails/translations/device-help_zh-chs.html
deleted file mode 100644
index 2f13132c0d..0000000000
--- a/emails/translations/device-help_zh-chs.html
+++ /dev/null
@@ -1,20 +0,0 @@
-[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
-
-
-
- [[[SERVERNAME]]] - Help Request
-
-
-
-
- Device "[[[DEVICENAME]]] " requested help.
-
-
- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]]
-
-
- 点击这里 to navigate to this device.
-
-
\ No newline at end of file
diff --git a/emails/translations/device-help_zh-chs.txt b/emails/translations/device-help_zh-chs.txt
deleted file mode 100644
index c2b3c09f4b..0000000000
--- a/emails/translations/device-help_zh-chs.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-[[[SERVERNAME]]] - Device Help Request
-Device "[[[DEVICENAME]]]" requested assistance.
-
-User: "[[[HELPUSERNAME]]]"
-Request: "[[[HELPREQUEST]]]"
-
-[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]]
\ No newline at end of file
diff --git a/emails/translations/device-help_zh-cht.html b/emails/translations/device-help_zh-cht.html
deleted file mode 100644
index 52d6e99080..0000000000
--- a/emails/translations/device-help_zh-cht.html
+++ /dev/null
@@ -1,20 +0,0 @@
-[[[SERVERNAME]]] - "[[[DEVICENAME]]]" Help Request
-
-
-
-
- [[[SERVERNAME]]] - Help Request
-
-
-
-
- Device "[[[DEVICENAME]]] " requested help.
-
-
- User: [[[HELPUSERNAME]]]
- Request: [[[HELPREQUEST]]]
-
-
- 點擊這裡 to navigate to this device.
-
-
\ No newline at end of file
diff --git a/emails/translations/device-help_zh-cht.txt b/emails/translations/device-help_zh-cht.txt
deleted file mode 100644
index c2b3c09f4b..0000000000
--- a/emails/translations/device-help_zh-cht.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-[[[SERVERNAME]]] - Device Help Request
-Device "[[[DEVICENAME]]]" requested assistance.
-
-User: "[[[HELPUSERNAME]]]"
-Request: "[[[HELPREQUEST]]]"
-
-[[[SERVERURL]]]?viewmode=10&gotonode=[[[NODEID]]]
\ No newline at end of file
diff --git a/emails/translations/device-notify_bs.html b/emails/translations/device-notify_bs.html
deleted file mode 100644
index c0b6ecd8c9..0000000000
--- a/emails/translations/device-notify_bs.html
+++ /dev/null
@@ -1,36 +0,0 @@
-[[[SERVERNAME]]] - Obavještenje uređaja
-
-
-
-
- [[[SERVERNAME]]] - Obavještenje uređaja
-
-
-
-
-
- Sljedeći uređaji su promijenili stanje veze.
-
-
-
-
- Povezani uređaji:
-
-
-[[[VEZE]]]
-
-
-
-
- Isključeni uređaji:
-
-
-[[[PREKIDI]]]
-
-
-
-
- Da odjavite pretplatu, kliknite ovdje u roku od 1 sata od prijema ove poruke.
-
-
-
\ No newline at end of file
diff --git a/emails/translations/device-notify_bs.txt b/emails/translations/device-notify_bs.txt
deleted file mode 100644
index ff3ba7e9ec..0000000000
--- a/emails/translations/device-notify_bs.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-[[[SERVERNAME]]] - Obavještenje uređaja
-~
-Sljedeći uređaji su promijenili stanje veze.
-~
-~
-~
-Povezani uređaji:
-~
-~[[[CONNECTIONS]]]
-~
-~
-~
-~
-Isključeni uređaji:
-~
-~[[[DISCONNECTIONS]]]
-~
-~
-~
-
-Da odjavite pretplatu, učitajte ovaj link u roku od 1 sata od prijema ove poruke: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]]
-~
\ No newline at end of file
diff --git a/emails/translations/device-notify_cs.html b/emails/translations/device-notify_cs.html
deleted file mode 100644
index 37b8536e8a..0000000000
--- a/emails/translations/device-notify_cs.html
+++ /dev/null
@@ -1,36 +0,0 @@
-[[[SERVERNAME]]] - Oznámení zařízení
-
-
-
-
- [[[SERVERNAME]]] - Oznámení zařízení
-
-
-
-
-
- Následující zařízení změnila svůj stav připojení.
-
-
-
-
- Připojená zařízení:
-
-
-[[[CONNECTIONS]]]
-
-
-
-
- Odpojená zařízení:
-
-
-[[[DISCONNECTIONS]]]
-
-
-
-
- Chcete-li se odhlásit, klikněte zde do 1 hodiny od obdržení této zprávy.
-
-
-
\ No newline at end of file
diff --git a/emails/translations/device-notify_cs.txt b/emails/translations/device-notify_cs.txt
deleted file mode 100644
index fad0096bfa..0000000000
--- a/emails/translations/device-notify_cs.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-[[[SERVERNAME]]] - Oznámení zařízení
-~
-Následující zařízení změnila svůj stav připojení.
-~
-~
-~
-Připojená zařízení:
-~
-~[[[CONNECTIONS]]]
-~
-~
-~
-~
-Odpojená zařízení:
-~
-~[[[DISCONNECTIONS]]]
-~
-~
-~
-
-Chcete-li se odhlásit, načtěte tento odkaz do 1 hodiny od obdržení této zprávy: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]]
-~
\ No newline at end of file
diff --git a/emails/translations/device-notify_da.html b/emails/translations/device-notify_da.html
deleted file mode 100644
index 419eff00c4..0000000000
--- a/emails/translations/device-notify_da.html
+++ /dev/null
@@ -1,36 +0,0 @@
-[[[SERVERNAME]]] - Enheds Notifikation
-
-
-
-
- [[[SERVERNAME]]] - Enheds Notifikation
-
-
-
-
-
- Følgende enheder har ændret deres forbindelsestilstand.
-
-
-
-
- Forbundne enheder:
-
-
-[[[CONNECTIONS]]]
-
-
-
-
- Afbrudte enheder:
-
-
-[[[DISCONNECTIONS]]]
-
-
-
-
- For at afmelde, Klik her inden for 1 time efter at have modtaget denne besked.
-
-
-
\ No newline at end of file
diff --git a/emails/translations/device-notify_da.txt b/emails/translations/device-notify_da.txt
deleted file mode 100644
index 2ca27e1938..0000000000
--- a/emails/translations/device-notify_da.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-[[[SERVERNAME]]] - Enheds Notifikation
-~
-Følgende enheder har ændret deres forbindelsestilstand.
-~
-~
-~
-Forbundne enheder:
-~
-~[[[CONNECTIONS]]]
-~
-~
-~
-~
-Afbrudte enheder:
-~
-~[[[DISCONNECTIONS]]]
-~
-~
-~
-
-For at afmelde, skal du indlæse dette link inden for 1 time efter at have modtaget denne besked: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]]
-~
\ No newline at end of file
diff --git a/emails/translations/device-notify_de.html b/emails/translations/device-notify_de.html
deleted file mode 100644
index 7b97f8c2f5..0000000000
--- a/emails/translations/device-notify_de.html
+++ /dev/null
@@ -1,36 +0,0 @@
-[[[SERVERNAME]]] – Gerätebenachrichtigung
-
-
-
-
- [[[SERVERNAME]]] – Gerätebenachrichtigung
-
-
-
-
-
- Die folgenden Geräte haben ihren Verbindungsstatus geändert.
-
-
-
-
- Verbundene Geräte:
-
-
-[[[CONNECTIONS]]]
-
-
-
-
- Getrennte Geräte:
-
-
-[[[DISCONNECTIONS]]]
-
-
-
-
- Deabonnieren, hier klicken innerhalb 1 Stunde nach erhalt dieser Nachricht.
-
-
-
\ No newline at end of file
diff --git a/emails/translations/device-notify_de.txt b/emails/translations/device-notify_de.txt
deleted file mode 100644
index 282010179d..0000000000
--- a/emails/translations/device-notify_de.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-[[[SERVERNAME]]] – Gerätebenachrichtigung
-~
-Die folgenden Geräte haben ihren Verbindungsstatus geändert.
-~
-~
-~
-Verbundene Geräte:
-~
-~[[[CONNECTIONS]]]
-~
-~
-~
-~
-Getrennte Geräte:
-~
-~[[[DISCONNECTIONS]]]
-~
-~
-~
-
-Um sich abzumelden, laden Sie diesen Link innerhalb von 1 Stunde nach Erhalt dieser Nachricht: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]]
-~
\ No newline at end of file
diff --git a/emails/translations/device-notify_es.html b/emails/translations/device-notify_es.html
deleted file mode 100644
index 171ec64d16..0000000000
--- a/emails/translations/device-notify_es.html
+++ /dev/null
@@ -1,36 +0,0 @@
-[[[SERVERNAME]]] - Notificación del dispositivo
-
-
-
-
- [[[SERVERNAME]]] - Notificación del dispositivo
-
-
-
-
-
- Los siguientes dispositivos han cambiado su estado de conexión.
-
-
-
-
- Dispositivos conectados:
-
-
-[[[CONNECTIONS]]]
-
-
-
-
- Dispositivos desconectados:
-
-
-[[[DISCONNECTIONS]]]
-
-
-
-
- Para darse de baja, haz clic aquí en 1 hora desde la recepción de este mensaje.
-
-
-
\ No newline at end of file
diff --git a/emails/translations/device-notify_es.txt b/emails/translations/device-notify_es.txt
deleted file mode 100644
index f3ccc002ac..0000000000
--- a/emails/translations/device-notify_es.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-[[[SERVERNAME]]] - Notificación del dispositivo
-~
-Los siguientes dispositivos han cambiado su estado de conexión.
-~
-~
-~
-Dispositivos conectados:
-~
-~[[[CONNECTIONS]]]
-~
-~
-~
-~
-Dispositivos desconectados:
-~
-~[[[DISCONNECTIONS]]]
-~
-~
-~
-
-Para darse de baja, abrir este enlace antes de 1 hora desde la recepción de este mensaje: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]]
-~
\ No newline at end of file
diff --git a/emails/translations/device-notify_fi.html b/emails/translations/device-notify_fi.html
deleted file mode 100644
index e32cce33af..0000000000
--- a/emails/translations/device-notify_fi.html
+++ /dev/null
@@ -1,36 +0,0 @@
-[[[SERVERNAME]]] - Laitteen ilmoitus
-
-
-
-
- [[[SERVERNAME]]] - Laitteen ilmoitus
-
-
-
-
-
- Seuraavat laitteet ovat muuttaneet yhteystilaa.
-
-
-
-
- Kytketyt laitteet:
-
-
-[[[CONNECTIONS]]]
-
-
-
-
- Irrotetut laitteet:
-
-
-[[[DISCONNECTIONS]]]
-
-
-
-
- Lopettaa tilauksen, klikkaa tästä tunnin sisällä tämän viestin vastaanottamisesta.
-
-
-
\ No newline at end of file
diff --git a/emails/translations/device-notify_fi.txt b/emails/translations/device-notify_fi.txt
deleted file mode 100644
index 80e09f6a03..0000000000
--- a/emails/translations/device-notify_fi.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-[[[SERVERNAME]]] - Laitteen ilmoitus
-~
-Seuraavat laitteet ovat muuttaneet yhteystilaa.
-~
-~
-~
-Kytketyt laitteet:
-~
-~[[[CONNECTIONS]]]
-~
-~
-~
-~
-Irrotetut laitteet:
-~
-~[[[DISCONNECTIONS]]]
-~
-~
-~
-
-Peruuta tilaus lataamalla tämä linkki tunnin sisällä tämän viestin vastaanottamisesta: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]]
-~
\ No newline at end of file
diff --git a/emails/translations/device-notify_fr.html b/emails/translations/device-notify_fr.html
deleted file mode 100644
index 86cbb4d426..0000000000
--- a/emails/translations/device-notify_fr.html
+++ /dev/null
@@ -1,36 +0,0 @@
-[[[SERVERNAME]]] - Notification de l'appareil
-
-
-
-
- [[[SERVERNAME]]] - Notification de l'appareil
-
-
-
-
-
- Ces appareils ont changé leur statut de connexion :
-
-
-
-
- Appareils connectés :
-
-
-[[[CONNECTIONS]]]
-
-
-
-
- Appareils déconnectés :
-
-
-[[[DISCONNECTIONS]]]
-
-
-
-
- Pour se désinscrire, cliquez ici dans l'heure suivant la réception de ce message.
-
-
-
\ No newline at end of file
diff --git a/emails/translations/device-notify_fr.txt b/emails/translations/device-notify_fr.txt
deleted file mode 100644
index 7ff94a0bfb..0000000000
--- a/emails/translations/device-notify_fr.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-[[[SERVERNAME]]] - Notification de l'appareil
-~
-Ces appareils ont changé leur statut de connexion :
-~
-~
-~
-Appareils connectés :
-~
-~[[[CONNECTIONS]]]
-~
-~
-~
-~
-Appareils déconnectés :
-~
-~[[[DISCONNECTIONS]]]
-~
-~
-~
-
-Pour se désinscrire, chargez le lien suivant (valable une heure à réception de ce message) : [[[SERVERURL]]][[[UNSUBSCRIBELINK]]]
-~
\ No newline at end of file
diff --git a/emails/translations/device-notify_hi.html b/emails/translations/device-notify_hi.html
deleted file mode 100644
index 255658611a..0000000000
--- a/emails/translations/device-notify_hi.html
+++ /dev/null
@@ -1,36 +0,0 @@
-[[[SERVERNAME]]] - डिवाइस नोटिफिकेशन
-
-
-
-
- [[[SERVERNAME]]] - डिवाइस नोटिफिकेशन
-
-
-
-
-
- निम्न उपकरणों ने अपनी कनेक्शन स्थिति बदल दी है।
-
-
-
-
- जुड़ी हुई डिवाइसेज:
-
-
-[[[CONNECTIONS]]]
-
-
-
-
- डिस्कनेक्ट किए गए डिवाइस:
-
-
-[[[DISCONNECTIONS]]]
-
-
-
-
- सदस्यता रद्द करने के लिए, यहाँ क्लिक करें यह संदेश मिलने के 1 घंटे के भीतर।
-
-
-
\ No newline at end of file
diff --git a/emails/translations/device-notify_hi.txt b/emails/translations/device-notify_hi.txt
deleted file mode 100644
index 949ab1fb73..0000000000
--- a/emails/translations/device-notify_hi.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-[[[SERVERNAME]]] - डिवाइस नोटिफिकेशन
-~
-निम्न उपकरणों ने अपनी कनेक्शन स्थिति बदल दी है।
-~
-~
-~
-जुड़ी हुई डिवाइसेज:
-~
-~[[[CONNECTIONS]]]
-~
-~
-~
-~
-डिस्कनेक्ट किए गए डिवाइस:
-~
-~[[[DISCONNECTIONS]]]
-~
-~
-~
-
-सदस्यता समाप्त करने के लिए, यह संदेश मिलने के 1 घंटे के भीतर इस लिंक को लोड करें: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]]
-~
\ No newline at end of file
diff --git a/emails/translations/device-notify_hu.html b/emails/translations/device-notify_hu.html
deleted file mode 100644
index 556ec2244f..0000000000
--- a/emails/translations/device-notify_hu.html
+++ /dev/null
@@ -1,36 +0,0 @@
-[[[SERVERNAME]]] - Eszköz értesítés
-
-
-
-
- [[[SERVERNAME]]] - Eszköz értesítés
-
-
-
-
-
- A következő eszközök kapcsolati állapota megváltozott.
-
-
-
-
- Csatlakozott eszközök:
-
-
-[[[CONNECTIONS]]]
-
-
-
-
- Lecsatlakozott eszközök:
-
-
-[[[DISCONNECTIONS]]]
-
-
-
-
- A leiratkozáshoz, Kattintson ide az üzenet kézhezvételétől számított 1 órán belül.
-
-
-
\ No newline at end of file
diff --git a/emails/translations/device-notify_hu.txt b/emails/translations/device-notify_hu.txt
deleted file mode 100644
index d025d6a05b..0000000000
--- a/emails/translations/device-notify_hu.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-[[[SERVERNAME]]] - Eszköz értesítés
-~
-A következő eszközök kapcsolati állapota megváltozott.
-~
-~
-~
-Csatlakozott eszközök:
-~
-~[[[CONNECTIONS]]]
-~
-~
-~
-~
-Lecsatlakozott eszközök:
-~
-~[[[DISCONNECTIONS]]]
-~
-~
-~
-
-A leiratkozáshoz lépjen erre a linkre az üzenet kézhezvételétől számított 1 órán belül: [[[SERVERURL]]][[UNSUBSCRIBELINK]]]
-~
\ No newline at end of file
diff --git a/emails/translations/device-notify_it.html b/emails/translations/device-notify_it.html
deleted file mode 100644
index d182ab6f87..0000000000
--- a/emails/translations/device-notify_it.html
+++ /dev/null
@@ -1,36 +0,0 @@
-[[[SERVERNAME]]] - Notifica dispositivo
-
-
-
-
- [[[SERVERNAME]]] - Notifica dispositivo
-
-
-
-
-
- I seguenti dispositivi hanno cambiato il loro stato di connessione.
-
-
-
-
- Dispositivi collegati:
-
-
-[[[CONNECTIONS]]]
-
-
-
-
- Dispositivi disconnessi:
-
-
-[[[DISCONNECTIONS]]]
-
-
-
-
- Per annullare l'iscrizione, clicca qui entro 1 ora dalla ricezione di questo messaggio.
-
-
-
\ No newline at end of file
diff --git a/emails/translations/device-notify_it.txt b/emails/translations/device-notify_it.txt
deleted file mode 100644
index d8a15d3cca..0000000000
--- a/emails/translations/device-notify_it.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-[[[SERVERNAME]]] - Notifica dispositivo
-~
-I seguenti dispositivi hanno cambiato il loro stato di connessione.
-~
-~
-~
-Dispositivi collegati:
-~
-~[[[CONNECTIONS]]]
-~
-~
-~
-~
-Dispositivi disconnessi:
-~
-~[[[DISCONNECTIONS]]]
-~
-~
-~
-
-Per annullare l'iscrizione, carica questo link entro 1 ora dalla ricezione di questo messaggio: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]]
-~
\ No newline at end of file
diff --git a/emails/translations/device-notify_ja.html b/emails/translations/device-notify_ja.html
deleted file mode 100644
index ae29f65425..0000000000
--- a/emails/translations/device-notify_ja.html
+++ /dev/null
@@ -1,36 +0,0 @@
-[[[SERVERNAME]]]-デバイス通知
-
-
-
-
- [[[SERVERNAME]]]-デバイス通知
-
-
-
-
-
- 次のデバイスは接続状態を変更しました。
-
-
-
-
- 接続されているデバイス:
-
-
-[[[CONNECTIONS]]]
-
-
-
-
- 切断されたデバイス:
-
-
-[[[DISCONNECTIONS]]]
-
-
-
-
- 退会するには、 ここをクリック このメッセージを受け取ってから1時間以内。
-
-
-
\ No newline at end of file
diff --git a/emails/translations/device-notify_ja.txt b/emails/translations/device-notify_ja.txt
deleted file mode 100644
index f6c794551f..0000000000
--- a/emails/translations/device-notify_ja.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-[[[SERVERNAME]]]-デバイス通知
-~
-次のデバイスは接続状態を変更しました。
-~
-~
-~
-接続されているデバイス:
-~
-~[[[CONNECTIONS]]]
-~
-~
-~
-~
-切断されたデバイス:
-~
-~[[[DISCONNECTIONS]]]
-~
-~
-~
-
-登録を解除するには、次のメッセージが表示されてから1時間以内に次のリンクを読み込みます:[[[SERVERURL]]] [[[[UNSUBSCRIBELINK]]]
-~
\ No newline at end of file
diff --git a/emails/translations/device-notify_ko.html b/emails/translations/device-notify_ko.html
deleted file mode 100644
index e397277ca2..0000000000
--- a/emails/translations/device-notify_ko.html
+++ /dev/null
@@ -1,36 +0,0 @@
-[[[SERVERNAME]]] - 기기 알림
-
-
-
-
- [[[SERVERNAME]]] - 기기 알림
-
-
-
-
-
- 다음 장치의 연결 상태가 변경되었습니다.
-
-
-
-
- 연결된 장치:
-
-
-[[[CONNECTIONS]]]
-
-
-
-
- 연결 해제된 장치:
-
-
-[[[DISCONNECTIONS]]]
-
-
-
-
- 구독을 취소하려면 여기를 클릭하십시오. 이 메시지를 받은 후 1시간 이내에
-
-
-
\ No newline at end of file
diff --git a/emails/translations/device-notify_ko.txt b/emails/translations/device-notify_ko.txt
deleted file mode 100644
index ebcba9b46c..0000000000
--- a/emails/translations/device-notify_ko.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-[[[SERVERNAME]]] - 기기 알림
-~
-다음 장치의 연결 상태가 변경되었습니다.
-~
-~
-~
-연결된 장치:
-~
-~[[[CONNECTIONS]]]
-~
-~
-~
-~
-연결 해제된 장치:
-~
-~[[[DISCONNECTIONS]]]
-~
-~
-~
-
-구독을 취소하려면 다음 메시지를 받은 후 1시간 이내에 다음 링크를 로드하세요. [[[SERVERURL]]][[[UNSUBSCRIBELINK]]]
-~
\ No newline at end of file
diff --git a/emails/translations/device-notify_nl.html b/emails/translations/device-notify_nl.html
deleted file mode 100644
index 7257df3530..0000000000
--- a/emails/translations/device-notify_nl.html
+++ /dev/null
@@ -1,36 +0,0 @@
-[[[SERVERNAME]]] - Apparaatmelding
-
-
-
-
- [[[SERVERNAME]]] - Apparaatmelding
-
-
-
-
-
- De volgende apparaten hebben hun verbindingsstatus gewijzigd.
-
-
-
-
- Verbonden apparaten:
-
-
-[[[CONNECTIONS]]]
-
-
-
-
- Losgekoppelde apparaten:
-
-
-[[[DISCONNECTIONS]]]
-
-
-
-
- Uitschrijven, Klik hier binnen 1 uur na ontvangst van dit bericht.
-
-
-
\ No newline at end of file
diff --git a/emails/translations/device-notify_nl.txt b/emails/translations/device-notify_nl.txt
deleted file mode 100644
index b0c3463930..0000000000
--- a/emails/translations/device-notify_nl.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-[[[SERVERNAME]]] - Apparaatmelding
-~
-De volgende apparaten hebben hun verbindingsstatus gewijzigd.
-~
-~
-~
-Verbonden apparaten:
-~
-~[[[CONNECTIONS]]]
-~
-~
-~
-~
-Losgekoppelde apparaten:
-~
-~[[[DISCONNECTIONS]]]
-~
-~
-~
-
-Om je af te melden, open je deze link binnen 1 uur nadat je dit bericht hebt ontvangen: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]]
-~
\ No newline at end of file
diff --git a/emails/translations/device-notify_pl.html b/emails/translations/device-notify_pl.html
deleted file mode 100644
index 8425ff0263..0000000000
--- a/emails/translations/device-notify_pl.html
+++ /dev/null
@@ -1,36 +0,0 @@
-[[[SERVERNAME]]] - Powiadomienia Urządzenia
-
-
-
-
- [[[SERVERNAME]]] - Powiadomienia Urządzenia
-
-
-
-
-
- Przedstawione urządzenia zmieniły stan połączenia.
-
-
-
-
- Podłączone urządzenia:
-
-
-[[[CONNECTIONS]]]
-
-
-
-
- Rozłączone urządzenia:
-
-
-[[[DISCONNECTIONS]]]
-
-
-
-
- By się wypisać. kliknij tutaj w ciągu 1 godziny od otrzymania wiadomości.
-
-
-
\ No newline at end of file
diff --git a/emails/translations/device-notify_pl.txt b/emails/translations/device-notify_pl.txt
deleted file mode 100644
index 17c09f830a..0000000000
--- a/emails/translations/device-notify_pl.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-[[[SERVERNAME]]] - Powiadomienia Urządzenia
-~
-Przedstawione urządzenia zmieniły stan połączenia.
-~
-~
-~
-Podłączone urządzenia:
-~
-~[[[CONNECTIONS]]]
-~
-~
-~
-~
-Rozłączone urządzenia:
-~
-~[[[DISCONNECTIONS]]]
-~
-~
-~
-
-By się wypisać, załaduj ten link w ciągu 1 godziny od otrzymania wiadomości: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]]
-~
\ No newline at end of file
diff --git a/emails/translations/device-notify_pt-br.html b/emails/translations/device-notify_pt-br.html
deleted file mode 100644
index 15af27c69e..0000000000
--- a/emails/translations/device-notify_pt-br.html
+++ /dev/null
@@ -1,36 +0,0 @@
-[[[SERVERNAME]]] - Notificação do dispositivo
-
-
-
-
- [[[SERVERNAME]]] - Notificação do dispositivo
-
-
-
-
-
- Os seguintes dispositivos mudaram seus estados de conexão.
-
-
-
-
- Dispositivos conectados
-
-
-[[[CONNECTIONS]]]
-
-
-
-
- Dispositivos desconectados
-
-
-[[[DISCONNECTIONS]]]
-
-
-
-
- Cancelar inscrição Clique aqui Com 1 hora após recebida está mensagem.
-
-
-
\ No newline at end of file
diff --git a/emails/translations/device-notify_pt-br.txt b/emails/translations/device-notify_pt-br.txt
deleted file mode 100644
index b9ab12b5db..0000000000
--- a/emails/translations/device-notify_pt-br.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-[[[SERVERNAME]]] - Notificação do dispositivo
-~
-Os seguintes dispositivos mudaram seus estados de conexão.
-~
-~
-~
-Dispositivos conectados
-~
-~[[[CONNECTIONS]]]
-~
-~
-~
-~
-Dispositivos desconectados
-~
-~[[[DISCONNECTIONS]]]
-~
-~
-~
-
-Para cancelar inscrição, clique no link dentro de 1 hora após receber esta mensagem: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]]
-~
\ No newline at end of file
diff --git a/emails/translations/device-notify_pt.html b/emails/translations/device-notify_pt.html
deleted file mode 100644
index 0e50ce6aa7..0000000000
--- a/emails/translations/device-notify_pt.html
+++ /dev/null
@@ -1,36 +0,0 @@
-[[[SERVERNAME]]] - Notificação do dispositivo
-
-
-
-
- [[[SERVERNAME]]] - Notificação do dispositivo
-
-
-
-
-
- Os seguintes dispositivos mudaram seu estado de conexão.
-
-
-
-
- Dispositivos conectados:
-
-
-[[[CONNECTIONS]]]
-
-
-
-
- Dispositivos desconectados:
-
-
-[[[DISCONNECTIONS]]]
-
-
-
-
- Para cancelar a inscrição, Clique aqui dentro de 1 hora após receber esta mensagem.
-
-
-
\ No newline at end of file
diff --git a/emails/translations/device-notify_pt.txt b/emails/translations/device-notify_pt.txt
deleted file mode 100644
index 7f7675c6b7..0000000000
--- a/emails/translations/device-notify_pt.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-[[[SERVERNAME]]] - Notificação do dispositivo
-~
-Os seguintes dispositivos mudaram seu estado de conexão.
-~
-~
-~
-Dispositivos conectados:
-~
-~[[[CONNECTIONS]]]
-~
-~
-~
-~
-Dispositivos desconectados:
-~
-~[[[DISCONNECTIONS]]]
-~
-~
-~
-
-Para cancelar a inscrição, carregue este link em até 1 hora após receber esta mensagem: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]]
-~
\ No newline at end of file
diff --git a/emails/translations/device-notify_ru.html b/emails/translations/device-notify_ru.html
deleted file mode 100644
index e8b8d2b398..0000000000
--- a/emails/translations/device-notify_ru.html
+++ /dev/null
@@ -1,36 +0,0 @@
-[[[SERVERNAME]]] - Уведомление устройства
-
-
-
-
- [[[SERVERNAME]]] - Уведомление устройства
-
-
-
-
-
- Следующие устройства изменили свое состояние подключения.
-
-
-
-
- Подключенные устройства:
-
-
-[[[CONNECTIONS]]]
-
-
-
-
- Отключенные устройства:
-
-
-[[[DISCONNECTIONS]]]
-
-
-
-
- Чтобы отписаться, кликните сюда в течение 1 часа после получения этого сообщения.
-
-
-
\ No newline at end of file
diff --git a/emails/translations/device-notify_ru.txt b/emails/translations/device-notify_ru.txt
deleted file mode 100644
index 139e9144b8..0000000000
--- a/emails/translations/device-notify_ru.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-[[[SERVERNAME]]] - Уведомление устройства
-~
-Следующие устройства изменили свое состояние подключения.
-~
-~
-~
-Подключенные устройства:
-~
-~[[[CONNECTIONS]]]
-~
-~
-~
-~
-Отключенные устройства:
-~
-~[[[DISCONNECTIONS]]]
-~
-~
-~
-
-Чтобы отказаться от подписки, откройте эту ссылку в течение 1 часа после получения этого сообщения: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]]
-~
\ No newline at end of file
diff --git a/emails/translations/device-notify_sv.html b/emails/translations/device-notify_sv.html
deleted file mode 100644
index a9c457d67b..0000000000
--- a/emails/translations/device-notify_sv.html
+++ /dev/null
@@ -1,36 +0,0 @@
-[[[SERVERNAME]]] – enhetsavisering
-
-
-
-
- [[[SERVERNAME]]] – enhetsavisering
-
-
-
-
-
- Följande enheter har ändrat anslutningsstatus.
-
-
-
-
- Anslutna enheter:
-
-
-[[[CONNECTIONS]]]
-
-
-
-
- Frånkopplade enheter:
-
-
-[[[DISCONNECTIONS]]]
-
-
-
-
- Att avprenumerera, Klicka här inom 1 timme efter att du fick detta meddelande.
-
-
-
\ No newline at end of file
diff --git a/emails/translations/device-notify_sv.txt b/emails/translations/device-notify_sv.txt
deleted file mode 100644
index be164b2b3a..0000000000
--- a/emails/translations/device-notify_sv.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-[[[SERVERNAME]]] – enhetsavisering
-~
-Följande enheter har ändrat anslutningsstatus.
-~
-~
-~
-Anslutna enheter:
-~
-~[[[CONNECTIONS]]]
-~
-~
-~
-~
-Frånkopplade enheter:
-~
-~[[[DISCONNECTIONS]]]
-~
-~
-~
-
-För att avsluta prenumerationen, ladda denna länk inom 1 timme efter att du fått detta meddelande: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]]
-~
\ No newline at end of file
diff --git a/emails/translations/device-notify_tr.html b/emails/translations/device-notify_tr.html
deleted file mode 100644
index 6f473e33f7..0000000000
--- a/emails/translations/device-notify_tr.html
+++ /dev/null
@@ -1,36 +0,0 @@
-[[[SERVERNAME]]] - Cihaz Bildirimi
-
-
-
-
- [[[SERVERNAME]]] - Cihaz Bildirimi
-
-
-
-
-
- Aşağıdaki cihazlar bağlantı durumlarını değiştirdi.
-
-
-
-
- Bağlı cihazlar:
-
-
-[[[CONNECTIONS]]]
-
-
-
-
- Bağlantısı kesilen cihazlar:
-
-
-[[[DISCONNECTIONS]]]
-
-
-
-
- Abonelikten çıkmak, buraya Tıkla Bu mesajı aldıktan sonra 1 saat içinde.
-
-
-
\ No newline at end of file
diff --git a/emails/translations/device-notify_tr.txt b/emails/translations/device-notify_tr.txt
deleted file mode 100644
index be4553eb82..0000000000
--- a/emails/translations/device-notify_tr.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-[[[SERVERNAME]]] - Cihaz Bildirimi
-~
-Aşağıdaki cihazlar bağlantı durumlarını değiştirdi.
-~
-~
-~
-Bağlı cihazlar:
-~
-~[[[CONNECTIONS]]]
-~
-~
-~
-~
-Bağlantısı kesilen cihazlar:
-~
-~[[[DISCONNECTIONS]]]
-~
-~
-~
-
-Aboneliğinizi iptal etmek için, bu mesajı aldıktan sonraki 1 saat içinde bu bağlantıyı yükleyin: [[[SERVERURL]]][[[UNSUBSCRIBELINK]]]
-~
\ No newline at end of file
diff --git a/emails/translations/device-notify_zh-chs.html b/emails/translations/device-notify_zh-chs.html
deleted file mode 100644
index c24ac8c95f..0000000000
--- a/emails/translations/device-notify_zh-chs.html
+++ /dev/null
@@ -1,36 +0,0 @@
-[[[SERVERNAME]]] - 设备通知
-
-
-
-
- [[[SERVERNAME]]] - 设备通知
-
-
-
-
-
- 以下设备已更改其连接状态。
-
-
-
-
- 连接的设备:
-
-
-[[[CONNECTIONS]]]
-
-
-
-
- 断开连接的设备:
-
-
-[[[DISCONNECTIONS]]]
-
-
-
-
- 退订, 点击这里 在收到此消息的 1 小时内。
-
-
-
\ No newline at end of file
diff --git a/emails/translations/device-notify_zh-chs.txt b/emails/translations/device-notify_zh-chs.txt
deleted file mode 100644
index d992e0c01d..0000000000
--- a/emails/translations/device-notify_zh-chs.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-[[[SERVERNAME]]] - 设备通知
-~
-以下设备已更改其连接状态。
-~
-~
-~
-连接的设备:
-~
-~[[[CONNECTIONS]]]
-~
-~
-~
-~
-断开连接的设备:
-~
-~[[[DISCONNECTIONS]]]
-~
-~
-~
-
-要取消订阅,请在收到此消息后 1 小时内加载此链接:[[[SERVERURL]]][[[UNSUBSCRIBELINK]]]
-~
\ No newline at end of file
diff --git a/emails/translations/device-notify_zh-cht.html b/emails/translations/device-notify_zh-cht.html
deleted file mode 100644
index 4d0ef07f65..0000000000
--- a/emails/translations/device-notify_zh-cht.html
+++ /dev/null
@@ -1,36 +0,0 @@
-[[[SERVERNAME]]] - 設備通知
-
-
-
-
- [[[SERVERNAME]]] - 設備通知
-
-
-
-
-
- 以下設備已更改其連接狀態。
-
-
-
-
- 連接的設備:
-
-
-[[[CONNECTIONS]]]
-
-
-
-
- 斷開連接的設備:
-
-
-[[[DISCONNECTIONS]]]
-
-
-
-
- 退訂, 點擊這裡 在收到此消息的 1 小時內。
-
-
-
\ No newline at end of file
diff --git a/emails/translations/device-notify_zh-cht.txt b/emails/translations/device-notify_zh-cht.txt
deleted file mode 100644
index 22e63c0764..0000000000
--- a/emails/translations/device-notify_zh-cht.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-[[[SERVERNAME]]] - 設備通知
-~
-以下設備已更改其連接狀態。
-~
-~
-~
-連接的設備:
-~
-~[[[CONNECTIONS]]]
-~
-~
-~
-~
-斷開連接的設備:
-~
-~[[[DISCONNECTIONS]]]
-~
-~
-~
-
-要取消訂閱,請在收到此消息後 1 小時內加載此鏈接:[[[SERVERURL]]][[[UNSUBSCRIBELINK]]]
-~
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_bs.html b/emails/translations/mesh-invite_bs.html
deleted file mode 100644
index 1011318aee..0000000000
--- a/emails/translations/mesh-invite_bs.html
+++ /dev/null
@@ -1,47 +0,0 @@
-[[[SERVERNAME]]] - Pozivnica
-
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_bs.txt b/emails/translations/mesh-invite_bs.txt
deleted file mode 100644
index 3080b36451..0000000000
--- a/emails/translations/mesh-invite_bs.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-[[[SERVERNAME]]] - Pozivnica
-~
-Zdravo [[[NAME]]],
-~
-Korisnik [[[USERNAME]]] na serveru [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) traži da instalirate softver za pokretanje sesije daljinskog upravljanja.
-~
-~
-Poruka: [[[MSG]]]
-~
-~
-~
-Za Windows, idite na sljedeću vezu da dovršite proces:
-~
-~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Za Apple OSX, idite na sljedeću vezu da dovršite proces:
-~
-~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Za Linux, izrežite i zalijepite sljedeće u terminal da biste instalirali agenta:
-~
-~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]'
-~
-~
-~
-Za MeshCentral Assistant na Windows-u, idite na sljedeću vezu da dovršite proces:
-~
-~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]]
-~
-~
-~
-Da instalirate softver, idite na [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] i slijedite upute.
-~
-Ako niste vi inicirali ovaj zahtjev, zanemarite ovu poruku.
-~
-Srdačan pozdrav,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_cs.html b/emails/translations/mesh-invite_cs.html
deleted file mode 100644
index fd58933826..0000000000
--- a/emails/translations/mesh-invite_cs.html
+++ /dev/null
@@ -1,47 +0,0 @@
-[[[SERVERNAME]]] - Pozvánka
-
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_cs.txt b/emails/translations/mesh-invite_cs.txt
deleted file mode 100644
index a1075a5301..0000000000
--- a/emails/translations/mesh-invite_cs.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-[[[SERVERNAME]]] - Pozvánka
-~
-Dobrý den, [[[NAME]]],
-~
-Uživatel [[[USERNAME]]] na serveru [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) požaduje instalaci softwaru k zahájení relace vzdáleného řízení.
-~
-~
-Zpráva: [[[MSG]]]
-~
-~
-~
-V případě systému Windows dokončete proces pomocí následujícího odkazu:
-~
-~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-V případě systému Apple OSX dokončete proces pomocí následujícího odkazu:
-~
-~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-V případě systému Linux vyjměte a vložte do terminálu a nainstalujte agenta:
-~
-~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]'
-~
-~
-~
-Pro MeshCentral Assistant v systému Windows přejděte na následující odkaz a dokončete proces:
-~
-~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]]
-~
-~
-~
-Chcete-li nainstalovat software, přejděte na [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] a postupujte podle pokynů.
-~
-Pokud jste tento požadavek nezačali, ignorujte tento e-mail.
-~
-S pozdravem,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_da.html b/emails/translations/mesh-invite_da.html
deleted file mode 100644
index 320e962a36..0000000000
--- a/emails/translations/mesh-invite_da.html
+++ /dev/null
@@ -1,47 +0,0 @@
-[[[SERVERNAME]]] - Invitation
-
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_da.txt b/emails/translations/mesh-invite_da.txt
deleted file mode 100644
index 7c96ccf8d0..0000000000
--- a/emails/translations/mesh-invite_da.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-[[[SERVERNAME]]] - Invitation
-~
-Hej [[[NAME]]]
-~
-Bruger [[[USERNAME]]] på serveren [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) anmoder dig om at installere software for at starte fjernbetjeningssessionen.
-~
-~
-Meddelelse: [[[[MSG]]]
-~
-~
-~
-For Windows skal du navigere til følgende link for at fuldføre processen:
-~
-~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-For Apple OSX skal du navigere til følgende link for at fuldføre processen:
-~
-~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-For Linux skal du klippe og indsætte følgende i en terminal for at installere agenten:
-~
-~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]'
-~
-~
-~
-For MeshCentral Assistant på Windows skal du gå til følgende link for at fuldføre processen:
-~
-~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]]
-~
-~
-~
-For at installere softwaren skal du navigere til [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] og følge instruktionerne.
-~
-Hvis du ikke startede denne anmodning, bedes du ignorere denne mail.
-~
-Venlig hilsen,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_de.html b/emails/translations/mesh-invite_de.html
deleted file mode 100644
index b9e61c2c7f..0000000000
--- a/emails/translations/mesh-invite_de.html
+++ /dev/null
@@ -1,47 +0,0 @@
-[[[SERVERNAME]]] - Einladung
-
-
-
-
- [[[SERVERNAME]]] - Agent-Installation
-
-
-
-
-
- Hallo [[[NAME]]],
-
-
-
Benutzer [[[USERNAME]]] auf dem Server [[[SERVERNAME]]] fordert Sie auf, Software zu installieren, um eine Fernsteuerungssitzung zu starten.
-
-
- Nachricht: [[[MSG]]]
-
-
-
-
- Klicken Sie hier, um den MeshAgent für Windows herunterzuladen.
-
-
-
- Klicken Sie hier, um den MeshAgent für Apple OSX herunterzuladen.
-
-
-
- Schneiden Sie unter Linux Folgendes aus und fügen Sie es in ein Terminal ein, um den Agent zu installieren:
-
wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'
-
-
-
-
- Klicken Sie hier, um den MeshCentral-Assistenten für Windows herunterzuladen.
-
-
-
-
- Um die Software zu installieren, hier klicken und folgen Sie den Anweisungen.
-
-
-
Wenn Sie diese Anfrage nicht initiiert haben, ignorieren Sie diese Mail bitte.
- Freundliche Grüße,
[[[USERNAME]]]
-
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_de.txt b/emails/translations/mesh-invite_de.txt
deleted file mode 100644
index 46699aa31c..0000000000
--- a/emails/translations/mesh-invite_de.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-[[[SERVERNAME]]] - Einladung
-~
-Hallo [[[NAME]]],
-~
-Benutzer [[[USERNAME]]] auf dem Server [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) fordert Sie auf, Software zu installieren, um die Fernsteuerungssitzung zu starten.
-~
-~
-Nachricht: [[[MSG]]]
-~
-~
-~
-Nagivieren Sie unter Windows den folgenden Link, um den Vorgang abzuschließen:
-~
-~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Nagivieren Sie unter Apple OSX den folgenden Link, um den Vorgang abzuschließen:
-~
-~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Schneiden Sie unter Linux Folgendes aus und fügen Sie es in ein Terminal ein, um den Agent zu installieren:
-~
-~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]'
-~
-~
-~
-Navigieren Sie für MeshCentral Assistant unter Windows zum folgenden Link, um den Vorgang abzuschließen:
-~
-~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]]
-~
-~
-~
-Navigieren Sie zum Installieren der Software zu [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] und befolgen Sie die Anweisungen.
-~
-Wenn Sie diese Anfrage nicht initiiert haben, ignorieren Sie diese Mail bitte.
-~
-Freundliche Grüße,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_es.html b/emails/translations/mesh-invite_es.html
deleted file mode 100644
index 3a1d396957..0000000000
--- a/emails/translations/mesh-invite_es.html
+++ /dev/null
@@ -1,47 +0,0 @@
-[[[SERVERNAME]]] - Invitación
-
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_es.txt b/emails/translations/mesh-invite_es.txt
deleted file mode 100644
index 8ec8f17244..0000000000
--- a/emails/translations/mesh-invite_es.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-[[[SERVERNAME]]] - Invitación
-~
-Hola [[[NAME]]],
-~
-Usuario [[[USERNAME]]] en servitor [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) solicita que instale el software para iniciar la sesión de control remoto.
-~
-~
-Mensaje: [[[MSG]]]
-~
-~
-~
-Para Windows, navega al siguiente enlace para completar el proceso:
-~
-~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Para Apple OSX, navega hasta el siguiente enlace para completar el proceso:
-~
-~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Para Linux, copia y pega lo siguiente en la terminal para instalar el agente:
-~
-~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]'
-~
-~
-~
-Para el Asistente de MeshCentral en Windows, navegue hasta el siguiente enlace para completar el proceso:
-~
-~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]]
-~
-~
-~
-Para instalar el software, navega a [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] y siga las instrucciones.
-~
-Si tu no iniciaste este requerimiento, por favor ignora este correo.
-~
-Atentamente,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_fi.html b/emails/translations/mesh-invite_fi.html
deleted file mode 100644
index 4e5f4862be..0000000000
--- a/emails/translations/mesh-invite_fi.html
+++ /dev/null
@@ -1,47 +0,0 @@
-[[[SERVERNAME]]] - Kutsu
-
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_fi.txt b/emails/translations/mesh-invite_fi.txt
deleted file mode 100644
index c1ceef63a3..0000000000
--- a/emails/translations/mesh-invite_fi.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-[[[SERVERNAME]]] - Kutsu
-~
-Hei [[[NAME]]],
-~
-Käyttäjä[[[USERNAME]]] palvelimella [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) pyytää sinua asentamaan ohjelmiston etähallintaistunnon käynnistämiseksi.
-~
-~
-Viesti: [[[MSG]]]
-~
-~
-~
-Jos kyseessä on Windows, suorita prosessi loppuun avaamalla seuraava linkki:
-~
-~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Apple OSX -käyttöjärjestelmässä prosessin loppuun saattamiseksi napsauta seuraavaa linkkiä:
-~
-~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Linux: leikkaa ja liitä seuraava päätelaitteeseen agentin asentamiseksi:
-~
-~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]'
-~
-~
-~
-Jos käytät MeshCentral Assistantia Windowsissa, siirry seuraavaan linkkiin prosessin viimeistelemiseksi:
-~
-~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]]
-~
-~
-~
-Asenna ohjelmisto siirtymällä kohtaan [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] ja noudattamalla ohjeita.
-~
-Jos et suorittanut tätä pyyntöä, voit ohitaa tämän sähköpostin.
-~
-Ystävällisin terveisin,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_fr.html b/emails/translations/mesh-invite_fr.html
deleted file mode 100644
index 6e9f82c4d1..0000000000
--- a/emails/translations/mesh-invite_fr.html
+++ /dev/null
@@ -1,47 +0,0 @@
-[[[SERVERNAME]]] - Invitation
-
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_fr.txt b/emails/translations/mesh-invite_fr.txt
deleted file mode 100644
index 723e6ff86c..0000000000
--- a/emails/translations/mesh-invite_fr.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-[[[SERVERNAME]]] - Invitation
-~
-Bonjour [[[NAME]]],
-~
-L'utilisateur [[[USERNAME]]] sur le serveur [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) vous demande d'installer un logiciel pour démarrer la session de contrôle à distance.
-~
-~
-Message: [[[MSG]]]
-~
-~
-~
-Pour Windows, accédez au lien suivant pour terminer le processus:
-~
-~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Pour Apple OSX, accédez au lien suivant pour terminer le processus:
-~
-~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Pour Linux, copiez et collez les éléments suivants dans un terminal pour installer l'agent:
-~
-~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]'
-~
-~
-~
-Pour l'assistant MeshCentral sous Windows, cliquez sur le lien suivant pour compléter le processus :
-~
-~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]]
-~
-~
-~
-Pour installer le logiciel, accédez à [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] et suivez les instructions.
-~
-Si vous n'avez pas initié cette demande, veuillez ignorer ce courrier.
-~
-Meilleures salutations,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_hi.html b/emails/translations/mesh-invite_hi.html
deleted file mode 100644
index b59cb88cdd..0000000000
--- a/emails/translations/mesh-invite_hi.html
+++ /dev/null
@@ -1,47 +0,0 @@
-[[[SERVERNAME]]] - निमंत्रण
-
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_hi.txt b/emails/translations/mesh-invite_hi.txt
deleted file mode 100644
index 70c2899f9b..0000000000
--- a/emails/translations/mesh-invite_hi.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-[[[SERVERNAME]]] - निमंत्रण
-~
-नमस्कार [[[NAME]]],
-~
-उपयोगकर्ता [[[USERNAME]]] सर्वर पर [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) आपसे रिमोट कंट्रोल सेशन शुरू करने के लिए सॉफ़्टवेयर स्थापित करने का अनुरोध कर रहा है।
-~
-~
-संदेश: [[[MSG]]]
-~
-~
-~
-विंडोज के लिए, प्रक्रिया को पूरा करने के लिए निम्नलिखित लिंक पर जाएं:
-~
-~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Apple OSX के लिए, इस प्रक्रिया को पूरा करने के लिए निम्नलिखित लिंक पर जाएं:
-~
-~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-लिनक्स में, एजेंट को स्थापित करने के लिए टर्मिनल में निम्नलिखित को काटें और चिपकाएँ:
-~
-~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]'
-~
-~
-~
-विंडोज़ पर मेशसेंट्रल असिस्टेंट के लिए, प्रक्रिया को पूरा करने के लिए निम्न लिंक पर जाएँ:
-~
-~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]]
-~
-~
-~
-सॉफ़्टवेयर को स्थापित करने के लिए, [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] पर जाएँ और निर्देशों का पालन करें।
-~
-यदि आपने यह अनुरोध आरंभ नहीं किया है, तो कृपया इस मेल को अनदेखा करें।
-~
-सादर,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_hu.html b/emails/translations/mesh-invite_hu.html
deleted file mode 100644
index 00c299361a..0000000000
--- a/emails/translations/mesh-invite_hu.html
+++ /dev/null
@@ -1,47 +0,0 @@
-[[[SERVERNAME]]] - Meghívás
-
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_hu.txt b/emails/translations/mesh-invite_hu.txt
deleted file mode 100644
index a73f47465c..0000000000
--- a/emails/translations/mesh-invite_hu.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-[[[SERVERNAME]]] - Meghívás
-~
-Tisztelt [[[NAME]]],
-~
-A [[[USERNAME]]] felhasználó a [[[SERVERNAME]]] kiszolgálón ([[[[SERVERURL]]]/[[[URLARGS1]]]) a távvezérlési munkamenet indításához szükséges szoftver telepítését kéri.
-~
-~
-Üzenet: [[[MSG]]]
-~
-~
-~
-Windows esetén a folyamat befejezéséhez menjen a következő hivatkozásra:
-~
-~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Apple OSX esetén kattintson az alábbi linkre a folyamat befejezéséhez:
-~
-~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Linux esetén az Agent telepítéséhez vágja ki és illessze be egy terminálba a következőt:
-~
-~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]'
-~
-~
-~
-Windows rendszeren futó MeshCentral Assistant esetén menjen a következő hivatkozásra a folyamat befejezéséhez:
-~
-~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]]
-~
-~
-~
-A szoftver telepítéséhez kérjük lépjen a [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] oldalra, és kövesse az utasításokat.
-~
-Ha nem Ön kezdeményezte ezt a kérést, kérjük, hagyja figyelmen kívül ezt a levelet.
-~
-Üdvözlettel,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_it.html b/emails/translations/mesh-invite_it.html
deleted file mode 100644
index 3e767207ed..0000000000
--- a/emails/translations/mesh-invite_it.html
+++ /dev/null
@@ -1,47 +0,0 @@
-[[[SERVERNAME]]] - Invito
-
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_it.txt b/emails/translations/mesh-invite_it.txt
deleted file mode 100644
index c008c7808f..0000000000
--- a/emails/translations/mesh-invite_it.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-[[[SERVERNAME]]] - Invito
-~
-Salve [[[NAME]]],
-~
-L'utente [[[USERNAME]]] sul server [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) sta richiedendo l'installazione del software per avviare la sessione di controllo remoto.
-~
-~
-Messaggio: [[[MSG]]]
-~
-~
-~
-Per Windows, accedi al seguente collegamento per completare il processo:
-~
-~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Per Apple OSX, vai al seguente link per completare il processo:
-~
-~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Per Linux, taglia e incolla quanto segue in un terminale per installare l'agente:
-~
-~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]'
-~
-~
-~
-Per MeshCentral Assistant su Windows, accedere al seguente collegamento per completare il processo:
-~
-~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]]
-~
-~
-~
-Per installare il software, vai a [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] e segui le istruzioni.
-~
-Se non hai avviato questa richiesta, ignora questo messaggio.
-~
-I migliori saluti,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_ja.html b/emails/translations/mesh-invite_ja.html
deleted file mode 100644
index 3cd59ed9a3..0000000000
--- a/emails/translations/mesh-invite_ja.html
+++ /dev/null
@@ -1,47 +0,0 @@
-[[[SERVERNAME]]] - 招待
-
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_ja.txt b/emails/translations/mesh-invite_ja.txt
deleted file mode 100644
index 248a316d05..0000000000
--- a/emails/translations/mesh-invite_ja.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-[[[SERVERNAME]]] - 招待
-~
-[[[NAME]]]様
-~
-サーバー[[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) のユーザー[[[USERNAME]]]が、リモートコントロールセッションを開始するためのソフトウェアのインストールを要求しています。
-~
-~
-メッセージ:[[[MSG]]]
-~
-~
-~
-Windowsの場合、次のリンクに移動してプロセスを完了します。
-~
-~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Apple OSXの場合、次のリンクに移動してプロセスを完了します。
-~
-~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Linuxの場合は、ターミナルで以下をカットアンドペーストしてエージェントをインストールします。
-~
-~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]'
-~
-~
-~
-WindowsのMeshCentral Assistantの場合、次のリンクに移動してプロセスを完了します。
-~
-~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]]
-~
-~
-~
-ソフトウェアをインストールするには、[[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]]に移動し、指示に従います。
-~
-このリクエストを開始していない場合は、このメールを無視してください。
-~
-宜しくお願いします、
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_ko.html b/emails/translations/mesh-invite_ko.html
deleted file mode 100644
index a01a297b2a..0000000000
--- a/emails/translations/mesh-invite_ko.html
+++ /dev/null
@@ -1,47 +0,0 @@
-[[[SERVERNAME]]] - 초대
-
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_ko.txt b/emails/translations/mesh-invite_ko.txt
deleted file mode 100644
index 35175f80fe..0000000000
--- a/emails/translations/mesh-invite_ko.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-[[[SERVERNAME]]] - 초대
-~
-안녕하세요, [[[NAME]]]님.
-~
-[[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) 서버의 [[[USERNAME]]] 사용자가 원격 제어 세션을 시작하기 위해서는 소프트웨어 설치가 요구됩니다.
-~
-~
-메시지: [[[MSG]]]
-~
-~
-~
-Windows의 경우, 다음 링크로 이동하여 과정을 완료하십시오.
-~
-~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Apple OSX인 경우, 다음 링크로 이동하여 과정을 완료하십시오.
-~
-~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Linux의 경우, 다음을 잘라내어 터미널에 붙여 넣어 에이전트를 설치하십시오.
-~
-~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]'
-~
-~
-~
-Windows용 MeshCentral Assistant의 경우 다음 링크로 이동하여 프로세스를 완료하십시오.
-~
-~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]]
-~
-~
-~
-소프트웨어를 설치하려면, 다음에 접속하여 지시에 따르십시오 : [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]]
-~
-이 요청을 시작하지 않은 경우, 이 메일을 무시하십시오.
-~
-최고의 안부를 전합니다,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_nl.html b/emails/translations/mesh-invite_nl.html
deleted file mode 100644
index 0cbfa53d87..0000000000
--- a/emails/translations/mesh-invite_nl.html
+++ /dev/null
@@ -1,47 +0,0 @@
-[[[SERVERNAME]]] - Uitnodiging
-
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_nl.txt b/emails/translations/mesh-invite_nl.txt
deleted file mode 100644
index dbaebaddd1..0000000000
--- a/emails/translations/mesh-invite_nl.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-[[[SERVERNAME]]] - Uitnodiging
-~
-Hallo [[[NAME]]],
-~
-Gebruiker [[[USERNAME]]] op server [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) vraagt u de software te installeren om de sessie voor afstandsbediening te starten.
-~
-~
-Bericht: [[[MSG]]]
-~
-~
-~
-Voor Windows, ga je naar de volgende link om het proces te voltooien:
-~
-~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Voor Apple OSX ga je naar de volgende link om het proces te voltooien:
-~
-~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Voor Linux, knip het volgende en plak dit in een terminal om de agent te installeren:
-~
-~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]'
-~
-~
-~
-Voor MeshCentral Assistant voor Windows navigeert u naar de volgende link om het proces te voltooien:
-~
-~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]]
-~
-~
-~
-Om de software te installeren, navigeert u naar [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] en volgt u de instructies.
-~
-Als u dit verzoek niet heeft ingediend, dan kunt u deze e-mail negeren.
-~
-Vriendelijke groeten,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_pl.html b/emails/translations/mesh-invite_pl.html
deleted file mode 100644
index 35f3e02abf..0000000000
--- a/emails/translations/mesh-invite_pl.html
+++ /dev/null
@@ -1,47 +0,0 @@
-[[[SERVERNAME]]] - Zaproszenie
-
-
-
-
- [[[SERVERNAME]]] - Instalacja Agenta
-
-
-
-
-
- Witaj [[[NAME]]],
-
-
-
Użytkownik [[[USERNAME]]] na serwerze [[[SERVERNAME]]] prosi o zainstalowanie oprogramowania w celu rozpoczęcia sesji zdalnego sterowania.
-
-
- Wiadomość: [[[MSG]]]
-
-
-
-
- Kliknij tutaj aby pobrać MeshAgent dla Windows.
-
-
-
- Kliknij tutaj aby pobrać MeshAgent dla Apple OSX.
-
-
-
- W systemie Linuks, wytnij i wklej poniższe polecenia w terminalu, aby zainstalować agenta:
-
wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'
-
-
-
-
- Kliknij tutaj by pobrać Asystenta MeshCentral dla Windows.
-
-
-
-
- Aby zainstalować oprogramowanie, kliknij tutaj i postępuj zgodnie z instrukcjami.
-
-
-
Jeśli nie byłeś inicjatorem tej prośby, proszę zignorować ten email.
- Z wyrazami szacunku,
[[[USERNAME]]]
-
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_pl.txt b/emails/translations/mesh-invite_pl.txt
deleted file mode 100644
index 4eef498476..0000000000
--- a/emails/translations/mesh-invite_pl.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-[[[SERVERNAME]]] - Zaproszenie
-~
-Witaj [[[NAME]]],
-~
-Użytkownik [[[USERNAME]]] na serwerze [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) prosi o zainstalowanie oprogramowania w celu uruchomienia sesji zdalnego sterowania.
-~
-~
-Wiadomość: [[[MSG]]]
-~
-~
-~
-W przypadku systemu Windows należy przejść do następującego linku, aby zakończyć proces:
-~
-~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Dla Apple OSX, przejdź do następującego linku, aby zakończyć proces:
-~
-~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-W systemie Linuks, wytnij i wklej poniższe polecenia w terminalu, aby zainstalować agenta:
-~
-~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]'
-~
-~
-~
-W przypadku Asystenta MeshCentral w Windows, przejdź do tego linku aby zakończyć proces:
-~
-~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]]
-~
-~
-~
-Aby zainstalować oprogramowanie, przejdź do [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] i postępuj zgodnie z instrukcjami.
-~
-Jeśli nie byłeś inicjatorem tej prośby, proszę zignorować ten email.
-~
-Z wyrazami szacunku,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_pt-br.html b/emails/translations/mesh-invite_pt-br.html
deleted file mode 100644
index 00fc013bde..0000000000
--- a/emails/translations/mesh-invite_pt-br.html
+++ /dev/null
@@ -1,47 +0,0 @@
-[[[SERVERNAME]]] - Convite
-
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_pt-br.txt b/emails/translations/mesh-invite_pt-br.txt
deleted file mode 100644
index 83fe1d1dea..0000000000
--- a/emails/translations/mesh-invite_pt-br.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-[[[SERVERNAME]]] - Convite
-~
-Olá [[[NAME]]],
-~
-O usuário [[[USERNAME]]] no servidor [[[SERVERNAME]]] ([[[SERVERURL]]] / [[[URLARGS1]]]) está solicitando a instalação do software para iniciar a sessão de controle remoto.
-~
-~
-Mensagem: [[[MSG]]]
-~
-~
-~
-Para Windows, vá ao seguinte link para concluir o processo:
-~
-~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Para Apple OSX, vá ao seguinte link para concluir o processo:
-~
-~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Para Linux, recorte e cole o seguinte em um terminal para instalar o agente:
-~
-~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]'
-~
-~
-~
-Para o MeshCentral Assistant no Windows, abre o seguinte link para completar o processo:
-~
-~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]]
-~
-~
-~
-Para instalar o software, navegue até [[[SERVERURL]]] [[[LINKURL]]] [[[URLARGS2]]] e siga as instruções.
-~
-Se você não iniciou esta solicitação, ignore este e-mail.
-~
-Atenciosamente,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_pt.html b/emails/translations/mesh-invite_pt.html
deleted file mode 100644
index 71603863f3..0000000000
--- a/emails/translations/mesh-invite_pt.html
+++ /dev/null
@@ -1,47 +0,0 @@
-[[[SERVERNAME]]] - Convite
-
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_pt.txt b/emails/translations/mesh-invite_pt.txt
deleted file mode 100644
index 945095e3e3..0000000000
--- a/emails/translations/mesh-invite_pt.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-[[[SERVERNAME]]] - Convite
-~
-Olá [[[NAME]]],
-~
-O usuário [[[USERNAME]]] no servidor [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) está solicitando a instalação do software para iniciar a sessão de controle remoto.
-~
-~
-Mensagem: [[[MSG]]]
-~
-~
-~
-Para Windows, vá ao seguinte link para concluir o processo:
-~
-~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Para Apple OSX, vá ao seguinte link para concluir o processo:
-~
-~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Para Linux, recorte e cole o seguinte em um terminal para instalar o agente:
-~
-~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]'
-~
-~
-~
-Para o MeshCentral Assistant no Windows, navegue até o seguinte link para concluir o processo:
-~
-~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]]
-~
-~
-~
-Para instalar o software, navegue até [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] e siga as instruções.
-~
-Se você não iniciou esta solicitação, ignore este e-mail.
-~
-Cumprimentos,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_ru.html b/emails/translations/mesh-invite_ru.html
deleted file mode 100644
index 8e90fbc2c2..0000000000
--- a/emails/translations/mesh-invite_ru.html
+++ /dev/null
@@ -1,47 +0,0 @@
-[[[SERVERNAME]]] - Приглашение
-
-
-
-
- [[[SERVERNAME]]] - Установка агента
-
-
-
-
-
- Здравствуйте, [[[NAME]]],
-
-
-
Пользователь [[[USERNAME]]] на сервере [[[SERVERNAME]]] просит вас установить программное обеспечение, чтобы начать сеанс удаленного управления.
-
-
- Сообщение: [[[MSG]]]
-
-
-
-
- Нажмите здесь, чтобы загрузить MeshAgent для Windows.
-
-
-
- Нажмите здесь, чтобы загрузить MeshAgent для Apple OSX.
-
-
-
- Для Linux вырезайте и вставляйте в терминал следующее, чтобы установить агент:
-
wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'
-
-
-
-
- Нажмите здесь, чтобы загрузить MeshCentral Assistant для Windows.
-
-
-
-
- Чтобы установить программное обеспечение, кликните сюда и следуйте инструкциям.
-
-
-
Если вы не инициировали этот запрос, игнорируйте это письмо.
- С уважением,
[[[USERNAME]]]
-
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_ru.txt b/emails/translations/mesh-invite_ru.txt
deleted file mode 100644
index db629717c9..0000000000
--- a/emails/translations/mesh-invite_ru.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-[[[SERVERNAME]]] - Приглашение
-~
-Здравствуйте, [[[NAME]]],
-~
-Пользователь [[[USERNAME]]] на сервере [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) запрашивает установку программного обеспечения для запуска сеанса удаленного управления.
-~
-~
-Сообщение: [[[MSG]]]
-~
-~
-~
-Для Windows откройте следующую ссылку, чтобы завершить процесс:
-~
-~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Для Apple OSX перейдите по следующей ссылке, чтобы завершить процесс:
-~
-~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Для Linux вырезайте и вставляйте в терминал следующее, чтобы установить агент:
-~
-~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]'
-~
-~
-~
-Для MeshCentral Assistant под Windows перейдите по следующей ссылке, чтобы завершить процесс:
-~
-~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]]
-~
-~
-~
-Для установки программного обеспечения перейдите к [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] и следуйте инструкциям.
-~
-Если вы не инициировали этот запрос, игнорируйте это письмо.
-~
-С уважением,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_sv.html b/emails/translations/mesh-invite_sv.html
deleted file mode 100644
index 478a60d1f7..0000000000
--- a/emails/translations/mesh-invite_sv.html
+++ /dev/null
@@ -1,47 +0,0 @@
-[[[SERVERNAME]]] - Inbjudan
-
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_sv.txt b/emails/translations/mesh-invite_sv.txt
deleted file mode 100644
index c32bd51b6b..0000000000
--- a/emails/translations/mesh-invite_sv.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-[[[SERVERNAME]]] - Inbjudan
-~
-Hej [[[NAME]]]
-~
-Användaren [[[USERNAME]]] på servern [[[SERVERNAME]]] ([[[SERVERURL]]/[[[URLARGS1]]) ber dig installera programvara för att starta fjärrkontrollsessionen.
-~
-~
-Meddelande: [[[[MSG]]]
-~
-~
-~
-För Windows, nagivera till följande länk för att slutföra processen:
-~
-~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-För Apple OSX, nagivera till följande länk för att slutföra processen:
-~
-~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-För Linux, klipp och klistra in följande i en terminal för att installera agenten:
-~
-~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]'
-~
-~
-~
-För MeshCentral Assistant på Windows, gå till följande länk för att slutföra processen:
-~
-~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]]
-~
-~
-~
-För att installera programvaran, navigera till [[[SERVERURL]]] [[[LINKURL]]] [[[URLARGS2]]] och följ instruktionerna.
-~
-Om du inte initierade denna begäran, ignorerar du det här meddelandet.
-~
-Vänliga hälsningar,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_tr.html b/emails/translations/mesh-invite_tr.html
deleted file mode 100644
index 1036a9d0ad..0000000000
--- a/emails/translations/mesh-invite_tr.html
+++ /dev/null
@@ -1,47 +0,0 @@
-[[[SERVERNAME]]] - Davet
-
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_tr.txt b/emails/translations/mesh-invite_tr.txt
deleted file mode 100644
index df97c4bc4f..0000000000
--- a/emails/translations/mesh-invite_tr.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-[[[SERVERNAME]]] - Davet
-~
-Merhaba [[[NAME]]],
-~
-[[[SERVERNAME]]] [[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) sunucusundaki [[[USERNAME]]] uzaktan kontrol oturumunu başlatmak için yazılım yüklemenizi istiyor.
-~
-~
-Mesaj: [[[MSG]]]
-~
-~
-~
-Windows için, işlemi tamamlamak için aşağıdaki bağlantıya gidin:
-~
-~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Apple OSX için, işlemi tamamlamak için aşağıdaki bağlantıya gidin:
-~
-~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-Linux için, aracıyı yüklemek için aşağıdakileri kesip bir terminale yapıştırın:
-~
-~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]'
-~
-~
-~
-Windows'ta MeshCentral Assistant için işlemi tamamlamak üzere aşağıdaki bağlantıya gidin:
-~
-~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]]
-~
-~
-~
-Yazılımı kurmak için [[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]] adresine gidin ve talimatları takip edin.
-~
-Bu isteği siz başlatmadıysanız, lütfen bu postayı dikkate almayın.
-~
-Saygılarımla,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_zh-chs.html b/emails/translations/mesh-invite_zh-chs.html
deleted file mode 100644
index 6c5b9412d2..0000000000
--- a/emails/translations/mesh-invite_zh-chs.html
+++ /dev/null
@@ -1,47 +0,0 @@
-[[[SERVERNAME]]] - 邀请
-
-
-
-
- [[[SERVERNAME]]]-代理程序安装
-
-
-
-
-
- 您好[[[NAME]]],
-
-
-
服务器上的使用者[[[USERNAME]]] [[[SERVERNAME]]] 正在要求您安装软件以启动远程控制会话。
-
-
- 消息: [[[MSG]]]
-
-
-
-
- 单击此处下载适用于Windows的MeshAgent。
-
-
-
- 单击此处下载适用于Apple OSX的MeshAgent。
-
-
-
- 对于Linux,将以下内容剪切并粘贴到终端中以安装代理软件:
-
wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'
-
-
-
-
- 单击此处下载适用于 Windows 的 MeshCentral 助手。
-
-
-
-
- 要安装软件, 点击这里 并按照说明进行操作。
-
-
-
如果您没有发起此请求,请忽略此邮件。
- 最好的祝福,
[[[USERNAME]]]
-
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_zh-chs.txt b/emails/translations/mesh-invite_zh-chs.txt
deleted file mode 100644
index 20d8af69d4..0000000000
--- a/emails/translations/mesh-invite_zh-chs.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-[[[SERVERNAME]]] - 邀请
-~
-您好[[[NAME]]],
-~
-服务器[[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) 上的用户[[[USERNAME]]]请求您安装软件以启动远程控制。
-~
-~
-消息:[[[MSG]]]
-~
-~
-~
-对于Windows,请导航至以下连结以完成该过程:
-~
-~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-对于Apple OSX,请导航至以下连结以完成该过程:
-~
-~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-对于Linux,将以下内容剪切并粘贴到终端中以安装代理软件:
-~
-~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]'
-~
-~
-~
-对于 Windows 上的 MeshCentral Assistant,导航到以下链接以完成该过程:
-~
-~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]]
-~
-~
-~
-要安装软件,请导航至[[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]],然后按照说明进行操作。
-~
-如果您没有发起此请求,请忽略此邮件。
-~
-最好的祝福,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_zh-cht.html b/emails/translations/mesh-invite_zh-cht.html
deleted file mode 100644
index 8d6d011f60..0000000000
--- a/emails/translations/mesh-invite_zh-cht.html
+++ /dev/null
@@ -1,47 +0,0 @@
-[[[SERVERNAME]]] - 邀請
-
-
-
-
- [[[SERVERNAME]]]-代理程序安裝
-
-
-
-
-
- 你好[[[NAME]]],
-
-
-
伺服器上的使用者[[[USERNAME]]] [[[SERVERNAME]] 正在要求你安裝軟體以啟動遠程控制會話。
-
-
- 訊息: [[[MSG]]]
-
-
-
-
- 單擊此處下載適用於Windows的MeshAgent。
-
-
-
- 單擊此處下載適用於Apple OSX的MeshAgent。
-
-
-
- 對於Linux,將以下內容剪切並粘貼到終端中以安裝代理軟體:
-
wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHIDHEX]]]\'
-
-
-
-
- 單擊此處下載適用於 Windows 的 MeshCentral 助手。
-
-
-
-
- 要安裝軟體, 點擊這裡 並按照說明進行操作。
-
-
-
如果你沒有發起此請求,請不理此電郵。
- 最好的祝福,
[[[USERNAME]]]
-
\ No newline at end of file
diff --git a/emails/translations/mesh-invite_zh-cht.txt b/emails/translations/mesh-invite_zh-cht.txt
deleted file mode 100644
index 0d6ef61601..0000000000
--- a/emails/translations/mesh-invite_zh-cht.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-[[[SERVERNAME]]] - 邀請
-~
-你好[[[NAME]]],
-~
-伺服器[[[SERVERNAME]]] ([[[SERVERURL]]]/[[[URLARGS1]]]) 上的用戶[[[USERNAME]]]請求你安裝軟體以啟動遠程控制。
-~
-~
-訊息:[[[MSG]]]
-~
-~
-~
-對於Windows,請導航至以下鏈結以完成該過程:
-~
-~[[[SERVERURL]]]/meshagents?id=4&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-對於Apple OSX,請導航至以下鏈結以完成該過程:
-~
-~[[[SERVERURL]]]/meshosxagent?id=16&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&installflags=[[[INSTALLFLAGS]]]
-~
-~
-~
-對於Linux,將以下內容剪切並粘貼到終端中以安裝代理軟體:
-~
-~wget -q "[[[SERVERURL]]]/meshagents?script=1" --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] '[[[MESHIDHEX]]]'
-~
-~
-~
-對於 Windows 上的 MeshCentral Assistant,導航到以下鏈接以完成該過程:
-~
-~[[[SERVERURL]]]/meshagents?id=10006&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]&ac=[[[ASSISTANTTYPE]]]
-~
-~
-~
-要安裝軟體,請導航至[[[SERVERURL]]][[[LINKURL]]][[[URLARGS2]]],然後按照說明進行操作。
-~
-如果你沒有發起此請求,請不理此電郵。
-~
-最好的祝福,
-~[[[USERNAME]]]
\ No newline at end of file
diff --git a/emails/translations/sms-messages_bs.txt b/emails/translations/sms-messages_bs.txt
deleted file mode 100644
index 768b0043b0..0000000000
--- a/emails/translations/sms-messages_bs.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-[[0]] verifikacioni kod je: [[1]]
-[[0]] pristupni token je: [[1]]
diff --git a/emails/translations/sms-messages_cs.txt b/emails/translations/sms-messages_cs.txt
deleted file mode 100644
index c859513d42..0000000000
--- a/emails/translations/sms-messages_cs.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-[[0]] ověřovací kód je: [[1]]
-[[0]] přístupový token je: [[1]]
diff --git a/emails/translations/sms-messages_da.txt b/emails/translations/sms-messages_da.txt
deleted file mode 100644
index f6f39e34a6..0000000000
--- a/emails/translations/sms-messages_da.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-[[0]] verifikations kode er: [[1]]
-[[0]] adgangs token er: [[1]]
diff --git a/emails/translations/sms-messages_de.txt b/emails/translations/sms-messages_de.txt
deleted file mode 100644
index 8ff877f88b..0000000000
--- a/emails/translations/sms-messages_de.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-[[0]] Bestätigungscode lautet: [[1]]
-[[0]] Zugriffstoken ist: [[1]]
diff --git a/emails/translations/sms-messages_es.txt b/emails/translations/sms-messages_es.txt
deleted file mode 100644
index 68a40081c8..0000000000
--- a/emails/translations/sms-messages_es.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-El código de verificación de [[0]] es: [[1]]
-El token de acceso de [[0]] es: [[1]]
diff --git a/emails/translations/sms-messages_fi.txt b/emails/translations/sms-messages_fi.txt
deleted file mode 100644
index 86bc6f51ae..0000000000
--- a/emails/translations/sms-messages_fi.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-[[0]] vahvistuskoodi on: [[1]]
-[[0]] pääsy koodi on: [[1]]
diff --git a/emails/translations/sms-messages_fr.txt b/emails/translations/sms-messages_fr.txt
deleted file mode 100644
index fda9fe69dc..0000000000
--- a/emails/translations/sms-messages_fr.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-[[0]] le code de vérification est: [[1]]
-[[0]] le jeton d'accès est: [[1]]
diff --git a/emails/translations/sms-messages_hi.txt b/emails/translations/sms-messages_hi.txt
deleted file mode 100644
index 81e0954b6e..0000000000
--- a/emails/translations/sms-messages_hi.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-[[0]] सत्यापन कोड है: [[1]]
-[[0]] अभिगमन टोकन है: [[१]]
diff --git a/emails/translations/sms-messages_hu.txt b/emails/translations/sms-messages_hu.txt
deleted file mode 100644
index 07b75b61ef..0000000000
--- a/emails/translations/sms-messages_hu.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-[[0]] ellenőrző kód: [[1]]
-[[0]] hozzáférés token: [[1]]
diff --git a/emails/translations/sms-messages_it.txt b/emails/translations/sms-messages_it.txt
deleted file mode 100644
index 6ede48550e..0000000000
--- a/emails/translations/sms-messages_it.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-[[0]] il codice di verifica è: [[1]]
-[0]] token di accesso è: [[1]]
diff --git a/emails/translations/sms-messages_ja.txt b/emails/translations/sms-messages_ja.txt
deleted file mode 100644
index a1a9414914..0000000000
--- a/emails/translations/sms-messages_ja.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-[[0]]確認コード:[[1]]
-[[0]]アクセストークンは:[[1]]
diff --git a/emails/translations/sms-messages_ko.txt b/emails/translations/sms-messages_ko.txt
deleted file mode 100644
index 997bc7b1ad..0000000000
--- a/emails/translations/sms-messages_ko.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-[[0]] 인증된 코드는 다음과 같습니다 : [[1]]
-[[0]] 접근 토큰은 다음과 같습니다 : [[1]]
diff --git a/emails/translations/sms-messages_nl.txt b/emails/translations/sms-messages_nl.txt
deleted file mode 100644
index 249be2aadf..0000000000
--- a/emails/translations/sms-messages_nl.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-[[0]] verificatie code is: [[1]]
-[[0]] Toegangs token is: [[1]]
diff --git a/emails/translations/sms-messages_pl.txt b/emails/translations/sms-messages_pl.txt
deleted file mode 100644
index efb43ce004..0000000000
--- a/emails/translations/sms-messages_pl.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-[[0]] kod weryfikacyjny to: [[1]]
-[[0]] token dostępu to: [[1]]
diff --git a/emails/translations/sms-messages_pt-br.txt b/emails/translations/sms-messages_pt-br.txt
deleted file mode 100644
index 6e1f0f695e..0000000000
--- a/emails/translations/sms-messages_pt-br.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-[[0]] código de verificação é: [[1]]
-[[0]] token de acesso é: [[1]]
diff --git a/emails/translations/sms-messages_pt.txt b/emails/translations/sms-messages_pt.txt
deleted file mode 100644
index 6e1f0f695e..0000000000
--- a/emails/translations/sms-messages_pt.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-[[0]] código de verificação é: [[1]]
-[[0]] token de acesso é: [[1]]
diff --git a/emails/translations/sms-messages_ru.txt b/emails/translations/sms-messages_ru.txt
deleted file mode 100644
index 7c2cbc3a2b..0000000000
--- a/emails/translations/sms-messages_ru.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-[[0]] код подтверждения: [[1]]
-[[0]] токен доступа: [[1]]
diff --git a/emails/translations/sms-messages_sv.txt b/emails/translations/sms-messages_sv.txt
deleted file mode 100644
index 970fa91915..0000000000
--- a/emails/translations/sms-messages_sv.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-[[0]] verifieringskod är: [[1]]
-[[0]] åtkomsttoken är: [[1]]
diff --git a/emails/translations/sms-messages_tr.txt b/emails/translations/sms-messages_tr.txt
deleted file mode 100644
index 921f4c35de..0000000000
--- a/emails/translations/sms-messages_tr.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-[[0]] doğrulama kodu: [[1]]
-[[0]] erişim anahtarı: [[1]]
diff --git a/emails/translations/sms-messages_zh-chs.txt b/emails/translations/sms-messages_zh-chs.txt
deleted file mode 100644
index 9d2e320e24..0000000000
--- a/emails/translations/sms-messages_zh-chs.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-[[0]]验证码是:[[1]]
-[[0]]访问令牌是:[[1]]
diff --git a/emails/translations/sms-messages_zh-cht.txt b/emails/translations/sms-messages_zh-cht.txt
deleted file mode 100644
index bb274698ef..0000000000
--- a/emails/translations/sms-messages_zh-cht.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-[[0]]驗證碼是:[[1]]
-[[0]]訪問保安編碼是:[[1]]
diff --git a/firebase.js b/firebase.js
index 124acfa07b..36d7c94824 100644
--- a/firebase.js
+++ b/firebase.js
@@ -166,7 +166,7 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) {
const WebSocket = require('ws');
const https = require('https');
const querystring = require('querystring');
- const relayUrl = require('url').parse(url);
+ const relayUrl = new URL(url);
parent.debug('email', 'CreateFirebaseRelay-Setup');
// Setup logging
@@ -294,7 +294,7 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) {
const httpOptions = {
hostname: relayUrl.hostname,
port: relayUrl.port ? relayUrl.port : 443,
- path: relayUrl.path + (key ? ('?key=' + key) : ''),
+ path: relayUrl.pathname + (key ? ('?key=' + key) : ''),
method: 'POST',
//rejectUnauthorized: false, // DEBUG
headers: {
diff --git a/letsencrypt.js b/letsencrypt.js
index f52f9094c7..348d074322 100644
--- a/letsencrypt.js
+++ b/letsencrypt.js
@@ -29,6 +29,7 @@ module.exports.CreateLetsEncrypt = function (parent) {
obj.runAsProduction = false;
obj.redirWebServerHooked = false;
obj.zerossl = false;
+ obj.custom = false;
obj.csr = null;
obj.configErr = null;
obj.configOk = false;
@@ -60,6 +61,7 @@ module.exports.CreateLetsEncrypt = function (parent) {
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.custom = ((typeof obj.parent.config.letsencrypt.custom == 'object') ? obj.parent.config.letsencrypt.custom : 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; }
diff --git a/macosinstaller.js b/macosinstaller.js
new file mode 100644
index 0000000000..015a4d776b
--- /dev/null
+++ b/macosinstaller.js
@@ -0,0 +1,391 @@
+/*
+ * @description Cross-platform macOS flat package builder for MeshAgent installers.
+ * Creates a XAR-based distribution package instead of the legacy bundle .mpkg
+ * format that macOS Sequoia/Tahoe rejects.
+ */
+
+'use strict';
+
+const crypto = require('crypto');
+const fs = require('fs');
+const fsp = fs.promises;
+const os = require('os');
+const path = require('path');
+const zlib = require('zlib');
+const childProcess = require('child_process');
+const { promisify } = require('util');
+
+const deflate = promisify(zlib.deflate);
+const execFile = promisify(childProcess.execFile);
+
+const LAUNCH_DAEMON_PLIST = `
+
+
+
+ Label
+ ###SERVICENAME###
+ ProgramArguments
+
+ /usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/###EXECUTABLENAME###
+
+ WorkingDirectory
+ /usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/
+ RunAtLoad
+
+ KeepAlive
+
+ ThrottleInterval
+ 1
+
+
+`;
+
+const LAUNCH_AGENT_PLIST = `
+
+
+
+ Label
+ ###SERVICENAME###-launchagent
+ ProgramArguments
+
+ /usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/###EXECUTABLENAME###
+ -kvmagent
+
+ LimitLoadToSessionType
+
+ LoginWindow
+ Aqua
+
+ WorkingDirectory
+ /usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/
+ RunAtLoad
+
+ KeepAlive
+
+
+
+`;
+
+const POSTINSTALL = `#!/bin/bash
+set -e
+
+SERVICENAME="###SERVICENAME###"
+COMPANYNAME="###COMPANYNAME###"
+EXECUTABLENAME="###EXECUTABLENAME###"
+INSTALLDIR="/usr/local/mesh_services/\${COMPANYNAME}/\${SERVICENAME}"
+
+chown -R root:wheel "/usr/local/mesh_services/\${COMPANYNAME}" || true
+chown root:wheel "\${INSTALLDIR}/\${EXECUTABLENAME}" "\${INSTALLDIR}/\${EXECUTABLENAME}.msh"
+chown root:wheel "/Library/LaunchDaemons/\${SERVICENAME}.plist" "/Library/LaunchAgents/\${SERVICENAME}-launchagent.plist"
+
+chmod 755 "\${INSTALLDIR}" "\${INSTALLDIR}/\${EXECUTABLENAME}"
+chmod 644 "\${INSTALLDIR}/\${EXECUTABLENAME}.msh" "/Library/LaunchDaemons/\${SERVICENAME}.plist" "/Library/LaunchAgents/\${SERVICENAME}-launchagent.plist"
+
+/bin/launchctl bootout system "/Library/LaunchDaemons/\${SERVICENAME}.plist" >/dev/null 2>&1 || true
+/bin/launchctl bootstrap system "/Library/LaunchDaemons/\${SERVICENAME}.plist" >/dev/null 2>&1 || /bin/launchctl load "/Library/LaunchDaemons/\${SERVICENAME}.plist"
+
+CONSOLE_USER=$(stat -f '%Su' /dev/console 2>/dev/null || true)
+CONSOLE_UID=$(id -u "\${CONSOLE_USER}" 2>/dev/null || true)
+if [ -n "\${CONSOLE_UID}" ] && [ "\${CONSOLE_UID}" != "0" ]; then
+ /bin/launchctl bootout "gui/\${CONSOLE_UID}" "/Library/LaunchAgents/\${SERVICENAME}-launchagent.plist" >/dev/null 2>&1 || true
+ /bin/launchctl bootstrap "gui/\${CONSOLE_UID}" "/Library/LaunchAgents/\${SERVICENAME}-launchagent.plist" >/dev/null 2>&1 || true
+fi
+`;
+
+const UNINSTALL = `#!/bin/bash
+
+echo "Stopping ###SERVICENAME###..."
+sudo /bin/launchctl bootout system "/Library/LaunchDaemons/###SERVICENAME###.plist" &> /dev/null || sudo /bin/launchctl unload "/Library/LaunchDaemons/###SERVICENAME###.plist" &> /dev/null
+sudo pkill -9 "###SERVICENAME###" &> /dev/null || true
+CONSOLE_USER=$(stat -f '%Su' /dev/console 2>/dev/null || true)
+CONSOLE_UID=$(id -u "\${CONSOLE_USER}" 2>/dev/null || true)
+if [ -n "\${CONSOLE_UID}" ] && [ "\${CONSOLE_UID}" != "0" ]; then
+ sudo /bin/launchctl bootout "gui/\${CONSOLE_UID}" "/Library/LaunchAgents/###SERVICENAME###-launchagent.plist" &> /dev/null || true
+fi
+sudo /bin/launchctl unload "/Library/LaunchDaemons/meshagentDiagnostic_periodicStart.plist" &> /dev/null
+sudo /bin/launchctl unload "/Library/LaunchDaemons/meshagentDiagnostic.plist" &> /dev/null
+sudo rm "/Library/LaunchDaemons/meshagentDiagnostic_periodicStart.plist" &> /dev/null
+sudo rm "/Library/LaunchDaemons/meshagentDiagnostic.plist" &> /dev/null
+
+echo "Resetting TCC permissions for ###SERVICENAME###..."
+BUNDLE_ID=$(mdls -name kMDItemCFBundleIdentifier -raw "/usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/###EXECUTABLENAME###" 2>/dev/null || true)
+if [ -n "\${BUNDLE_ID}" ] && [ "\${BUNDLE_ID}" != "(null)" ]; then
+ sudo tccutil reset All "\${BUNDLE_ID}" &> /dev/null || true
+fi
+sudo tccutil reset All "###SERVICENAME###" &> /dev/null || true
+
+sudo rm "/usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/###EXECUTABLENAME###" &> /dev/null
+sudo rm "/usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/###EXECUTABLENAME###.msh" &> /dev/null
+sudo rm "/usr/local/mesh_services/###COMPANYNAME###/###SERVICENAME###/###EXECUTABLENAME###.db" &> /dev/null
+sudo rm "/usr/local/mesh_services/meshagentDiagnostic/meshagentDiagnostic" &> /dev/null
+sudo rm "/Library/LaunchDaemons/###SERVICENAME###.plist" &> /dev/null
+sudo rm "/Library/LaunchAgents/###SERVICENAME###-launchagent.plist" &> /dev/null
+echo "###SERVICENAME### was uninstalled."
+`;
+
+function replaceTokens(str, tokens) {
+ return str.split('###SERVICENAME###').join(tokens.serviceName)
+ .split('###COMPANYNAME###').join(tokens.companyName)
+ .split('###EXECUTABLENAME###').join(tokens.executableName);
+}
+
+function xmlEscape(str) {
+ return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''');
+}
+
+function pkgIdentifierSegment(str) {
+ return String(str).toLowerCase().replace(/[^a-z0-9.-]/g, '-').replace(/^-+|-+$/g, '') || 'meshagent';
+}
+
+async function chmodIfExists(file, mode) {
+ try { await fsp.chmod(file, mode); } catch (ex) { }
+}
+
+async function walk(dir) {
+ const entries = await fsp.readdir(dir, { withFileTypes: true });
+ let files = 0, bytes = 0;
+ for (const entry of entries) {
+ const p = path.join(dir, entry.name);
+ if (entry.isDirectory()) {
+ const r = await walk(p);
+ files += r.files;
+ bytes += r.bytes;
+ } else if (entry.isFile()) {
+ const s = await fsp.stat(p);
+ files++;
+ bytes += s.size;
+ }
+ }
+ return { files, bytes };
+}
+
+function pad4(buffer) {
+ const pad = (4 - (buffer.length % 4)) % 4;
+ return (pad === 0) ? buffer : Buffer.concat([buffer, Buffer.alloc(pad)]);
+}
+
+function octal(value, width) {
+ const max = Math.pow(8, width) - 1;
+ const n = Math.max(0, Math.min(Number(value) || 0, max));
+ return Math.floor(n).toString(8).padStart(width, '0').slice(-width);
+}
+
+async function collectPayloadEntries(root, relativePath) {
+ const fullPath = path.join(root, relativePath);
+ const stat = await fsp.stat(fullPath);
+ const entries = [];
+ if (relativePath !== '') {
+ entries.push({ name: relativePath.split(path.sep).join('/'), stat: stat, data: stat.isFile() ? await fsp.readFile(fullPath) : null });
+ }
+ if (stat.isDirectory()) {
+ const names = (await fsp.readdir(fullPath)).sort();
+ for (const name of names) { entries.push.apply(entries, await collectPayloadEntries(root, path.join(relativePath, name))); }
+ }
+ return entries;
+}
+
+function cpioOdcRecord(name, mode, data, ino, mtime) {
+ data = data || Buffer.alloc(0);
+ const nameBuffer = Buffer.from(name + '\0', 'utf8');
+ const header = [
+ '070707',
+ octal(0, 6), // dev
+ octal(ino, 6),
+ octal(mode, 6),
+ octal(0, 6), // uid
+ octal(0, 6), // gid
+ octal(1, 6), // nlink
+ octal(0, 6), // rdev
+ octal(mtime || Math.floor(Date.now() / 1000), 11),
+ octal(nameBuffer.length, 6),
+ octal(data.length, 11)
+ ].join('');
+ return Buffer.concat([Buffer.from(header, 'ascii'), nameBuffer, data]);
+}
+
+async function createPayload(payloadRoot, targetFile) {
+ const entries = await collectPayloadEntries(payloadRoot, '');
+ const records = [];
+ let ino = 1;
+ for (const entry of entries) {
+ records.push(cpioOdcRecord(entry.name, entry.stat.mode, entry.data, ino++, Math.floor(entry.stat.mtimeMs / 1000)));
+ }
+ records.push(cpioOdcRecord('TRAILER!!!', 0, Buffer.alloc(0), ino));
+ await fsp.writeFile(targetFile, zlib.gzipSync(Buffer.concat(records)));
+}
+
+async function createBom(payloadRoot, targetFile) {
+ try {
+ await execFile('mkbom', [payloadRoot, targetFile], { timeout: 30000 });
+ } catch (ex) {
+ // Linux/Windows hosts can still build the package archive without a
+ // third-party BOM dependency. macOS hosts use the native mkbom tool
+ // above so local validation keeps the richer bill of materials.
+ await fsp.writeFile(targetFile, Buffer.alloc(0));
+ }
+}
+
+async function collectXarEntry(filePath, name, id) {
+ const stat = await fsp.stat(filePath);
+ const entry = {
+ id: id,
+ name: name,
+ type: stat.isDirectory() ? 'directory' : 'file',
+ mode: stat.mode,
+ uid: stat.uid,
+ gid: stat.gid,
+ atime: stat.atime,
+ mtime: stat.mtime,
+ ctime: stat.ctime
+ };
+ if (stat.isFile()) {
+ entry.data = await fsp.readFile(filePath);
+ } else if (stat.isDirectory()) {
+ const names = (await fsp.readdir(filePath)).sort();
+ entry.children = [];
+ for (const childName of names) {
+ entry.children.push(await collectXarEntry(path.join(filePath, childName), childName, ++collectXarEntry.nextId));
+ }
+ }
+ return entry;
+}
+
+function xarDate(d) {
+ return d.toISOString();
+}
+
+function xarFileXml(entry, depth, heapParts) {
+ const indent = ' '.repeat(depth);
+ let xml = indent + '\n'
+ + indent + ' ' + xmlEscape(entry.name) + ' \n'
+ + indent + ' ' + entry.type + ' \n'
+ + indent + ' ' + entry.mode.toString(8) + ' \n'
+ + indent + ' ' + entry.uid + ' \n'
+ + indent + ' ' + entry.gid + ' \n'
+ + indent + ' ' + xarDate(entry.atime) + ' \n'
+ + indent + ' ' + xarDate(entry.mtime) + ' \n'
+ + indent + ' ' + xarDate(entry.ctime) + ' \n';
+ if (entry.type == 'file') {
+ const offset = 20 + heapParts.reduce(function (total, part) { return total + part.length; }, 0);
+ const sum = crypto.createHash('sha1').update(entry.data).digest('hex');
+ heapParts.push(entry.data);
+ xml += indent + ' \n'
+ + indent + ' ' + sum + ' \n'
+ + indent + ' ' + sum + ' \n'
+ + indent + ' ' + offset + ' \n'
+ + indent + ' \n'
+ + indent + ' ' + entry.data.length + ' \n'
+ + indent + ' ' + entry.data.length + ' \n'
+ + indent + ' \n';
+ } else {
+ for (const child of entry.children) { xml += xarFileXml(child, depth + 1, heapParts); }
+ }
+ return xml + indent + ' \n';
+}
+
+async function createXarPackage(paths) {
+ collectXarEntry.nextId = 0;
+ const entries = [];
+ for (const p of paths) { entries.push(await collectXarEntry(p, path.basename(p), ++collectXarEntry.nextId)); }
+
+ const heapParts = [];
+ let toc = '\n\n \n'
+ + ' \n 20 \n 0 \n \n'
+ + ' ' + (new Date()).toISOString() + ' \n';
+ for (const entry of entries) { toc += xarFileXml(entry, 2, heapParts); }
+ toc += ' \n ';
+
+ const tocBuffer = Buffer.from(toc, 'utf8');
+ const compressedToc = await deflate(tocBuffer);
+ const tocChecksum = crypto.createHash('sha1').update(compressedToc).digest();
+ const header = Buffer.alloc(28);
+ header.writeUInt32BE(0x78617221, 0); // xar!
+ header.writeUInt16BE(28, 4);
+ header.writeUInt16BE(1, 6);
+ header.writeBigUInt64BE(BigInt(compressedToc.length), 8);
+ header.writeBigUInt64BE(BigInt(tocBuffer.length), 16);
+ header.writeUInt32BE(1, 24); // sha1
+ return Buffer.concat([header, compressedToc, tocChecksum].concat(heapParts));
+}
+
+async function createMacOSInstaller(opts) {
+ const tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'meshcentral-macos-pkg-'));
+ try {
+ const payloadRoot = path.join(tmpRoot, 'payload');
+ const scriptsRoot = path.join(tmpRoot, 'scripts');
+ const basePkg = path.join(tmpRoot, 'internal.pkg');
+ const resourcesDir = path.join(tmpRoot, 'Resources');
+ const installDir = path.join(payloadRoot, 'usr', 'local', 'mesh_services', opts.companyName, opts.serviceName);
+ const launchDaemons = path.join(payloadRoot, 'Library', 'LaunchDaemons');
+ const launchAgents = path.join(payloadRoot, 'Library', 'LaunchAgents');
+ const tokens = { serviceName: opts.serviceName, companyName: opts.companyName, executableName: opts.executableName };
+
+ await fsp.mkdir(installDir, { recursive: true });
+ await fsp.mkdir(launchDaemons, { recursive: true });
+ await fsp.mkdir(launchAgents, { recursive: true });
+ await fsp.mkdir(basePkg, { recursive: true });
+ await fsp.mkdir(scriptsRoot, { recursive: true });
+ await fsp.mkdir(resourcesDir, { recursive: true });
+
+ await fsp.copyFile(opts.agentPath, path.join(installDir, opts.executableName));
+ await fsp.writeFile(path.join(installDir, opts.executableName + '.msh'), opts.meshSettings);
+ await fsp.writeFile(path.join(launchDaemons, opts.serviceName + '.plist'), replaceTokens(LAUNCH_DAEMON_PLIST, tokens));
+ await fsp.writeFile(path.join(launchAgents, opts.serviceName + '-launchagent.plist'), replaceTokens(LAUNCH_AGENT_PLIST, tokens));
+ await fsp.writeFile(path.join(scriptsRoot, 'postinstall'), replaceTokens(POSTINSTALL, tokens));
+
+ if (opts.backgroundPath) {
+ await fsp.copyFile(opts.backgroundPath, path.join(resourcesDir, 'background'));
+ } else {
+ const backgroundPath = path.join(__dirname, 'agents', 'macosinstallerbackground.png');
+ await fsp.copyFile(backgroundPath, path.join(resourcesDir, 'background'));
+ }
+
+ await chmodIfExists(path.join(installDir, opts.executableName), 0o755);
+ await chmodIfExists(path.join(scriptsRoot, 'postinstall'), 0o755);
+ await chmodIfExists(path.join(installDir, opts.executableName + '.msh'), 0o644);
+ await chmodIfExists(path.join(launchDaemons, opts.serviceName + '.plist'), 0o644);
+ await chmodIfExists(path.join(launchAgents, opts.serviceName + '-launchagent.plist'), 0o644);
+
+ const payloadStats = await walk(payloadRoot);
+ const installKBytes = Math.ceil(payloadStats.bytes / 1000);
+ await createPayload(payloadRoot, path.join(basePkg, 'Payload'));
+ await createPayload(scriptsRoot, path.join(basePkg, 'Scripts'));
+ await createBom(payloadRoot, path.join(basePkg, 'Bom'));
+
+ const packageInfo = '\n'
+ + ' \n'
+ + ' \n'
+ + ' \n'
+ + ' \n'
+ + ' \n';
+ await fsp.writeFile(path.join(basePkg, 'PackageInfo'), packageInfo);
+
+ const welcome = 'Welcome to the MeshCentral agent for MacOS\n\nThis installer will install the mesh agent for "' + opts.meshName + '" and allow the administrator to remotely monitor and control this computer over the internet. For more information, go to https://meshcentral.com.\n\nThis software is provided under Apache 2.0 license.\n';
+ const distribution = '\n'
+ + '\n'
+ + ' ' + xmlEscape(opts.displayName) + ' \n'
+ + ' \n'
+ + ' \n'
+ + ' ').join(']]]]>') + ']]> \n'
+ + ' \n'
+ + ' \n'
+ + ' \n'
+ + ' \n'
+ + ' \n'
+ + ' \n'
+ + ' #internal.pkg \n'
+ + ' \n'
+ + ' \n';
+ await fsp.writeFile(path.join(tmpRoot, 'Distribution'), distribution);
+
+ const pkgBuffer = await createXarPackage([basePkg, resourcesDir, path.join(tmpRoot, 'Distribution')]);
+ return {
+ pkg: pkgBuffer,
+ uninstall: replaceTokens(UNINSTALL, tokens)
+ };
+ } finally {
+ await fsp.rm(tmpRoot, { recursive: true, force: true });
+ }
+}
+
+module.exports = { createMacOSInstaller };
diff --git a/mcrec.js b/mcrec.js
index 6761227807..d23afcdbfe 100644
--- a/mcrec.js
+++ b/mcrec.js
@@ -119,6 +119,7 @@ function processBlock(state, block, err) {
switch (command) {
case 3: // Tile
+ if (state.swidth == null) { break; }
var x = block.data.readUInt16BE(4);
var y = block.data.readUInt16BE(6);
var dimensions = require('image-size').imageSize(block.data.slice(8));
diff --git a/meshagent.js b/meshagent.js
index 07a5f7e1e5..d31ebff716 100644
--- a/meshagent.js
+++ b/meshagent.js
@@ -98,7 +98,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
db.Remove('si' + obj.dbNodeKey); // Remove system information
db.Remove('al' + obj.dbNodeKey); // Remove error log last time
if (db.RemoveSMBIOS) { db.RemoveSMBIOS(obj.dbNodeKey); } // Remove SMBios data
- db.RemoveAllNodeEvents(obj.dbNodeKey); // Remove all events for this node
+ db.RemoveAllNodeEvents(domain.id, obj.dbNodeKey); // Remove all events for this node
db.removeAllPowerEventsForNode(obj.dbNodeKey); // Remove all power events for this node
// Event node deletion
@@ -208,7 +208,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
// Clear the core
obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0)); // MeshCommand_CoreModule, ask mesh agent to clear the core
parent.agentStats.clearingCoreCount++;
- parent.parent.debug('agent', "Clearing core");
+ parent.parent.debug('agent', "Clearing core for agent " + obj.nodeid);
} else {
// Setup task limiter options, this system limits how many tasks can run at the same time to spread the server load.
var taskLimiterOptions = { hash: meshcorehash, core: parent.parent.defaultMeshCores[corename], name: corename };
@@ -226,7 +226,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
delete obj.agentCoreUpdatePending;
obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0) + argument.hash + argument.core.toString('binary'), function () { parent.parent.taskLimiter.completed(taskid); }); // MeshCommand_CoreModule, start core update
parent.agentStats.updatingCoreCount++;
- parent.parent.debug('agent', "Updating core " + argument.name);
+ parent.parent.debug('agent', "Updating core " + argument.name + " for agent " + obj.nodeid);
} else {
// This agent is probably disconnected, nothing to do.
parent.parent.taskLimiter.completed(taskid);
@@ -1224,6 +1224,12 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
parent.routeAgentCommand(command, obj.domain.id, obj.dbNodeKey, obj.dbMeshKey);
break;
}
+ case 'software':
+ {
+ // Todo - save software into database for offline access but send to web clients for now
+ parent.routeAgentCommand(command, obj.domain.id, obj.dbNodeKey, obj.dbMeshKey);
+ break;
+ }
case 'coreinfo':
{
// Sent by the agent to update agent information
@@ -1237,7 +1243,8 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
// Agent update. The recovery core was loaded in the agent, send a command to update the agent
obj.agentCoreUpdateTaskId = taskid;
- const url = '*' + require('url').parse(obj.agentExeInfo.url).path;
+ const getme = new URL(obj.agentExeInfo.url);
+ const url = '*' + getme.pathname + getme.search;
var cmd = { action: 'agentupdate', url: url, hash: obj.agentExeInfo.hashhex };
parent.parent.debug('agentupdate', "Sending agent update url: " + cmd.url);
@@ -1318,30 +1325,6 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
}
break;
}
- case 'mc1migration':
- {
- if (command.oldnodeid.length != 64) break;
- const oldNodeKey = 'node//' + command.oldnodeid.toLowerCase();
- db.Get(oldNodeKey, function (err, nodes) {
- if ((nodes == null) || (nodes.length != 1)) return;
- const node = nodes[0];
- if (node.meshid == obj.dbMeshKey) {
- // Update the device name & host
- const newNode = { "name": node.name };
- if (node.intelamt != null) { newNode.intelamt = node.intelamt; }
- ChangeAgentCoreInfo(newNode);
-
- // Delete this node including network interface information and events
- db.Remove(node._id);
- db.Remove('if' + node._id);
-
- // Event node deletion
- const change = 'Migrated device ' + node.name;
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(node.meshid, [obj.dbNodeKey]), obj, { etype: 'node', action: 'removenode', nodeid: node._id, msg: change, domain: node.domain });
- }
- });
- break;
- }
case 'openUrl':
{
// Sent by the agent to return the status of a open URL action.
@@ -1375,7 +1358,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
db.Get(obj.dbNodeKey, function (err, nodes) { // TODO: THIS IS A BIG RACE CONDITION HERE, WE NEED TO FIX THAT. If this call is made twice at the same time on the same device, data will be missed.
if ((nodes == null) || (nodes.length != 1)) { delete obj.deviceChanging; return; }
const device = nodes[0];
- if (typeof device.name == 'string') { parent.parent.NotifyUserOfDeviceHelpRequest(domain, device.meshid, device._id, device.name, command.msgArgs[0], command.msgArgs[1]); }
+ if ((typeof device.name == 'string') && Array.isArray(command.msgArgs) && (command.msgArgs.length >= 2)) { parent.parent.NotifyUserOfDeviceHelpRequest(domain, device.meshid, device._id, device.name, command.msgArgs[0], command.msgArgs[1]); }
});
}
}
@@ -1454,11 +1437,43 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
command.data.type = 'sysinfo';
command.data.domain = domain.id;
command.data.time = Date.now();
- db.Set(command.data); // Update system information in the database.
- // Event the new sysinfo hash, this will notify everyone that the sysinfo document was changed
- var event = { etype: 'node', action: 'sysinfohash', nodeid: obj.dbNodeKey, domain: domain.id, hash: command.data.hash, nolog: 1 };
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, event);
+ // Store the document and notify viewers that the sysinfo hash changed.
+ var saveSysInfo = function () {
+ db.Set(command.data);
+ // Event the new sysinfo hash, this will notify everyone that the sysinfo document was changed
+ var event = { etype: 'node', action: 'sysinfohash', nodeid: obj.dbNodeKey, domain: domain.id, hash: command.data.hash, nolog: 1 };
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, event);
+ };
+
+ var volumes = command.data.hardware?.windows?.volumes;
+ if (volumes) {
+ // BitLocker recovery keys are kept in hardware.windows.bitlocker, keyed by protector identifier
+ // (decoupled from the drive letter, which can change). A key is retained for 'bitlockerKeyRetentionDays'
+ // days after it was last read; 0 (default) disables carry-forward, keeping only keys read in this scan.
+ var ttl = (parent.parent.config.settings?.bitlockerkeyretentiondays > 0) ? (parent.parent.config.settings.bitlockerkeyretentiondays * 86400000) : 0;
+ var updateBLKeys = function (prevKeys) {
+ var keys = {};
+ // Carry forward keys last read within the retention window.
+ if ((ttl > 0) && prevKeys) {
+ for (const id in prevKeys) { if ((command.data.time - prevKeys[id].t) <= ttl) { keys[id] = prevKeys[id]; } }
+ }
+ // Record keys actually read this scan (refreshes the timestamp).
+ for (const v of Object.values(volumes)) {
+ if (v.identifier && v.recoveryPassword) { keys[v.identifier] = { rp: v.recoveryPassword, t: command.data.time }; }
+ }
+ command.data.hardware.windows.bitlocker = keys;
+ saveSysInfo();
+ };
+ if (ttl > 0) {
+ // Need the previous doc to carry keys forward.
+ db.Get(command.data._id, function (err, nodes) { updateBLKeys((nodes && nodes.length > 0) ? nodes[0].hardware?.windows?.bitlocker : null); });
+ } else {
+ updateBLKeys(null); // no carry-forward, no previous-doc read needed
+ }
+ } else {
+ saveSysInfo(); // non-Windows
+ }
}
break;
}
@@ -1476,6 +1491,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
if (command.type == 'kvm') { obj.sessions.kvm = command.value; }
else if (command.type == 'terminal') { obj.sessions.terminal = command.value; }
else if (command.type == 'files') { obj.sessions.files = command.value; }
+ else if (command.type == 'registry') { obj.sessions.registry = command.value; }
else if (command.type == 'help') { obj.sessions.help = command.value; }
else if (command.type == 'tcp') { obj.sessions.tcp = command.value; }
else if (command.type == 'udp') { obj.sessions.udp = command.value; }
@@ -1561,6 +1577,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
// Information includes file hash and download location URL
if (typeof command.name != 'string') break;
var info = parent.parent.meshToolsBinaries[command.name];
+ if (info == null) break;
if ((command.hash != null) && (info.hash == command.hash)) return;
// To build the connection URL, if we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
@@ -1584,7 +1601,8 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
// Agent is requesting an agent update
obj.agentCoreUpdateTaskId = taskid;
- const url = '*' + require('url').parse(obj.agentExeInfo.url).path;
+ const getme = new URL(obj.agentExeInfo.url);
+ const url = '*' + getme.pathname + getme.search;
var cmd = { action: 'agentupdate', url: url, hash: obj.agentExeInfo.hashhex, sessionid: agentUpdateFunc.sessionid };
parent.parent.debug('agentupdate', "Sending user requested agent update url: " + cmd.url);
@@ -1629,12 +1647,13 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
if ((typeof command.url != 'string') || (typeof command.approved != 'boolean') || (command.url.startsWith('2fa://') == false)) return;
// parse the URL
+ // Node 22's WHATWG URL parser rejects custom schemes like 2fa://, so swap to https:// for parsing only.
var url = null;
- try { url = require('url').parse(command.url); } catch (ex) { }
+ try { url = new URL(command.url.replace(/^2fa:\/\//, 'https://')); } catch (ex) { }
if (url == null) return;
// Decode the cookie
- var urlSplit = url.query.split('&c=');
+ var urlSplit = url.search.slice(1).split('&c=');
if (urlSplit.length != 2) return;
const authCookie = parent.parent.decodeCookie(urlSplit[1], null, 1);
if ((authCookie == null) || (typeof authCookie.c != 'string') || (('code=' + authCookie.c) != urlSplit[0])) return;
@@ -1925,6 +1944,14 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
if (!device.wsc) { device.wsc = {}; }
if (JSON.stringify(device.wsc) != JSON.stringify(command.wsc)) { /*changes.push('Windows Security Center status');*/ device.wsc = command.wsc; change = 1; log = 1; }
}
+ if (command.lsc != null) { // Linux Security Center
+ if (!device.lsc) { device.lsc = {}; }
+ if (JSON.stringify(device.lsc) != JSON.stringify(command.lsc)) { /*changes.push('Linux Security Center status');*/ device.lsc = command.lsc; change = 1; log = 1; }
+ }
+ if (command.pr != null) { // Pending Reboot
+ if (!device.pr) { device.pr = {}; }
+ if (JSON.stringify(device.pr) != JSON.stringify(command.pr)) { /*changes.push('Pending Reboot status');*/ device.pr = command.pr; change = 1; log = 1; }
+ }
if (command.defender != null) { // Defender For Windows Server
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; }
diff --git a/meshbot.js b/meshbot.js
index 5ff0de9b33..1c946ada0c 100644
--- a/meshbot.js
+++ b/meshbot.js
@@ -61,7 +61,7 @@ function serverConnect() {
// Setup the HTTP proxy if needed
if (args.proxy != null) {
const HttpsProxyAgent = require('https-proxy-agent');
- options.agent = new HttpsProxyAgent(require('url').parse(args.proxy));
+ options.agent = new HttpsProxyAgent(new URL(args.proxy));
}
// Authentication setup
diff --git a/meshcentral-config-schema.json b/meshcentral-config-schema.json
index dc5da1aeb6..a30fe494ea 100644
--- a/meshcentral-config-schema.json
+++ b/meshcentral-config-schema.json
@@ -569,6 +569,11 @@
"default": false,
"description": "Enables agent-side, websocket per-message deflate compression. wscompression must also be true for this to work."
},
+ "bitlockerKeyRetentionDays": {
+ "type": "integer",
+ "default": 0,
+ "description": "How many days to retain a BitLocker recovery key after the agent last read it, so a key stays available while a volume is locked or detached. Especially useful for Bitlocker To Go devices. 0 (default) disables carry-forward: only keys read in the latest scan are kept."
+ },
"noAgentUpdate": {
"type": "integer",
"default": 0,
@@ -1951,6 +1956,15 @@
"default": false,
"description": "When enabled, device groups will be collapsed by default in the devices tab."
},
+ "terminalUserVariable": {
+ "type": "string",
+ "enum": [
+ "realname",
+ "identifier",
+ "username"
+ ],
+ "description": "Adds MESHCENTRAL_USER environment variable to linux terminal sessions using either 'realname', 'identifier', or 'username'."
+ },
"userSessionsSort": {
"type": "string",
"default": "SessionId",
@@ -2221,6 +2235,11 @@
"default": null,
"description": "The filename of a image file in .png format located in meshcentral-data to display in the MeshCentral Agent installation dialog, image should be square and from 64x64 to 200x200."
},
+ "macosinstallerimage": {
+ "type": "string",
+ "default": null,
+ "description": "The filename of a background image file in .png format located in meshcentral-data to display in the macOS installer."
+ },
"fileName": {
"type": "string",
"default": "meshagent",
@@ -3996,15 +4015,13 @@
"description": "REQUIRED IF USING GROUPS: Customer ID from Google Workspace Admin Console (https://admin.google.com/ac/accountsettings/profile)"
},
"authorities": {
- "type": [
- "array"
- ],
- "description": "Default value is groups. Roles can be used in azure to assign app permissons",
- "enum": [
- "groups",
- "roles"
- ]
+ "type": "array",
+ "description": "Default value is groups. Roles can be used in azure to assign app permissions",
+ "items": {
+ "type": "string",
+ "enum": ["groups", "roles"]
}
+ }
}
},
"groups": {
diff --git a/meshcentral.js b/meshcentral.js
index 0723d5d3c8..62b1f8964c 100644
--- a/meshcentral.js
+++ b/meshcentral.js
@@ -15,6 +15,7 @@
"use strict";
const common = require('./common.js');
+const { zipExtract } = require('./backup.js');
// If app metrics is available
if (process.argv[2] == '--launch') { try { require('appmetrics-dash').monitor({ url: '/', title: 'MeshCentral', port: 88, host: '127.0.0.1' }); } catch (ex) { } }
@@ -163,7 +164,7 @@ function CreateMeshCentralServer(config, args) {
'setuptelegram', 'resetaccount', 'pass', 'removesubdomain', 'adminaccount',
'domain', 'email', 'configfile', 'maintenancemode', 'nedbtodb',
'removetestagents', 'agentupdatetest', 'hashpassword', 'hashpass',
- 'indexmcrec', 'mpsdebug', 'dumpcores', 'dev', 'mysql', 'mariadb', 'trustedproxy'
+ 'indexmcrec', 'mpsdebug', 'dumpcores', 'dev', 'mysql', 'mariadb', 'trustedproxy', 'migratevolumeinfo'
];
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_"
@@ -189,25 +190,31 @@ function CreateMeshCentralServer(config, args) {
console.log('Details at: https://www.meshcentral.com\r\n');
if ((obj.platform == 'win32') || (obj.platform == 'linux')) {
console.log('Run as a background service');
- console.log(' --install/uninstall Install MeshCentral as a background service.');
- console.log(' --start/stop/restart Control MeshCentral background service.');
+ console.log(' --install/uninstall\t\t\t\t\tInstall MeshCentral as a background service.');
+ console.log(' --start/stop/restart\t\t\t\t\tControl MeshCentral background service.');
console.log('');
console.log('Run standalone, console application');
}
- console.log(' --user [username] Always login as [username] if account exists.');
- console.log(' --port [number] Web server port number.');
- console.log(' --redirport [number] Creates an additional HTTP server to redirect users to the HTTPS server.');
- console.log(' --exactports Server must run with correct ports or exit.');
- console.log(' --noagentupdate Server will not update mesh agent native binaries.');
- console.log(' --nedbtodb Transfer all NeDB records into current database.');
- console.log(' --listuserids Show a list of a user identifiers in the database.');
- console.log(' --cert [name], (country), (org) Create a web server certificate with [name] server name.');
- console.log(' country and organization can optionally be set.');
- console.log('');
- console.log('Server recovery commands, use only when MeshCentral is offline.');
- console.log(' --createaccount [userid] Create a new user account.');
- console.log(' --resetaccount [userid] Unlock an account, disable 2FA and set a new account password.');
- console.log(' --adminaccount [userid] Promote account to site administrator.');
+ console.log(' --user [username]\t\t\t\t\tAlways login as [username] if account exists.');
+ console.log(' --port [number]\t\t\t\t\tWeb server port number.');
+ console.log(' --redirport [number]\t\t\t\t\tCreates an additional HTTP server to redirect users to the HTTPS server.');
+ console.log(' --exactports\t\t\t\t\t\tServer must run with correct ports or exit.');
+ console.log(' --noagentupdate\t\t\t\t\tServer will not update mesh agent native binaries.');
+ console.log(' --nedbtodb\t\t\t\t\t\tTransfer all NeDB records into current database.');
+ console.log(' --listuserids\t\t\t\t\tShow a list of a user identifiers in the database.');
+ console.log(' --cert [name], (country), (org)\t\t\tCreate a web server certificate with [name] server name.');
+ console.log('\t\t\t\t\t\t\tcountry and organization can optionally be set.');
+ console.log('\r\nRun stateless, config files (config.json and certificates) are stored encrypted with [password] in the database.');
+ console.log(' --dblistconfigfile (password)\t\t\tShow the files in the db and optionally check the files against the password.');
+ console.log(' --dbpushconfigfiles (path) --configkey [password]\tThis will import the current configfiles or from (path) into the database.');
+ console.log(' --dbshowconfigfile [file] --configkey [password]\tShow contents of config [file].');
+ console.log(' --dbpullconfigfiles [path] --configkey [password]\tPull configfiles out of the database into [path].');
+ console.log(' --dbdeleteconfigfiles\t\t\t\tDelete config files from the database.');
+ console.log(' --loadconfigfromdb [password]\t\t\tRun meshcentral with configfiles found in the database.')
+ console.log('\r\nServer recovery commands, use only when MeshCentral is offline.');
+ console.log(' --createaccount [userid]\t\t\t\tCreate a new user account.');
+ console.log(' --resetaccount [userid]\t\t\t\tUnlock an account, disable 2FA and set a new account password.');
+ console.log(' --adminaccount [userid]\t\t\t\tPromote account to site administrator.');
return;
}
@@ -676,7 +683,32 @@ function CreateMeshCentralServer(config, args) {
else if (data.indexOf('Server Ctrl-C exit...') >= 0) { childProcess.xrestart = 2; }
else if (data.indexOf('Starting self upgrade...') >= 0) { childProcess.xrestart = 3; }
else if (data.indexOf('Server restart...') >= 0) { childProcess.xrestart = 1; }
- else if (data.indexOf('Starting self upgrade to: ') >= 0) { obj.args.specificupdate = data.substring(26).split('\r')[0].split('\n')[0]; childProcess.xrestart = 3; }
+ else if (data.indexOf('Starting self upgrade to: ') >= 0) {
+ const specificupdate = data.substring(data.indexOf('Starting self upgrade to: ') + 26).split('\r')[0].split('\n')[0];
+ if (/^[0-9\.\-]+$/i.test(specificupdate)) {
+ var isUpgrade = false;
+ try {
+ var currentVersion = getCurrentVersion();
+ if (currentVersion) {
+ var sVer = specificupdate.split('-')[0].split('.');
+ var cVer = currentVersion.split('-')[0].split('.');
+ for (var i = 0; i <= 2; i++) {
+ var sVal = parseInt(sVer[i] || 0);
+ var cVal = parseInt(cVer[i] || 0);
+ if (sVal > cVal) { isUpgrade = true; break; }
+ else if (sVal < cVal) { break; }
+ }
+ }
+ } catch (ex) { }
+ if (isUpgrade) {
+ obj.args.specificupdate = specificupdate;
+ childProcess.xrestart = 3;
+ } else {
+ data += '\nERROR: Downgrade from ' + currentVersion + ' to ' + specificupdate + ' is not allowed.';
+ childProcess.xrestart = 1;
+ }
+ }
+ }
var datastr = data;
while (datastr.endsWith('\r') || datastr.endsWith('\n')) { datastr = datastr.substring(0, datastr.length - 1); }
logFromChildProcess(datastr);
@@ -990,6 +1022,7 @@ function CreateMeshCentralServer(config, args) {
if (obj.args.logintokenkey) { obj.showLoginTokenKey(function (r) { console.log(r); process.exit(); }); return; }
if (obj.args.recordencryptionrecode) { obj.db.performRecordEncryptionRecode(function (count) { console.log('Re-encoded ' + count + ' record(s).'); process.exit(); }); return; }
if (obj.args.dbstats) { obj.db.getDbStats(function (stats) { console.log(stats); process.exit(); }); return; }
+ if (obj.args.migratevolumeinfo) { require('./migrate-volume-info.js').migrateVolumeInfo(obj.db, function (err, r) { if (err != null) { console.log('Volume info migration error: ' + err); } else { console.log('Volume info migration complete. Scanned ' + r.scanned + ' sysinfo document(s), migrated ' + r.migrated + ', moved ' + r.keysMoved + ' key(s).'); } process.exit(); }); return; }
if (obj.args.createaccount) { // Create a new user account
if ((typeof obj.args.createaccount != 'string') || ((obj.args.pass == null) && (obj.args.hashpass == null)) || (obj.args.pass == '') || (obj.args.hashpass == '') || (obj.args.createaccount.indexOf(' ') >= 0)) { console.log("Usage: --createaccount [userid] --pass [password] --domain (domain) --email (email) --name (name)."); process.exit(); return; }
var userid = 'user/' + (obj.args.domain ? obj.args.domain : '') + '/' + obj.args.createaccount.toLowerCase(), domainid = obj.args.domain ? obj.args.domain : '';
@@ -1024,7 +1057,7 @@ function CreateMeshCentralServer(config, args) {
if (err != null) { console.log("Database error: " + err); process.exit(); return; }
if ((docs == null) || (docs.length == 0)) { console.log("Unknown userid, usage: --resetaccount [userid] --domain (domain) --pass [password]."); process.exit(); return; }
const user = docs[0]; if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { user.siteadmin -= 32; } // Unlock the account.
- delete user.phone; delete user.otpekey; delete user.otpsecret; delete user.otpkeys; delete user.otphkeys; delete user.otpdev; delete user.otpsms; delete user.otpmsg; user.otpduo; // Disable 2FA
+ delete user.phone; delete user.otpekey; delete user.otpsecret; delete user.otpkeys; delete user.otphkeys; delete user.otpdev; delete user.otpsms; delete user.otpmsg; delete user.otpduo; // 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) {
@@ -1098,7 +1131,7 @@ function CreateMeshCentralServer(config, args) {
db.Remove('lc' + node._id); // Remove last connect time
db.Remove('si' + node._id); // Remove system information
if (db.RemoveSMBIOS) { db.RemoveSMBIOS(node._id); } // Remove SMBios data
- db.RemoveAllNodeEvents(node._id); // Remove all events for this node
+ db.RemoveAllNodeEvents(node.domain, node._id); // Remove all events for this node
db.removeAllPowerEventsForNode(node._id); // Remove all power events for this node
if (typeof node.pmt == 'string') { db.Remove('pmt_' + node.pmt); } // Remove Push Messaging Token
db.Get('ra' + node._id, function (err, nodes) {
@@ -1175,7 +1208,7 @@ function CreateMeshCentralServer(config, args) {
if (err == null) {
if (docs.length == 0) { console.log("File not found."); } else {
const data = obj.db.decryptData(obj.args.configkey, docs[0].data);
- if (data == null) { console.log("Invalid config key."); } else { console.log(data); }
+ if (data == null) { console.log("Invalid config key."); } else { console.log(data.toString()); }
}
} else { console.log("Unable to read from database."); }
process.exit();
@@ -1819,23 +1852,25 @@ function CreateMeshCentralServer(config, args) {
}
// Load CloudFlare trusted proxies list if needed
- if ((obj.config.settings.trustedproxy != null) && (typeof obj.config.settings.trustedproxy == 'string') && (obj.config.settings.trustedproxy.toLowerCase() == 'cloudflare')) {
+ const trustedproxyIsCloudflareString = (obj.args.trustedproxy != null) && (typeof obj.args.trustedproxy == 'string') && (obj.args.trustedproxy.toLowerCase() == 'cloudflare');
+ const trustedproxyIsCloudflareArray = Array.isArray(obj.args.trustedproxy) && obj.args.trustedproxy.some(function (x) { return (typeof x == 'string') && (x.toLowerCase() == 'cloudflare'); });
+ if (trustedproxyIsCloudflareString || trustedproxyIsCloudflareArray) {
obj.config.settings.extrascriptsrc = 'ajax.cloudflare.com'; // Add CloudFlare as a trusted script source. This allows for CloudFlare's RocketLoader feature.
- delete obj.args.trustedproxy;
- delete obj.config.settings.trustedproxy;
+ // Preserve any non-'cloudflare' entries already in the array
+ const existingProxies = trustedproxyIsCloudflareArray ? obj.args.trustedproxy.filter(function (x) { return !(typeof x == 'string' && x.toLowerCase() == 'cloudflare'); }) : [];
obj.certificateOperations.loadTextFile('https://www.cloudflare.com/ips-v4', null, function (url, data, tag) {
if (data != null) {
- if (Array.isArray(obj.args.trustedproxy) == false) { obj.args.trustedproxy = []; }
+ const newProxies = existingProxies.slice();
const ipranges = data.split('\n');
- for (var i in ipranges) { if (ipranges[i] != '') { obj.args.trustedproxy.push(ipranges[i]); } }
+ for (var i in ipranges) { if (ipranges[i] != '') { newProxies.push(ipranges[i]); } }
obj.certificateOperations.loadTextFile('https://www.cloudflare.com/ips-v6', null, function (url, data, tag) {
if (data != null) {
var ipranges = data.split('\n');
- for (var i in ipranges) { if (ipranges[i] != '') { obj.args.trustedproxy.push(ipranges[i]); } }
- obj.config.settings.trustedproxy = obj.args.trustedproxy;
+ for (var i in ipranges) { if (ipranges[i] != '') { newProxies.push(ipranges[i]); } }
} else {
addServerWarning("Unable to load CloudFlare trusted proxy IPv6 address list.", 16);
}
+ obj.args.trustedproxy = newProxies;
obj.StartEx4(); // Keep going
});
} else {
@@ -2148,6 +2183,15 @@ function CreateMeshCentralServer(config, args) {
if (obj.config.settings.autobackup == false || obj.config.settings.autobackup == 'false') { obj.config.settings.autobackup = {backupintervalhours: -1}; } //block all autobackup functions
else {
if (typeof obj.config.settings.autobackup != 'object') { obj.config.settings.autobackup = {}; };
+ if (Object.hasOwn(obj.config.settings.autobackup, "zippassword")) {
+ if ((obj.config.settings.autobackup.zippassword).length == 0) {
+ delete obj.config.settings.autobackup.zippassword;
+ obj.addServerWarning('Empty zip password in config.json', true);
+ } else {
+ // convert to string regardless of type
+ obj.config.settings.autobackup.zippassword = String(obj.config.settings.autobackup.zippassword);
+ }
+ }
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; }}
@@ -2355,49 +2399,25 @@ function CreateMeshCentralServer(config, args) {
obj.Stop = function (restoreFile) {
// If the database is not setup, exit now.
if (!obj.db) return;
-
// Dispatch an event saying the server is now stopping
obj.DispatchEvent(['*'], obj, { etype: 'server', action: 'stopped', msg: "Server stopped" });
-
+ const restorePassword = obj.config.settings.autobackup.zippasswordrequest;
+ delete obj.config.settings.autobackup.zippasswordrequest;
// Set all nodes to power state of unknown (0)
obj.db.storePowerEvent({ time: new Date(), nodeid: '*', power: 0, s: 2 }, obj.multiServer, function () { // s:2 indicates that the server is shutting down.
if (restoreFile) {
obj.debug('main', obj.common.format("Server stopped, updating settings: {0}", restoreFile));
- console.log("Updating settings folder...");
-
- const yauzl = require('yauzl');
- yauzl.open(restoreFile, { lazyEntries: true }, function (err, zipfile) {
- if (err) throw err;
- zipfile.readEntry();
- zipfile.on('entry', function (entry) {
- if (/\/$/.test(entry.fileName)) {
- // Directory file names end with '/'.
- // Note that entires for directories themselves are optional.
- // An entry's fileName implicitly requires its parent directories to exist.
- zipfile.readEntry();
- } else {
- // File entry
- zipfile.openReadStream(entry, function (err, readStream) {
- if (err) throw err;
- readStream.on('end', function () { zipfile.readEntry(); });
- var directory = obj.path.dirname(entry.fileName);
- if (directory != '.') {
- directory = obj.getConfigFilePath(directory)
- if (obj.fs.existsSync(directory) == false) { obj.fs.mkdirSync(directory); }
- }
- //console.log('Extracting:', obj.getConfigFilePath(entry.fileName));
- readStream.pipe(obj.fs.createWriteStream(obj.getConfigFilePath(entry.fileName)));
- });
- }
+ console.log("Updating settings folder..."); // do not alter. This specific log message, with the process.exit(123) further on, triggers a process restart. See obj.launchChildServer>childProcess.stdout.on function
+ zipExtract(restoreFile, obj.datapath, 'meshcentral-data/', restorePassword)
+ .then((res) => {
+ res['res'] ? console.log(res['mes']) : console.error(res['mes']);
+ process.exit(123); // this triggers the childserver process restart
});
- zipfile.on('end', function () { setTimeout(function () { obj.fs.unlinkSync(restoreFile); process.exit(123); }); });
- });
} else {
obj.debug('main', "Server stopped");
process.exit(0);
}
});
-
// Update the server state
obj.updateServerState('state', "stopped");
};
@@ -2418,8 +2438,8 @@ function CreateMeshCentralServer(config, args) {
delete obj.eventsDispatch[id];
} else {
const newList = []; // We create a new list so not to modify the original list. Allows this function to be called during an event dispatch.
- for (var k in obj.eventsDispatch[i]) { if (obj.eventsDispatch[i][k] != target) { newList.push(obj.eventsDispatch[i][k]); } }
- obj.eventsDispatch[i] = newList;
+ for (var k in obj.eventsDispatch[id]) { if (obj.eventsDispatch[id][k] != target) { newList.push(obj.eventsDispatch[id][k]); } }
+ obj.eventsDispatch[id] = newList;
}
}
}
@@ -3311,11 +3331,22 @@ function CreateMeshCentralServer(config, args) {
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;
-
+ // Collect architectures that need signing
+ var archIds = [];
for (var archid in obj.meshAgentsArchitectureNumbers) {
if (obj.meshAgentsArchitectureNumbers[archid].codesign !== true) continue;
+ archIds.push(archid);
+ }
+
+ if (archIds.length === 0) { func(); return; }
+
+ var currentArchIndex = 0;
+
+ function signNextAgent() {
+ if (currentArchIndex >= archIds.length) { func(); return; }
+
+ var archid = archIds[currentArchIndex];
+ currentArchIndex++;
var agentpath;
if (domain.id == '') {
@@ -3326,7 +3357,7 @@ function CreateMeshCentralServer(config, args) {
} else {
// When processing an extra domain, only load agents that are specific to that domain
agentpath = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
- if (obj.fs.existsSync(agentpath)) { delete obj.meshAgentsArchitectureNumbers[archid].codesign; } else { continue; } // If the agent is not present in "meshcentral-data/agents" skip.
+ if (obj.fs.existsSync(agentpath)) { delete obj.meshAgentsArchitectureNumbers[archid].codesign; } else { signNextAgent(); return; } // If the agent is not present in "meshcentral-data/agents" skip.
}
// Open the original agent with authenticode
@@ -3459,9 +3490,10 @@ function CreateMeshCentralServer(config, args) {
addServerWarning('Failed to sign \"' + agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname + '\": ' + err, 22, [agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname, err]);
}
obj.callExternalSignJob(agentSignedFunc.signingArguments); // Call external signing job regardless of success or failure
- if (--pendingOperations === 0) { agentSignedFunc.func(); }
+ // Wait 2 seconds between each codesign to avoid rate limiting from Sectigo's timestamp server
+ // https://www.sectigo.com/resource-library/time-stamping-server
+ setTimeout(signNextAgent, 2000);
}
- pendingOperations++;
xagentSignedFunc.func = func;
xagentSignedFunc.objx = objx;
xagentSignedFunc.archid = archid;
@@ -3530,13 +3562,15 @@ function CreateMeshCentralServer(config, args) {
} else {
// Signed agent is already ok, use it.
originalAgent.close();
+ signNextAgent();
}
-
+ } else {
+ signNextAgent();
}
}
- if (--pendingOperations === 0) { func(); }
+ signNextAgent();
}
obj.callExternalSignJob = function (signingArguments) {
@@ -4169,7 +4203,12 @@ function InstallModules(modules, args, func) {
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 (ex.code == "ERR_PACKAGE_PATH_NOT_EXPORTED") {
+ var rootPath = __dirname;
+ if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) { rootPath = require('path').join(__dirname, '../..'); }
+ var pkgPath = require('path').join(rootPath, 'node_modules', moduleName, 'package.json');
+ if (require('fs').existsSync(pkgPath)) { modulePath = pkgPath; }
+ } 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)) {
@@ -4332,23 +4371,13 @@ 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.indexOf('passport') == -1) { passport.push('passport@0.7.0','connect-flash@0.1.1'); } // 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@1.0.4'); }
- 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@2.0.0'); }
- 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@0.1.12'); }
- 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@0.1.0'); passport.push('jwt-simple@0.5.6'); }
- 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'); }
+ passport.push('passport@0.7.0','connect-flash@0.1.1'); // 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.push('passport-twitter@1.0.4'); }
+ 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.push('passport-google-oauth20@2.0.0'); }
+ 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.push('passport-github2@0.1.12'); }
+ 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.push('passport-azure-oauth2@0.1.0'); passport.push('jwt-simple@0.5.6'); }
+ if (typeof config.domains[i].authstrategies.oidc == 'object') { passport.push('openid-client@5.7.1'); }
+ if ((typeof config.domains[i].authstrategies.saml == 'object') || (typeof config.domains[i].authstrategies.jumpcloud == 'object')) { passport.push('passport-saml@3.2.4'); }
}
if (config.domains[i].sessionrecording != null) { sessionRecording = true; }
if ((config.domains[i].passwordrequirements != null) && (config.domains[i].passwordrequirements.bancommonpasswords == true)) { wildleek = true; }
@@ -4358,11 +4387,11 @@ function mainStart() {
// 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@7.0.1', 'body-parser@1.20.4', 'cbor@5.2.0', 'compression@1.8.1', 'cookie-session@2.1.1', 'express@4.22.1', '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@4.1.2', 'node-forge@1.3.2', 'ua-parser-js@1.0.40', 'ua-client-hints-js@0.1.2', 'ws@8.18.3', 'yauzl@2.10.0'];
+ var modules = ['archiver@7.0.1', 'cbor@5.2.0', 'compression@1.8.1', 'cookie-session@2.1.1', 'express@4.22.2', 'express-handlebars@7.1.3', 'express-ws@5.0.2', 'ipcheck@0.1.0', 'minimist@1.2.8', 'multiparty@4.3.0', '@seald-io/nedb@4.1.2', 'node-forge@1.4.0', 'ua-parser-js@1.0.40', 'ua-client-hints-js@0.1.2', 'ws@8.21.1', 'yauzl@2.10.0', '@zip.js/zip.js@2.8.26']; // Base modules
if (require('os').platform() == 'win32') { modules.push('node-windows@0.1.14'); modules.push('loadavg-windows@1.1.1'); if (sspi == true) { modules.push('node-sspi@0.2.10'); } } // Add Windows modules
if (ldap == true) { modules.push('ldapauth-fork@5.0.5'); }
if (ssh == true) { modules.push('ssh2@1.17.0'); }
- if (passport != null) { modules.push(...passport); }
+ if (passport != null) { passport = passport.filter((value, index, self) => self.indexOf(value) === index); modules.push(...passport); }
if (captcha == true) { modules.push('svg-captcha@1.4.0'); }
if (sessionRecording == true) { modules.push('image-size@2.0.2'); } // Need to get the remote desktop JPEG sizes to index the recording file.
@@ -4405,7 +4434,7 @@ function mainStart() {
if (config.settings.no2factorauth !== true) {
// Setup YubiKey OTP if configured
if (yubikey == true) { modules.push('yub@0.11.1'); } // Add YubiKey OTP support (replaced yubikeyotp due to form-data issues)
- if (allsspi == false) { modules.push('otplib@12.0.1'); } // Google Authenticator support (v10 supports older NodeJS versions).
+ if (allsspi == false) { modules.push('otplib@13.4.1'); } // Google Authenticator support (v10 supports older NodeJS versions).
}
// Desktop multiplexor support
diff --git a/meshctrl.js b/meshctrl.js
index dc1d902097..581d0dbaae 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', 'addlocaldevice', 'addamtdevice', 'addusergroup', 'listusergroups', 'removeusergroup', 'runcommand', 'shell', 'upload', 'download', 'deviceopenurl', 'devicemessage', 'devicetoast', 'addtousergroup', 'removefromusergroup', 'removeallusersfromusergroup', 'devicesharing', 'devicepower', 'indexagenterrorlog', 'agentdownload', 'report', 'grouptoast', 'groupmessage', 'webrelay'];
+const possibleCommands = ['edituser', 'listusers', 'listusersessions', 'listdevicegroups', 'listdevices', 'listusersofdevicegroup', 'listevents', 'logintokens', 'serverinfo', 'serverversion', '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) {
@@ -26,6 +26,7 @@ if (args['_'].length == 0) {
console.log("Supported actions:");
console.log(" Help [action] - Get help on an action.");
console.log(" ServerInfo - Show server information.");
+ console.log(" ServerVersion - Show server version.");
console.log(" UserInfo - Show user information.");
console.log(" ListUsers - List user accounts.");
console.log(" ListUserSessions - List online users.");
@@ -96,6 +97,7 @@ if (args['_'].length == 0) {
case 'config': { performConfigOperations(args); return; }
case 'indexagenterrorlog': { indexAgentErrorLog(); return; }
case 'serverinfo': { ok = true; break; }
+ case 'serverversion': { ok = true; break; }
case 'userinfo': { ok = true; break; }
case 'listusers': { ok = true; break; }
case 'listusersessions': { ok = true; break; }
@@ -378,6 +380,14 @@ if (args['_'].length == 0) {
console.log(" --json - Show result as JSON.");
break;
}
+ case 'serverversion': {
+ console.log("Get the version of the MeshCentral server, Example usages:\r\n");
+ console.log(" MeshCtrl ServerVersion --loginuser myaccountname --loginpass mypassword");
+ console.log(" MeshCtrl ServerVersion --loginuser myaccountname --loginkeyfile key.txt");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --json - Show result as JSON.");
+ break;
+ }
case 'userinfo': {
console.log("Get account information for the login account, Example usages:\r\n");
console.log(" MeshCtrl UserInfo --loginuser myaccountname --loginpass mypassword");
@@ -578,7 +588,7 @@ if (args['_'].length == 0) {
console.log(" 4096 = Desktop Limited Input 8192 = Limit Events ");
console.log(" 16384 = Chat / Notify 32768 = Uninstall Agent ");
console.log(" 65536 = No Remote Desktop 131072 = Remote Commands ");
- console.log(" 262144 = Reset / Power off ");
+ console.log(" 262144 = Reset / Power off 4194304 = No Registry ");
break;
}
case 'removefromusergroup': {
@@ -714,6 +724,7 @@ if (args['_'].length == 0) {
console.log(" --limiteddesktop - Limit remote desktop keys.");
console.log(" --noterminal - Hide the terminal tab from this user.");
console.log(" --nofiles - Hide the files tab from this user.");
+ console.log(" --noregistry - Hide the registry tab from this user.");
console.log(" --noamt - Hide the Intel AMT tab from this user.");
console.log(" --limitedevents - User can only see his own events.");
console.log(" --chatnotify - Allow chat and notification options.");
@@ -760,6 +771,7 @@ if (args['_'].length == 0) {
console.log(" --limiteddesktop - Limit remote desktop keys.");
console.log(" --noterminal - Hide the terminal tab from this user.");
console.log(" --nofiles - Hide the files tab from this user.");
+ console.log(" --noregistry - Hide the registry tab from this user.");
console.log(" --noamt - Hide the Intel AMT tab from this user.");
console.log(" --limitedevents - User can only see his own events.");
console.log(" --chatnotify - Allow chat and notification options.");
@@ -865,6 +877,8 @@ if (args['_'].length == 0) {
case 'editdevice': {
console.log("Change information about a device, Example usages:\r\n");
console.log(winRemoveSingleQuotes(" MeshCtrl EditDevice --id 'deviceid' --name 'device1'"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl EditDevice --id 'deviceid' --addtag 'newtag'"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl EditDevice --id 'deviceid' --removetag 'oldtag'"));
console.log("\r\nRequired arguments:\r\n");
if (process.platform == 'win32') {
console.log(" --id [deviceid] - The device identifier.");
@@ -873,13 +887,17 @@ if (args['_'].length == 0) {
}
console.log("\r\nOptional arguments:\r\n");
if (process.platform == 'win32') {
- console.log(" --name [name] - Change device name.");
- console.log(" --desc [description] - Change device description.");
- console.log(" --tags [tag1,tags2] - Change device tags.");
+ console.log(" --name [name] - Change device name.");
+ console.log(" --desc [description] - Change device description.");
+ console.log(" --tags [tag1,tag2] - Set device tags (replaces all existing tags).");
+ console.log(" --addtag [tag1,tag2] - Add tags to existing tags.");
+ console.log(" --removetag [tag1,tag2] - Remove tags from existing tags.");
} else {
- console.log(" --name '[name]' - Change device name.");
- console.log(" --desc '[description]' - Change device description.");
- console.log(" --tags '[tag1,tags2]' - Change device tags.");
+ console.log(" --name '[name]' - Change device name.");
+ console.log(" --desc '[description]' - Change device description.");
+ console.log(" --tags '[tag1,tag2]' - Set device tags (replaces all existing tags).");
+ console.log(" --addtag '[tag1,tag2]' - Add tags to existing tags.");
+ console.log(" --removetag '[tag1,tag2]' - Remove tags from existing tags.");
}
console.log(" --icon [number] - Change the device icon (1 to 8).");
console.log(" --consent [flags] - Sum of the following numbers:");
@@ -1326,7 +1344,7 @@ function serverConnect() {
// Setup the HTTP proxy if needed
if (args.proxy != null) {
const HttpsProxyAgent = require('https-proxy-agent');
- options.agent = new HttpsProxyAgent(require('url').parse(args.proxy));
+ options.agent = new HttpsProxyAgent(new URL(args.proxy));
}
// Password authentication
@@ -1373,6 +1391,7 @@ function serverConnect() {
//console.log('Connected.');
switch (settings.cmd) {
case 'serverinfo': { break; }
+ case 'serverversion': { ws.send(JSON.stringify({ action: 'serverversion', responseid: 'meshctrl' })); break; }
case 'userinfo': { break; }
case 'listusers': { ws.send(JSON.stringify({ action: 'users', responseid: 'meshctrl' })); break; }
case 'listusersessions': { ws.send(JSON.stringify({ action: 'wssessioncount', responseid: 'meshctrl' })); break; }
@@ -1648,12 +1667,14 @@ function serverConnect() {
if (args.desktopviewonly) { meshrights |= 256; }
if (args.noterminal) { meshrights |= 512; }
if (args.nofiles) { meshrights |= 1024; }
+ if (args.noregistry) { meshrights |= 4194304; }
+ if (args.nosoftware) { meshrights |= 8388608; }
if (args.noamt) { meshrights |= 2048; }
if (args.limiteddesktop) { meshrights |= 4096; }
if (args.limitedevents) { meshrights |= 8192; }
if (args.chatnotify) { meshrights |= 16384; }
if (args.uninstall) { meshrights |= 32768; }
- var op = { action: 'addmeshuser', usernames: [args.userid], meshadmin: meshrights, responseid: 'meshctrl' };
+ var op = { action: 'addmeshuser', userids: [args.userid], meshadmin: meshrights, responseid: 'meshctrl' };
if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshname = args.group; }
ws.send(JSON.stringify(op));
break;
@@ -1675,6 +1696,8 @@ function serverConnect() {
if (args.desktopviewonly) { meshrights |= 256; }
if (args.noterminal) { meshrights |= 512; }
if (args.nofiles) { meshrights |= 1024; }
+ if (args.noregistry) { meshrights |= 4194304; }
+ if (args.nosoftware) { meshrights |= 8388608; }
if (args.noamt) { meshrights |= 2048; }
if (args.limiteddesktop) { meshrights |= 4096; }
if (args.limitedevents) { meshrights |= 8192; }
@@ -1728,14 +1751,20 @@ function serverConnect() {
break;
}
case 'editdevice': {
- var op = { action: 'changedevice', nodeid: args.id, responseid: 'meshctrl' };
- if (typeof args.name == 'string') { op.name = args.name; }
- if (typeof args.name == 'number') { op.name = '' + args.name; }
- if (args.desc) { if (args.desc === true) { op.desc = ''; } else if (typeof args.desc == 'string') { op.desc = args.desc; } else if (typeof args.desc == 'number') { op.desc = '' + args.desc; } }
- if (args.tags) { if (args.tags === true) { op.tags = ''; } else if (typeof args.tags == 'string') { op.tags = args.tags.split(','); } else if (typeof args.tags == 'number') { op.tags = '' + args.tags; } }
- if (args.icon) { op.icon = parseInt(args.icon); if ((typeof op.icon != 'number') || isNaN(op.icon) || (op.icon < 1) || (op.icon > 8)) { console.log("Icon must be between 1 and 8."); process.exit(1); return; } }
- if (args.consent) { op.consent = parseInt(args.consent); if ((typeof op.consent != 'number') || isNaN(op.consent) || (op.consent < 1)) { console.log("Invalid consent flags."); process.exit(1); return; } }
- ws.send(JSON.stringify(op));
+ if (args.addtag || args.removetag) {
+ // we need to fetch the node data first to then modify the tags
+ var nodeid = args.id;
+ ws.send(JSON.stringify({ action: 'nodes', id: args.id, responseid: 'meshctrl' }));
+ } else {
+ var op = { action: 'changedevice', nodeid: args.id, responseid: 'meshctrl' };
+ if (typeof args.name == 'string') { op.name = args.name; }
+ if (typeof args.name == 'number') { op.name = '' + args.name; }
+ if (args.desc) { if (args.desc === true) { op.desc = ''; } else if (typeof args.desc == 'string') { op.desc = args.desc; } else if (typeof args.desc == 'number') { op.desc = '' + args.desc; } }
+ if (args.tags) { if (args.tags === true) { op.tags = ''; } else if (typeof args.tags == 'string') { op.tags = args.tags.split(','); } else if (typeof args.tags == 'number') { op.tags = '' + args.tags; } }
+ if (args.icon) { op.icon = parseInt(args.icon); if ((typeof op.icon != 'number') || isNaN(op.icon) || (op.icon < 1) || (op.icon > 8)) { console.log("Icon must be between 1 and 8."); process.exit(1); return; } }
+ if (args.consent) { op.consent = parseInt(args.consent); if ((typeof op.consent != 'number') || isNaN(op.consent) || (op.consent < 1)) { console.log("Invalid consent flags."); process.exit(1); return; } }
+ ws.send(JSON.stringify(op));
+ }
break;
}
case 'runcommand': {
@@ -2076,6 +2105,23 @@ function serverConnect() {
}
break;
}
+ case 'serverversion': { // SERVERVERSION
+ if (settings.cmd == 'serverversion') {
+ if (data.responseid == 'meshctrl') {
+ if (data.result != 'OK') { console.log(data.result); process.exit(); }
+ if (args.json) {
+ console.log(JSON.stringify(data.tags, null, 2));
+ } else {
+ var svmsg = 'MeshCentral version: ' + data.tags.current;
+ if (typeof data.tags.latest == 'string') { svmsg += ' (latest: ' + data.tags.latest + ')'; }
+ if (typeof data.tags.stable == 'string') { svmsg += ' (stable: ' + data.tags.stable + ')'; }
+ console.log(svmsg);
+ }
+ process.exit();
+ }
+ }
+ break;
+ }
case 'events': {
if (settings.cmd == 'listevents') {
if (args.raw) {
@@ -2132,7 +2178,8 @@ function serverConnect() {
if ((settings.cmd == 'shell') || (settings.cmd == 'upload') || (settings.cmd == 'download')) {
var protocol = 1; // Terminal
if ((settings.cmd == 'upload') || (settings.cmd == 'download')) { protocol = 5; } // Files
- if ((args.id.split('/') != 3) && (settings.currentDomain != null)) { args.id = 'node/' + settings.currentDomain + '/' + args.id; }
+ if (args.powershell) { protocol = 6; } // PowerShell
+ if ((args.id.split('/').length != 3) && (settings.currentDomain != null)) { args.id = 'node/' + settings.currentDomain + '/' + args.id; }
var id = getRandomHex(6);
ws.send(JSON.stringify({ action: 'msg', nodeid: args.id, type: 'tunnel', usage: 1, value: '*/meshrelay.ashx?p=' + protocol + '&nodeid=' + args.id + '&id=' + id + '&rauth=' + data.rcookie, responseid: 'meshctrl' }));
connectTunnel(url.replace('/control.ashx', '/meshrelay.ashx?browser=1&p=' + protocol + '&nodeid=' + encodeURIComponent(args.id) + '&id=' + id + '&auth=' + data.cookie));
@@ -2472,6 +2519,43 @@ function serverConnect() {
}
}
}
+ if ((settings.cmd == 'editdevice') && (data.responseid == 'meshctrl')) {
+ // Find the node to get its current tags
+ var targetNode = null;
+ for (var i in data.nodes) {
+ for (var j in data.nodes[i]) {
+ if (data.nodes[i][j]._id == args.id) { targetNode = data.nodes[i][j]; break; }
+ }
+ if (targetNode != null) { break; }
+ }
+ if (targetNode == null) {
+ console.log('Node not found.');
+ process.exit();
+ return;
+ }
+ // Start with current tags or empty array
+ var tags = (Array.isArray(targetNode.tags)) ? targetNode.tags.slice() : [];
+ // Add tags: --addtag tag1,tag2
+ if (args.addtag) {
+ var addtags = (typeof args.addtag == 'string') ? args.addtag.split(',') : ['' + args.addtag];
+ for (var i in addtags) { var t = addtags[i].trim(); if (t && (tags.indexOf(t) < 0)) { tags.push(t); } }
+ }
+ // Remove tags: --removetag tag1,tag2
+ if (args.removetag) {
+ var removetags = (typeof args.removetag == 'string') ? args.removetag.split(',') : ['' + args.removetag];
+ var removetrimmed = removetags.map(function(r) { return r.trim(); });
+ tags = tags.filter(function(t) { return removetrimmed.indexOf(t) < 0; });
+ }
+ // Build and send the changedevice op
+ var op = { action: 'changedevice', nodeid: args.id, responseid: 'meshctrl' };
+ if (typeof args.name == 'string') { op.name = args.name; }
+ if (typeof args.name == 'number') { op.name = '' + args.name; }
+ if (args.desc) { if (args.desc === true) { op.desc = ''; } else if (typeof args.desc == 'string') { op.desc = args.desc; } else if (typeof args.desc == 'number') { op.desc = '' + args.desc; } }
+ if (args.icon) { op.icon = parseInt(args.icon); if ((typeof op.icon != 'number') || isNaN(op.icon) || (op.icon < 1) || (op.icon > 8)) { console.log("Icon must be between 1 and 8."); process.exit(1); return; } }
+ if (args.consent) { op.consent = parseInt(args.consent); if ((typeof op.consent != 'number') || isNaN(op.consent) || (op.consent < 1)) { console.log("Invalid consent flags."); process.exit(1); return; } }
+ op.tags = tags;
+ ws.send(JSON.stringify(op));
+ }
break;
}
case 'meshes': { // LISTDEVICEGROUPS
@@ -2705,7 +2789,7 @@ function getDevicesThatMatchFilter(nodes, x) {
} else {
// Device name search
try {
- var rs = x.split(/\s+/).join('|'), rx = new RegExp(rs); // In some cases (like +), this can throw an exception.
+ var rs = x.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), rx = new RegExp(rs); // In some cases (like +), this can throw an exception.
for (var d in nodes) {
//if (showRealNames) {
//if (nodes[d].rnamel != null && rx.test(nodes[d].rnamel.toLowerCase())) { r.push(nodes[d]); }
@@ -2726,7 +2810,7 @@ function connectTunnel(url) {
var options = { rejectUnauthorized: false, checkServerIdentity: onVerifyServer }
// Setup the HTTP proxy if needed
- if (args.proxy != null) { const HttpsProxyAgent = require('https-proxy-agent'); options.agent = new HttpsProxyAgent(require('url').parse(args.proxy)); }
+ if (args.proxy != null) { const HttpsProxyAgent = require('https-proxy-agent'); options.agent = new HttpsProxyAgent(new URL(args.proxy)); }
// Connect the WebSocket
console.log('Connecting...');
@@ -2756,7 +2840,7 @@ function connectTunnel(url) {
if (typeof process.stdout.getWindowSize == 'function') { termSize = process.stdout.getWindowSize(); }
if (termSize != null) { settings.tunnelws.send(JSON.stringify({ ctrlChannel: '102938', type: 'options', cols: termSize[0], rows: termSize[1] })); }
settings.tunnelwsstate = 1;
- settings.tunnelws.send('1'); // Terminal
+ settings.tunnelws.send((args.powershell ? 6 : 1)); // Powershell or Terminal
process.stdin.setEncoding('utf8');
process.stdin.setRawMode(true);
process.stdout.setEncoding('utf8');
@@ -2926,7 +3010,7 @@ function displayDeviceInfo(sysinfo, lastconnect, network, nodes) {
if (node.desc != null) { output["Description"] = node.desc; outputCount++; }
if (node.icon != null) { output["Icon"] = node.icon; outputCount++; }
if (node.tags) { output["Tags"] = node.tags; outputCount++; }
- if (node.av) {
+ if (node.av && node.av.length > 0) {
var av = [];
for (var i in node.av) {
if (typeof node.av[i]['product'] == 'string') {
@@ -2940,19 +3024,36 @@ function displayDeviceInfo(sysinfo, lastconnect, network, nodes) {
}
output["AntiVirus"] = av; outputCount++;
}
+ // Defender for Windows Server
+ if(typeof node.defender == 'object') {
+ output["Windows Defender"] = node.defender; outputCount++;
+ }
+ if (node.pr && node.pr.length > 0) {
+ var pr = [];
+ for (var i in node.pr) { pr.push(node.pr[i]); }
+ output["Pending Reboot"] = pr; outputCount++;
+ }
if (typeof node.wsc == 'object') {
- output["WindowsSecurityCenter"] = node.wsc; outputCount++;
+ output["Windows Security Center"] = node.wsc; outputCount++;
+ }
+ if (typeof node.lsc == 'object') {
+ output["Linux Security Center"] = node.lsc; outputCount++;
}
if (outputCount > 0) { info["General"] = output; }
// Operating System
var hardware = null;
if ((sysinfo != null) && (sysinfo.hardware != null)) { hardware = sysinfo.hardware; }
- if ((hardware && hardware.windows && hardware.windows.osinfo) || node.osdesc) {
+ if ((hardware && hardware.windows && hardware.windows.osinfo) || (hardware && hardware.linux) || node.osdesc) {
var output = {}, outputCount = 0;
if (node.rname) { output["Name"] = node.rname; outputCount++; }
if (node.osdesc) { output["Version"] = node.osdesc; outputCount++; }
if (hardware && hardware.windows && hardware.windows.osinfo) { var m = hardware.windows.osinfo; if (m.OSArchitecture) { output["Architecture"] = m.OSArchitecture; outputCount++; } }
+ if (hardware && hardware.linux) {
+ if (hardware.linux.arch) { output["Architecture"] = hardware.linux.arch; outputCount++; }
+ if (hardware.linux.kernel_release) { output["Kernel Release"] = hardware.linux.kernel_release; outputCount++; }
+ if (hardware.linux.kernel_build) { output["Kernel Build"] = hardware.linux.kernel_build; outputCount++; }
+ }
if (outputCount > 0) { info["Operating System"] = output; }
}
@@ -3078,6 +3179,8 @@ function displayDeviceInfo(sysinfo, lastconnect, network, nodes) {
// BIOS
if (ident.bios_vendor) { output["Vendor"] = ident.bios_vendor; outputCount++; }
if (ident.bios_version) { output["Version"] = ident.bios_version; outputCount++; }
+ if (ident.bios_serial) { output["Serial"] = ident.bios_serial; outputCount++; }
+ if (ident.bios_mode) { output["Mode"] = ident.bios_mode; outputCount++; }
if (outputCount > 0) { info["BIOS"] = output; }
output = {}, outputCount = 0;
@@ -3090,6 +3193,28 @@ function displayDeviceInfo(sysinfo, lastconnect, network, nodes) {
if (ident.cpu_name) { output["CPU"] = ident.cpu_name; }
if (ident.gpu_name) { for (var i in ident.gpu_name) { output["GPU" + (parseInt(i) + 1)] = ident.gpu_name[i]; } }
if (outputCount > 0) { info["Motherboard"] = output; }
+ output = {}, outputCount = 0;
+
+ // System
+ if (ident.chassis_manufacturer) { output["Manufacturer"] = ident.chassis_manufacturer; outputCount++; }
+ if (ident.product_name) { output["Product Name"] = ident.product_name; outputCount++; }
+ if (ident.chassis_serial) { output["Serial"] = ident.chassis_serial; outputCount++; }
+ if (ident.chassis_assettag) { output["Asset Tag"] = ident.chassis_assettag; outputCount++; }
+ if (outputCount > 0) { info["System"] = output; }
+ output = {}, outputCount = 0;
+ }
+
+ // TPM
+ if (hardware.tpm) {
+ var output = {}, outputCount = 0, tpm = hardware.tpm;
+ if (tpm.SpecVersion) { output["SpecVersion"] = parseFloat(tpm.SpecVersion).toFixed(1); outputCount++; }
+ if (tpm.ManufacturerId) { output["Identifier"] = tpm.ManufacturerId; outputCount++; }
+ if (tpm.ManufacturerVersion) { output["Version"] = tpm.ManufacturerVersion; outputCount++; }
+ if (tpm.IsActivated != null) { output["Activated"] = (tpm.IsActivated ? "Yes" : "No"); outputCount++; }
+ if (tpm.IsEnabled != null) { output["Enabled"] = (tpm.IsEnabled ? "Yes" : "No"); outputCount++; }
+ if (tpm.IsOwned != null) { output["Owned"] = (tpm.IsOwned ? "Yes" : "No"); outputCount++; }
+ if (outputCount > 0) { info["TPM"] = output; }
+ output = {}, outputCount = 0;
}
// Memory
@@ -3099,9 +3224,10 @@ function displayDeviceInfo(sysinfo, lastconnect, network, nodes) {
hardware.windows.memory.sort(function (a, b) { if (a.BankLabel > b.BankLabel) return 1; if (a.BankLabel < b.BankLabel) return -1; return 0; });
for (var i in hardware.windows.memory) {
var m = hardware.windows.memory[i], moutput = {}, moutputCount = 0;
- if (m.Capacity) { moutput["Capacity/Speed"] = (m.Capacity / 1024 / 1024) + " Mb, " + m.Speed + " Mhz"; moutputCount++; }
+ if (m.Capacity && m.Speed) { moutput["Capacity/Speed"] = (m.Capacity / 1024 / 1024) + " Mb, " + m.Speed + " Mhz"; moutputCount++; }
+ else if (m.Capacity) { moutput["Capacity"] = (m.Capacity / 1024 / 1024) + " Mb"; moutputCount++; }
if (m.PartNumber) { moutput["Part Number"] = ((m.Manufacturer && m.Manufacturer != 'Undefined') ? (m.Manufacturer + ', ') : '') + m.PartNumber; moutputCount++; }
- if (moutputCount > 0) { minfo[m.BankLabel] = moutput; info["Memory"] = minfo; }
+ if (moutputCount > 0) { minfo[m.BankLabel ? m.BankLabel : (m.DeviceLocator ? m.DeviceLocator : 'Unknown')] = moutput; info["Memory"] = minfo; }
}
}
}
@@ -3122,6 +3248,12 @@ function displayDeviceInfo(sysinfo, lastconnect, network, nodes) {
}
}
}
+
+ // Windows volumes
+ if ((hardware?.windows?.volumes)) { info["Volumes"] = hardware.windows.volumes; }
+
+ // Bitlocker cache
+ if ((hardware?.windows?.bitlocker)) { info["Bitlocker cache"] = hardware.windows.bitlocker; }
}
// Display everything
diff --git a/meshdesktopmultiplex.js b/meshdesktopmultiplex.js
index b4550faff5..e88111c60c 100644
--- a/meshdesktopmultiplex.js
+++ b/meshdesktopmultiplex.js
@@ -1052,8 +1052,8 @@ function CreateMeshRelayEx2(parent, ws, req, domain, user, cookie) {
// Check relay authentication
if ((user == null) && (obj.req.query != null) && (obj.req.query.rauth != null)) {
const rcookie = parent.parent.decodeCookie(obj.req.query.rauth, parent.parent.loginCookieEncryptionKey, 240); // Cookie with 4 hour timeout
- if (rcookie.ruserid != null) { obj.ruserid = rcookie.ruserid; } else if (rcookie.nouser === 1) { obj.rnouser = true; }
- if (rcookie.nodeid != null) { obj.nodeid = rcookie.nodeid; }
+ if (rcookie != null) { if (rcookie.ruserid != null) { obj.ruserid = rcookie.ruserid; } else if (rcookie.nouser === 1) { obj.rnouser = true; } }
+ if (rcookie != null) { if (rcookie.nodeid != null) { obj.nodeid = rcookie.nodeid; } }
}
// If there is no authentication, drop this connection
@@ -1340,6 +1340,7 @@ function CreateMeshRelayEx2(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);
const command = { nodeid: node._id, action: 'msg', type: 'tunnel', value: '*/meshrelay.ashx?p=2&id=' + obj.id + '&rauth=' + rcookie + '&nodeid=' + node._id, soptions: {}, usage: 2, rights: cookie.r, guestuserid: user._id, guestname: cookie.gn, consent: cookie.cf, remoteaddr: cleanRemoteAddr(obj.req.clientIp) };
+ if (typeof domain.terminaluservariable == 'string') { command.soptions.terminalUserVariable = domain.terminaluservariable; }
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; }
@@ -1350,7 +1351,7 @@ function CreateMeshRelayEx2(parent, ws, req, domain, user, cookie) {
if (domain.consentmessages.autoacceptifnouser === true) { command.soptions.consentAutoAcceptIfNoUser = true; }
if (domain.consentmessages.autoacceptifdesktopnouser === true) { command.soptions.consentAutoAcceptIfDesktopNoUser = true; }
if (domain.consentmessages.autoacceptifterminalnouser === true) { command.soptions.consentAutoAcceptIfTerminalNoUser = true; }
- if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAautoAcceptIfFileNoUser = true; }
+ if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAutoAcceptIfFileNoUser = true; }
if (domain.consentmessages.autoacceptiflocked === true) { command.soptions.consentAutoAcceptIfLocked = true; }
if (domain.consentmessages.autoacceptifdesktoplocked === true) { command.soptions.consentAutoAcceptIfDesktopLocked = true; }
if (domain.consentmessages.autoacceptifterminallocked === true) { command.soptions.consentAutoAcceptIfTerminalLocked = true; }
diff --git a/meshdevicefile.js b/meshdevicefile.js
index 695a0f65b9..9cbf9ae90b 100644
--- a/meshdevicefile.js
+++ b/meshdevicefile.js
@@ -26,7 +26,7 @@ module.exports.CreateMeshDeviceFile = function (parent, ws, res, req, domain, us
// Check relay authentication
if ((user == null) && (obj.req.query != null) && (obj.req.query.rauth != null)) {
const rcookie = parent.parent.decodeCookie(obj.req.query.rauth, parent.parent.loginCookieEncryptionKey, 240); // Cookie with 4 hour timeout
- if (rcookie.ruserid != null) { obj.ruserid = rcookie.ruserid; }
+ if (rcookie != null) { if (rcookie.ruserid != null) { obj.ruserid = rcookie.ruserid; } }
}
// Relay session count (we may remove this in the future)
diff --git a/meshipkvm.js b/meshipkvm.js
index 2623eb9040..ead44363d5 100644
--- a/meshipkvm.js
+++ b/meshipkvm.js
@@ -34,6 +34,7 @@ function CreateIPKVMManager(parent) {
const MESHRIGHT_RESETOFF = 0x00040000; // 262144
const MESHRIGHT_GUESTSHARING = 0x00080000; // 524288
const MESHRIGHT_DEVICEDETAILS = 0x00100000; // ?1048576?
+ const MESHRIGHT_NOREGISTRY = 0x00400000; // 4194304
const MESHRIGHT_ADMIN = 0xFFFFFFFF;
// Subscribe for mesh creation events
@@ -262,10 +263,10 @@ function CreateIPKVMManager(parent) {
// Parse an incoming HTTP request URL
function parseIpKvmUrl(domain, url) {
- const q = require('url').parse(url, true);
- const i = q.path.indexOf('/ipkvm.ashx/');
+ const q = new URL(url, 'http://localhost');
+ const i = q.pathname.indexOf('/ipkvm.ashx/');
if (i == -1) return null;
- const urlargs = q.path.substring(i + 12).split('/');
+ const urlargs = q.pathname.substring(i + 12).split('/');
if (urlargs[0].length != 64) return null;
const nodeid = 'node/' + domain.id + '/' + urlargs[0];
const nid = urlargs[0];
@@ -1079,4 +1080,4 @@ function CreateMiniRouter(parent, nodeid, targetHost, targetPort) {
return obj;
}
-module.exports.CreateIPKVMManager = CreateIPKVMManager;
\ No newline at end of file
+module.exports.CreateIPKVMManager = CreateIPKVMManager;
diff --git a/meshmail.js b/meshmail.js
index 2c88051e82..330c61c7d0 100644
--- a/meshmail.js
+++ b/meshmail.js
@@ -31,6 +31,10 @@ module.exports.CreateMeshMail = function (parent, domain) {
const sortCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })
const constants = (obj.parent.crypto.constants ? obj.parent.crypto.constants : require('constants')); // require('constants') is deprecated in Node 11.10, use require('crypto').constants instead.
+ // Safe JSON stringify resistant to circular references.
+ // Node TLS errors (e.g. SMTP cert mismatch) include the certificate chain, where each X509 cert has an 'issuerCertificate' field pointing back at its issuer, closing the cycle. Plain JSON.stringify throws "Converting circular structure to JSON" and crashes the server. This helper drops cert-related fields and tracks seen objects via a WeakSet.
+ function safeStringify(o) { try { const seen = new WeakSet(); return JSON.stringify(o, function (k, v) { if (typeof v === 'object' && v !== null) { if (seen.has(v)) return '[Circular]'; seen.add(v); } if (k === 'issuerCertificate' || k === 'cert' || k === 'raw') return '[Cert]'; return v; }); } catch (ex) { return '[stringify-error: ' + ex.message + ']'; } }
+
function EscapeHtml(x) { if (typeof x == 'string') return x.replace(/&/g, '&').replace(/>/g, '>').replace(//g, '>').replace(/').replace(/\n/g, '').replace(/\t/g, ' '); if (typeof x == "boolean") return x; if (typeof x == "number") return x; }
@@ -488,15 +492,15 @@ module.exports.CreateMeshMail = function (parent, domain) {
sendNextMail();
}, function (error) {
obj.sendingMail = false;
- parent.debug('email', 'SendGrid sending error: ' + JSON.stringify(error));
+ parent.debug('email', 'SendGrid sending error: ' + safeStringify(error));
obj.retry++;
// Wait and try again
if (obj.retry < 3) {
setTimeout(sendNextMail, 10000);
} else {
// Failed, send the next mail
- parent.debug('email', 'SendGrid server failed (Skipping): ' + JSON.stringify(err));
- console.log('SendGrid server failed (Skipping): ' + JSON.stringify(err));
+ parent.debug('email', 'SendGrid server failed (Skipping): ' + safeStringify(err));
+ console.log('SendGrid server failed (Skipping): ' + safeStringify(err));
obj.pendingMails.shift();
obj.retry = 0;
sendNextMail();
@@ -516,7 +520,7 @@ module.exports.CreateMeshMail = function (parent, domain) {
} else {
// SMTP send
obj.smtpServer.sendMail(mailToSend, function (err, info) {
- parent.debug('email', 'SMTP response: ' + JSON.stringify(err) + ', ' + JSON.stringify(info));
+ parent.debug('email', 'SMTP response: ' + safeStringify(err) + ', ' + JSON.stringify(info));
obj.sendingMail = false;
if (err == null) {
// Send the next mail
@@ -525,15 +529,15 @@ module.exports.CreateMeshMail = function (parent, domain) {
sendNextMail();
} else {
obj.retry++;
- parent.debug('email', 'SMTP server failed (Retry:' + obj.retry + '): ' + JSON.stringify(err));
- console.log('SMTP server failed (Retry:' + obj.retry + '/3): ' + JSON.stringify(err));
+ parent.debug('email', 'SMTP server failed (Retry:' + obj.retry + '): ' + safeStringify(err));
+ console.log('SMTP server failed (Retry:' + obj.retry + '/3): ' + safeStringify(err));
// Wait and try again
if (obj.retry < 3) {
setTimeout(sendNextMail, 10000);
} else {
// Failed, send the next mail
- parent.debug('email', 'SMTP server failed (Skipping): ' + JSON.stringify(err));
- console.log('SMTP server failed (Skipping): ' + JSON.stringify(err));
+ parent.debug('email', 'SMTP server failed (Skipping): ' + safeStringify(err));
+ console.log('SMTP server failed (Skipping): ' + safeStringify(err));
obj.pendingMails.shift();
obj.retry = 0;
sendNextMail();
@@ -558,8 +562,8 @@ module.exports.CreateMeshMail = function (parent, domain) {
// Remove all non-object types from error to avoid a JSON stringify error.
var err2 = {};
for (var i in err) { if (typeof (err[i]) != 'object') { err2[i] = err[i]; } }
- parent.debug('email', 'SMTP mail server ' + obj.config.smtp.host + ' failed: ' + JSON.stringify(err2));
- console.log('SMTP mail server ' + obj.config.smtp.host + ' failed: ' + JSON.stringify(err2));
+ parent.debug('email', 'SMTP mail server ' + obj.config.smtp.host + ' failed: ' + safeStringify(err2));
+ console.log('SMTP mail server ' + obj.config.smtp.host + ' failed: ' + safeStringify(err2));
}
});
};
diff --git a/meshmessaging.js b/meshmessaging.js
index 9a722792a0..073937844b 100644
--- a/meshmessaging.js
+++ b/meshmessaging.js
@@ -334,7 +334,7 @@ module.exports.CreateServer = function (parent) {
console.log('Sending CallMeBot message to: ' + to.substring(10) + ': ' + msg);
var toData = to.substring(10).split('|');
if ((toData[0] == 'signal') && (toData.length == 3)) {
- var url = 'https://api.callmebot.com/signal/send.php?phone=' + encodeURIComponent(toData[1]) + '&apikey=' + encodeURIComponent(toData[2]) + '&text=' + encodeURIComponent(msg);
+ var url = 'https://signal.callmebot.com/signal/send.php?phone=' + encodeURIComponent(toData[1]) + '&apikey=' + encodeURIComponent(toData[2]) + '&text=' + encodeURIComponent(msg);
require('https').get(url, function (r) { if (func != null) { func(r.statusCode == 200); } });
} else if ((toData[0] == 'whatsapp') && (toData.length == 3)) {
var url = 'https://api.callmebot.com/whatsapp.php?phone=' + encodeURIComponent(toData[1]) + '&apikey=' + encodeURIComponent(toData[2]) + '&text=' + encodeURIComponent(msg);
@@ -365,7 +365,11 @@ module.exports.CreateServer = function (parent) {
});
if (func != null) { func(true); }
}else if ((to.startsWith('slack:')) && (obj.slackClient != null)) { //slack
- const req = require('https').request(new URL(to.substring(6)), { method: 'POST' }, function (res) { if (func != null) { func(true); } });
+ // Slack incoming webhooks are always on hooks.slack.com, so only allow that host.
+ var slackUrl;
+ try { slackUrl = new URL(to.substring(6)); } catch (ex) { if (func != null) { func(false); } return; }
+ if (slackUrl.hostname != 'hooks.slack.com') { if (func != null) { func(false); } return; }
+ const req = require('https').request(slackUrl, { method: 'POST' }, function (res) { if (func != null) { func(true); } });
req.on('error', function (err) { if (func != null) { func(false); } });
req.write(JSON.stringify({"text": msg }));
req.end();
@@ -378,18 +382,18 @@ module.exports.CreateServer = function (parent) {
// Convert a CallMeBot URL into a handle
obj.callmebotUrlToHandle = function (xurl) {
var url = null;
- try { url = require('url').parse(xurl); } catch (ex) { return; }
- if ((url == null) || (url.host != 'api.callmebot.com') || (url.query == null)) return;
- var urlArgs = {}, urlArgs2 = url.query.split('&');
+ try { url = new URL(xurl); } catch (ex) { return; }
+ if ((url == null) || !url.host.endsWith('.callmebot.com') || !url.search) return;
+ var urlArgs = {}, urlArgs2 = url.search.slice(1).split('&');
for (var i in urlArgs2) { var j = urlArgs2[i].indexOf('='); if (j > 0) { urlArgs[urlArgs2[i].substring(0, j)] = urlArgs2[i].substring(j + 1); } }
if ((urlArgs['phone'] != null) && (urlArgs['phone'].indexOf('|') >= 0)) return;
if ((urlArgs['apikey'] != null) && (urlArgs['apikey'].indexOf('|') >= 0)) return;
if ((urlArgs['user'] != null) && (urlArgs['user'].indexOf('|') >= 0)) return;
// Signal Messenger, Whatapp, Facebook and Telegram
- if (url.path.startsWith('/signal') && (urlArgs['phone'] != null) && (urlArgs['apikey'] != null)) { return 'callmebot:signal|' + urlArgs['phone'] + '|' + urlArgs['apikey']; }
- if (url.path.startsWith('/whatsapp') && (urlArgs['phone'] != null) && (urlArgs['apikey'] != null)) { return 'callmebot:whatsapp|' + urlArgs['phone'] + '|' + urlArgs['apikey']; }
- if (url.path.startsWith('/facebook') && (urlArgs['apikey'] != null)) { return 'callmebot:facebook|' + urlArgs['apikey']; }
- if (url.path.startsWith('/text') && (urlArgs['user'] != null)) { return 'callmebot:telegram|' + urlArgs['user']; }
+ if (url.pathname.startsWith('/signal') && (urlArgs['phone'] != null) && (urlArgs['apikey'] != null)) { return 'callmebot:signal|' + urlArgs['phone'] + '|' + urlArgs['apikey']; }
+ if (url.pathname.startsWith('/whatsapp') && (urlArgs['phone'] != null) && (urlArgs['apikey'] != null)) { return 'callmebot:whatsapp|' + urlArgs['phone'] + '|' + urlArgs['apikey']; }
+ if (url.pathname.startsWith('/facebook') && (urlArgs['apikey'] != null)) { return 'callmebot:facebook|' + urlArgs['apikey']; }
+ if (url.pathname.startsWith('/text') && (urlArgs['user'] != null)) { return 'callmebot:telegram|' + urlArgs['user']; }
return null;
}
diff --git a/meshrelay.js b/meshrelay.js
index 116add1f2b..c7390f32c4 100644
--- a/meshrelay.js
+++ b/meshrelay.js
@@ -36,11 +36,13 @@ const MESHRIGHT_RESETOFF = 0x00040000; // 262144
const MESHRIGHT_GUESTSHARING = 0x00080000; // 524288
const MESHRIGHT_DEVICEDETAILS = 0x00100000; // 1048576
const MESHRIGHT_RELAY = 0x00200000; // 2097152
+const MESHRIGHT_NOREGISTRY = 0x00400000; // 4194304
const MESHRIGHT_ADMIN = 0xFFFFFFFF;
// Protocol:
// 1 = Terminal
// 2 = Desktop
+// 4 = Registry
// 5 = Files
// 6 = Admin PowerShell
// 8 = User Shell
@@ -68,6 +70,15 @@ function checkDeviceSharePublicIdentifier(parent, domain, nodeid, pid, extraKey,
});
}
+function isProtocolAllowedByRights(rights, protocol) {
+ if ((rights == null) || (rights == MESHRIGHT_ADMIN)) { return true; }
+ if ((protocol == 1) && ((rights & MESHRIGHT_NOTERMINAL) != 0)) { return false; }
+ if ((protocol == 2) && ((rights & MESHRIGHT_NODESKTOP) != 0)) { return false; }
+ if ((protocol == 4) && ((rights & MESHRIGHT_NOREGISTRY) != 0)) { return false; }
+ if ((protocol == 5) && ((rights & MESHRIGHT_NOFILES) != 0)) { return false; }
+ return true;
+}
+
module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie) {
if ((cookie != null) && (typeof cookie.nid == 'string') && (typeof cookie.pid == 'string')) {
checkDeviceSharePublicIdentifier(parent, domain, cookie.nid, cookie.pid, cookie.k, function (result) {
@@ -172,7 +183,7 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) {
// Check relay authentication
if ((user == null) && (obj.req.query != null) && (obj.req.query.rauth != null)) {
const rcookie = parent.parent.decodeCookie(obj.req.query.rauth, parent.parent.loginCookieEncryptionKey, 240); // Cookie with 4 hour timeout
- if (rcookie.ruserid != null) { obj.ruserid = rcookie.ruserid; } else if (rcookie.nouser === 1) { obj.rnouser = true; }
+ if (rcookie != null) { if (rcookie.ruserid != null) { obj.ruserid = rcookie.ruserid; } else if (rcookie.nouser === 1) { obj.rnouser = true; } }
}
// If there is no authentication, drop this connection
@@ -885,7 +896,9 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) {
const node = docs[0];
// Check if this user has permission to relay thru this computer (MESHRIGHT_REMOTECONTROL or MESHRIGHT_RELAY rights)
- if ((obj.nouser !== true) && ((parent.GetNodeRights(obj.user, node.meshid, node._id) & 0x00200008) == 0)) { console.log('ERR: Access denied (1)'); try { obj.close(); } catch (ex) { } return; }
+ const rights = parent.GetNodeRights(obj.user, node.meshid, node._id);
+ if ((obj.nouser !== true) && ((rights & 0x00200008) == 0)) { console.log('ERR: Access denied (1)'); try { obj.close(); } catch (ex) { } return; }
+ if ((obj.nouser !== true) && (isProtocolAllowedByRights(rights, parseInt(obj.req.query.p)) == false)) { console.log('ERR: Access denied (3)'); try { obj.close(); } catch (ex) { } return; }
// Set nodeid and meshid
obj.nodeid = node._id;
@@ -898,6 +911,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 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.terminaluservariable == 'string') { command.soptions.terminalUserVariable = domain.terminaluservariable; }
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; }
@@ -908,7 +922,7 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) {
if (domain.consentmessages.autoacceptifnouser === true) { command.soptions.consentAutoAcceptIfNoUser = true; }
if (domain.consentmessages.autoacceptifdesktopnouser === true) { command.soptions.consentAutoAcceptIfDesktopNoUser = true; }
if (domain.consentmessages.autoacceptifterminalnouser === true) { command.soptions.consentAutoAcceptIfTerminalNoUser = true; }
- if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAautoAcceptIfFileNoUser = true; }
+ if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAutoAcceptIfFileNoUser = true; }
if (domain.consentmessages.autoacceptiflocked === true) { command.soptions.consentAutoAcceptIfLocked = true; }
if (domain.consentmessages.autoacceptifdesktoplocked === true) { command.soptions.consentAutoAcceptIfDesktopLocked = true; }
if (domain.consentmessages.autoacceptifterminallocked === true) { command.soptions.consentAutoAcceptIfTerminalLocked = true; }
@@ -933,7 +947,9 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) {
const node = docs[0];
// Check if this user has permission to relay thru this computer (MESHRIGHT_REMOTECONTROL or MESHRIGHT_RELAY rights)
- if ((parent.GetNodeRights(obj.user, node.meshid, node._id) & 0x00200008) == 0) { console.log('ERR: Access denied (2)'); try { obj.close(); } catch (ex) { } return; }
+ const rights = parent.GetNodeRights(obj.user, node.meshid, node._id);
+ if ((rights & 0x00200008) == 0) { console.log('ERR: Access denied (2)'); try { obj.close(); } catch (ex) { } return; }
+ if (isProtocolAllowedByRights(rights, parseInt(obj.req.query.p)) == false) { console.log('ERR: Access denied (3)'); try { obj.close(); } catch (ex) { } return; }
// Set nodeid and meshid
obj.nodeid = node._id;
@@ -954,7 +970,7 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) {
if (domain.consentmessages.autoacceptifnouser === true) { command.soptions.consentAutoAcceptIfNoUser = true; }
if (domain.consentmessages.autoacceptifdesktopnouser === true) { command.soptions.consentAutoAcceptIfDesktopNoUser = true; }
if (domain.consentmessages.autoacceptifterminalnouser === true) { command.soptions.consentAutoAcceptIfTerminalNoUser = true; }
- if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAautoAcceptIfFileNoUser = true; }
+ if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAutoAcceptIfFileNoUser = true; }
if (domain.consentmessages.autoacceptiflocked === true) { command.soptions.consentAutoAcceptIfLocked = true; }
if (domain.consentmessages.autoacceptifdesktoplocked === true) { command.soptions.consentAutoAcceptIfDesktopLocked = true; }
if (domain.consentmessages.autoacceptifterminallocked === true) { command.soptions.consentAutoAcceptIfTerminalLocked = true; }
@@ -980,7 +996,7 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) {
if (domain.consentmessages.autoacceptifnouser === true) { command.soptions.consentAutoAcceptIfNoUser = true; }
if (domain.consentmessages.autoacceptifdesktopnouser === true) { command.soptions.consentAutoAcceptIfDesktopNoUser = true; }
if (domain.consentmessages.autoacceptifterminalnouser === true) { command.soptions.consentAutoAcceptIfTerminalNoUser = true; }
- if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAautoAcceptIfFileNoUser = true; }
+ if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAutoAcceptIfFileNoUser = true; }
if (domain.consentmessages.autoacceptiflocked === true) { command.soptions.consentAutoAcceptIfLocked = true; }
if (domain.consentmessages.autoacceptifdesktoplocked === true) { command.soptions.consentAutoAcceptIfDesktopLocked = true; }
if (domain.consentmessages.autoacceptifterminallocked === true) { command.soptions.consentAutoAcceptIfTerminalLocked = true; }
@@ -1006,7 +1022,9 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) {
const node = docs[0];
// Check if this user has permission to relay thru this computer (MESHRIGHT_REMOTECONTROL or MESHRIGHT_RELAY rights)
- if ((obj.nouser !== true) && ((parent.GetNodeRights(obj.user, node.meshid, node._id) & 0x00200008) == 0)) { console.log('ERR: Access denied (2)'); try { obj.close(); } catch (ex) { } return; }
+ const rights = parent.GetNodeRights(obj.user, node.meshid, node._id);
+ if ((obj.nouser !== true) && ((rights & 0x00200008) == 0)) { console.log('ERR: Access denied (2)'); try { obj.close(); } catch (ex) { } return; }
+ if ((obj.nouser !== true) && (isProtocolAllowedByRights(rights, parseInt(obj.req.query.p)) == false)) { console.log('ERR: Access denied (3)'); try { obj.close(); } catch (ex) { } return; }
// Set nodeid and meshid
obj.nodeid = node._id;
@@ -1040,7 +1058,7 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) {
if (domain.consentmessages.autoacceptifnouser === true) { command.soptions.consentAutoAcceptIfNoUser = true; }
if (domain.consentmessages.autoacceptifdesktopnouser === true) { command.soptions.consentAutoAcceptIfDesktopNoUser = true; }
if (domain.consentmessages.autoacceptifterminalnouser === true) { command.soptions.consentAutoAcceptIfTerminalNoUser = true; }
- if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAautoAcceptIfFileNoUser = true; }
+ if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAutoAcceptIfFileNoUser = true; }
if (domain.consentmessages.autoacceptiflocked === true) { command.soptions.consentAutoAcceptIfLocked = true; }
if (domain.consentmessages.autoacceptifdesktoplocked === true) { command.soptions.consentAutoAcceptIfDesktopLocked = true; }
if (domain.consentmessages.autoacceptifterminallocked === true) { command.soptions.consentAutoAcceptIfTerminalLocked = true; }
@@ -1069,7 +1087,9 @@ function CreateMeshRelayEx(parent, ws, req, domain, user, cookie) {
const node = docs[0];
// Check if this user has permission to relay thru this computer (MESHRIGHT_REMOTECONTROL or MESHRIGHT_RELAY rights)
- if ((parent.GetNodeRights(obj.user, node.meshid, node._id) & 0x00200008) == 0) { console.log('ERR: Access denied (2)'); try { obj.close(); } catch (ex) { } return; }
+ const rights = parent.GetNodeRights(obj.user, node.meshid, node._id);
+ if ((rights & 0x00200008) == 0) { console.log('ERR: Access denied (2)'); try { obj.close(); } catch (ex) { } return; }
+ if (isProtocolAllowedByRights(rights, parseInt(obj.req.query.p)) == false) { console.log('ERR: Access denied (3)'); try { obj.close(); } catch (ex) { } return; }
// Set nodeid and meshid
obj.nodeid = node._id;
@@ -1316,7 +1336,9 @@ function CreateLocalRelayEx(parent, ws, req, domain, user, cookie) {
obj.meshid = node.meshid;
// Check if this user has permission to relay thru this computer (MESHRIGHT_REMOTECONTROL or MESHRIGHT_RELAY rights)
- if ((parent.GetNodeRights(obj.user, node.meshid, node._id) & 0x00200008) == 0) { console.log('ERR: Access denied (2)'); try { obj.close(); } catch (ex) { } return; }
+ const rights = parent.GetNodeRights(obj.user, node.meshid, node._id);
+ if ((rights & 0x00200008) == 0) { console.log('ERR: Access denied (2)'); try { obj.close(); } catch (ex) { } return; }
+ if (isProtocolAllowedByRights(rights, parseInt(obj.req.query.p)) == false) { console.log('ERR: Access denied (3)'); try { obj.close(); } catch (ex) { } return; }
// Setup TCP client
obj.client = new net.Socket();
diff --git a/meshsms.js b/meshsms.js
index 441e7c4d3c..6619a32eaf 100644
--- a/meshsms.js
+++ b/meshsms.js
@@ -147,16 +147,16 @@ module.exports.CreateMeshSMS = function (parent) {
} else {
var sms = parent.config.sms.url.split('{{phone}}').join(encodeURIComponent(to)).split('{{message}}').join(encodeURIComponent(msg));
parent.debug('email', 'SMS URL: ' + sms);
- sms = require('url').parse(sms);
+ sms = new URL(sms);
if (sms.protocol == 'https:') {
// HTTPS GET request
- const options = { hostname: sms.hostname, port: sms.port ? sms.port : 443, path: sms.path, method: 'GET', rejectUnauthorized: false };
+ const options = { hostname: sms.hostname, port: sms.port ? sms.port : 443, path: sms.pathname + sms.search, method: 'GET', rejectUnauthorized: false };
const request = require('https').request(options, function (res) { parent.debug('email', 'SMS result: ' + res.statusCode); if (func != null) { func(res.statusCode == 200, (res.statusCode == 200) ? null : res.statusCode, null); } res.on('data', function (d) { }); });
request.on('error', function (err) { parent.debug('email', 'SMS error: ' + err); if (func != null) { func(false, err, null); } });
request.end();
} else {
// HTTP GET request
- const options = { hostname: sms.hostname, port: sms.port ? sms.port : 80, path: sms.path, method: 'GET' };
+ const options = { hostname: sms.hostname, port: sms.port ? sms.port : 80, path: sms.pathname + sms.search, method: 'GET' };
const request = require('http').request(options, function (res) { parent.debug('email', 'SMS result: ' + res.statusCode); if (func != null) { func(res.statusCode == 200, (res.statusCode == 200) ? null : res.statusCode, null); } res.on('data', function (d) { }); });
request.on('error', function (err) { parent.debug('email', 'SMS error: ' + err); if (func != null) { func(false, err, null); } });
request.end();
diff --git a/meshuser.js b/meshuser.js
index 34013e1a8b..b7b12024d8 100644
--- a/meshuser.js
+++ b/meshuser.js
@@ -13,6 +13,12 @@
/*jshint esversion: 6 */
"use strict";
+// volume & bitlocker statuses
+const encMethod = { 0: '', 1: "AES-128 with diffuser", 2: "AES-256 with diffuser", 3: 'AES-128', 4: 'AES-256', 5: "Hardware encryption", 6: 'XTS-AES-128', 7: 'XTS-AES-256' };
+const driveType = { 0: "Unknown", 1: "No Root Directory", 2: "Removable Disk", 3: "Local Disk", 4: "Network Drive", 5: "Compact Disc", 6: "RAM Disk" };
+const conversionStatus = { "-1": "Unknown", 0: "Fully Decrypted", 1: "Fully Encrypted", 2: "Encryption In Progress", 3: "Decryption In Progress", 4: "Encryption Paused", 5: "Decryption Paused" };
+const protectionStatus = { 0: "Off", 1: "On", 2: "Locked"};
+
// Construct a MeshAgent object, called upon connection
module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, user) {
const fs = require('fs');
@@ -53,6 +59,8 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
const MESHRIGHT_GUESTSHARING = 0x00080000; // 524288
const MESHRIGHT_DEVICEDETAILS = 0x00100000; // 1048576
const MESHRIGHT_RELAY = 0x00200000; // 2097152
+ const MESHRIGHT_NOREGISTRY = 0x00400000; // 4194304
+ const MESHRIGHT_NOSOFTWARE = 0x00800000; // 8388608
const MESHRIGHT_ADMIN = 0xFFFFFFFF;
// Site rights
@@ -646,6 +654,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
}
serverinfo.preConfiguredScripts = r;
}
+
if (domain.maxdeviceview != null) { serverinfo.maxdeviceview = domain.maxdeviceview; } // Maximum number of devices a user can view at any given time
// Send server information
@@ -729,6 +738,12 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
if (command.meshid == null) { err = 'Invalid group id'; }
}
+ // Check if command.id is set with a domain id, if not, add the domain id to it.
+ if (typeof command.id == 'string' && command.id !== '') {
+ if (common.validateString(command.id, 1, 1024) == false) { err = 'Invalid device identifier'; }
+ else if (command.id.indexOf('/') == -1) { command.id = 'node/' + domain.id + '/' + command.id; }
+ }
+
if (err == null) {
try {
if (command.meshid == null) {
@@ -971,6 +986,24 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
}
break;
}
+ case 'software': {
+ if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'software', responseid: command.responseid, result: 'Denied' })); } catch (ex) { } }
+
+ parent.GetNodeWithRights(domain, user, command.nodeid, function (node, rights, visible) {
+ var mesh = parent.meshes[node.meshid];
+ if ((node != null) && (mesh != null) && (rights === MESHRIGHT_ADMIN) || ((rights & MESHRIGHT_NOSOFTWARE) === 0)) {
+ var agent = parent.wsagents[command.nodeid];
+ if (agent != null) {
+ routeCommandToNode(command, requiredRights, requiredNonRights, func, routingOptions);
+ } else {
+ if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'software', responseid: command.responseid, result: 'Agent offline' })); } catch (ex) { } }
+ }
+ } else {
+ if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'software', responseid: command.responseid, result: 'Denied' })); } catch (ex) { } }
+ }
+ });
+ break;
+ }
case 'msg':
{
// Check the nodeid
@@ -994,16 +1027,18 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
if (command.type == 'tunnel') {
if ((typeof command.value != 'string') || (typeof command.nodeid != 'string')) break;
var url = null;
- try { url = require('url').parse(command.value, true); } catch (ex) { }
+ try { url = new URL(command.value, 'http://localhost'); } catch (ex) { }
if (url == null) break; // Bad URL
- if (url.query && url.query.nodeid && (url.query.nodeid != command.nodeid)) break; // Bad NodeID in URL query string
+ if (url.searchParams.get('nodeid') && (url.searchParams.get('nodeid') != command.nodeid)) break; // Bad NodeID in URL query string
// Check rights
- if (url.query.p == '1') { requiredNonRights = MESHRIGHT_NOTERMINAL; }
- else if ((url.query.p == '4') || (url.query.p == '5')) { requiredNonRights = MESHRIGHT_NOFILES; }
+ if (url.searchParams.get('p') == '1') { requiredNonRights = MESHRIGHT_NOTERMINAL; }
+ else if (url.searchParams.get('p') == '4') { requiredNonRights = MESHRIGHT_NOREGISTRY; }
+ else if (url.searchParams.get('p') == '5') { requiredNonRights = MESHRIGHT_NOFILES; }
+ else if (url.searchParams.get('p') == '6') { requiredNonRights = MESHRIGHT_NOSOFTWARE; }
// If we are using the desktop multiplexor, remove the VIEWONLY limitation. The multiplexor will take care of enforcing that limitation when needed.
- if (((parent.parent.config.settings.desktopmultiplex === true) || (domain.desktopmultiplex === true)) && (url.query.p == '2')) { routingOptions = { removeViewOnlyLimitation: true }; }
+ if (((parent.parent.config.settings.desktopmultiplex === true) || (domain.desktopmultiplex === true)) && (url.searchParams.get('p') == '2')) { routingOptions = { removeViewOnlyLimitation: true }; }
// Add server TLS cert hash
var tlsCertHash = null;
@@ -1024,7 +1059,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
if (domain.consentmessages.autoacceptifnouser === true) { command.soptions.consentAutoAcceptIfNoUser = true; }
if (domain.consentmessages.autoacceptifdesktopnouser === true) { command.soptions.consentAutoAcceptIfDesktopNoUser = true; }
if (domain.consentmessages.autoacceptifterminalnouser === true) { command.soptions.consentAutoAcceptIfTerminalNoUser = true; }
- if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAautoAcceptIfFileNoUser = true; }
+ if (domain.consentmessages.autoacceptiffilenouser === true) { command.soptions.consentAutoAcceptIfFileNoUser = true; }
if (domain.consentmessages.autoacceptiflocked === true) { command.soptions.consentAutoAcceptIfLocked = true; }
if (domain.consentmessages.autoacceptifdesktoplocked === true) { command.soptions.consentAutoAcceptIfDesktopLocked = true; }
if (domain.consentmessages.autoacceptifterminallocked === true) { command.soptions.consentAutoAcceptIfTerminalLocked = true; }
@@ -1037,6 +1072,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
if (typeof domain.notificationmessages.terminal == 'string') { command.soptions.notifyMsgTerminal = domain.notificationmessages.terminal; }
if (typeof domain.notificationmessages.files == 'string') { command.soptions.notifyMsgFiles = domain.notificationmessages.files; }
}
+ if (typeof domain.terminaluservariable == 'string') { command.soptions.terminalUserVariable = domain.terminaluservariable; }
// Add userid
command.userid = user._id;
@@ -1048,7 +1084,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
// If a response is needed, set a callback function
var func = null;
if (command.responseid != null) { func = function (r) { try { ws.send(JSON.stringify({ action: 'msg', result: r ? 'OK' : 'Unable to route', tag: command.tag, responseid: command.responseid })); } catch (ex) { } } }
-
+
// Route this command to a target node
routeCommandToNode(command, requiredRights, requiredNonRights, func, routingOptions);
break;
@@ -2132,6 +2168,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
else if (((command.meshtype == 3) || (command.meshtype == 4)) && (parent.args.wanonly == true) && (typeof command.relayid != 'string')) { err = 'Invalid group type'; } // Local device group type wihtout relay is not allowed in WAN mode
else if (((command.meshtype == 3) || (command.meshtype == 4)) && (parent.args.lanonly == true) && (typeof command.relayid == 'string')) { err = 'Invalid group type'; } // Local device group type with relay is not allowed in WAN mode
else if ((domain.ipkvm == null) && (command.meshtype == 4)) { err = 'Invalid group type'; } // IP KVM device group type is not allowed unless enabled
+ else if ((command.parent != null) && (typeof command.parent !== 'string' || !parent.meshes[command.parent] || parent.meshes[command.parent].domain !== domain.id)) { err = 'Invalid parent group'; }
if ((err == null) && (command.meshtype == 4)) {
if ((command.kvmmodel < 1) || (command.kvmmodel > 2)) { err = 'Invalid KVM model'; }
else if (common.validateString(command.kvmhost, 1, 128) == false) { err = 'Invalid KVM hostname'; }
@@ -2156,6 +2193,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
links[user._id] = { name: user.name, rights: 4294967295 };
mesh = { type: 'mesh', _id: meshid, name: command.meshname, mtype: command.meshtype, desc: command.desc, domain: domain.id, links: links, creation: Date.now(), creatorid: user._id, creatorname: user.name };
+ // Set parent mesh if provided
+ if (command.parent && parent.meshes[command.parent]) { mesh.parent = command.parent; }
+
// Add flags and consent if present
if (typeof command.flags == 'number') { mesh.flags = command.flags; }
if (typeof command.consent == 'number') { mesh.consent = command.consent; }
@@ -2845,7 +2885,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
db.Remove('si' + node._id); // Remove system information
db.Remove('al' + node._id); // Remove error log last time
if (db.RemoveSMBIOS) { db.RemoveSMBIOS(node._id); } // Remove SMBios data
- db.RemoveAllNodeEvents(node._id); // Remove all events for this node
+ db.RemoveAllNodeEvents(node.domain, node._id); // Remove all events for this node
db.removeAllPowerEventsForNode(node._id); // Remove all power events for this node
if (typeof node.pmt == 'string') { db.Remove('pmt_' + node.pmt); } // Remove Push Messaging Token
db.Get('ra' + node._id, function (err, nodes) {
@@ -3027,7 +3067,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
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 webRelayDns = (args.relaydns != null) ? args.relaydns[0] : parent.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);
@@ -3730,7 +3770,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
var otplib = null;
try { otplib = require('otplib'); } catch (ex) { }
if (otplib == null) { ws.send(JSON.stringify({ action: 'otpauth-request', err: 6 })); return; }
- const secret = otplib.authenticator.generateSecret(); // TODO: Check the random source of this value.
+ const secret = otplib.generateSecret(); // TODO: Check the random source of this value.
var domainName = parent.certificates.CommonName;
if (domain.dns != null) {
@@ -3738,7 +3778,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
} else if (domain.dns == null && domain.id != '') {
domainName += "/" + domain.id;
}
- ws.send(JSON.stringify({ action: 'otpauth-request', secret: secret, url: otplib.authenticator.keyuri(user.name, domainName, secret) }));
+ ws.send(JSON.stringify({ action: 'otpauth-request', secret: secret, url: otplib.generateURI({ issuer: domainName, label: user.name, secret: secret }) }));
}
break;
}
@@ -3762,8 +3802,15 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
var otplib = null;
try { otplib = require('otplib'); } catch (ex) { }
if (otplib == null) { break; }
- otplib.authenticator.options = { window: 2 }; // Set +/- 1 minute window
- if (otplib.authenticator.check(command.token, command.secret) === true) {
+ const verified = require('otplib').verifySync({
+ epochTolerance: 60,
+ token: command.token,
+ secret: command.secret,
+ guardrails: otplib.createGuardrails({
+ MIN_SECRET_BYTES: 10, // https://github.com/yeojz/otplib/issues/671#issuecomment-4368647105
+ })
+ });
+ if (verified.valid === true) {
// Token is valid, activate 2-step login on this account.
user.otpsecret = command.secret;
parent.db.SetUser(user);
@@ -4329,7 +4376,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
} else {
// This share is ok, remove extra data we don't need to send.
delete doc._id; delete doc.domain; delete doc.nodeid; delete doc.type; delete doc.xmeshid;
- if (doc.userid != user._id) { delete doc.url; } // If this is not the user who created this link, don't give the link.
+ if ((user.siteadmin !== SITERIGHT_ADMIN) && (doc.userid != user._id)) { delete doc.url; } // If this is not the user who created this link AND the user is not a site admin, don't give the link.
okDocs.push(doc);
}
}
@@ -4955,6 +5002,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
case 'meshToolInfo': {
if (typeof command.name != 'string') break;
var info = parent.parent.meshToolsBinaries[command.name];
+ if (info == null) break;
var responseCmd = { action: 'meshToolInfo', name: command.name, hash: info.hash, size: info.size, url: info.url };
if (parent.webCertificateHashs[domain.id] != null) { responseCmd.serverhash = Buffer.from(parent.webCertificateHashs[domain.id], 'binary').toString('hex'); }
try { ws.send(JSON.stringify(responseCmd)); } catch (ex) { }
@@ -5264,7 +5312,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
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,boardSerial,chassisSerial,chassisAssetTag,chassisManufacturer,productUuid,tpmversion,tpmmanufacturer,tpmmanufacturerversion,tpmisactivated,tpmisenabled,tpmisowned,totalMemory,agentOpenSSL,agentCommitDate,agentCommitHash,agentCompileTime,netIfCount,macs,addresses,lastConnectTime,lastConnectAddr\r\n';
+ output = 'id,name,rname,host,icon,ip,osdesc,groupname,av,update,firewall,bitlocker,avdetails,tags,lastbootuptime,cpu,osbuild,arch,kernelRelease,kernelBuild,biosDate,biosVendor,biosVersion,biosSerial,biosMode,boardName,boardVendor,boardVersion,boardSerial,chassisSerial,chassisAssetTag,chassisManufacturer,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];
@@ -5274,10 +5322,22 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
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 if (typeof n.lsc == 'object') {
+ output += ',' + csvClean(n.lsc.antiVirus ? n.lsc.antiVirus : '') + ',,' + csvClean(n.lsc.firewall ? n.lsc.firewall : '')
} else { output += ',,,'; }
- if (typeof n.volumes == 'object') {
+ // BitLocker info sits in the sysinfo node
+ // present when the user has device-details rights (otherwise sys was deleted above).
+ var bitlockervolumes = (nodeinfo.sys && nodeinfo.sys.hardware && nodeinfo.sys.hardware.windows) ? nodeinfo.sys.hardware.windows.volumes : null;
+ if (bitlockervolumes != null) {
+ // Map the Win32_EncryptableVolume.ConversionStatus codes to labels (legacy string values pass through).
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; } }
+ for (var a in bitlockervolumes) {
+ var bv = bitlockervolumes[a];
+ if ((bv.volumeStatus == null) && (bv.protectionStatus == null)) continue; // no BitLocker info for this volume
+ if (firstbitlocker) { firstbitlocker = false; } else { bitlockerdetails += '|'; }
+ var status = (bv.protectionStatus == 2) ? 'Locked' : ((typeof bv.volumeStatus === 'string') ? bv.volumeStatus : (conversionStatus[bv.volumeStatus] || 'Unknown'));
+ bitlockerdetails += a + '/' + status;
+ }
output += ',' + csvClean(bitlockerdetails);
} else {
output += ',';
@@ -5309,6 +5369,10 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
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.windows.osinfo && nodeinfo.sys.hardware.windows.osinfo.OSArchitecture) { output += csvClean(nodeinfo.sys.hardware.windows.osinfo.OSArchitecture); }
+ output += ',';
+ output += ',';
+ 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); }
@@ -5364,6 +5428,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
output += ',';
output += ',';
output += ',';
+ output += ',';
+ output += ',';
+ output += ',';
if (nodeinfo.sys.hardware.mobile && (nodeinfo.sys.hardware.mobile.bootloader)) { output += csvClean(nodeinfo.sys.hardware.mobile.bootloader); }
output += ',';
output += ',';
@@ -5390,6 +5457,12 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
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.arch) { output += csvClean(nodeinfo.sys.hardware.linux.arch); }
+ output += ',';
+ if (nodeinfo.sys.hardware.linux && nodeinfo.sys.hardware.linux.kernel_release) { output += csvClean(nodeinfo.sys.hardware.linux.kernel_release); }
+ output += ',';
+ if (nodeinfo.sys.hardware.linux && nodeinfo.sys.hardware.linux.kernel_build) { output += csvClean(nodeinfo.sys.hardware.linux.kernel_build); }
+ 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); }
@@ -5442,7 +5515,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
}
}
} else {
- output += ',,,,,,,,,,,,,,,,,,,,,,';
+ output += ',,,,,,,,,,,,,,,,,,,,,,,,,';
}
// Agent information
@@ -5501,18 +5574,30 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
}
} catch (ex) { console.log(ex); }
} else {
- // Create the JSON file
-
- // Add the device group name to each device
+ // Create the JSON file, convert codes into description
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; }
}
+ // add a decoded per-volume status summary and remove the raw recovery keys
+ var bvols = nodeinfo.sys?.hardware?.windows?.volumes;
+ if (bvols) {
+ for (var a in bvols) {
+ var bv = bvols[a];
+ if (bv.dType) (bv.dType = driveType[bv.dType] ? driveType[bv.dType] : 'Unknown');
+ delete bv.recoveryPassword; // never include raw recovery keys in a report
+ if ((bv.volumeStatus == null) && (bv.protectionStatus == null)) continue;
+ if (bv.volumeStatus != null) { bv.volumeStatus = (typeof bv.volumeStatus === 'string') ? bv.volumeStatus : (conversionStatus[bv.volumeStatus] || 'Unknown'); }
+ if (bv.protectionStatus != null) { bv.protectionStatus = (typeof bv.protectionStatus === 'boolean') ? (bv.protectionStatus ? 'On' : 'Off') : ((typeof bv.protectionStatus === 'string') ? bv.protectionStatus : (protectionStatus[bv.protectionStatus] || 'Unknown')); }
+ if (bv.encryptionMethod != null) { bv.encryptionMethod = (typeof bv.encryptionMethod === 'string') ? bv.encryptionMethod : (encMethod[bv.encryptionMethod] || 'Unknown'); }
+ }
+ delete nodeinfo.sys.hardware.windows.bitlocker; // drop the recovery-key map from the export
+ }
}
- output = JSON.stringify(results);
+ output = JSON.stringify(results, null, 1);
}
try { ws.send(JSON.stringify({ action: 'getDeviceDetails', data: output, type: type })); } catch (ex) { }
});
@@ -5718,6 +5803,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
'serverconsole': serverCommandServerConsole,
'servererrors': serverCommandServerErrors,
'serverconfig': serverCommandServerConfig,
+ 'serverbackuppassword': serverCommandServerBackuppassword,
'serverstats': serverCommandServerStats,
'servertimelinestats': serverCommandServerTimelineStats,
'serverupdate': serverCommandServerUpdate,
@@ -6523,6 +6609,15 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
parent.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
}
+ function serverCommandServerBackuppassword(command) {
+ if (command.check) {
+ //check for existing config.json password for requestercheckbox
+ obj.send({ action: 'backuprestore', data: (Object.hasOwn(parent.parent.config.settings.autobackup, 'zippassword')), dialog: command.dialog });
+ } else {
+ parent.parent.config.settings.autobackup.zippasswordrequest = command.override ? parent.parent.config.settings.autobackup.zippassword : command.password;
+ }
+ }
+
function serverCommandEmailUser(command) {
var errMsg = null, emailuser = null;
if (domain.mailserver == null) { errMsg = 'Email server not enabled'; }
@@ -6621,9 +6716,39 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
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 this is not an admin user, don't send any BitLocker recovery info
+ if (rights != MESHRIGHT_ADMIN && doc?.hardware?.windows) {
+ if (doc.hardware.windows?.volumes) {
+ for (var i in doc.hardware.windows.volumes) {
+ delete doc.hardware.windows.volumes[i].recoveryPassword; // previous single bitlocker schema
+ }
+ }
+ delete doc.hardware.windows.bitlocker; // new bitlocker driveletter independent cache
+ }
+
+ // deviceinfo for meshctrl.js, replace raw codes with readable strings and add recoveryPassword if possible
+ if (command.responseid && command.responseid == 'meshctrl') {
+ if (doc.hardware?.windows?.volumes) {
+ for (const [drive, volumeInfo] of Object.entries(doc.hardware.windows.volumes)) {
+ if (typeof volumeInfo.dType == 'number') { volumeInfo.dType = driveType[volumeInfo.dType] ?? 'Unknown'; }
+ if (typeof volumeInfo.volumeStatus == 'number') {
+ if (volumeInfo.volumeStatus == 0) {volumeInfo.volumeStatus = conversionStatus[0]; continue; }
+ // only do volumes with a encryption status
+ volumeInfo.volumeStatus = conversionStatus[volumeInfo.volumeStatus] ?? 'Unknown';
+ if (volumeInfo.protectionStatus && typeof volumeInfo.protectionStatus == 'number') { volumeInfo.protectionStatus = protectionStatus[volumeInfo.protectionStatus] ?? 'Unknown'; }
+ if (volumeInfo.encryptionMethod) { volumeInfo.encryptionMethod = encMethod[volumeInfo.encryptionMethod] ?? 'Unknown'; }
+ if ((rights == MESHRIGHT_ADMIN) && volumeInfo.identifier && !(volumeInfo.hasOwnProperty('recoveryPassword')) && doc.hardware.windows?.bitlocker?.[volumeInfo.identifier])
+ { volumeInfo.recoveryPassword = doc.hardware.windows.bitlocker[volumeInfo.identifier].rp; }
+ }
+ }
+ }
+ var b;
+ if ((rights == MESHRIGHT_ADMIN) && (b = doc.hardware?.windows?.bitlocker)) {
+ for (const robj of Object.values(b)) {
+ robj.recoveryPassword = robj.rp; delete robj.rp;
+ robj.lastSeen = new Date(robj.t); delete robj.t;
+ }
+ }
}
if (command.nodeinfo === true) {
@@ -7004,12 +7129,16 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
}
function serverCommandServerVersion(command) {
- // Do not allow this command when logged in using a login token
- if (req.session.loginToken != null) return;
-
// Check the server version
- if (userHasSiteUpdate() && domainHasMyServerUpgrade())
+ if (command.responseid != null) {
+ if (userHasSiteUpdate() && domainHasMyServerUpgrade()) {
+ parent.parent.getServerTags(function (tags, err) { obj.send({ action: 'serverversion', responseid: command.responseid, result: 'OK', tags: tags }); });
+ }
+ else { obj.send({ action: 'serverversion', responseid: command.responseid, result: 'Permission denied' }); }
+ }
+ else if (userHasSiteUpdate() && domainHasMyServerUpgrade()) {
parent.parent.getServerTags(function (tags, err) { obj.send({ action: 'serverversion', tags: tags }); });
+ }
}
function serverCommandSetClip(command) {
diff --git a/migrate-volume-info.js b/migrate-volume-info.js
new file mode 100644
index 0000000000..1679d12fd1
--- /dev/null
+++ b/migrate-volume-info.js
@@ -0,0 +1,67 @@
+/**
+* @description One-shot migration: convert pre-key-list BitLocker sysinfo documents to the
+* identifier-keyed hardware.windows.bitlocker map and convert DriveType
+*
+* Run with: node meshcentral --migrateVolumeInfo
+*
+* Old sysinfo documents stored BitLocker recovery keys per-volume (recoveryPassword).
+* The current schema keeps them in hardware.windows.bitlocker[identifier] = { rp, t }.
+* Devices whose agents have not reconnected still hold the old shape.
+* This moves the existing keys into the new map.
+* Old DriveTypes (cdrom, removable) are converted to the raw windows code
+*
+* Notes:
+* - Idempotent: any document that already has hardware.windows.bitlocker is left untouched, so it is
+* safe to run repeatedly and a no-op once every device has re-sent sysinfo.
+* - Volume status strings written by the old agent are converted back to their raw codes.
+* Any unrecognized string is left untouched and still renders via the views' typeof==='string' branch.
+* - The timestamp (t) is taken from the document's own 'time' field (last sysinfo). With the default
+* bitlockerKeyRetentionDays=0, migrated keys persist for non-reconnecting devices and are rebuilt
+* from the live scan once a device reconnects.
+*
+* @license Apache-2.0
+*/
+'use strict';
+
+// db: the MeshCentral database object (obj.db). callback(err, { scanned, migrated, keysMoved }).
+module.exports.migrateVolumeInfo = function (db, callback) {
+ // Legacy string (old agent stringified values) -> raw code, to restore the schema the current views expect.
+ var conversionStatusCodes = { 'FullyDecrypted': 0, 'FullyEncrypted': 1, 'EncryptionInProgress': 2, 'DecryptionInProgress': 3, 'EncryptionPaused': 4, 'DecryptionPaused': 5 };
+ db.GetAllType('sysinfo', function (err, docs) {
+ if (err != null) { if (callback != null) { callback(err); } return; }
+ var scanned = 0, migrated = 0, keysMoved = 0, writesPending = 0, listed = false;
+ var done = function () { if (listed && (writesPending == 0) && (callback != null)) { callback(null, { scanned: scanned, migrated: migrated, keysMoved: keysMoved }); } };
+ for (var d in docs) {
+ var doc = docs[d];
+ scanned++;
+ var windows = (doc.hardware != null) ? doc.hardware.windows : null;
+ // Skip non-Windows documents and anything already on the new schema.
+ if ((windows == null) || (windows.volumes == null) || (windows.bitlocker != null)) { continue; }
+
+ var keys = {};
+ for (var letter in windows.volumes) {
+ var v = windows.volumes[letter];
+ // Live key read at the time of the last sysinfo, for the current protector.
+ if (v.identifier && v.recoveryPassword) { keys[v.identifier] = { rp: v.recoveryPassword, t: (doc.time || 0) }; }
+ // Reconstruct the raw Win32_LogicalDisk.DriveType code from the legacy boolean flags
+ if (v.dType == null) {
+ if (v.removable) { v.dType = 2; } // Removable Disk
+ else if (v.cdrom) { v.dType = 5; } // Compact Disc / CD-ROM
+ }
+ delete v.removable; delete v.cdrom;
+ // Restore raw status/method codes from the legacy stringified values. Unrecognized strings are left as is, handled in the views
+ if ((typeof v.volumeStatus === 'string') && (conversionStatusCodes[v.volumeStatus] != null)) { v.volumeStatus = conversionStatusCodes[v.volumeStatus]; }
+ // Old documents stored protectionStatus as a boolean (true = On; Off/Locked were dropped). Restore the
+ // raw code: true -> 1 (On). Locked (2) was not retained by the old agent, so it cannot be recovered.
+ if (typeof v.protectionStatus === 'boolean') { if (v.protectionStatus) { v.protectionStatus = 1; } else { delete v.protectionStatus; } }
+ }
+ windows.bitlocker = keys;
+ migrated++;
+ keysMoved += Object.keys(keys).length;
+ writesPending++;
+ db.Set(doc, function () { writesPending--; done(); });
+ }
+ listed = true;
+ done(); // handle the case where nothing needed writing
+ });
+};
diff --git a/mpsserver.js b/mpsserver.js
index b7f440e2f7..084643f8ec 100644
--- a/mpsserver.js
+++ b/mpsserver.js
@@ -439,7 +439,9 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) {
socket.tag.first = false;
// Setup this node with certificate authentication
- if (socket.tag.clientCert && socket.tag.clientCert.subject && socket.tag.clientCert.subject.O && socket.tag.clientCert.subject.O.length == 64) {
+ // Note: getPeerCertificate() returns subject.O as an array when the certificate
+ // carries repeated O attributes; require a string so the .split() below cannot throw.
+ if (socket.tag.clientCert && socket.tag.clientCert.subject && (typeof socket.tag.clientCert.subject.O == 'string') && socket.tag.clientCert.subject.O.length == 64) {
// This is a node where the MeshID is indicated within the CIRA certificate
var domainid = '', meshid;
var xx = socket.tag.clientCert.subject.O.split('/');
@@ -679,6 +681,11 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) {
}
}
+ // A real Intel AMT device always sends PROTOCOLVERSION before USERAUTH. Without it,
+ // socket.tag.SystemId is undefined and SystemId.split() below (including inside the
+ // async DNS/DB callbacks) throws uncaught and terminates the server process.
+ if (typeof socket.tag.SystemId != 'string') { SendUserAuthFail(socket); return -1; }
+
// If this is a agent-less mesh, use the device guid 3 times as ID.
if (initialMesh.mtype == 1) {
// Intel AMT GUID (socket.tag.SystemId) will be used as NodeID
@@ -1111,7 +1118,7 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) {
if (len < 9) return 0;
var RecipientChannel = common.ReadInt(data, 1);
var LengthOfData = common.ReadInt(data, 5);
- if (SourceLen > 1048576) return -1;
+ if (LengthOfData > 1048576) return -1;
if (len < (9 + LengthOfData)) return 0;
parent.debug('mpscmddata', '--> CHANNEL_DATA', RecipientChannel, LengthOfData);
var cirachannel = socket.tag.channels[RecipientChannel];
diff --git a/node11.bat b/node11.bat
deleted file mode 100644
index 79c6956db8..0000000000
--- a/node11.bat
+++ /dev/null
@@ -1 +0,0 @@
-@%LOCALAPPDATA%\..\Roaming\nvm\v11.10.1\node meshcentral %1 %2 %3 %4 %5 %6 %7 %8 %9
\ No newline at end of file
diff --git a/node15.bat b/node15.bat
deleted file mode 100644
index 22e5a4e452..0000000000
--- a/node15.bat
+++ /dev/null
@@ -1 +0,0 @@
-@%LOCALAPPDATA%\..\Roaming\nvm\v15.0.1\node meshcentral %1 %2 %3 %4 %5 %6 %7 %8 %9
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 82778fbc1d..c11a8abae6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,38 +1,38 @@
{
"name": "meshcentral",
- "version": "1.1.57",
+ "version": "1.2.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "meshcentral",
- "version": "1.1.57",
+ "version": "1.2.3",
"license": "Apache-2.0",
"dependencies": {
"@seald-io/nedb": "4.1.2",
+ "@zip.js/zip.js": "2.8.26",
"archiver": "7.0.1",
- "body-parser": "1.20.4",
"cbor": "5.2.0",
"compression": "1.8.1",
"cookie-session": "2.1.1",
- "express": "4.22.1",
+ "express": "4.22.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.2",
- "otplib": "12.0.1",
+ "multiparty": "4.3.0",
+ "node-forge": "1.4.0",
+ "otplib": "13.4.1",
"ua-client-hints-js": "0.1.2",
"ua-parser-js": "1.0.40",
- "ws": "8.18.3",
+ "ws": "8.21.1",
"yauzl": "2.10.0"
},
"bin": {
"meshcentral": "bin/meshcentral"
},
"engines": {
- "node": ">=16.0.0"
+ "node": ">=20.0.0"
}
},
"node_modules/@isaacs/cliui": {
@@ -52,54 +52,72 @@
"node": ">=12"
}
},
+ "node_modules/@noble/hashes": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz",
+ "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/@otplib/core": {
- "version": "12.0.1",
- "resolved": "https://registry.npmjs.org/@otplib/core/-/core-12.0.1.tgz",
- "integrity": "sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==",
+ "version": "13.4.1",
+ "resolved": "https://registry.npmjs.org/@otplib/core/-/core-13.4.1.tgz",
+ "integrity": "sha512-KIXgK1hNtWJEBMTastbe1bpmuais+3f+ATeO8TkMs2rNkfGO1FbQy8+/UWVEu3TR/iTJerU0idkPudaPmLP2BA==",
"license": "MIT"
},
- "node_modules/@otplib/plugin-crypto": {
- "version": "12.0.1",
- "resolved": "https://registry.npmjs.org/@otplib/plugin-crypto/-/plugin-crypto-12.0.1.tgz",
- "integrity": "sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==",
- "deprecated": "Please upgrade to v13 of otplib. Refer to otplib docs for migration paths",
+ "node_modules/@otplib/hotp": {
+ "version": "13.4.1",
+ "resolved": "https://registry.npmjs.org/@otplib/hotp/-/hotp-13.4.1.tgz",
+ "integrity": "sha512-g9q04SwpG5ZtMnVkUcgcoAlwCH4YLROZN1qhyBwgkBzqYYVSYhpP6gSGaxGHwePLt1c+e6NqDlgIZN+e1/XPuA==",
+ "license": "MIT",
+ "dependencies": {
+ "@otplib/core": "13.4.1",
+ "@otplib/uri": "13.4.1"
+ }
+ },
+ "node_modules/@otplib/plugin-base32-scure": {
+ "version": "13.4.1",
+ "resolved": "https://registry.npmjs.org/@otplib/plugin-base32-scure/-/plugin-base32-scure-13.4.1.tgz",
+ "integrity": "sha512-Fs/r5qisC05SRhT6xWXaypB6PVC0vgWf6zztmi0J5RnQ09OJiPDWCJFH6cDm6ANsrdvB9di7X+Jb7L13BoEbUA==",
"license": "MIT",
"dependencies": {
- "@otplib/core": "^12.0.1"
+ "@otplib/core": "13.4.1",
+ "@scure/base": "^2.2.0"
}
},
- "node_modules/@otplib/plugin-thirty-two": {
- "version": "12.0.1",
- "resolved": "https://registry.npmjs.org/@otplib/plugin-thirty-two/-/plugin-thirty-two-12.0.1.tgz",
- "integrity": "sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==",
- "deprecated": "Please upgrade to v13 of otplib. Refer to otplib docs for migration paths",
+ "node_modules/@otplib/plugin-crypto-noble": {
+ "version": "13.4.1",
+ "resolved": "https://registry.npmjs.org/@otplib/plugin-crypto-noble/-/plugin-crypto-noble-13.4.1.tgz",
+ "integrity": "sha512-PJfVW8/1hdS6CfxLheKPZSLTwDq4TijZbN4yRjxlv0ODdzmxpM+wGwWr1JXMdy0xJPxLziydQD5gdVqrR4/gAg==",
"license": "MIT",
"dependencies": {
- "@otplib/core": "^12.0.1",
- "thirty-two": "^1.0.2"
+ "@noble/hashes": "^2.2.0",
+ "@otplib/core": "13.4.1"
}
},
- "node_modules/@otplib/preset-default": {
- "version": "12.0.1",
- "resolved": "https://registry.npmjs.org/@otplib/preset-default/-/preset-default-12.0.1.tgz",
- "integrity": "sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==",
- "deprecated": "Please upgrade to v13 of otplib. Refer to otplib docs for migration paths",
+ "node_modules/@otplib/totp": {
+ "version": "13.4.1",
+ "resolved": "https://registry.npmjs.org/@otplib/totp/-/totp-13.4.1.tgz",
+ "integrity": "sha512-QOkBVPrf6AM4qZaReZPSk9/I8ATVdZpIISJz115MqeVtcrbcr5llPZ0J7804tpnjnp1vCRkI5Qjd47HhgVteBQ==",
"license": "MIT",
"dependencies": {
- "@otplib/core": "^12.0.1",
- "@otplib/plugin-crypto": "^12.0.1",
- "@otplib/plugin-thirty-two": "^12.0.1"
+ "@otplib/core": "13.4.1",
+ "@otplib/hotp": "13.4.1",
+ "@otplib/uri": "13.4.1"
}
},
- "node_modules/@otplib/preset-v11": {
- "version": "12.0.1",
- "resolved": "https://registry.npmjs.org/@otplib/preset-v11/-/preset-v11-12.0.1.tgz",
- "integrity": "sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==",
+ "node_modules/@otplib/uri": {
+ "version": "13.4.1",
+ "resolved": "https://registry.npmjs.org/@otplib/uri/-/uri-13.4.1.tgz",
+ "integrity": "sha512-xaIm7bvICMhoB2rZIR5luiaMdssWR5nY5nXnR1fdezUgZuEO58D6zrGzLp7pQuBmlpmL0HagnscDQFoskp9yiA==",
"license": "MIT",
"dependencies": {
- "@otplib/core": "^12.0.1",
- "@otplib/plugin-crypto": "^12.0.1",
- "@otplib/plugin-thirty-two": "^12.0.1"
+ "@otplib/core": "13.4.1"
}
},
"node_modules/@pkgjs/parseargs": {
@@ -112,6 +130,15 @@
"node": ">=14"
}
},
+ "node_modules/@scure/base": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.2.0.tgz",
+ "integrity": "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"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",
@@ -128,6 +155,17 @@
"util": "^0.12.5"
}
},
+ "node_modules/@zip.js/zip.js": {
+ "version": "2.8.26",
+ "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.26.tgz",
+ "integrity": "sha512-RQ4h9F6DOiHxpdocUDrOl6xBM+yOtz+LkUol47AVWcfebGBDpZ7w7Xvz9PS24JgXvLGiXXzSAfdCdVy1tPlaFA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "bun": ">=0.7.0",
+ "deno": ">=1.0.0",
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
@@ -250,9 +288,9 @@
}
},
"node_modules/b4a": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz",
- "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==",
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz",
+ "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==",
"license": "Apache-2.0",
"peerDependencies": {
"react-native-b4a": "*"
@@ -270,9 +308,9 @@
"license": "MIT"
},
"node_modules/bare-events": {
- "version": "2.8.2",
- "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
- "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz",
+ "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==",
"license": "Apache-2.0",
"peerDependencies": {
"bare-abort-controller": "*"
@@ -284,9 +322,9 @@
}
},
"node_modules/bare-fs": {
- "version": "4.5.5",
- "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.5.tgz",
- "integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==",
+ "version": "4.7.4",
+ "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.4.tgz",
+ "integrity": "sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.5.4",
@@ -307,38 +345,31 @@
}
}
},
- "node_modules/bare-os": {
- "version": "3.7.1",
- "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.7.1.tgz",
- "integrity": "sha512-ebvMaS5BgZKmJlvuWh14dg9rbUI84QeV3WlWn6Ph6lFI8jJoh7ADtVTyD2c93euwbe+zgi0DVrl4YmqXeM9aIA==",
- "license": "Apache-2.0",
- "engines": {
- "bare": ">=1.14.0"
- }
- },
"node_modules/bare-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
- "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
- "license": "Apache-2.0",
- "dependencies": {
- "bare-os": "^3.0.1"
- }
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz",
+ "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==",
+ "license": "Apache-2.0"
},
"node_modules/bare-stream": {
- "version": "2.8.0",
- "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.0.tgz",
- "integrity": "sha512-reUN0M2sHRqCdG4lUK3Fw8w98eeUIZHL5c3H7Mbhk2yVBL+oofgaIp0ieLfD5QXwPCypBpmEEKU2WZKzbAk8GA==",
+ "version": "2.13.3",
+ "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz",
+ "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==",
"license": "Apache-2.0",
"dependencies": {
- "streamx": "^2.21.0",
+ "b4a": "^1.8.1",
+ "streamx": "^2.25.0",
"teex": "^1.0.1"
},
"peerDependencies": {
+ "bare-abort-controller": "*",
"bare-buffer": "*",
"bare-events": "*"
},
"peerDependenciesMeta": {
+ "bare-abort-controller": {
+ "optional": true
+ },
"bare-buffer": {
"optional": true
},
@@ -348,9 +379,9 @@
}
},
"node_modules/bare-url": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
- "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
+ "version": "2.4.5",
+ "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.5.tgz",
+ "integrity": "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==",
"license": "Apache-2.0",
"dependencies": {
"bare-path": "^3.0.0"
@@ -386,9 +417,9 @@
}
},
"node_modules/body-parser": {
- "version": "1.20.4",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
- "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
+ "version": "1.20.6",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz",
+ "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
@@ -399,7 +430,7 @@
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"on-finished": "~2.4.1",
- "qs": "~6.14.0",
+ "qs": "~6.15.1",
"raw-body": "~2.5.3",
"type-is": "~1.6.18",
"unpipe": "~1.0.0"
@@ -410,9 +441,9 @@
}
},
"node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
+ "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
@@ -461,14 +492,14 @@
}
},
"node_modules/call-bind": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
- "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
+ "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==",
"license": "MIT",
"dependencies": {
- "call-bind-apply-helpers": "^1.0.0",
- "es-define-property": "^1.0.0",
- "get-intrinsic": "^1.2.4",
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "get-intrinsic": "^1.3.0",
"set-function-length": "^1.2.2"
},
"engines": {
@@ -813,9 +844,9 @@
}
},
"node_modules/es-object-atoms": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
- "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@@ -867,14 +898,14 @@
}
},
"node_modules/express": {
- "version": "4.22.1",
- "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
- "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
+ "version": "4.22.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
+ "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
- "body-parser": "~1.20.3",
+ "body-parser": "~1.20.5",
"content-disposition": "~0.5.4",
"content-type": "~1.0.4",
"cookie": "~0.7.1",
@@ -893,7 +924,7 @@
"parseurl": "~1.3.3",
"path-to-regexp": "~0.1.12",
"proxy-addr": "~2.0.7",
- "qs": "~6.14.0",
+ "qs": "~6.15.1",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "~0.19.0",
@@ -942,9 +973,9 @@
}
},
"node_modules/express-ws/node_modules/ws": {
- "version": "7.5.10",
- "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
- "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
+ "version": "7.5.13",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz",
+ "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==",
"license": "MIT",
"engines": {
"node": ">=8.3.0"
@@ -1139,9 +1170,9 @@
"license": "ISC"
},
"node_modules/handlebars": {
- "version": "4.7.8",
- "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
- "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
+ "version": "4.7.9",
+ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz",
+ "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==",
"license": "MIT",
"dependencies": {
"minimist": "^1.2.5",
@@ -1199,9 +1230,9 @@
}
},
"node_modules/hasown": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -1490,9 +1521,9 @@
}
},
"node_modules/lodash": {
- "version": "4.17.23",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
- "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/lru-cache": {
@@ -1619,12 +1650,12 @@
"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==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.3.0.tgz",
+ "integrity": "sha512-LD3YDFI9KrDoOGHsPM+hNraPDQQDPLe8Un/kvJfsZCsHKriA4mphg6Ctc2Cuup/59DtHMdAPm6ICXlUmhwTiug==",
"license": "MIT",
"dependencies": {
- "http-errors": "~1.8.1",
+ "http-errors": "2.0.0",
"safe-buffer": "5.2.1",
"uid-safe": "2.1.5"
},
@@ -1632,38 +1663,29 @@
"node": ">= 0.10"
}
},
- "node_modules/multiparty/node_modules/depd": {
- "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"
- }
- },
"node_modules/multiparty/node_modules/http-errors": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
- "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
+ "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": "~1.1.2",
+ "depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
- "statuses": ">= 1.5.0 < 2",
+ "statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
- "node": ">= 0.6"
+ "node": ">= 0.8"
}
},
"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==",
+ "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.6"
+ "node": ">= 0.8"
}
},
"node_modules/negotiator": {
@@ -1682,9 +1704,9 @@
"license": "MIT"
},
"node_modules/node-forge": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.2.tgz",
- "integrity": "sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz",
+ "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==",
"license": "(BSD-3-Clause OR GPL-2.0)",
"engines": {
"node": ">= 6.13.0"
@@ -1742,14 +1764,17 @@
}
},
"node_modules/otplib": {
- "version": "12.0.1",
- "resolved": "https://registry.npmjs.org/otplib/-/otplib-12.0.1.tgz",
- "integrity": "sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==",
+ "version": "13.4.1",
+ "resolved": "https://registry.npmjs.org/otplib/-/otplib-13.4.1.tgz",
+ "integrity": "sha512-o5CxfDw6bh7hoDv0NUUIcc0RqzJ9ipfUrzeKheKJ+vs4rXZnDlA9n4a/7R1cDjpmLjKLix4BgNVRmoDkm5rLSQ==",
"license": "MIT",
"dependencies": {
- "@otplib/core": "^12.0.1",
- "@otplib/preset-default": "^12.0.1",
- "@otplib/preset-v11": "^12.0.1"
+ "@otplib/core": "13.4.1",
+ "@otplib/hotp": "13.4.1",
+ "@otplib/plugin-base32-scure": "13.4.1",
+ "@otplib/plugin-crypto-noble": "13.4.1",
+ "@otplib/totp": "13.4.1",
+ "@otplib/uri": "13.4.1"
}
},
"node_modules/package-json-from-dist": {
@@ -1793,9 +1818,9 @@
}
},
"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==",
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
"license": "MIT"
},
"node_modules/pend": {
@@ -1842,12 +1867,13 @@
}
},
"node_modules/qs": {
- "version": "6.14.2",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
- "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
+ "version": "6.15.3",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
+ "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
"license": "BSD-3-Clause",
"dependencies": {
- "side-channel": "^1.1.0"
+ "es-define-property": "^1.0.1",
+ "side-channel": "^1.1.1"
},
"engines": {
"node": ">=0.6"
@@ -2059,14 +2085,14 @@
}
},
"node_modules/side-channel": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
- "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
- "object-inspect": "^1.13.3",
- "side-channel-list": "^1.0.0",
+ "object-inspect": "^1.13.4",
+ "side-channel-list": "^1.0.1",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
@@ -2078,13 +2104,13 @@
}
},
"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==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
- "object-inspect": "^1.13.3"
+ "object-inspect": "^1.13.4"
},
"engines": {
"node": ">= 0.4"
@@ -2161,9 +2187,9 @@
}
},
"node_modules/streamx": {
- "version": "2.23.0",
- "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
- "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
+ "version": "2.28.0",
+ "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz",
+ "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==",
"license": "MIT",
"dependencies": {
"events-universal": "^1.0.0",
@@ -2277,9 +2303,9 @@
}
},
"node_modules/tar-stream": {
- "version": "3.1.8",
- "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz",
- "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==",
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz",
+ "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==",
"license": "MIT",
"dependencies": {
"b4a": "^1.6.4",
@@ -2306,14 +2332,6 @@
"b4a": "^1.6.4"
}
},
- "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==",
- "engines": {
- "node": ">=0.2.6"
- }
- },
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -2478,13 +2496,13 @@
}
},
"node_modules/which-typed-array": {
- "version": "1.1.20",
- "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
- "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==",
+ "version": "1.1.22",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz",
+ "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==",
"license": "MIT",
"dependencies": {
"available-typed-arrays": "^1.0.7",
- "call-bind": "^1.0.8",
+ "call-bind": "^1.0.9",
"call-bound": "^1.0.4",
"for-each": "^0.3.5",
"get-proto": "^1.0.1",
@@ -2596,9 +2614,9 @@
}
},
"node_modules/ws": {
- "version": "8.18.3",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
- "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "version": "8.21.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
+ "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
diff --git a/package.json b/package.json
index c3b6c25746..da286aa236 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "meshcentral",
- "version": "1.1.57",
+ "version": "1.2.4",
"keywords": [
"Remote Device Management",
"Remote Device Monitoring",
@@ -38,26 +38,26 @@
],
"dependencies": {
"@seald-io/nedb": "4.1.2",
+ "@zip.js/zip.js": "2.8.26",
"archiver": "7.0.1",
- "body-parser": "1.20.4",
"cbor": "5.2.0",
"compression": "1.8.1",
"cookie-session": "2.1.1",
- "express": "4.22.1",
+ "express": "4.22.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.2",
- "otplib": "12.0.1",
+ "multiparty": "4.3.0",
+ "node-forge": "1.4.0",
+ "otplib": "13.4.1",
"ua-client-hints-js": "0.1.2",
"ua-parser-js": "1.0.40",
- "ws": "8.18.3",
+ "ws": "8.21.1",
"yauzl": "2.10.0"
},
"engines": {
- "node": ">=16.0.0"
+ "node": ">=20.0.0"
},
"repository": {
"type": "git",
diff --git a/pluginHandler.js b/pluginHandler.js
index 3b7ba2051c..fdc69cd453 100644
--- a/pluginHandler.js
+++ b/pluginHandler.js
@@ -251,9 +251,10 @@ module.exports.pluginHandler = function (parent) {
return new Promise(function (resolve, reject) {
var http = (configUrl.indexOf('https://') >= 0) ? require('https') : require('http');
if (configUrl.indexOf('://') === -1) reject("Unable to fetch the config: Bad URL (" + configUrl + ")");
- var options = require('url').parse(configUrl);
+ const getme = new URL(configUrl);
+ var options = { protocol: getme.protocol, hostname: getme.hostname, port: getme.port || undefined, path: getme.pathname + getme.search };
if (typeof parent.config.settings.plugins.proxy == 'string' || process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']) { // Proxy support
- options.agent = new (require('https-proxy-agent').HttpsProxyAgent)(require('url').parse(parent.config.settings.plugins.proxy) || process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']);
+ options.agent = new (require('https-proxy-agent').HttpsProxyAgent)(new URL(parent.config.settings.plugins.proxy) || process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']);
}
http.get(options, function (res) {
var configStr = '';
@@ -422,8 +423,7 @@ module.exports.pluginHandler = function (parent) {
var dl_url = plugin.downloadUrl;
if (version_only != null && version_only != false) dl_url = version_only.url;
if (force_url != null) dl_url = force_url;
- var url = require('url');
- var q = url.parse(dl_url, true);
+ var q = new URL(dl_url);
var http = (q.protocol == "http:") ? require('http') : require('https');
var opts = {
path: q.pathname,
@@ -436,7 +436,7 @@ module.exports.pluginHandler = function (parent) {
method: 'GET'
};
if (typeof parent.config.settings.plugins.proxy == 'string' || process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']) { // Proxy support
- opts.agent = new (require('https-proxy-agent').HttpsProxyAgent)(require('url').parse(parent.config.settings.plugins.proxy) || process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']);
+ opts.agent = new (require('https-proxy-agent').HttpsProxyAgent)(new URL(parent.config.settings.plugins.proxy) || process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']);
}
var done = false;
var request = http.get(opts, function (response) {
@@ -522,8 +522,7 @@ module.exports.pluginHandler = function (parent) {
parent.db.getPlugin(id, function (err, docs) {
var plugin = docs[0];
if (plugin.versionHistoryUrl == null) reject("No version history available for this plugin.");
- var url = require('url');
- var q = url.parse(plugin.versionHistoryUrl, true);
+ var q = new URL(plugin.versionHistoryUrl);
var http = (q.protocol == 'http:') ? require('http') : require('https');
var opts = {
path: q.pathname,
@@ -535,7 +534,7 @@ module.exports.pluginHandler = function (parent) {
}
};
if (typeof parent.config.settings.plugins.proxy == 'string' || process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']) { // Proxy support
- options.agent = new (require('https-proxy-agent').HttpsProxyAgent)(require('url').parse(parent.config.settings.plugins.proxy) || process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']);
+ options.agent = new (require('https-proxy-agent').HttpsProxyAgent)(new URL(parent.config.settings.plugins.proxy) || process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']);
}
http.get(opts, function (res) {
var versStr = '';
diff --git a/public/commander-de.htm b/public/commander-de.htm
index 91a57400c3..13fdbcab22 100644
--- a/public/commander-de.htm
+++ b/public/commander-de.htm
@@ -1,8 +1,8 @@
-Getrennt
Wird geladen...
Systemstatus
Hardware-Informationen
Ereignisprotokoll
Netzwerkeinstellungen
Benutzerkonten Serial-over-LAN-Terminal
Der Intel® AMT-Umleitungsport oder die Serial-over-LAN-Funktion sind deaktiviertKlicken Sie hier, um es zu aktivieren.
Der Remotecomputer ist nicht eingeschaltet. Klicken Sie hier, um einen Energiebefehl auszugeben.
Remotedesktop
Der Intel® AMT-Umleitungsport oder die KVM-Funktion sind deaktiviertKlicken Sie hier, um es zu aktivieren.
Der Remotecomputer ist nicht eingeschaltet. Klicken Sie hier, um einen Energiebefehl auszugeben.
Audit-Log
Sicherheitseinstellungen
Interneteinstellungen
Systemverteidigung
Agentenpräsenz
Lager
Event-Abonnements
Weckalarme Lokal Netzwerk Irgendein Genehmigung
Erteilte Berechtigungen
* Mindestens 8 Zeichen mit Groß-, Klein-, 0-9 und einem von! @ # $% ^ & * () + -
Warnung: Einige Stromversorgungsaktionen können zu Datenverlust führen und die Desktop-, Terminal- oder Festplattenumleitungssitzungen trennen.
Primäre Anzeige Sekundäranzeige Dritte Anzeige Zustimmungsanzeige
Don't set Behindert Automatic aktiviert Downscaling
Software KVM 50% 40% 30% 20% 10% 5% 1% Qualität
100% 87,5% 75% 62,5% 50% 37,5% 25% 12,5% Skalierung
Umleitungsport KVM-Remotedesktop IDE-Umleitung Serial-over-LAN
Nicht benötigt Nur für KVM erforderlich Immer erforderlich
WPA3 SAE WPA2 PSK Wpa psk Authentifizierung
CCMP-AES TKIP-RC4 WEP Keiner Verschlüsselung
Dadurch wird der gesamte Status von Intel® AMT für diesen Computer in einer Datei gespeichert. Passwörter werden nicht gespeichert, es können jedoch einige vertrauliche Daten enthalten sein.
Behindert ICMP-Antwort RMCP-Antwort ICMP & RMCP Antwort
Behindert Deaktiviert, DHCP-Update aktiviert Dynamischer DNS-Client
Das Standardintervall beträgt 1440 Minuten, die Standard-TTL beträgt 900 Sekunden.
Einschalten Zurücksetzen Aus- und Wiedereinschalten Stromausfall OS Wake aus dem Standby Energiesparen des Betriebssystems Legen Sie die Startoptionen fest Fernbefehl
Keiner CD / DVD-Start erzwingen PXE-Start erzwingen Festplattenstart erzwingen Diagnosestart erzwingen Boot-Quelle
Keiner Index 1 Index 2 Index 3 Index 4 Boot Media
Booten Sie auf Diskette Booten Sie auf CD-ROM IDER-Startgerät
Systemfehler Ruhig Ausführlich Leerer Bildschirm Verbocity
Alarm halten Nach Abschluss löschen
Nach dem Aufwachen