Reduce security and reliability risk across the Proxmox extension by removing password exposure from Livewire state, hardening console auth/cookie handling, and improving sanitized error paths. Improve firewall and router performance by adding safer lazy loading, bounded pagination, cross-server compatibility guards, and lower-overhead API workflows. Made-with: Cursor
359 lines
16 KiB
PHP
359 lines
16 KiB
PHP
<?php
|
|
|
|
namespace Paymenter\Extensions\Servers\Proxmox\Http\Controllers;
|
|
|
|
use Carbon\Carbon;
|
|
use Illuminate\Http\Client\RequestException;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Routing\Controller;
|
|
use Paymenter\Extensions\Servers\Proxmox\Models\Server;
|
|
use Paymenter\Extensions\Servers\Proxmox\Proxmox;
|
|
|
|
class LiveTrafficController extends Controller
|
|
{
|
|
private const RATE_SCALE = 100 * 1024 * 1024; // 100 MiB/s default scale
|
|
|
|
public function __invoke(Server $server): JsonResponse
|
|
{
|
|
abort_if(auth()->guest() || (int) $server->user_id !== (int) auth()->id(), 403);
|
|
|
|
try {
|
|
$proxmox = new Proxmox;
|
|
|
|
// First, get the VM configuration to identify network interfaces
|
|
$configResponse = $proxmox->request(
|
|
'/nodes/' . $server->node->name . '/' . $server->node->vmType() . '/' . $server->vm_id . '/config'
|
|
);
|
|
$config = $configResponse->json()['data'] ?? [];
|
|
|
|
// Extract network interfaces from config
|
|
$interfaces = [];
|
|
foreach ($config as $key => $value) {
|
|
if (!preg_match('/^net\d+$/', (string) $key)) {
|
|
continue;
|
|
}
|
|
|
|
$netConfig = (string) $value;
|
|
preg_match('/bridge=([^,]+)/', $netConfig, $bridgeMatch);
|
|
preg_match('/^([^=]+)/', $netConfig, $modelMatch);
|
|
|
|
$interfaces[] = [
|
|
'id' => $key,
|
|
'name' => $key,
|
|
'bridge' => $bridgeMatch[1] ?? 'unknown',
|
|
'model' => $modelMatch[1] ?? 'virtio',
|
|
];
|
|
}
|
|
|
|
// Get RRD data once for all interfaces (more efficient)
|
|
$rrdResponse = null;
|
|
$rrdRows = null;
|
|
|
|
if (count($interfaces) > 0) {
|
|
try {
|
|
$rrdResponse = $proxmox->request(
|
|
'/nodes/' . $server->node->name . '/' . $server->node->vmType() . '/' . $server->vm_id . '/rrddata',
|
|
'get',
|
|
[
|
|
'timeframe' => 'hour',
|
|
'cf' => 'AVERAGE',
|
|
]
|
|
);
|
|
|
|
$rrdJson = $rrdResponse->json();
|
|
$rrdRows = $rrdJson['data'] ?? [];
|
|
} catch (\Throwable $e) {
|
|
// If RRD request fails, we'll handle it per interface
|
|
$rrdRows = [];
|
|
}
|
|
}
|
|
|
|
usort($interfaces, static fn (array $a, array $b) => strcmp($a['id'], $b['id']));
|
|
$primaryInterfaceId = $interfaces[0]['id'] ?? 'net0';
|
|
|
|
// Get RRD data for each interface
|
|
$interfaceData = [];
|
|
$totalInRate = 0;
|
|
$totalOutRate = 0;
|
|
|
|
foreach ($interfaces as $interface) {
|
|
try {
|
|
// RRD data uses netin0, netout0, netin1, netout1, etc.
|
|
$ifNum = str_replace('net', '', $interface['id']);
|
|
$dsNames = "netin{$ifNum},netout{$ifNum}";
|
|
|
|
$rows = $rrdRows;
|
|
|
|
if (!is_array($rows) || count($rows) === 0) {
|
|
// If no RRD data available, still create interface entry with zero rates
|
|
$interfaceData[] = [
|
|
'id' => $interface['id'],
|
|
'name' => 'Network Statistics',
|
|
'bridge' => $interface['bridge'],
|
|
'model' => $interface['model'],
|
|
'downloadRate' => 0,
|
|
'uploadRate' => 0,
|
|
'totalRate' => 0,
|
|
'totalIn' => 0,
|
|
'totalOut' => 0,
|
|
'history' => [
|
|
'download' => array_fill(0, 20, 0),
|
|
'upload' => array_fill(0, 20, 0),
|
|
],
|
|
];
|
|
continue;
|
|
}
|
|
|
|
// Get the latest row with valid data
|
|
$currentRow = null;
|
|
$netinKey = "netin{$ifNum}";
|
|
$netoutKey = "netout{$ifNum}";
|
|
|
|
// Try to find interface-specific data first
|
|
foreach (array_reverse($rows) as $row) {
|
|
if (isset($row[$netinKey], $row[$netoutKey])) {
|
|
$currentRow = $row;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If interface-specific data not found, use aggregate data as fallback for net0 only
|
|
// For other interfaces, use zeros to ensure each interface has unique data
|
|
if (!$currentRow) {
|
|
if ($interface['id'] === $primaryInterfaceId) {
|
|
// For net0, use aggregate data as fallback (since aggregate typically = net0)
|
|
foreach (array_reverse($rows) as $row) {
|
|
if (isset($row['netin'], $row['netout'])) {
|
|
$currentRow = $row;
|
|
$currentRow[$netinKey] = $row['netin'];
|
|
$currentRow[$netoutKey] = $row['netout'];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// For other interfaces or if net0 fallback failed, use zeros
|
|
if (!$currentRow) {
|
|
$currentRow = [$netinKey => 0, $netoutKey => 0];
|
|
}
|
|
}
|
|
|
|
$inRate = (float) ($currentRow[$netinKey] ?? 0);
|
|
$outRate = (float) ($currentRow[$netoutKey] ?? 0);
|
|
|
|
$totalInRate += $inRate;
|
|
$totalOutRate += $outRate;
|
|
|
|
// Calculate history for sparkline (last 20 samples)
|
|
$history = [
|
|
'download' => [],
|
|
'upload' => []
|
|
];
|
|
|
|
$sampleCount = min(20, count($rows));
|
|
$samples = array_slice($rows, -$sampleCount);
|
|
|
|
// Check if interface-specific keys exist in ANY sample
|
|
// This is more lenient - if the keys exist in some samples, we'll use them
|
|
$hasInterfaceSpecificData = false;
|
|
foreach ($samples as $sample) {
|
|
if (isset($sample[$netinKey]) || isset($sample[$netoutKey])) {
|
|
$hasInterfaceSpecificData = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Use interface-specific data if available, otherwise use zeros or aggregate for net0 only
|
|
// This ensures each interface has its own unique graph data (not duplicated)
|
|
foreach ($samples as $sample) {
|
|
if ($hasInterfaceSpecificData && isset($sample[$netinKey], $sample[$netoutKey])) {
|
|
// Use interface-specific data when available
|
|
$history['download'][] = (float) $sample[$netinKey];
|
|
$history['upload'][] = (float) $sample[$netoutKey];
|
|
} else {
|
|
// For net0, use aggregate data as fallback (since aggregate typically = net0)
|
|
// For other interfaces, use zeros to ensure unique graphs per interface
|
|
if ($interface['id'] === $primaryInterfaceId) {
|
|
$history['download'][] = (float) ($sample['netin'] ?? 0);
|
|
$history['upload'][] = (float) ($sample['netout'] ?? 0);
|
|
} else {
|
|
// Use zeros for other interfaces without specific data
|
|
// This ensures each interface has its own unique graph (even if zero)
|
|
$history['download'][] = 0.0;
|
|
$history['upload'][] = 0.0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get aggregate totals for net0 fallback (will be set later from status endpoint)
|
|
// For now, set per-interface totals
|
|
$totalIn = 0;
|
|
$totalOut = 0;
|
|
|
|
// For net0, we'll use aggregate totals (set later)
|
|
// For other interfaces, use zeros to ensure uniqueness
|
|
if ($interface['id'] !== $primaryInterfaceId) {
|
|
$totalIn = 0;
|
|
$totalOut = 0;
|
|
}
|
|
// Note: net0 totals will be set from aggregate data after status endpoint call
|
|
|
|
$interfaceData[] = [
|
|
'id' => $interface['id'],
|
|
'name' => 'Network Statistics',
|
|
'bridge' => $interface['bridge'],
|
|
'model' => $interface['model'],
|
|
'downloadRate' => $inRate,
|
|
'uploadRate' => $outRate,
|
|
'totalRate' => $inRate + $outRate,
|
|
'totalIn' => $totalIn,
|
|
'totalOut' => $totalOut,
|
|
'history' => $history,
|
|
];
|
|
|
|
} catch (\Throwable $e) {
|
|
// Skip this interface if there's an error
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// If no interface-specific data, fall back to general RRD data
|
|
if (empty($interfaceData)) {
|
|
$rows = is_array($rrdRows) ? $rrdRows : [];
|
|
|
|
if (is_array($rows) && count($rows) > 0) {
|
|
$currentRow = null;
|
|
foreach (array_reverse($rows) as $row) {
|
|
if (isset($row['netin'], $row['netout'])) {
|
|
$currentRow = $row;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($currentRow) {
|
|
$inRate = (float) ($currentRow['netin'] ?? 0);
|
|
$outRate = (float) ($currentRow['netout'] ?? 0);
|
|
|
|
// Calculate history
|
|
$history = [
|
|
'download' => [],
|
|
'upload' => []
|
|
];
|
|
|
|
$sampleCount = min(20, count($rows));
|
|
$samples = array_slice($rows, -$sampleCount);
|
|
|
|
foreach ($samples as $sample) {
|
|
$history['download'][] = (float) ($sample['netin'] ?? 0);
|
|
$history['upload'][] = (float) ($sample['netout'] ?? 0);
|
|
}
|
|
|
|
$interfaceData[] = [
|
|
'id' => 'net0',
|
|
'name' => 'Network Statistics',
|
|
'bridge' => 'default',
|
|
'model' => 'virtio',
|
|
'downloadRate' => $inRate,
|
|
'uploadRate' => $outRate,
|
|
'totalRate' => $inRate + $outRate,
|
|
'totalIn' => 0, // Will be set from aggregate data later
|
|
'totalOut' => 0, // Will be set from aggregate data later
|
|
'history' => $history,
|
|
];
|
|
|
|
$totalInRate = $inRate;
|
|
$totalOutRate = $outRate;
|
|
}
|
|
}
|
|
}
|
|
|
|
$timestamp = now();
|
|
|
|
// Calculate dynamic scale based on max rate
|
|
$maxRate = max($totalInRate, $totalOutRate);
|
|
$scale = self::RATE_SCALE;
|
|
|
|
if ($maxRate > 0) {
|
|
// Round up to nice numbers
|
|
if ($maxRate > 1000 * 1024 * 1024) { // > 1 GiB/s
|
|
$scale = ceil($maxRate / (100 * 1024 * 1024)) * (100 * 1024 * 1024);
|
|
} elseif ($maxRate > 100 * 1024 * 1024) { // > 100 MiB/s
|
|
$scale = ceil($maxRate / (50 * 1024 * 1024)) * (50 * 1024 * 1024);
|
|
} elseif ($maxRate > 10 * 1024 * 1024) { // > 10 MiB/s
|
|
$scale = ceil($maxRate / (10 * 1024 * 1024)) * (10 * 1024 * 1024);
|
|
} else {
|
|
$scale = max(10 * 1024 * 1024, ceil($maxRate / (1024 * 1024)) * (1024 * 1024));
|
|
}
|
|
}
|
|
|
|
// Get total statistics from status endpoint
|
|
$statusResponse = $proxmox->request(
|
|
'/nodes/' . $server->node->name . '/' . $server->node->vmType() . '/' . $server->vm_id . '/status/current'
|
|
);
|
|
$status = $statusResponse->json()['data'] ?? [];
|
|
|
|
$totalIn = (float) ($status['netin'] ?? 0);
|
|
$totalOut = (float) ($status['netout'] ?? 0);
|
|
|
|
// Update the first configured interface totals with aggregate data
|
|
// Other interfaces keep zeros to ensure uniqueness
|
|
foreach ($interfaceData as &$interface) {
|
|
if ($interface['id'] === $primaryInterfaceId) {
|
|
$interface['totalIn'] = $totalIn;
|
|
$interface['totalOut'] = $totalOut;
|
|
}
|
|
}
|
|
unset($interface); // Break reference
|
|
|
|
return response()->json([
|
|
'interfaces' => $interfaceData,
|
|
'total' => [
|
|
'downloadRate' => $totalInRate,
|
|
'uploadRate' => $totalOutRate,
|
|
'combinedRate' => $totalInRate + $totalOutRate,
|
|
'downloadTotal' => $totalIn,
|
|
'uploadTotal' => $totalOut,
|
|
'combinedTotal' => $totalIn + $totalOut,
|
|
],
|
|
'scale' => $scale,
|
|
'updatedAt' => $timestamp->toIso8601String(),
|
|
'timestamp' => $timestamp->timestamp,
|
|
]);
|
|
|
|
} catch (RequestException $e) {
|
|
return response()->json([
|
|
'interfaces' => [],
|
|
'total' => [
|
|
'downloadRate' => 0,
|
|
'uploadRate' => 0,
|
|
'combinedRate' => 0,
|
|
'downloadTotal' => 0,
|
|
'uploadTotal' => 0,
|
|
'combinedTotal' => 0,
|
|
],
|
|
'scale' => self::RATE_SCALE,
|
|
'updatedAt' => now()->toIso8601String(),
|
|
'timestamp' => now()->timestamp,
|
|
'error' => 'Unable to load live traffic from hypervisor.',
|
|
'code' => 'proxmox_request_failed',
|
|
], 502);
|
|
} catch (\Throwable $e) {
|
|
return response()->json([
|
|
'interfaces' => [],
|
|
'total' => [
|
|
'downloadRate' => 0,
|
|
'uploadRate' => 0,
|
|
'combinedRate' => 0,
|
|
'downloadTotal' => 0,
|
|
'uploadTotal' => 0,
|
|
'combinedTotal' => 0,
|
|
],
|
|
'scale' => self::RATE_SCALE,
|
|
'updatedAt' => now()->toIso8601String(),
|
|
'timestamp' => now()->timestamp,
|
|
'error' => 'An internal error occurred.',
|
|
'code' => 'internal_error',
|
|
], 500);
|
|
}
|
|
}
|
|
} |