From 3f28a92eceaff65f85778f1407fb90092a15d7f4 Mon Sep 17 00:00:00 2001 From: "Joseph.Rawlings" Date: Tue, 2 Dec 2025 16:54:50 -0500 Subject: [PATCH] refactor: simplify Proxmox Firewall log handling by removing unnecessary log grouping and optimizing log fetching logic --- .../Servers/Proxmox/Livewire/Firewall.php | 251 +----- .../Servers/Proxmox/views/firewall.blade.php | 749 +----------------- 2 files changed, 25 insertions(+), 975 deletions(-) diff --git a/extensions/Servers/Proxmox/Livewire/Firewall.php b/extensions/Servers/Proxmox/Livewire/Firewall.php index c93db3f65..4b6975049 100644 --- a/extensions/Servers/Proxmox/Livewire/Firewall.php +++ b/extensions/Servers/Proxmox/Livewire/Firewall.php @@ -21,7 +21,7 @@ class Firewall extends Component public array $networkAdapters = []; public array $adapterFirewallStates = []; public array $firewallLogs = []; - public int $logLimit = 500; + public int $logLimit = 100; public array $availableIps = []; public bool $showDeleteModal = false; public ?int $deletingRulePos = null; @@ -148,11 +148,6 @@ class Firewall extends Component $newState = $currentState ? 0 : 1; $payload = ['enable' => $newState]; - // If enabling firewall, also disable DHCP - if ($newState === 1) { - $payload['dhcp'] = 0; - } - $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/options', 'put', @@ -807,10 +802,7 @@ class Firewall extends Component $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/log', 'get', - [ - 'limit' => $this->logLimit, - 'start' => 0 - ] + ['limit' => $this->logLimit] ); if (! $response->successful()) { @@ -832,50 +824,17 @@ class Firewall extends Component return; } + // Proxmox firewall log endpoint returns text/plain format + // Each log entry is on a new line $body = $response->body(); $rawLogs = []; - $contentType = strtolower($response->header('Content-Type', '')); if (! empty($body)) { - if (str_contains($contentType, 'application/json')) { - // When Accept header allows JSON, Proxmox returns structured data - $jsonData = $response->json(); - $entries = []; - - if (is_array($jsonData)) { - $entries = $jsonData['data'] ?? $jsonData; - } - - if (is_array($entries)) { - foreach ($entries as $entry) { - $logText = null; - - if (is_array($entry)) { - $logText = $entry['log'] - ?? $entry['text'] - ?? $entry['t'] - ?? $entry['n'] - ?? null; - } elseif (is_string($entry)) { - $logText = $entry; - } - - if (is_string($logText) && trim($logText) !== '') { - $rawLogs[] = trim($logText); - } - } - } - } else { - // Proxmox firewall log endpoint may also return text/plain - // Each log entry is on a new line (handle both \n and \r\n) - $lines = preg_split('/\r\n|\r|\n/', $body); - foreach ($lines as $line) { - $line = trim((string) $line); - if ($line !== '') { - $rawLogs[] = $line; - } - } - } + // Split by newlines and filter empty lines + $lines = explode("\n", trim($body)); + $rawLogs = array_filter($lines, static function ($line) { + return ! empty(trim($line)); + }); } \Log::debug('Firewall logs fetched from Proxmox API', [ @@ -883,12 +842,11 @@ class Firewall extends Component 'body_length' => strlen($body), 'content_type' => $response->header('Content-Type'), 'sample' => ! empty($rawLogs) ? substr($rawLogs[array_key_first($rawLogs)], 0, 200) : null, - 'first_5_lines' => array_slice($rawLogs, 0, 5), ]); $this->firewallLogs = []; - foreach ($rawLogs as $index => $logEntry) { + foreach ($rawLogs as $logEntry) { $logText = ''; if (is_string($logEntry)) { @@ -910,36 +868,14 @@ class Firewall extends Component continue; } - $parsedLog = $this->parseFirewallLog($logText); - $this->firewallLogs[] = $parsedLog; - - // Debug first few parsed logs - if ($index < 3) { - \Log::debug('Parsed firewall log entry', [ - 'index' => $index, - 'raw' => substr($logText, 0, 200), - 'parsed' => $parsedLog, - ]); - } + $this->firewallLogs[] = $this->parseFirewallLog($logText); } - // Group similar logs - $this->firewallLogs = $this->groupSimilarLogs($this->firewallLogs); - - \Log::debug('Firewall logs processing complete', [ - 'raw_logs_count' => count($rawLogs), - 'parsed_logs_count' => count($this->firewallLogs), - 'sample_parsed_logs' => array_slice($this->firewallLogs, 0, 3), - ]); - if (empty($this->firewallLogs) && ! empty($rawLogs)) { $this->logError = 'Logs received but could not be processed.'; } elseif (empty($this->firewallLogs)) { $this->logError = 'No firewall logs available. Ensure firewall logging is enabled in Proxmox (Datacenter > Firewall > Options).'; } - - // Dispatch event to update frontend without full re-render - $this->dispatch('logs-updated', logs: $this->firewallLogs); } catch (\Exception $e) { $errorMessage = $e->getMessage(); if (method_exists($e, 'response') && $e->response()) { @@ -963,76 +899,6 @@ class Firewall extends Component } } - /** - * Group similar log entries together - */ - protected function groupSimilarLogs(array $logs): array - { - if (empty($logs)) { - return []; - } - - $grouped = []; - $groups = []; - - foreach ($logs as $log) { - // Create a signature for grouping: action + protocol + source + destination + dest_port - $signature = sprintf( - '%s|%s|%s|%s|%s', - $log['action'] ?? '', - $log['protocol'] ?? '', - $log['source'] ?? '', - $log['destination'] ?? '', - $log['destination_port'] ?? '' - ); - - if (!isset($groups[$signature])) { - $groups[$signature] = [ - 'signature' => $signature, - 'count' => 0, - 'first_log' => $log, - 'last_timestamp' => $log['timestamp_iso'] ?? null, - 'first_timestamp' => $log['timestamp_iso'] ?? null, - 'logs' => [], - ]; - } - - $groups[$signature]['count']++; - $groups[$signature]['logs'][] = $log; - - // Update timestamps - if ($log['timestamp_iso']) { - if (!$groups[$signature]['first_timestamp'] || $log['timestamp_iso'] < $groups[$signature]['first_timestamp']) { - $groups[$signature]['first_timestamp'] = $log['timestamp_iso']; - } - if (!$groups[$signature]['last_timestamp'] || $log['timestamp_iso'] > $groups[$signature]['last_timestamp']) { - $groups[$signature]['last_timestamp'] = $log['timestamp_iso']; - } - } - } - - // Convert groups to array and add group metadata - foreach ($groups as $group) { - $firstLog = $group['first_log']; - $firstLog['is_group'] = $group['count'] > 1; - $firstLog['group_count'] = $group['count']; - $firstLog['grouped_logs'] = $group['logs']; - $firstLog['group_first_timestamp'] = $group['first_timestamp']; - $firstLog['group_last_timestamp'] = $group['last_timestamp']; - - $grouped[] = $firstLog; - } - - // Sort by most recent timestamp - usort($grouped, function($a, $b) { - $aTime = $a['group_last_timestamp'] ?? $a['timestamp_iso'] ?? ''; - $bTime = $b['group_last_timestamp'] ?? $b['timestamp_iso'] ?? ''; - return strcmp($bTime, $aTime); // Descending order (most recent first) - }); - - return $grouped; - } - protected function parseFirewallLog(string $logText): array { $result = [ @@ -1135,101 +1001,6 @@ class Firewall extends Component return $result; } - /** - * Check if an IP address is public (callable from frontend) - */ - public function checkIsPublicIP(string $ip): bool - { - return $this->isPublicIP($ip); - } - - /** - * Check if an IP address is public (not private/localhost) - */ - protected function isPublicIP(string $ip): bool - { - $ip = $this->extractIPFromString($ip); - - if (empty($ip)) { - return false; - } - - // Check for localhost - if (in_array(strtolower($ip), ['localhost', '127.0.0.1', '::1'])) { - return false; - } - - // Validate IP format - if (!filter_var($ip, FILTER_VALIDATE_IP)) { - return false; - } - - // Check if it's a private IP - if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { - return true; - } - - return false; - } - - /** - * Extract IP address from a string (removes port if present) - */ - protected function extractIPFromString(string $input): string - { - $input = trim($input); - - // Handle IPv6 with port: [2001:db8::1]:8080 - if (preg_match('/^\[([^\]]+)\](?::\d+)?$/', $input, $matches)) { - return $matches[1]; - } - - // Handle IPv4 with port: 192.168.1.1:8080 - if (preg_match('/^([^:]+):\d+$/', $input, $matches)) { - // Make sure it's not IPv6 without brackets - if (!str_contains($matches[1], ':')) { - return $matches[1]; - } - } - - // Return as-is if no port detected - return $input; - } - - /** - * Fetch IP geolocation data - */ - public function fetchIPLocation($ip): array - { - if (empty($ip)) { - return ['status' => 'fail', 'message' => 'IP address is required']; - } - - // Extract and validate IP address - $cleanIP = $this->extractIPFromString((string)$ip); - - if (!$this->isPublicIP($cleanIP)) { - return ['status' => 'fail', 'message' => 'Not a valid public IP address']; - } - - try { - // Use HTTP protocol to fetch geolocation data - $response = \Illuminate\Support\Facades\Http::timeout(5)->get("http://ip-api.com/json/{$cleanIP}"); - - if ($response->successful()) { - return $response->json(); - } - - return ['status' => 'fail', 'message' => 'Failed to fetch location data']; - } catch (\Exception $e) { - \Log::error('Error fetching IP location', [ - 'ip' => $cleanIP, - 'error' => $e->getMessage(), - ]); - return ['status' => 'fail', 'message' => 'Error: ' . $e->getMessage()]; - } - } - public function render() { return view('proxmox::firewall')->layoutData([ diff --git a/extensions/Servers/Proxmox/views/firewall.blade.php b/extensions/Servers/Proxmox/views/firewall.blade.php index 3e34daaff..4da1f5735 100644 --- a/extensions/Servers/Proxmox/views/firewall.blade.php +++ b/extensions/Servers/Proxmox/views/firewall.blade.php @@ -74,9 +74,6 @@ opacity: 0.5; cursor: not-allowed; } - .firewall-log-search-input { - padding-left: 2.75rem; - }
@@ -328,41 +325,11 @@ searchTerm: '', sortField: 'timestamp_iso', sortDirection: 'desc', - initialized: false, - seenRaws: new Set(), initialize(initialLogs) { - if (this.initialized) return; - - if (initialLogs && initialLogs.length > 0) { - this.logs = initialLogs.map((log, index) => ({ - index, - ...log, - })); - // Track which logs we've seen - this.logs.forEach(log => { - if (log.raw) this.seenRaws.add(log.raw); - }); - this.initialized = true; - } - }, - updateLogs(newLogs) { - if (!newLogs || newLogs.length === 0) return; - - // Filter out logs we've already seen - const logsToAdd = newLogs.filter(log => { - if (!log.raw) return false; - if (this.seenRaws.has(log.raw)) return false; - this.seenRaws.add(log.raw); - return true; - }); - - if (logsToAdd.length > 0) { - // Add new logs to the beginning (most recent first) - this.logs = [...logsToAdd, ...this.logs].map((log, index) => ({ - index, - ...log, - })); - } + this.logs = (initialLogs || []).map((log, index) => ({ + index, + ...log, + })); }, sort(field) { if (this.sortField === field) { @@ -409,14 +376,6 @@ }, }" x-init="initialize(@js($firewallLogs))" - @logs-updated.window="updateLogs($event.detail.logs || $event.detail[0]?.logs || $event.detail[0] || [])" - x-effect=" - // Watch for changes in the backend data and merge new logs - const backendLogs = @js($firewallLogs); - if (initialized && backendLogs && backendLogs.length > 0) { - updateLogs(backendLogs); - } - " class="space-y-4" >
@@ -426,7 +385,7 @@ type="text" x-model="searchTerm" placeholder="Search logs..." - class="firewall-log-search-input w-full pr-4 py-2 border border-neutral rounded-lg bg-background focus:outline-none focus:ring-2 focus:ring-primary/40" + class="w-full pl-10 pr-4 py-2 border border-neutral rounded-lg bg-background focus:outline-none focus:ring-2 focus:ring-primary/40" />
@@ -490,543 +449,20 @@ @@ -1295,161 +731,4 @@ @endif
- -