diff options
author | Simon Rettberg | 2021-08-24 16:31:13 +0200 |
---|---|---|
committer | Simon Rettberg | 2021-08-24 16:31:13 +0200 |
commit | 664bc09f4f416df006e9ca1ce37d8dd93b8d3db7 (patch) | |
tree | 1b2f7064a053a00412f5d95d55fefda498d7b949 | |
parent | [eventlog] Check permissions; add synamic suggestions for keys (diff) | |
download | slx-admin-664bc09f4f416df006e9ca1ce37d8dd93b8d3db7.tar.gz slx-admin-664bc09f4f416df006e9ca1ce37d8dd93b8d3db7.tar.xz slx-admin-664bc09f4f416df006e9ca1ce37d8dd93b8d3db7.zip |
[statistics] Backup vorm urlaub
-rw-r--r-- | inc/crypto.inc.php | 3 | ||||
-rw-r--r-- | inc/database.inc.php | 4 | ||||
-rw-r--r-- | modules-available/statistics/api.inc.php | 17 | ||||
-rw-r--r-- | modules-available/statistics/inc/hardwareinfo.inc.php | 328 | ||||
-rw-r--r-- | modules-available/statistics/inc/parser.inc.php | 11 | ||||
-rw-r--r-- | modules-available/statistics/install.inc.php | 41 |
6 files changed, 390 insertions, 14 deletions
diff --git a/inc/crypto.inc.php b/inc/crypto.inc.php index 56f5073c..d3dd60dc 100644 --- a/inc/crypto.inc.php +++ b/inc/crypto.inc.php @@ -10,7 +10,8 @@ class Crypto */ public static function hash6($password) { - $salt = substr(str_replace('+', '.', base64_encode(pack('N4', mt_rand(), mt_rand(), mt_rand(), mt_rand()))), 0, 16); + $salt = substr(str_replace('+', '.', + base64_encode(pack('N4', mt_rand(), mt_rand(), mt_rand(), mt_rand()))), 0, 16); $hash = crypt($password, '$6$' . $salt); if (strlen($hash) < 60) Util::traceError('Error hashing password using SHA-512'); return $hash; diff --git a/inc/database.inc.php b/inc/database.inc.php index 09006f3e..48d8e3c6 100644 --- a/inc/database.inc.php +++ b/inc/database.inc.php @@ -375,9 +375,9 @@ class Database * @param string $aiKey name of the AUTO_INCREMENT column * @param array $uniqueValues assoc array containing columnName => value mapping * @param array $additionalValues assoc array containing columnName => value mapping - * @return int[] list of AUTO_INCREMENT values matching the list of $values + * @return int AUTO_INCREMENT value matching the given unique values entry */ - public static function insertIgnore($table, $aiKey, $uniqueValues, $additionalValues = false) + public static function insertIgnore($table, $aiKey, $uniqueValues, $additionalValues = false): int { // Sanity checks if (array_key_exists($aiKey, $uniqueValues)) { diff --git a/modules-available/statistics/api.inc.php b/modules-available/statistics/api.inc.php index 04614521..9a81871c 100644 --- a/modules-available/statistics/api.inc.php +++ b/modules-available/statistics/api.inc.php @@ -1,5 +1,11 @@ <?php +if (Request::any('action') === 'test' && isLocalExecution()) { + HardwareInfo::parseMachine('0A5D9E23-80F4-9C43-912C-96D80AE7E80B', + file_get_contents('/tmp/bla.json')); + exit; +} + if (empty($_POST['type'])) die('Missing options.'); $type = mb_strtolower($_POST['type']); @@ -62,7 +68,11 @@ if ($type[0] === '~') { if (!is_string($hostname) || $hostname === $ip) { $hostname = ''; } - $data = Util::cleanUtf8(Request::post('data', '', 'string')); + $data = Util::cleanUtf8(Request::post('json', '', 'string')); + $hasJson = !empty($data); + if (!$hasJson) { + $data = Util::cleanUtf8(Request::post('data', '', 'string')); + } // Prepare insert/update to machine table $new = array( 'machineuuid'=> $uuid, @@ -151,6 +161,10 @@ if ($type[0] === '~') { $new['locationid'] = $loc; // For Filter Event } + if ($hasJson) { + HardwareInfo::parseMachine($uuid, $data); + } + // Check for suspicious hardware changes if ($old !== false) { checkHardwareChange($old, $new); @@ -316,6 +330,7 @@ if ($type[0] === '~') { 'hwid' => $hwid, 'machineuuid' => $uuid, 'devpath' => $port, + 'serial' => '', ), array('disconnecttime' => 0)); $validProps = array(); if (count($screen) > 1) { diff --git a/modules-available/statistics/inc/hardwareinfo.inc.php b/modules-available/statistics/inc/hardwareinfo.inc.php new file mode 100644 index 00000000..22c5a6f1 --- /dev/null +++ b/modules-available/statistics/inc/hardwareinfo.inc.php @@ -0,0 +1,328 @@ +<?php + +class HardwareInfo +{ + + // Never change these! + const RAM_MODULE = 'RAM'; + const MAINBOARD = 'MAINBOARD'; + const DMI_SYSTEM = 'DMI_SYSTEM'; + const POWER_SUPPLY = 'POWER_SUPPLY'; + const SYSTEM_SLOT = 'SYSTEM_SLOT'; + const PCI_DEVICE = 'PCI_DEVICE'; + + + public static function parseMachine(string $uuid, string $data) + { + $data = json_decode($data, true); + $version = $data['version'] ?? 0; + if ($version != 2) { + error_log("Received unsupported hw json v$version"); + return; + } + // determine misc stuff first + $mainboardExtra = []; + // physical memory array + $memArrays = self::getDmiHandles($data, 16); + $mainboardExtra['Memory Slot Count'] = 0; + $mainboardExtra['Memory Maximum Capacity'] = 0; + foreach ($memArrays as $mem) { + $mem = self::prepareDmiProperties($mem); + if (isset($mem['Number Of Devices']) && ($mem['Use'] ?? 0) === 'System Memory') { + $mainboardExtra['Memory Slot Count'] += $mem['Number Of Devices']; + } + if (isset($mem['Maximum Capacity'])) { + $mainboardExtra['Memory Maximum Capacity'] += Parser::convertSize($mem['Maximum Capacity'], 'M', false); + } + } + // Using the general helper function + $ramModCount = self::updateHwTypeFromDmi($uuid, $data, 17, self::RAM_MODULE, function (array $flat): bool { + return ($flat['Size'] ?? 0) > 65 * 1024 * 1024; + }, + ['Locator'], + ['Data Width', 'Size', 'Form Factor', 'Type', 'Type Detail', 'Speed', 'Manufacturer', 'Part Number', + 'Minimum Voltage', 'Maximum Voltage'], + ['Locator', 'Bank Locator', 'Serial Number', 'Asset Tag', 'Configured Memory Speed', 'Configured Voltage'] + ); + // Fake RAM slots used/total into this + $mainboardExtra['Memory Slot Occupied'] = $ramModCount; + self::updateHwTypeFromDmi($uuid, $data, 2, self::MAINBOARD, ['Manufacturer', 'Product Name'], + [], + ['Manufacturer', 'Product Name', 'Type', 'Version'], + ['Serial Number', 'Asset Tag', 'Location In Chassis'], + $mainboardExtra + ); + self::updateHwTypeFromDmi($uuid, $data, 1, self::DMI_SYSTEM, ['Manufacturer', 'Product Name'], + [], + ['Manufacturer', 'Product Name', 'Version', 'Wake-up Type'], + ['Serial Number', 'UUID', 'SKU Number'] + ); + self::updateHwTypeFromDmi($uuid, $data, 39, self::POWER_SUPPLY, ['Manufacturer'], + ['Location', 'Power Unit Group', 'Name'], // Location might ne empty/"Unknown", but Name can be something like "PSU 2" + ['Manufacturer', 'Product Name', 'Model Part Number', 'Revision', 'Max Power Capacity'], + ['Serial Number', 'Asset Tag', 'Status', 'Plugged', 'Hot Replaceable'] + ); + self::updateHwTypeFromDmi($uuid, $data, 9, self::SYSTEM_SLOT, function(array &$entry): bool { + if (!isset($entry['Type'])) + return false; + // Split up PCIe info + if (preg_match('/^x(?<b>\d+) PCI Express( (?<g>\d+)( x(?<s>\d+))?)?$/', $entry['Type'], $out)) { + $entry['Type'] = 'PCI Express'; + $entry['PCIe Bus Width'] = $out['b']; + if (!empty($out['g'])) { + $entry['PCIe Gen'] = $out['g']; + } + if (!empty($out['s'])) { + $entry['PCIe Slot Width'] = $out['s']; + } + } + return true; + }, + ['Designation', 'ID', 'Bus Address'], + ['Type', 'PCIe Bus Width', 'PCIe Gen', 'PCIe Slot Width'], + ['Current Usage', 'Designation'] + ); + // ---- lspci ------------------------------------ + $pciHwIds = []; + foreach (($data['lspci'] ?? []) as $dev) { + $hwid = self::writeGlobalHardwareData(self::PCI_DEVICE, + self::propsFromArray($dev, 'vendor', 'device', 'rev', 'class')); + $mappingId = self::writeLocalHardwareData($uuid, $hwid, $dev['slot'] ?? 'unknown', + self::propsFromArray($dev, 'slot', 'subsystem', 'subsystem_vendor')); + $pciHwIds[] = $mappingId; + } + self::markDisconnected($uuid, self::PCI_DEVICE, $pciHwIds); + // ---- Disks ------------------------------------ + $hddHwIds = []; + foreach (($data['drives'] ?? []) as $dev) { + if (empty($dev['readlink']) || !isset($dev['smartctl']['device'])) + continue; + $hwid = self::writeGlobalHardwareData(self::HDD, [ + $dev['smartctl']['model_name'] ?? $dev['lsblk']['blockdevices'][0]['model'] ?? 'unknown', + $dev['lsblk']['blockdevices'][0]['size'] ?? 'unknown' + ]); + $mappingId = self::writeLocalHardwareData($uuid, $hwid, $dev['readlink'], + self::propsFromArray($dev['smartctl'] + ($dev['lsblk']['blockdevices'][0] ?? []), + 'serial_number', 'firmware_version', 'size')); + $hddHwIds[] = $mappingId; + } + } + + private static function updateHwTypeFromDmi(string $uuid, array $data, int $type, string $dbType, + $requiredPropsOrCallback, array $pathFields, array $globalProps, array $localProps, + array $globalExtra = []): int + { + $sections = self::getDmiHandles($data, $type); + $thisMachineHwIds = []; + foreach ($sections as $section) { + $flat = self::prepareDmiProperties($section); + if (is_array($requiredPropsOrCallback)) { + foreach ($requiredPropsOrCallback as $prop) { + if (!isset($flat[$prop])) + continue 2; + } + } + if (is_callable($requiredPropsOrCallback)) { + if (!$requiredPropsOrCallback($flat)) + continue; + } + $hwid = self::writeGlobalHardwareData($dbType, self::propsFromArray($flat, ...$globalProps) + $globalExtra); + $pathId = md5(self::idFromArray($flat, ...$pathFields)); + $props = self::propsFromArray($flat, ...$localProps); + $mappingId = self::writeLocalHardwareData($uuid, $hwid, $pathId, $props); + $thisMachineHwIds[] = $mappingId; + } + // Any hw <-> client mappings not in that list get marked as disconnected + self::markDisconnected($uuid, $dbType, $thisMachineHwIds); + return count($thisMachineHwIds); + } + + private static function writeGlobalHardwareData(string $dbType, array $global): int + { + static $cache = []; + // Since the global properties are supposed to be unique for a specific piece of hardware, use them all + // to generate a combined ID for this hardware entity, as opposed to $localProps, which should differ + // between instances of the same hardware entity, e.g. one specific HDD model has different serial numbers. + $id = md5(implode(' ', $global)); + if (!isset($cache[$id])) { + // Cache lookup, make sure we insert this only once for every run, as this is supposed to be general + // information about the hardware, e.g. model number, max. resultion, capacity, ... + $cache[$id] = self::updateHwEntity($dbType, $id, $global); + } + return $cache[$id]; + } + + private static function markDisconnected(string $uuid, string $dbType, array $excludedHwIds) + { + error_log("Marking disconnected for $dbType from " . implode(', ', $excludedHwIds)); + if (empty($excludedHwIds)) { + Database::exec("UPDATE machine_x_hw mxh, statistic_hw h + SET mxh.disconnecttime = UNIX_TIMESTAMP() + WHERE h.hwtype = :type AND h.hwid = mxh.hwid AND mxh.machineuuid = :uuid + AND mxh.disconnecttime = 0", + ['type' => $dbType, 'uuid' => $uuid]); + } else { + Database::exec("UPDATE machine_x_hw mxh, statistic_hw h + SET mxh.disconnecttime = UNIX_TIMESTAMP() + WHERE h.hwtype = :type AND h.hwid = mxh.hwid AND mxh.machineuuid = :uuid + AND mxh.disconnecttime = 0 AND mxh.machinehwid NOT IN (:hwids)", + ['type' => $dbType, 'uuid' => $uuid, 'hwids' => $excludedHwIds]); + } + } + + /** + * Takes key-value-array, returns a new array with only the keys listed in $fields. + */ + private static function propsFromArray(array $array, string ...$fields): array + { + $ret = []; + foreach ($fields as $field) { + if (isset($array[$field])) { + $ret[$field] = $array[$field]; + } + } + return $ret; + } + + /** + * Takes key-value-array, returns a concatenated string of all the values with the keys given in $fields. + * The items are separated by spaces, and returned in the order they were given in $fields. Missing keys + * are silently omitted. + */ + private static function idFromArray(array $array, string ...$fields): string + { + $out = ''; + foreach ($fields as $field) { + if (!isset($array[$field])) + continue; + if (empty($out)) { + $out = $array[$field]; + } else { + $out .= ' ' . $array[$field]; + } + } + return $out; + } + + private static function updateHwEntity(string $hwType, string $uniqueName, array $props): int + { + $hwid = Database::insertIgnore('statistic_hw', 'hwid', ['hwtype' => $hwType, 'hwname' => $uniqueName]); + $vals = []; + foreach ($props as $k => $v) { + $vals[] = [$hwid, $k, $v]; + } + if (!empty($vals)) { + Database::exec("INSERT IGNORE INTO statistic_hw_prop (hwid, prop, `value`) VALUES :vals", + ['vals' => $vals]); + } + return $hwid; + } + + /** + * Takes hwinfo json, then looks up and returns all sections from the + * dmidecode subtree that represent the given dmi table entry type, + * e.g. 17 for memory. It will then return an array of 'props' subtrees. + * @param array $data hwinfo tree + * @param int $type dmi type + * @return array [ <props>, <props>, ... ] + */ + private static function getDmiHandles(array $data, int $type): array + { + if (empty($data['dmidecode'])) + return []; + $ret = []; + foreach ($data['dmidecode'] as $section) { + if ($section['handle']['type'] == $type) { + $ret[] = $section['props']; + } + } + return $ret; + } + + const SIZE_LOOKUP = ['T' => 1099511627776, 'G' => 1073741824, 'M' => 1048576, 'K' => 1024]; + + /** + * Takes an array of type [ key1 => [ 'values' => [ <val1.1>, <val1.2>, ... ] ], key2 => ... ] + * and turns it into [ key1 => <val1.1>, key2 => <val2.1>, ... ] + * Along the way, any fields with bogus values, or values analogous to empty will get removed + */ + private static function prepareDmiProperties(array $data): array + { + $ret = []; + foreach ($data as $key => $vals) { + $val = trim($vals['values'][0]); + if ($val === '[Empty]' || $val === 'NULL') + continue; + $val = preg_replace('/[^a-z0-9]/', '', strtolower($val)); + if ($val === '' || $val === 'notspecified' || $val === 'tobefilledbyoem' || $val === 'unknown' + || $val === 'chassismanufacture' || $val === 'chassismanufacturer' || $val === 'chassisversion' + || $val === 'chassisserialnumber' || $val === 'defaultstring' || $val === 'productname' + || $val === 'manufacturer' || $val === 'systemmodel') { + continue; + } + $val = trim($vals['values'][0] ?? ''); + if ($key === 'Manufacturer') { + $val = self::fixManufacturer($val); + } elseif (preg_match('/^(\d+)\s+([TGMK])B$/i', $val, $out)) { + $val = $out[1] * self::SIZE_LOOKUP[strtoupper($out[2])]; + } + $ret[$key] = $val; + } + return $ret; + } + + private static function fixManufacturer(string $in): string + { + $in = Parser::decodeJedec($in); + switch (strtolower($in)) { + case 'advanced micro devices, inc.': + case 'advanced micro devices': + case 'authenticamd': + return 'AMD'; + case 'apple inc.': + return 'Apple'; + case 'asustek computer inc.': + return 'ASUS'; + case 'dell inc.': + return 'Dell'; + case 'fujitsu': + case 'fujitsu client computing limited': + return 'Fujitsu'; + case 'hewlett packard': + case 'hewlett-packard': + return 'HP'; + case 'hynix semiconduc': + case 'hynix/hyundai': + case 'hyundai electronics hynix semiconductor inc': + case 'hynix semiconductor inc sk hynix': + return 'SK Hynix'; + case 'genuineintel': + case 'intel corporation': + case 'intel(r) corp.': + case 'intel(r) corporation': + return 'Intel'; + case 'samsung sdi': + return 'Samsung'; + } + return $in; + } + + private static function writeLocalHardwareData(string $uuid, int $hwid, string $pathId, array $props): int + { + // Add mapping between hw entity and machine + $mappingId = Database::insertIgnore('machine_x_hw', 'machinehwid', + ['hwid' => $hwid, 'machineuuid' => $uuid, 'devpath' => $pathId], + ['disconnecttime' => 0]); + // And all the properties specific to this entity instance (e.g. serial number) + if (!empty($props)) { + $vals = []; + foreach ($props as $k => $v) { + $vals[] = [$mappingId, $k, $v]; + } + Database::exec("INSERT IGNORE INTO machine_x_hw_prop (machinehwid, prop, value) + VALUES :vals", ['vals' => $vals]); + } + return $mappingId; + } + +} diff --git a/modules-available/statistics/inc/parser.inc.php b/modules-available/statistics/inc/parser.inc.php index 84f98c40..150639c3 100644 --- a/modules-available/statistics/inc/parser.inc.php +++ b/modules-available/statistics/inc/parser.inc.php @@ -120,14 +120,16 @@ class Parser { * Convert/format size unit. Input string can be a size like * 8 GB or 1024 MB and will be converted according to passed parameters. * @param string $string Input string - * @param string $scale 'a' for auto, T/G/M/K for according units + * @param string $scale 'a' for auto, T/G/M/K/'' for according units * @param bool $appendUnit append unit string, e.g. 'GiB' * @return string|int Formatted result */ - private static function convertSize($string, $scale = 'a', $appendUnit = true) + public static function convertSize(string $string, string $scale = 'a', bool $appendUnit = true) { - if (!preg_match('/(\d+)\s*([TGMK]?)/i', $string, $out)) + if (!preg_match('/(\d+)\s*([TGMK]?)/i', $string, $out)) { + error_log("Not size: $string"); return false; + } $val = (int)$out[1] * self::LOOKUP[strtoupper($out[2])]; if (!array_key_exists($scale, self::LOOKUP)) { foreach (self::LOOKUP as $k => $v) { @@ -385,12 +387,13 @@ class Parser { } } - public static function decodeJedec($string) + public static function decodeJedec(string $string): string { // JEDEC ID:7F 7F 9E 00 00 00 00 00 if (preg_match('/JEDEC(?:\s*ID)?\s*:\s*([0-9a-f\s]+)/i', $string, $out)) { preg_match_all('/[0-9a-f]{2}/i', $out[1], $out); $bank = 0; + $id = 0; foreach ($out[0] as $id) { $bank++; $id = hexdec($id) & 0x7f; // Let's just ignore the parity bit, and any potential error diff --git a/modules-available/statistics/install.inc.php b/modules-available/statistics/install.inc.php index 15d0d633..99cc8277 100644 --- a/modules-available/statistics/install.inc.php +++ b/modules-available/statistics/install.inc.php @@ -64,7 +64,7 @@ $res[] = $machineHwCreate = tableCreate('machine_x_hw', " `machinehwid` int(10) unsigned NOT NULL AUTO_INCREMENT, `hwid` int(10) unsigned NOT NULL, `machineuuid` char(36) CHARACTER SET ascii NOT NULL, - `devpath` char(50) CHARACTER SET ascii NOT NULL, + `devpath` char(32) CHARACTER SET ascii NOT NULL, `disconnecttime` int(10) unsigned NOT NULL COMMENT 'time the device was not connected to the pc anymore for the first time, 0 if it is connected', PRIMARY KEY (`machinehwid`), UNIQUE KEY `hwid` (`hwid`,`machineuuid`,`devpath`), @@ -74,22 +74,22 @@ $res[] = $machineHwCreate = tableCreate('machine_x_hw', " $res[] = tableCreate('machine_x_hw_prop', " `machinehwid` int(10) unsigned NOT NULL, - `prop` char(16) CHARACTER SET ascii NOT NULL, + `prop` varchar(64) CHARACTER SET ascii NOT NULL, `value` varchar(500) NOT NULL, PRIMARY KEY (`machinehwid`,`prop`) "); $res[] = tableCreate('statistic_hw', " `hwid` int(10) unsigned NOT NULL AUTO_INCREMENT, - `hwtype` char(11) CHARACTER SET ascii NOT NULL, - `hwname` varchar(200) NOT NULL, + `hwtype` char(16) CHARACTER SET ascii NOT NULL, + `hwname` char(32) CHARACTER SET ascii NOT NULL, PRIMARY KEY (`hwid`), UNIQUE KEY `hwtype` (`hwtype`,`hwname`) "); $res[] = tableCreate('statistic_hw_prop', " `hwid` int(10) unsigned NOT NULL, - `prop` char(16) CHARACTER SET ascii NOT NULL, + `prop` varchar(64) CHARACTER SET ascii NOT NULL, `value` varchar(500) NOT NULL, PRIMARY KEY (`hwid`,`prop`) "); @@ -298,6 +298,35 @@ if (!tableHasColumn('machine', 'live_id45size')) { } $res[] = UPDATE_DONE; } - +// 2021-08-19 Enhanced machine property indexing +if (stripos(tableColumnType('statistic_hw_prop', 'prop'), 'varchar(64)') === false) { + $ret = Database::exec("ALTER TABLE statistic_hw_prop MODIFY `prop` varchar(64) CHARACTER SET ascii NOT NULL"); + if ($ret === false) { + finalResponse(UPDATE_FAILED, 'Changing prop of statistic_hw_prop failed: ' . Database::lastError()); + } + $res[] = UPDATE_DONE; +} +if (stripos(tableColumnType('machine_x_hw_prop', 'prop'), 'varchar(64)') === false) { + $ret = Database::exec("ALTER TABLE machine_x_hw_prop MODIFY `prop` varchar(64) CHARACTER SET ascii NOT NULL"); + if ($ret === false) { + finalResponse(UPDATE_FAILED, 'Changing prop of machine_x_hw_prop failed: ' . Database::lastError()); + } + $res[] = UPDATE_DONE; +} +if (stripos(tableColumnType('statistic_hw', 'hwname'), 'char(32)') === false) { + $ret = Database::exec("ALTER TABLE statistic_hw MODIFY `hwname` char(32) CHARACTER SET ascii NOT NULL, + MODIFY `hwtype` char(16) CHARACTER SET ascii NOT NULL"); + if ($ret === false) { + finalResponse(UPDATE_FAILED, 'Changing hwname/hwtype of statistic_hw failed: ' . Database::lastError()); + } + $res[] = UPDATE_DONE; +} +if (stripos(tableColumnType('machine_x_hw', 'devpath'), 'char(32)') === false) { + $ret = Database::exec("ALTER TABLE machine_x_hw MODIFY `devpath` char(32) CHARACTER SET ascii NOT NULL"); + if ($ret === false) { + finalResponse(UPDATE_FAILED, 'Changing devpath of machine_x_hw failed: ' . Database::lastError()); + } + $res[] = UPDATE_DONE; +} // Create response responseFromArray($res); |