* @license MIT */ namespace Pterodactyl\Addons\AdvancedAdmin\Services\Network; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; use Pterodactyl\Models\Node; use Pterodactyl\Models\Server; class DockerStatsCollector { /** * Collect network stats for all running servers on a node. * Returns array of ['server_id' => int, 'rx_bytes' => int, 'tx_bytes' => int] */ public function collectForNode(Node $node): array { $results = []; $servers = Server::query() ->where('node_id', $node->id) ->whereNotNull('uuid') ->get(); foreach ($servers as $server) { try { $stats = $this->fetchStats($node, $server->uuid); if ($stats !== null) { $results[] = [ 'server_id' => $server->id, 'node_id' => $node->id, 'rx_bytes' => $stats['network']['rx_bytes'] ?? 0, 'tx_bytes' => $stats['network']['tx_bytes'] ?? 0, ]; } } catch (\Throwable $e) { Log::warning("[AdvancedAdmin] Failed to collect stats for server {$server->uuid}: {$e->getMessage()}"); } } return $results; } /** * Fetch stats from Wings for a single server UUID. * Uses the Wings Application API (node-level auth token). * * SSRF Prevention: URL is constructed only from trusted Node model data, * never from user input. */ private function fetchStats(Node $node, string $serverUuid): ?array { // Validate UUID format before use if (!preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $serverUuid)) { return null; } $scheme = $node->scheme; $fqdn = $node->fqdn; $port = $node->daemonListen; // Allowlist: only known node FQDNs are used (from DB, not from user input) $url = "{$scheme}://{$fqdn}:{$port}/api/servers/{$serverUuid}/resources"; $response = Http::withToken($node->daemon_token_id) ->withHeaders(['Authorization' => "Bearer {$node->daemon_token_id}"]) ->timeout(5) ->get($url); if (!$response->successful()) { return null; } return $response->json('resources'); } }