refactor: simplify Proxmox Firewall log handling by removing unnecessary log grouping and optimizing log fetching logic
This commit is contained in:
@@ -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([
|
||||
|
||||
Reference in New Issue
Block a user