diff options
| author | Simon Rettberg | 2026-04-29 14:12:46 +0200 |
|---|---|---|
| committer | Simon Rettberg | 2026-04-29 14:12:46 +0200 |
| commit | e9dd3b47e64f43d967a08cfc78efdffa95130a95 (patch) | |
| tree | 7320ae4709724ccd769ebf9a6368565640afe85d /modules-available | |
| parent | [runmode] Add UUID to selected clients, close dropdown on select (diff) | |
| parent | [locationinfo] Use dedicated list permission for extdevices (diff) | |
| download | slx-admin-e9dd3b47e64f43d967a08cfc78efdffa95130a95.tar.gz slx-admin-e9dd3b47e64f43d967a08cfc78efdffa95130a95.tar.xz slx-admin-e9dd3b47e64f43d967a08cfc78efdffa95130a95.zip | |
Merge branch 'master' of git.openslx.org:openslx-ng/slx-admin
Diffstat (limited to 'modules-available')
36 files changed, 680 insertions, 179 deletions
diff --git a/modules-available/backup/page.inc.php b/modules-available/backup/page.inc.php index 44511367..0c417d9e 100644 --- a/modules-available/backup/page.inc.php +++ b/modules-available/backup/page.inc.php @@ -120,7 +120,7 @@ class Page_Backup extends Page if (empty($password)) { $password = null; } - $tempfile = '/tmp/bwlp-' . mt_rand(1, 100000) . '-' . crc32($_SERVER['REMOTE_ADDR']) . '.tgz'; + $tempfile = '/tmp/bwlp-' . mt_rand(1, 100000) . '-' . crc32(Util::getClientIp()) . '.tgz'; if (!move_uploaded_file($_FILES['backupfile']['tmp_name'], $tempfile)) { Message::addError('main.error-write', $tempfile); Util::redirect('?do=Backup'); diff --git a/modules-available/baseconfig/inc/baseconfig.inc.php b/modules-available/baseconfig/inc/baseconfig.inc.php index 36622dce..f31c521c 100644 --- a/modules-available/baseconfig/inc/baseconfig.inc.php +++ b/modules-available/baseconfig/inc/baseconfig.inc.php @@ -17,7 +17,7 @@ class BaseConfig */ public static function prepareFromRequest() { - $ip = $_SERVER['REMOTE_ADDR'] ?? null; + $ip = Util::getClientIp(); if ($ip === null) ErrorHandler::traceError('No REMOTE_ADDR given in $_SERVER'); if (substr($ip, 0, 7) === '::ffff:') { diff --git a/modules-available/dozmod/api.inc.php b/modules-available/dozmod/api.inc.php index 8f83f196..a4fd0233 100644 --- a/modules-available/dozmod/api.inc.php +++ b/modules-available/dozmod/api.inc.php @@ -335,10 +335,9 @@ if (!in_array($resource, $availableRessources)) { die("unknown resource: $resource"); } -$ip = $_SERVER['REMOTE_ADDR']; -if (substr($ip, 0, 7) === '::ffff:') { - $ip = substr($ip, 7); -} +$ip = Util::getClientIp(); +if ($ip === null) + ErrorHandler::traceError("could not determine client IP"); /* lookup location id(s) */ diff --git a/modules-available/locationinfo/api.inc.php b/modules-available/locationinfo/api.inc.php index cd2ffd5a..559161ba 100644 --- a/modules-available/locationinfo/api.inc.php +++ b/modules-available/locationinfo/api.inc.php @@ -17,7 +17,7 @@ function HandleParameters() $output = null; if ($get === "timestamp") { $output = [ - 'ts' => getLastChangeTs($uuid), + 'ts' => ApiPanelFrontend::getLastChangeTs($uuid), 'now' => round(microtime(true) * 1000), ]; } elseif ($get === "machines") { @@ -33,7 +33,7 @@ function HandleParameters() } } elseif ($get === "pcstates") { $locationIds = LocationInfo::getLocationsOr404($uuid); - $output = getPcStates($locationIds, $uuid); + $output = ApiPanelFrontend::getPcStates($locationIds, $uuid); } elseif ($get === "locationtree") { $locationIds = LocationInfo::getLocationsOr404($uuid); $output = Location::getTree(...$locationIds); @@ -41,9 +41,13 @@ function HandleParameters() $locationIds = LocationInfo::getLocationsOr404($uuid); $output = LocationInfo::getCalendar($locationIds, time() + 3); } elseif ($get === "manifest") { - $output = generateManifest($uuid); + $output = ApiPanelFrontend::generateManifest($uuid); } elseif ($get === 'list') { - $output = generatePublicPanelList(); + $output = ApiExternalPanels::generatePublicPanelList(); + } elseif ($get === 'external-register') { + $output = ApiExternalPanels::registerDevice(); + } elseif ($get === 'external-checkin') { + $output = ApiExternalPanels::checkinCallback(); } if ($output !== null) { Header('Content-Type: application/json; charset=utf-8'); @@ -53,145 +57,3 @@ function HandleParameters() echo 'Unknown get option'; } } - -/** - * Get last config modification timestamp for given panel. - * This is incomplete however, as it wouldn't react to the - * linked room plan being edited, or added/removed PCs - * etc. So the advice would simply be "if you want the - * panel to reload automatically, hit the edit button - * and click save". Might even add a shortcut - * reload-button to the list of panels at some point. - * - * @param string $paneluuid panels uuid - * @return int UNIX_TIMESTAMP - */ -function getLastChangeTs(string $paneluuid): int -{ - $panel = Database::queryFirst('SELECT lastchange, locationids FROM locationinfo_panel WHERE paneluuid = :paneluuid', - compact('paneluuid')); - if ($panel === false) { - http_response_code(404); - die('Panel not found'); - } - $lastChange = array((int)$panel['lastchange']); - if (!empty($panel['locationids'])) { - $res = Database::simpleQuery('SELECT lastchange FROM locationinfo_locationconfig - WHERE locationid IN (:locs)', array('locs' => explode(',', $panel['locationids']))); - while (($lc = $res->fetchColumn()) !== false) { - $lastChange[] = (int)$lc; - } - } - return max($lastChange); -} - -/** - * Gets the pc states of the given locations. - * - * @param int[] $idList list of the location ids. - * @return array aggregated PC states - */ -function getPcStates(array $idList, string $paneluuid): array -{ - $pcStates = array(); - foreach ($idList as $id) { - $pcStates[$id] = array( - 'id' => $id, - 'idle' => 0, - 'occupied' => 0, - 'offline' => 0, - 'broken' => 0, - 'standby' => 0, - ); - } - - $locationInfoList = array(); - InfoPanel::appendMachineData($locationInfoList, $idList); - - $panel = Database::queryFirst('SELECT paneluuid, panelconfig FROM locationinfo_panel WHERE paneluuid = :paneluuid', - compact('paneluuid')); - $config = json_decode($panel['panelconfig'], true); - - foreach ($locationInfoList as $locationInfo) { - $id = $locationInfo['id']; - foreach ($locationInfo['machines'] as $pc) { - $key = strtolower($pc['pcState']); - if (isset($pcStates[$id][$key])) { - if ($config['roomplanner']) { - if (isset($pc['x']) && isset($pc['y'])) { - $pcStates[$id][$key]++; - } - } else { - $pcStates[$id][$key]++; - } - } - } - } - - return array_values($pcStates); -} - -/** - * Generates a web application manifest for a panel. - * - * @param string $uuid The UUID of the panel. - * @return array The generated manifest file data. - */ -function generateManifest(string $uuid): array -{ - $row = Database::queryFirst("SELECT panelname FROM locationinfo_panel WHERE paneluuid = :paneluuid", - ['paneluuid' => $uuid]); - if ($row === false) - return ['error' => 'Not found']; - $data = [ - "name" => $row['panelname'], - "short_name" => $row['panelname'], - "start_url" => "/panel/$uuid", - "display_override" => [ - "fullscreen", - "standalone", - "tabbed", - "minimal-ui" - ], - "display" => "fullscreen", - "icons" => [ - [ - "src" => "/panel/style/icon.svg", - "sizes" => "any", - "type" => "image/svg+xml" - ] - ], - ]; - foreach (glob('style/icon*.png', GLOB_NOSORT) as $file) { - if (!preg_match('/icon(\d+)\.png$/', $file, $m)) - continue; - $data['icons'][] = [ - "src" => "/panel/$file", - "sizes" => "${m[1]}x${m[1]}", - "type" => "image/png", - ]; - } - return $data; -} - -function generatePublicPanelList(): array -{ - $base = Util::shouldRedirectDomain(); - if ($base !== null) { - $base = 'https://' . $base; - } else { - $base = ($_SERVER['HTTPS'] ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST']; - } - $result = [ - 'server_name' => 'To be filled by O.E.M.', - 'panels' => [], - ]; - $res = Database::simpleQuery("SELECT paneluuid, panelname FROM locationinfo_panel WHERE ispublic = 1"); - foreach ($res as $row) { - $result['panels'][] = [ - 'title' => $row['panelname'], - 'url' => $base . '/panel/' . $row['paneluuid'], - ]; - } - return $result; -}
\ No newline at end of file diff --git a/modules-available/locationinfo/inc/apiexternalpanels.inc.php b/modules-available/locationinfo/inc/apiexternalpanels.inc.php new file mode 100644 index 00000000..27b0322b --- /dev/null +++ b/modules-available/locationinfo/inc/apiexternalpanels.inc.php @@ -0,0 +1,103 @@ +<?php + +/** + * Helper functions for managing external panels, + * which are not running on bwLehrpool, but for + * example, an android app, and need additional + * callbacks to be managed by slx-admin. + */ +class ApiExternalPanels +{ + + private static function getBaseUrl(): string + { + $base = Util::shouldRedirectDomain(); + if ($base !== null) { + $base = 'https://' . $base; + } else { + $base = ($_SERVER['HTTPS'] ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST']; + } + return $base . '/panel/'; + } + + /** + * @return array{server_name: string, panels: array<array{title: string, url: string}>} + */ + public static function generatePublicPanelList(): array + { + $base = self::getBaseUrl(); + $result = [ + 'server_name' => 'To be filled by O.E.M.', + 'panels' => [], + ]; + $res = Database::simpleQuery("SELECT paneluuid, panelname FROM locationinfo_panel WHERE ispublic = 1"); + foreach ($res as $row) { + $result['panels'][] = [ + 'uuid' => $row['paneluuid'], + 'title' => $row['panelname'], + 'url' => $base . $row['paneluuid'], + ]; + } + return $result; + } + + /** + * Register an external device with the server. + */ + public static function registerDevice(): array + { + $deviceuuid = Request::any('deviceuuid', Request::REQUIRED, 'string'); + if (!Util::isValidUuid($deviceuuid)) { + http_response_code(400); + return ['error' => 'Invalid external device UUID']; + } + $paneluuid = Request::any('paneluuid', null, 'string'); + $titletext = Request::any('title', '', 'string'); + $ret = Database::exec("INSERT INTO locationinfo_externaldevice + (deviceuuid, paneluuid, title, registertime, registerip) + VALUES (:deviceuuid, :paneluuid, :title, UNIX_TIMESTAMP(), :ip)", [ + 'deviceuuid' => $deviceuuid, + 'paneluuid' => $paneluuid, + 'title' => $titletext, + 'ip' => Util::getClientIp() + ], true); + if ($ret === false) { + http_response_code(409); + return ['error' => 'Failed to register external device; device uuid already registered, or invalid paneluuid']; + } + return ['success' => 'External device registered successfully']; + } + + /** + * To be called every 5 minutes by a running external device. + * We'll consider devices that didn't report in for more than 10 + * minutes as offline. + * This also returns an array with configuration information to + * the device, and the app running on the device should then check + * if any of the parameters changed and react to this accordingly. + * @return array{url: string, brightness_pct: int, reboot_hour: int} + */ + public static function checkinCallback(): array + { + $row = Database::queryFirst("SELECT deviceuuid, isregistered, configjson FROM locationinfo_externaldevice + WHERE deviceuuid = :deviceuuid", ['deviceuuid' => Request::any('deviceuuid', Request::REQUIRED, 'string')]); + if ($row === false) { + http_response_code(404); + return ['error' => 'Unknown external panel uuid, please register first.']; + } + if ($row['paneluuid'] === null) { + http_response_code(202); + return ['error' => 'Your external device uuid has been submitted for registration, please wait for an Administrator to set up a configuration.']; + } + $config = json_decode($row['configjson'], true); + if (!is_array($config)) { + $config = []; + } + $config['time_ms'] = ceil(microtime(true) * 1000); + $config['url'] = self::getBaseUrl() . $row['paneluuid']; + Database::exec("UPDATE locationinfo_externaldevice SET lastseen = UNIX_TIMESTAMP() WHERE deviceuuid = :deviceuuid", + ['deviceuuid' => $row['deviceuuid']]); + return $config; + } + +}
\ No newline at end of file diff --git a/modules-available/locationinfo/inc/apipanelfrontend.inc.php b/modules-available/locationinfo/inc/apipanelfrontend.inc.php new file mode 100644 index 00000000..70ea93c0 --- /dev/null +++ b/modules-available/locationinfo/inc/apipanelfrontend.inc.php @@ -0,0 +1,129 @@ +<?php + +/** + * Functions the panel frontends access to display their data, + * usually via AJAX calls. + */ +class ApiPanelFrontend +{ + + /** + * Gets the pc states of the given locations. + * + * @param int[] $idList list of the location ids. + * @return array aggregated PC states + */ + public static function getPcStates(array $idList, string $paneluuid): array + { + $pcStates = array(); + foreach ($idList as $id) { + $pcStates[$id] = array( + 'id' => $id, + 'idle' => 0, + 'occupied' => 0, + 'offline' => 0, + 'broken' => 0, + 'standby' => 0, + ); + } + + $locationInfoList = array(); + InfoPanel::appendMachineData($locationInfoList, $idList); + + $panel = Database::queryFirst('SELECT paneluuid, panelconfig FROM locationinfo_panel WHERE paneluuid = :paneluuid', + compact('paneluuid')); + $config = json_decode($panel['panelconfig'], true); + + foreach ($locationInfoList as $locationInfo) { + $id = $locationInfo['id']; + foreach ($locationInfo['machines'] as $pc) { + $key = strtolower($pc['pcState']); + if (isset($pcStates[$id][$key])) { + if ($config['roomplanner']) { + if (isset($pc['x']) && isset($pc['y'])) { + $pcStates[$id][$key]++; + } + } else { + $pcStates[$id][$key]++; + } + } + } + } + + return array_values($pcStates); + } + + /** + * Generates a web application manifest for a panel. + * + * @param string $uuid The UUID of the panel. + * @return array The generated manifest file data. + */ + public static function generateManifest(string $uuid): array + { + $row = Database::queryFirst("SELECT panelname FROM locationinfo_panel WHERE paneluuid = :paneluuid", + ['paneluuid' => $uuid]); + if ($row === false) + return ['error' => 'Not found']; + $data = [ + "name" => $row['panelname'], + "short_name" => $row['panelname'], + "start_url" => "/panel/$uuid", + "display_override" => [ + "fullscreen", + "standalone", + "tabbed", + "minimal-ui" + ], + "display" => "fullscreen", + "icons" => [ + [ + "src" => "/panel/style/icon.svg", + "sizes" => "any", + "type" => "image/svg+xml" + ] + ], + ]; + foreach (glob('style/icon*.png', GLOB_NOSORT) as $file) { + if (!preg_match('/icon(\d+)\.png$/', $file, $m)) + continue; + $data['icons'][] = [ + "src" => "/panel/$file", + "sizes" => "${m[1]}x${m[1]}", + "type" => "image/png", + ]; + } + return $data; + } + + /** + * Get last config modification timestamp for given panel. + * This is incomplete however, as it wouldn't react to the + * linked room plan being edited, or added/removed PCs + * etc. So the advice would simply be "if you want the + * panel to reload automatically, hit the edit button + * and click save". Might even add a shortcut + * reload-button to the list of panels at some point. + * + * @param string $paneluuid panels uuid + * @return int UNIX_TIMESTAMP + */ + public static function getLastChangeTs(string $paneluuid): int + { + $panel = Database::queryFirst('SELECT lastchange, locationids FROM locationinfo_panel WHERE paneluuid = :paneluuid', + compact('paneluuid')); + if ($panel === false) { + http_response_code(404); + die('Panel not found'); + } + $lastChange = array((int)$panel['lastchange']); + if (!empty($panel['locationids'])) { + $res = Database::simpleQuery('SELECT lastchange FROM locationinfo_locationconfig + WHERE locationid IN (:locs)', array('locs' => explode(',', $panel['locationids']))); + while (($lc = $res->fetchColumn()) !== false) { + $lastChange[] = (int)$lc; + } + } + return max($lastChange); + } +}
\ No newline at end of file diff --git a/modules-available/locationinfo/install.inc.php b/modules-available/locationinfo/install.inc.php index f4bbca1c..799b460f 100644 --- a/modules-available/locationinfo/install.inc.php +++ b/modules-available/locationinfo/install.inc.php @@ -45,6 +45,19 @@ $res[] = tableCreate('locationinfo_backendlog', " KEY (`serverid`) "); +$res[] = tableCreate('locationinfo_externaldevice', " + `deviceuuid` char(36) CHARACTER SET ascii NOT NULL, + `paneluuid` char(36) CHARACTER SET ascii, + `title` varchar(250) NOT NULL DEFAULT '', + `registertime` int(10) UNSIGNED NOT NULL, + `registerip` varchar(45) NOT NULL, + `isregistered` tinyint(1) NOT NULL DEFAULT 0, + `configjson` blob, + `lastseen` int(10) UNSIGNED NOT NULL DEFAULT 0, + PRIMARY KEY (`deviceuuid`), + KEY `paneluuid` (`paneluuid`) +"); + // Update if ($t1 === UPDATE_NOOP) { @@ -122,6 +135,13 @@ if (!tableHasColumn('locationinfo_panel', 'ispublic')) { } } +// 2026-04-28: New external device table, add paneluuid constraint +if (tableGetConstraints('locationinfo_externaldevice', 'paneluuid', + 'locationinfo_panel', 'paneluuid') === false) { + $res[] = tableAddConstraint('locationinfo_externaldevice', 'paneluuid', + 'locationinfo_panel', 'paneluuid', 'ON UPDATE CASCADE ON DELETE CASCADE'); +} + // Create response for browser if (in_array(UPDATE_RETRY, $res)) { diff --git a/modules-available/locationinfo/lang/de/messages.json b/modules-available/locationinfo/lang/de/messages.json index fe024d44..32f358ec 100644 --- a/modules-available/locationinfo/lang/de/messages.json +++ b/modules-available/locationinfo/lang/de/messages.json @@ -4,5 +4,8 @@ "invalid-panel-id": "Ung\u00fcltige Panel-ID '{{0}}'", "invalid-panel-type": "Ung\u00fcltiger Panel-Typ '{{0}}'", "invalid-server-id": "Ung\u00fcltige Server-ID '{{0}}'", - "server-id-missing": "Server-ID fehlt" + "server-id-missing": "Server-ID fehlt", + "unregistered-devices-deleted": "{{count}} unregistriete Ger\u00e4te gel\u00f6scht.", + "invalid-device-id": "Ung\u00fcltige Ger\u00e4te-ID '{{0}}'", + "device-deleted": "Gerät '{{0}}' erfolgreich gelöscht." }
\ No newline at end of file diff --git a/modules-available/locationinfo/lang/de/permissions.json b/modules-available/locationinfo/lang/de/permissions.json index 1cd78eab..bae83ccb 100644 --- a/modules-available/locationinfo/lang/de/permissions.json +++ b/modules-available/locationinfo/lang/de/permissions.json @@ -4,5 +4,7 @@ "location.edit": "Raum\/Ort Einstellungen bearbeiten", "panel.assign-client": "Client als Infoscreen festlegen", "panel.edit": "Panel bearbeiten", - "panel.list": "Panel anzeigen" + "panel.list": "Panel anzeigen", + "external-device.list": "Externe Ger\u00e4te anzeigen", + "external-device.edit": "Externe Ger\u00e4te bearbeiten" }
\ No newline at end of file diff --git a/modules-available/locationinfo/lang/de/template-tags.json b/modules-available/locationinfo/lang/de/template-tags.json index 6ffa8cd7..9bc8890e 100644 --- a/modules-available/locationinfo/lang/de/template-tags.json +++ b/modules-available/locationinfo/lang/de/template-tags.json @@ -138,5 +138,21 @@ "lang_verticalTooltip": "Legt fest, ob Kalender und Raum \u00fcbereinander angezeigt werden sollen", "lang_wednesday": "Mittwoch", "lang_when": "Wann", - "lang_whitelist": "Whitelist" + "lang_whitelist": "Whitelist", + "lang_externalDevices": "Externe Ger\u00e4te", + "lang_externalDevicesTable": "Externe Ger\u00e4te verwalten", + "lang_externalDevicesTableHints": "Dies zeigt Ihnen alle externen Ger\u00e4te (z.B. Android Tablets), die mit diesem Server registriert sind.", + "lang_deviceUuid": "Ger\u00e4te UUID", + "lang_deviceTitle": "Titel", + "lang_deviceRegistered": "Registriert", + "lang_deviceLastSeen": "Zuletzt gesehen", + "lang_deleteUnregistered": "Unregistrierte Ger\u00e4te l\u00f6schen", + "lang_noEntries": "Keine Eintr\u00e4ge", + "lang_editExternalDevice": "Externes Ger\u00e4t bearbeiten", + "lang_rebootHour": "Neustartstunde", + "lang_rebootHourTooltip": "Stunde des Tages, an der das Ger\u00e4t neu gestartet werden soll. -1, um das Ger\u00e4t nicht neu zu starten.", + "lang_brightness": "Helligkeit", + "lang_brightnessTooltip": "Bildschirmhelligkeit in Prozent (1-100).", + "lang_register": "Registrieren", + "lang_delete": "Löschen" }
\ No newline at end of file diff --git a/modules-available/locationinfo/lang/en/messages.json b/modules-available/locationinfo/lang/en/messages.json index c459f1ee..1e323b26 100644 --- a/modules-available/locationinfo/lang/en/messages.json +++ b/modules-available/locationinfo/lang/en/messages.json @@ -4,5 +4,8 @@ "invalid-panel-id": "Invalid panel id '{{0}}'", "invalid-panel-type": "Invalid panel type '{{0}}'", "invalid-server-id": "Invalid server id '{{0}}'", - "server-id-missing": "Server id is missing" + "server-id-missing": "Server id is missing", + "unregistered-devices-deleted": "{{count}} unregistered devices deleted.", + "invalid-device-id": "Invalid device id '{{0}}'", + "device-deleted": "Device '{{0}}' successfully deleted." }
\ No newline at end of file diff --git a/modules-available/locationinfo/lang/en/permissions.json b/modules-available/locationinfo/lang/en/permissions.json index 4b620b04..a5123b60 100644 --- a/modules-available/locationinfo/lang/en/permissions.json +++ b/modules-available/locationinfo/lang/en/permissions.json @@ -4,5 +4,7 @@ "location.edit": "Edit location settings", "panel.assign-client": "Set client as infoscreen", "panel.edit": "Edit panel", - "panel.list": "List panels" + "panel.list": "List panels", + "external-device.list": "List external devices", + "external-device.edit": "Edit external devices" }
\ No newline at end of file diff --git a/modules-available/locationinfo/lang/en/template-tags.json b/modules-available/locationinfo/lang/en/template-tags.json index 9c945692..fc88a724 100644 --- a/modules-available/locationinfo/lang/en/template-tags.json +++ b/modules-available/locationinfo/lang/en/template-tags.json @@ -138,5 +138,21 @@ "lang_verticalTooltip": "Defines whether the room and calendar are shown above each other", "lang_wednesday": "Wednesday", "lang_when": "When", - "lang_whitelist": "Whitelist" + "lang_whitelist": "Whitelist", + "lang_externalDevices": "External Devices", + "lang_externalDevicesTable": "Manage external devices", + "lang_externalDevicesTableHints": "This shows you all external devices (e.g. Android tablets) that have registered with this server.", + "lang_deviceUuid": "Device UUID", + "lang_deviceTitle": "Title", + "lang_deviceRegistered": "Registered", + "lang_deviceLastSeen": "Last seen", + "lang_deleteUnregistered": "Delete unregistered devices", + "lang_noEntries": "No entries", + "lang_editExternalDevice": "Edit external device", + "lang_rebootHour": "Reboot hour", + "lang_rebootHourTooltip": "Hour of the day (0-23) when the device should reboot. -1 to disable.", + "lang_brightness": "Brightness", + "lang_brightnessTooltip": "Screen brightness in percent (1-100).", + "lang_register": "Register", + "lang_delete": "Delete" }
\ No newline at end of file diff --git a/modules-available/locationinfo/page.inc.php b/modules-available/locationinfo/page.inc.php index 3db6d216..13a5e181 100644 --- a/modules-available/locationinfo/page.inc.php +++ b/modules-available/locationinfo/page.inc.php @@ -1,5 +1,7 @@ <?php +use JetBrains\PhpStorm\NoReturn; + class Page_LocationInfo extends Page { private $show; @@ -12,7 +14,6 @@ class Page_LocationInfo extends Page $this->show = Request::any('show', false, 'string'); if ($this->show === 'panel') { $this->showPanel(); - exit(0); } User::load(); if (!User::isLoggedIn()) { @@ -32,6 +33,15 @@ class Page_LocationInfo extends Page } elseif ($action === 'updateServerSettings') { $this->updateServerSettings(); $show = 'backends'; + } elseif ($action === 'deleteUnregisteredDevices') { + $this->deleteUnregisteredDevices(); + $show = 'external-devices'; + } elseif ($action === 'writeExternalDeviceConfig') { + $this->writeExternalDeviceConfig(); + $show = 'external-devices'; + } elseif ($action === 'deleteExternalDevice') { + $this->deleteExternalDevice(); + $show = 'external-devices'; } else { if (($id = Request::post('del-serverid', false, 'int')) !== false) { $this->deleteServer($id); @@ -43,6 +53,10 @@ class Page_LocationInfo extends Page Message::addWarning('main.invalid-action', $action); } } + $action = Request::any('action'); + if ($action === 'edit-device') { + $this->show = 'edit-device'; + } if (Request::isPost() || $this->show === false) { if (!empty($show)) { // @@ -74,7 +88,7 @@ class Page_LocationInfo extends Page } } } - Permission::addGlobalTags($data['perms'], null, ['backend.*', 'location.*', 'panel.list']); + Permission::addGlobalTags($data['perms'], null, ['backend.*', 'location.*', 'panel.list', 'external-device.list']); Render::addTemplate('page-tabs', $data); switch ($this->show) { case 'locations': @@ -92,6 +106,12 @@ class Page_LocationInfo extends Page case 'backendlog': $this->showBackendLog(); break; + case 'external-devices': + $this->showExternalDevicesTable(); + break; + case 'edit-device': + $this->showExternalDeviceEdit(); + break; default: Util::redirect('?do=locationinfo'); } @@ -601,6 +621,107 @@ class Page_LocationInfo extends Page Render::addTemplate('page-panels', compact('panels', 'hasRunmode')); } + private function showExternalDevicesTable(): void + { + User::assertPermission('external-device.list'); + $res = Database::simpleQuery('SELECT d.deviceuuid, d.title, p.panelname, d.isregistered, d.lastseen + FROM locationinfo_externaldevice d + LEFT JOIN locationinfo_panel p USING (paneluuid) + ORDER BY d.title ASC'); + $devices = []; + foreach ($res as $row) { + $row['lastseen_s'] = $row['lastseen'] > 0 ? Util::prettyTime($row['lastseen']) : '-'; + $devices[] = $row; + } + $edit = User::hasPermission('external-device.edit'); + Render::addTemplate('page-external-devices', compact('devices', 'edit')); + } + + private function deleteUnregisteredDevices(): void + { + User::assertPermission('external-device.edit'); + $count = Database::exec('DELETE FROM locationinfo_externaldevice WHERE isregistered = 0'); + Message::addSuccess('unregistered-devices-deleted', ['count' => $count]); + } + + private function showExternalDeviceEdit(): void + { + User::assertPermission('external-device.edit'); + $deviceid = Request::any('deviceid'); + $device = Database::queryFirst('SELECT deviceuuid, paneluuid, title, isregistered, configjson + FROM locationinfo_externaldevice WHERE deviceuuid = :deviceid', ['deviceid' => $deviceid]); + if ($device === false) { + Message::addError('invalid-device-id', $deviceid); + Util::redirect('?do=locationinfo&show=external-devices'); + } + + $config = json_decode($device['configjson'], true); + if (!is_array($config)) { + $config = []; + } + $device['reboot_hour'] = $config['reboot_hour'] ?? -1; + $device['brightness_pct'] = $config['brightness_pct'] ?? 100; + + $panels = Database::simpleQuery('SELECT paneluuid, panelname FROM locationinfo_panel ORDER BY panelname ASC'); + foreach ($panels as &$panel) { + $panel['selected'] = ($panel['paneluuid'] === $device['paneluuid']) ? 'selected' : ''; + } + unset($panel); + + Render::addTemplate('page-external-device-edit', [ + 'device' => $device, + 'panels' => $panels, + ]); + } + + private function writeExternalDeviceConfig(): void + { + User::assertPermission('external-device.edit'); + $deviceid = Request::post('deviceid'); + $title = Request::post('title', '', 'string'); + $paneluuid = Request::post('paneluuid', null, 'string'); + if ($paneluuid === '') { + $paneluuid = null; + } + $isregistered = Request::post('isregistered', 0, 'int'); + $reboot_hour = Request::post('reboot_hour', -1, 'int'); + $brightness_pct = Request::post('brightness_pct', 100, 'int'); + + $config = [ + 'reboot_hour' => $reboot_hour, + 'brightness_pct' => $brightness_pct, + ]; + + $res = Database::exec('UPDATE locationinfo_externaldevice SET + title = :title, + paneluuid = :paneluuid, + isregistered = :isregistered, + configjson = :configjson + WHERE deviceuuid = :deviceid', [ + 'title' => $title, + 'paneluuid' => $paneluuid, + 'isregistered' => $isregistered, + 'configjson' => json_encode($config), + 'deviceid' => $deviceid, + ]); + + if ($res !== false) { + Message::addSuccess('config-saved'); + } + } + + private function deleteExternalDevice(): void + { + User::assertPermission('external-device.edit'); + $deviceid = Request::post('deviceid'); + $res = Database::exec('DELETE FROM locationinfo_externaldevice WHERE deviceuuid = :deviceid', ['deviceid' => $deviceid]); + if ($res === 1) { + Message::addSuccess('device-deleted', $deviceid); + } else { + Message::addWarning('invalid-device-id', $deviceid); + } + } + /** * AJAX */ @@ -943,6 +1064,7 @@ class Page_LocationInfo extends Page } } + #[NoReturn] private function showPanel(): void { $uuid = Request::get('uuid', false, 'string'); diff --git a/modules-available/locationinfo/permissions/permissions.json b/modules-available/locationinfo/permissions/permissions.json index be95a7bd..b1b7e8fa 100644 --- a/modules-available/locationinfo/permissions/permissions.json +++ b/modules-available/locationinfo/permissions/permissions.json @@ -16,5 +16,11 @@ }, "panel.assign-client": { "location-aware": true + }, + "external-device.list": { + "location-aware": false + }, + "external-device.edit": { + "location-aware": false } }
\ No newline at end of file diff --git a/modules-available/locationinfo/templates/page-external-device-edit.html b/modules-available/locationinfo/templates/page-external-device-edit.html new file mode 100644 index 00000000..388bdafb --- /dev/null +++ b/modules-available/locationinfo/templates/page-external-device-edit.html @@ -0,0 +1,66 @@ +<h2>{{lang_editExternalDevice}}</h2> + +<form method="post" action="?do=locationinfo"> + <input type="hidden" name="token" value="{{token}}"> + <input type="hidden" name="action" value="writeExternalDeviceConfig"> + <input type="hidden" name="deviceid" value="{{device.deviceuuid}}"> + + <div class="form-group"> + <label for="deviceuuid">{{lang_deviceUuid}}</label> + <input type="text" class="form-control" id="deviceuuid" value="{{device.deviceuuid}}" readonly> + </div> + + <div class="form-group"> + <label for="title">{{lang_deviceTitle}}</label> + <input type="text" class="form-control" name="title" id="title" value="{{device.title}}"> + </div> + + <div class="form-group"> + <label for="paneluuid">{{lang_panel}}</label> + <select name="paneluuid" id="paneluuid" class="form-control"> + <option value="">{{lang_pleaseSelect}}</option> + {{#panels}} + <option value="{{paneluuid}}" {{selected}}>{{panelname}}</option> + {{/panels}} + </select> + </div> + + <div class="form-group"> + <label for="reboot_hour">{{lang_rebootHour}}</label> + <span class="glyphicon glyphicon-question-sign" data-toggle="tooltip" title="{{lang_rebootHourTooltip}}"></span> + <input type="number" name="reboot_hour" id="reboot_hour" class="form-control" min="-1" max="23" value="{{device.reboot_hour}}"> + </div> + + <div class="form-group"> + <label for="brightness_pct">{{lang_brightness}}</label> + <span class="glyphicon glyphicon-question-sign" data-toggle="tooltip" title="{{lang_brightnessTooltip}}"></span> + <div class="input-group"> + <input type="number" name="brightness_pct" id="brightness_pct" class="form-control" min="1" max="100" value="{{device.brightness_pct}}"> + <span class="input-group-addon">%</span> + </div> + </div> + + <div class="buttonbar"> + <a href="?do=locationinfo&show=external-devices" class="btn btn-default">{{lang_cancel}}</a> + {{#perms.external-device.edit}} + <button type="submit" class="btn btn-primary"> + {{#device.isregistered}}{{lang_save}}{{/device.isregistered}} + {{^device.isregistered}}{{lang_register}}{{/device.isregistered}} + </button> + {{/perms.external-device.edit}} + </div> +</form> + +{{#perms.external-device.edit}} +<form method="post" action="?do=locationinfo" class="pull-right"> + <input type="hidden" name="token" value="{{token}}"> + <input type="hidden" name="action" value="deleteExternalDevice"> + <input type="hidden" name="deviceid" value="{{device.deviceuuid}}"> + <button type="submit" class="btn btn-danger" data-confirm="#confirm-delete-device"> + <span class="glyphicon glyphicon-trash"></span> + {{lang_delete}} + </button> +</form> +{{/perms.external-device.edit}} + +<div class="hidden" id="confirm-delete-device">{{lang_deleteConfirmation}}</div> diff --git a/modules-available/locationinfo/templates/page-external-devices.html b/modules-available/locationinfo/templates/page-external-devices.html new file mode 100644 index 00000000..17ace3ff --- /dev/null +++ b/modules-available/locationinfo/templates/page-external-devices.html @@ -0,0 +1,58 @@ +<h2>{{lang_externalDevicesTable}}</h2> + +<p>{{lang_externalDevicesTableHints}}</p> + +<table class="table table-hover"> + <thead> + <tr> + <th>{{lang_deviceTitle}}</th> + <th>{{lang_panel}}</th> + <th class="text-center slx-smallcol">{{lang_deviceRegistered}}</th> + <th class="slx-smallcol">{{lang_deviceLastSeen}}</th> + </tr> + </thead> + <tbody> + {{#devices}} + <tr> + <td> + {{#edit}} + <a href="?do=locationinfo&action=edit-device&deviceid={{deviceuuid}}">{{title}}</a> + {{/edit}} + {{^edit}} + {{title}} + {{/edit}} + <div class="small text-muted">{{deviceuuid}}</div> + </td> + <td>{{panelname}}</td> + <td class="text-center"> + {{#isregistered}}<span class="glyphicon glyphicon-ok"></span>{{/isregistered}} + </td> + <td> + {{lastseen_s}} + </td> + </tr> + {{/devices}} + {{^devices}} + <tr> + <td colspan="3" class="text-center italic"> + {{lang_noEntries}} + </td> + </tr> + {{/devices}} + </tbody> +</table> + +{{#edit}} +<div class="buttonbar text-right"> + <form method="post" action="?do=locationinfo"> + <input type="hidden" name="token" value="{{token}}"> + <input type="hidden" name="action" value="deleteUnregisteredDevices"> + <button type="submit" class="btn btn-danger" data-confirm="#confirm-delete-unregistered"> + <span class="glyphicon glyphicon-trash"></span> + {{lang_deleteUnregistered}} + </button> + </form> +</div> + +<div class="hidden" id="confirm-delete-unregistered">{{lang_deleteConfirmation}}</div> +{{/edit}}
\ No newline at end of file diff --git a/modules-available/locationinfo/templates/page-tabs.html b/modules-available/locationinfo/templates/page-tabs.html index 14730e2f..65453a13 100644 --- a/modules-available/locationinfo/templates/page-tabs.html +++ b/modules-available/locationinfo/templates/page-tabs.html @@ -11,5 +11,6 @@ <li class="{{class-panels}} {{perms.panel.list.disabled}}"><a href="?do=locationinfo&show=panels">{{lang_panels}}</a></li> <li class="{{class-locations}} {{perms.location.disabled}}"><a href="?do=locationinfo&show=locations">{{lang_locationSettings}}</a></li> <li class="{{class-backends}} {{perms.backend.disabled}}"><a href="?do=locationinfo&show=backends">{{lang_backends}}</a></li> + <li class="{{class-external-devices}} {{perms.external-device.list.disabled}}"><a href="?do=locationinfo&show=external-devices">{{lang_externalDevices}}</a></li> </ul> <br>
\ No newline at end of file diff --git a/modules-available/news/api.inc.php b/modules-available/news/api.inc.php index e7c3129f..9c1f281f 100644 --- a/modules-available/news/api.inc.php +++ b/modules-available/news/api.inc.php @@ -7,7 +7,7 @@ $type = Request::any('type', 'news', 'string'); if (Module::isAvailable('locations')) { $locationId = Request::any('location', 0, 'int'); if ($locationId === 0) { - $locationId = Location::getFromIp($_SERVER['REMOTE_ADDR']); + $locationId = Location::getFromIp(Util::getClientIp()); } if ($locationId === null) { $locations = [0]; diff --git a/modules-available/remoteaccess/api.inc.php b/modules-available/remoteaccess/api.inc.php index c558d126..16510ed9 100644 --- a/modules-available/remoteaccess/api.inc.php +++ b/modules-available/remoteaccess/api.inc.php @@ -1,7 +1,8 @@ <?php -$ip = $_SERVER['REMOTE_ADDR']; -if (substr($ip, 0, 7) === '::ffff:') $ip = substr($ip, 7); +$ip = Util::getClientIp(); +if ($ip === null) + ErrorHandler::traceError("could not determine client IP"); $password = Request::post('password', false, 'string'); if ($password !== false) { diff --git a/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbase.inc.php b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbase.inc.php index 9cd07388..b9638ff1 100644 --- a/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbase.inc.php +++ b/modules-available/serversetup-bwlp-ipxe/inc/scriptbuilderbase.inc.php @@ -47,10 +47,7 @@ abstract class ScriptBuilderBase public function __construct(?string $platform = null, ?string $serverIp = null, ?bool $slxExtensions = null) { - $this->clientIp = (string)$_SERVER['REMOTE_ADDR']; - if (substr($this->clientIp, 0, 7) === '::ffff:') { - $this->clientIp = substr($this->clientIp, 7); - } + $this->clientIp = (string)Util::getClientIp(); $this->serverIp = $serverIp ?? $_SERVER['SERVER_ADDR'] ?? Property::getServerIp(); $this->platform = $platform ?? Request::any('platform', null, 'string'); if ($this->platform !== null) { diff --git a/modules-available/statistics/api.inc.php b/modules-available/statistics/api.inc.php index 8f0f0810..a3f1757f 100644 --- a/modules-available/statistics/api.inc.php +++ b/modules-available/statistics/api.inc.php @@ -3,8 +3,9 @@ if (empty($_POST['type'])) die('Missing options.'); $type = mb_strtolower($_POST['type']); -$ip = $_SERVER['REMOTE_ADDR']; -if (substr($ip, 0, 7) === '::ffff:') $ip = substr($ip, 7); +$ip = Util::getClientIp(); +if ($ip === null) + ErrorHandler::traceError("could not determine client IP"); /* * Section 1/2 diff --git a/modules-available/sysconfig/addmodule_custommodule.inc.php b/modules-available/sysconfig/addmodule_custommodule.inc.php index bd94ebe9..746a0c23 100644 --- a/modules-available/sysconfig/addmodule_custommodule.inc.php +++ b/modules-available/sysconfig/addmodule_custommodule.inc.php @@ -40,7 +40,7 @@ class CustomModule_ProcessUpload extends AddModule_Base Message::addError('upload-failed', Util::uploadErrorString($_FILES['modulefile']['error'])); Util::redirect('?do=SysConfig', 400); } - $tempfile = '/tmp/bwlp-' . mt_rand(1, 100000) . '-' . crc32($_SERVER['REMOTE_ADDR']) . '.tmp'; + $tempfile = '/tmp/bwlp-' . mt_rand(1, 100000) . '-' . crc32(Util::getClientIp()) . '.tmp'; if (!move_uploaded_file($_FILES['modulefile']['tmp_name'], $tempfile)) { Message::addError('main.error-write', $tempfile); Util::redirect('?do=SysConfig'); diff --git a/modules-available/sysconfig/api.inc.php b/modules-available/sysconfig/api.inc.php index d639cbae..c37efe77 100644 --- a/modules-available/sysconfig/api.inc.php +++ b/modules-available/sysconfig/api.inc.php @@ -10,10 +10,9 @@ if (Request::any('action') === 'rebuild' && isLocalExecution()) { exit(1); } -$ip = $_SERVER['REMOTE_ADDR']; -if (substr($ip, 0, 7) === '::ffff:') { - $ip = substr($ip, 7); -} +$ip = Util::getClientIp(); +if ($ip === null) + ErrorHandler::traceError("could not determine client IP"); $uuid = Request::any('uuid', null, 'string'); if ($uuid !== null && strlen($uuid) !== 36) { diff --git a/modules-available/syslog/api.inc.php b/modules-available/syslog/api.inc.php index c810feb7..eb223ae1 100644 --- a/modules-available/syslog/api.inc.php +++ b/modules-available/syslog/api.inc.php @@ -42,8 +42,9 @@ if (($user = Request::post('export-user', false, 'string')) !== false) { $type = Request::post('type', Request::REQUIRED, 'string'); -$ip = $_SERVER['REMOTE_ADDR']; -if (substr($ip, 0, 7) === '::ffff:') $ip = substr($ip, 7); +$ip = Util::getClientIp(); +if ($ip === null) + ErrorHandler::traceError("could not determine client IP"); // TODO: Handle UUID in appropriate modules (optional) $uuid = Request::post('uuid', '', 'string'); diff --git a/modules-available/webinterface/api.inc.php b/modules-available/webinterface/api.inc.php index 271ccc60..3be7d882 100644 --- a/modules-available/webinterface/api.inc.php +++ b/modules-available/webinterface/api.inc.php @@ -21,7 +21,7 @@ if (empty($newCert)) { // Import will try to validate the certificate too $task = WebInterface::tmImportCustomCert($newKey, $newCert, 'api', - 'Applying new HTTPS certificate uploaded via API from ' . $_SERVER['REMOTE_ADDR']); + 'Applying new HTTPS certificate uploaded via API from ' . Util::getClientIp()); $task = Taskmanager::waitComplete($task, 10000); if (!Taskmanager::isTask($task)) { http_send_status(500); diff --git a/modules-available/webinterface/inc/webinterface.inc.php b/modules-available/webinterface/inc/webinterface.inc.php index d50acd50..2541ea24 100644 --- a/modules-available/webinterface/inc/webinterface.inc.php +++ b/modules-available/webinterface/inc/webinterface.inc.php @@ -11,6 +11,8 @@ class WebInterface public const PROP_API_KEY = 'webinterface.api-key'; + public const PROP_PROXIES_TRUSTED = 'webinterface.proxies-trusted'; + /** * Read data all handled domains from current certificate. * SAN takes precedence, if empty, we fall back to CN. @@ -139,4 +141,20 @@ class WebInterface Property::set(self::PROP_API_KEY, empty($key) ? null : $key); } + /** + * List of trusted proxies. Key is the IP address, value is optional comment. + */ + public static function getProxiesTrusted(): array + { + return Property::get(self::PROP_PROXIES_TRUSTED, []); + } + + /** + * Set list of trusted proxies. Key is the IP address, value is optional comment. + */ + public static function setProxiesTrusted(array $proxies): void + { + Property::set(self::PROP_PROXIES_TRUSTED, $proxies); + } + }
\ No newline at end of file diff --git a/modules-available/webinterface/lang/de/messages.json b/modules-available/webinterface/lang/de/messages.json index f87f3d3a..32cac43d 100644 --- a/modules-available/webinterface/lang/de/messages.json +++ b/modules-available/webinterface/lang/de/messages.json @@ -7,5 +7,6 @@ "https-on-cert-missing": "HTTPS ist aktiviert, das Zertifikat ist jedoch nicht vorhanden. Bitte nehmen Sie die HTTPS-Konfiguration erneut vor.", "https-want-redirect-is-plain": "Weiterleitung von HTTP auf HTTPS ist aktiviert, trotzdem scheint die Verbindung Ihres Browsers mit dem Server unverschl\u00fcsselt zu sein. Nehmen Sie die Konfiguration erneut vor und wenden Sie sich an den Support, wenn das Problem weiterhin besteht.", "invalid-domain": "Ung\u00fcltige Domain: {{0}}", + "invalid-proxy-ip": "Ung\u00fcltige Proxy-IP: {{0}}", "mw-acme-errors": "Fehler beim erneuern\/abrufen des Zertifikats via ACME" }
\ No newline at end of file diff --git a/modules-available/webinterface/lang/de/permissions.json b/modules-available/webinterface/lang/de/permissions.json index 213ebd8f..5694f37b 100644 --- a/modules-available/webinterface/lang/de/permissions.json +++ b/modules-available/webinterface/lang/de/permissions.json @@ -2,5 +2,6 @@ "access-page": "Seite sehen.", "edit.design": "Seitentitel und Hintergrundfarbe des Logos bearbeiten.", "edit.https": "HTTPS Einstellungen bearbeiten.", - "edit.password": "\u00c4ndern, ob Passwortfelder in der Web-Schnittstelle maskiert werden sollen." + "edit.password": "\u00c4ndern, ob Passwortfelder in der Web-Schnittstelle maskiert werden sollen.", + "edit.trusted-proxies": "Vertrauensw\u00fcrdige Proxies bearbeiten." }
\ No newline at end of file diff --git a/modules-available/webinterface/lang/de/template-tags.json b/modules-available/webinterface/lang/de/template-tags.json index dfe7bac5..28adcdb3 100644 --- a/modules-available/webinterface/lang/de/template-tags.json +++ b/modules-available/webinterface/lang/de/template-tags.json @@ -51,6 +51,10 @@ "lang_regenerate": "(Re)generieren", "lang_showPasswords": "Passw\u00f6rter anzeigen", "lang_suppliedSelected": "Der Server verwendet zur Zeit ein \u00fcber die Option \"Eigenes Zertifikat\" hochgeladenes Zertifikat.", + "lang_trustedProxiesDescription": "Wenn dieser Server hinter einem Reverse-Proxy betrieben wird (z.B. nginx oder HAProxy), sollten Sie hier die IP-Adressen dieser Proxies hinterlegen. Dies stellt sicher, dass die korrekte Client-IP-Adresse f\u00fcr Protokollierung und Zugriffskontrolle erkannt wird.", + "lang_trustedProxiesList": "Vertrauensw\u00fcrdige Proxy-Server", + "lang_trustedProxiesListHelp": "Geben Sie eine IP-Adresse pro Zeile ein.", + "lang_trustedProxiesSettings": "Vertrauensw\u00fcrdige Proxies", "lang_unknownSelected": "Unbekanntes oder ung\u00fcltiges Zertifikat vorhanden. Wahrscheinlich wurde der Server von einer alten Version aktualisiert. Um diese Meldung zu entfernen, die HTTPS-Konfiguration erneut vornehmen.", "lang_useHsts": "HSTS aktivieren (dies erh\u00f6ht die Sicherheit, kann aber bei sp\u00e4terem Deaktivieren von HTTPS zu Zugriffsproblemen f\u00fchren)", "lang_youreNotUsingHttps": "Sie besuchen diese Seite nicht per HTTPS (oder die HTTPS-Terminierung wird von einem vorgeschalteten Proxy \u00fcbernommen).", diff --git a/modules-available/webinterface/lang/en/messages.json b/modules-available/webinterface/lang/en/messages.json index b4083ec3..c55b670e 100644 --- a/modules-available/webinterface/lang/en/messages.json +++ b/modules-available/webinterface/lang/en/messages.json @@ -7,5 +7,6 @@ "https-on-cert-missing": "HTTPS is enabled, but the certificate is missing. Please redo the configuration steps.", "https-want-redirect-is-plain": "HTTP to HTTPS redirects are enabled, but the connection from your browser appears to be unencrypted. Please redo the HTTPS configuration and contact support if the problem persists.", "invalid-domain": "Invalid domain: {{0}}", + "invalid-proxy-ip": "Invalid proxy IP: {{0}}", "mw-acme-errors": "Error renewing\/requesting certificate via ACME" }
\ No newline at end of file diff --git a/modules-available/webinterface/lang/en/permissions.json b/modules-available/webinterface/lang/en/permissions.json index 8ebb2830..1d56f06b 100644 --- a/modules-available/webinterface/lang/en/permissions.json +++ b/modules-available/webinterface/lang/en/permissions.json @@ -2,5 +2,6 @@ "access-page": "View page.", "edit.design": "Edit page title and logo background color.", "edit.https": "Edit HTTPS settings.", - "edit.password": "Change whether password fields should be masked or not." + "edit.password": "Change whether password fields should be masked or not.", + "edit.trusted-proxies": "Edit trusted proxies." }
\ No newline at end of file diff --git a/modules-available/webinterface/lang/en/template-tags.json b/modules-available/webinterface/lang/en/template-tags.json index cc6e45a5..9c367377 100644 --- a/modules-available/webinterface/lang/en/template-tags.json +++ b/modules-available/webinterface/lang/en/template-tags.json @@ -51,6 +51,10 @@ "lang_regenerate": "(Re)generate", "lang_showPasswords": "Show passwords", "lang_suppliedSelected": "The server is currently using a certificate supplied using the \"Supply own certificate\" option.", + "lang_trustedProxiesDescription": "If this server is behind a reverse proxy (like nginx or HAProxy), you should list the IP addresses of those proxies here. This ensures that the correct client IP address is identified for logging and access control.", + "lang_trustedProxiesList": "Trusted proxy servers", + "lang_trustedProxiesListHelp": "Enter one IP address per line.", + "lang_trustedProxiesSettings": "Trusted Proxies", "lang_unknownSelected": "Unknown or invalid certificate in use. The server was probably updated from an old version while HTTPS was already enabled. Redo the HTTPS configuration steps to get rid of this message.", "lang_useHsts": "Use HSTS (increases security but might lead to problems accessing the site if you disable HTTPS later)", "lang_youreNotUsingHttps": "You're not using HTTPS to visit this website (or the HTTPS termination is done by a reverse proxy).", diff --git a/modules-available/webinterface/page.inc.php b/modules-available/webinterface/page.inc.php index b721da27..1b082000 100644 --- a/modules-available/webinterface/page.inc.php +++ b/modules-available/webinterface/page.inc.php @@ -29,6 +29,10 @@ class Page_WebInterface extends Page User::assertPermission("edit.https"); $this->handleApiKey(substr($action, 14)); break; + case 'trusted-proxies': + User::assertPermission("edit.proxies"); + $this->actionConfigureProxies(); + break; default: if ($action !== null) { Message::addWarning('main.invalid-action', $action); @@ -87,6 +91,24 @@ class Page_WebInterface extends Page Util::redirect('?do=WebInterface'); } + private function actionConfigureProxies(): void + { + $trustedProxies = Request::post('trusted-proxies-list', '', 'string'); + $trustedProxies = preg_split('/[\r\n]+/', $trustedProxies, 0, PREG_SPLIT_NO_EMPTY); + $cleaned = []; + foreach ($trustedProxies as $line) { + $line = preg_split("~(#|//|'|;)~", $line, 2, PREG_SPLIT_NO_EMPTY); + $ip = trim($line[0]); + $ipNormal = IpUtil::normalizeIp($ip); + if ($ipNormal !== null) { + $cleaned[$ip] = $line[1] ?? ''; + } else { + Message::addWarning('invalid-proxy-ip', $ip); + } + } + WebInterface::setProxiesTrusted($cleaned); + } + protected function doRender() { Render::addTemplate("heading"); @@ -192,6 +214,20 @@ class Page_WebInterface extends Page Permission::addGlobalTags($data['perms'], null, ['edit.password']); Render::addTemplate('passwords', $data); // + // Trusted Proxies + // + $list = ''; + foreach (WebInterface::getProxiesTrusted() as $ip => $comment) { + $list .= $ip; + if (!empty($comment)) { + $list .= " # $comment"; + } + $list .= "\r\n"; + } + $data = ['trustedProxiesList' => $list]; + Permission::addGlobalTags($data['perms'], null, ['edit.trusted-proxies']); + Render::addTemplate('trusted-proxies', $data); + // // Colors/Prefix // $data = array('prefix' => Property::get('page-title-prefix')); diff --git a/modules-available/webinterface/permissions/permissions.json b/modules-available/webinterface/permissions/permissions.json index ed81602a..cbdc4738 100644 --- a/modules-available/webinterface/permissions/permissions.json +++ b/modules-available/webinterface/permissions/permissions.json @@ -10,5 +10,8 @@ }, "edit.password": { "location-aware": false + }, + "edit.trusted-proxies": { + "location-aware": false } }
\ No newline at end of file diff --git a/modules-available/webinterface/templates/trusted-proxies.html b/modules-available/webinterface/templates/trusted-proxies.html new file mode 100644 index 00000000..a2461edc --- /dev/null +++ b/modules-available/webinterface/templates/trusted-proxies.html @@ -0,0 +1,25 @@ +<div class="panel panel-default"> + <div class="panel-heading">{{lang_trustedProxiesSettings}}</div> + <div class="panel-body"> + <p>{{lang_trustedProxiesDescription}}</p> + + <form action="?do=WebInterface" method="post"> + <input type="hidden" name="token" value="{{token}}"> + <input type="hidden" name="action" value="trusted-proxies"> + + <div class="form-group"> + <label for="trusted-proxies-list">{{lang_trustedProxiesList}}</label> + <textarea class="form-control" name="trusted-proxies-list" id="trusted-proxies-list" rows="10" + placeholder="10.0.0.1 # public proxy 192.168.1.0 # VPN proxy">{{trustedProxiesList}}</textarea> + <p class="help-block">{{lang_trustedProxiesListHelp}}</p> + </div> + + <div class="pull-right"> + <button type="submit" class="btn btn-primary" {{perms.edit.trusted-proxies.disabled}}> + <span class="glyphicon glyphicon-floppy-disk"></span> + {{lang_save}} + </button> + </div> + </form> + </div> +</div> |
