0) { $cpuPercent = round(max(0.0, min(100.0, (1.0 - ($dIdle / $dTotal)) * 100.0)), 1); } } $loadAvg = sys_getloadavg() ?: [0.0, 0.0, 0.0]; $cpuCores = 1; $cpuModel = 'Processeur Debian'; if (is_readable('/proc/cpuinfo')) { $cpuinfo = (string)file_get_contents('/proc/cpuinfo'); $cpuCores = max(1, substr_count($cpuinfo, 'processor')); if (preg_match('/model name\s*:\s*(.+)/i', $cpuinfo, $m)) { $cpuModel = trim($m[1]); } } // 2. Mémoire RAM & Swap (/proc/meminfo) $mem = self::readMemInfo(); // 3. Disque HDD/SSD (/) $diskTotal = (float)(@disk_total_space('/') ?: 1); $diskFree = (float)(@disk_free_space('/') ?: 0); $diskUsed = max(0.0, $diskTotal - $diskFree); $diskPct = round(($diskUsed / $diskTotal) * 100.0, 1); // 4. Réseau (/proc/net/dev) $net = self::readNetDev(); $rxRateKbps = 0.0; $txRateKbps = 0.0; if (isset($prev['net'], $prev['timestamp']) && ($now - $prev['timestamp']) > 0.2) { $dt = $now - $prev['timestamp']; $dRx = max(0, $net['rx_bytes'] - $prev['net']['rx_bytes']); $dTx = max(0, $net['tx_bytes'] - $prev['net']['tx_bytes']); // En Ko/s (KB/s) $rxRateKbps = round(($dRx / $dt) / 1024, 2); $txRateKbps = round(($dTx / $dt) / 1024, 2); } // Sauvegarde de l'échantillon pour le calcul différentiel CPU & Réseau self::savePrevSample([ 'timestamp' => $now, 'cpu' => $cpuStat, 'net' => $net, ]); // 5. Uptime Debian (/proc/uptime) $uptimeSeconds = 0; if (is_readable('/proc/uptime')) { $parts = explode(' ', trim((string)file_get_contents('/proc/uptime'))); $uptimeSeconds = (int)floor((float)($parts[0] ?? 0)); } // 6. Version Debian & État temps réel Asterisk via AMI (Postes enregistrés, latence RTT, appels actifs) $debianVer = is_readable('/etc/debian_version') ? trim((string)file_get_contents('/etc/debian_version')) : '13'; $asteriskInfo = self::readAsteriskLiveStatus($db); // 7. Statut des Services Debian & Fail2ban / Pare-feu $fwStatus = FirewallManager::getStatus(); $services = self::readServicesStatus($asteriskInfo, $fwStatus); return [ 'timestamp' => date('H:i:s'), 'hostname' => gethostname() ?: 'debian-pbx', 'os_version' => "Debian {$debianVer}", 'uptime' => self::formatDuration($uptimeSeconds), 'cpu' => [ 'percent' => $cpuPercent, 'cores' => $cpuCores, 'model' => $cpuModel, 'load_1' => round((float)$loadAvg[0], 2), 'load_5' => round((float)$loadAvg[1], 2), 'load_15' => round((float)$loadAvg[2], 2), ], 'memory' => $mem, 'disk' => [ 'total_gb' => round($diskTotal / 1073741824, 2), 'used_gb' => round($diskUsed / 1073741824, 2), 'free_gb' => round($diskFree / 1073741824, 2), 'percent' => $diskPct, ], 'network' => [ 'interface' => $net['interface'], 'ip' => $net['ip'], 'rx_kbps' => $rxRateKbps, 'tx_kbps' => $txRateKbps, 'rx_total_mb' => round($net['rx_bytes'] / 1048576, 1), 'tx_total_mb' => round($net['tx_bytes'] / 1048576, 1), ], 'asterisk' => $asteriskInfo, 'services' => $services, 'security' => [ 'firewall_enabled' => (bool)($fwStatus['settings']['firewall_enabled'] ?? false), 'fail2ban_running' => (bool)($fwStatus['is_running'] ?? false), 'admin_port' => (int)($fwStatus['settings']['admin_https_port'] ?? 8443), 'strict_port80' => (bool)($fwStatus['settings']['certbot_strict_port80'] ?? false), 'total_banned' => (int)($fwStatus['total_banned'] ?? 0), 'banned_ips' => $fwStatus['banned_ips'] ?? [], 'jails' => $fwStatus['jails'] ?? [], ], ]; } private static function readServicesStatus(array $astInfo, array $fwStatus): array { $out = trim((string)@shell_exec('systemctl is-active asterisk nginx php8.4-fpm fail2ban 2>/dev/null')); $lines = preg_split('/\r?\n/', $out) ?: []; $astActive = ($lines[0] ?? '') === 'active' || ($astInfo['uptime'] ?? '') !== 'Hors-ligne'; $nginxActive = ($lines[1] ?? '') === 'active'; $phpActive = ($lines[2] ?? '') === 'active'; $f2bActive = ($lines[3] ?? '') === 'active' || !empty($fwStatus['is_running']); $fwEnabled = !empty($fwStatus['settings']['firewall_enabled']) && $nginxActive; $adminPort = $fwStatus['settings']['admin_https_port'] ?? 8443; $isStrictPort80 = !empty($fwStatus['settings']['certbot_strict_port80']); return [ [ 'id' => 'asterisk', 'name' => 'Asterisk 22 PBX', 'desc' => 'Moteur SIP PJSIP & Dialplan', 'port' => '5060 / 5061', 'active' => $astActive, 'status_text' => $astActive ? 'Opérationnel' : 'Arrêté', 'uptime' => $astInfo['uptime'] ?? 'Actif', ], [ 'id' => 'nginx', 'name' => 'Serveur Web Nginx', 'desc' => 'Portail GUI & Reverse Proxy', 'port' => "80 / 443 / {$adminPort}", 'active' => $nginxActive, 'status_text' => $nginxActive ? 'Opérationnel' : 'Arrêté', 'uptime' => 'En ligne', ], [ 'id' => 'php', 'name' => 'PHP 8.4 FPM', 'desc' => 'API FastCGI & SQLite', 'port' => 'Unix Socket', 'active' => $phpActive, 'status_text' => $phpActive ? 'Opérationnel' : 'Arrêté', 'uptime' => 'En ligne', ], [ 'id' => 'fail2ban', 'name' => 'Fail2ban IDS / IPS', 'desc' => 'Jails [asterisk, web, ssh]', 'port' => 'Surveillance logs', 'active' => $f2bActive, 'status_text' => $f2bActive ? 'En surveillance' : 'Arrêté', 'uptime' => $f2bActive ? 'Actif' : 'Arrêté', ], [ 'id' => 'firewall', 'name' => 'Pare-feu & Filtrage', 'desc' => $fwEnabled ? ($isStrictPort80 ? 'Mode Strict (Port 80 Isolé)' : 'Filtrage Nginx & Ports Actif') : 'Inactif (Tout autoriser)', 'port' => 'Filtrage IP & Nginx', 'active' => $fwEnabled, 'status_text' => $fwEnabled ? 'Filtrage Actif' : 'Inactif', 'uptime' => $fwEnabled ? 'Actif' : 'Inactif', ], ]; } private static function readCpuStat(): array { if (!is_readable('/proc/stat')) { return ['total' => 0, 'idle' => 0]; } $firstLine = fgets(fopen('/proc/stat', 'r')) ?: ''; $parts = preg_split('/\s+/', trim($firstLine)) ?: []; array_shift($parts); // retire "cpu" $vals = array_map('intval', $parts); $idle = ($vals[3] ?? 0) + ($vals[4] ?? 0); // idle + iowait $total = array_sum($vals); return ['total' => $total, 'idle' => $idle]; } private static function readMemInfo(): array { $data = []; if (is_readable('/proc/meminfo')) { foreach (file('/proc/meminfo') ?: [] as $line) { if (preg_match('/^(\w+):\s+(\d+)/', $line, $m)) { $data[$m[1]] = (int)$m[2]; // en kB } } } $totalKb = max(1, $data['MemTotal'] ?? 1); $availKb = $data['MemAvailable'] ?? (($data['MemFree'] ?? 0) + ($data['Buffers'] ?? 0) + ($data['Cached'] ?? 0)); $usedKb = max(0, $totalKb - $availKb); $swapTotalKb = $data['SwapTotal'] ?? 0; $swapFreeKb = $data['SwapFree'] ?? 0; $swapUsedKb = max(0, $swapTotalKb - $swapFreeKb); return [ 'total_mb' => (int)round($totalKb / 1024), 'used_mb' => (int)round($usedKb / 1024), 'free_mb' => (int)round($availKb / 1024), 'percent' => round(($usedKb / $totalKb) * 100.0, 1), 'swap_total_mb' => (int)round($swapTotalKb / 1024), 'swap_used_mb' => (int)round($swapUsedKb / 1024), ]; } private static function readNetDev(): array { $iface = 'eth0'; $rxBytes = 0; $txBytes = 0; if (is_readable('/proc/net/dev')) { $lines = file('/proc/net/dev') ?: []; foreach ($lines as $line) { if (!str_contains($line, ':')) { continue; } [$name, $rest] = explode(':', $line, 2); $name = trim($name); if ($name === 'lo') { continue; } $cols = preg_split('/\s+/', trim($rest)) ?: []; $iface = $name; $rxBytes = (int)($cols[0] ?? 0); $txBytes = (int)($cols[8] ?? 0); break; } } $ip = $_SERVER['SERVER_ADDR'] ?? ''; if ($ip === '' || $ip === '127.0.0.1') { $out = @shell_exec("ip -4 -o addr show dev " . escapeshellarg($iface) . " 2>/dev/null"); if (is_string($out) && preg_match('/inet\s+([0-9.]+)/', $out, $m)) { $ip = $m[1]; } } return [ 'interface' => $iface, 'ip' => $ip ?: '127.0.0.1', 'rx_bytes' => $rxBytes, 'tx_bytes' => $txBytes, ]; } private static function readAsteriskLiveStatus(PDO $db): array { $contacts = []; $activeCalls = 0; $astUptime = 'Actif'; $astVersion = 'Asterisk 22 • PJSIP'; $onlineCount = 0; try { $ami = AmiClient::fromDatabase($db); $rawVer = $ami->runCommand('core show version'); if (preg_match('/Asterisk\s+([0-9]+\.[0-9]+(?:\.[0-9]+)?)/i', $rawVer, $vm)) { $astVersion = "Asterisk {$vm[1]} • PJSIP"; } $rawContacts = $ami->runCommand('pjsip show contacts'); $astDbRaw = $ami->runCommand('database show registrar/contact'); $contactMeta = []; foreach (preg_split('/\r?\n/', $astDbRaw) ?: [] as $dbLine) { if (preg_match('/^\/registrar\/contact\/([^;]+);@[a-f0-9]+:\s*(\{.*\})/i', $dbLine, $dbM)) { $extKey = $dbM[1]; $parsedJson = json_decode($dbM[2], true); if ($parsedJson) { $contactMeta[$extKey] = $parsedJson; } } } $seenExts = []; foreach (preg_split('/\r?\n/', $rawContacts) ?: [] as $line) { // Supporte sip:, sips:, sans préfixe, tous types de hash et RTT (numérique, nan, N/A ou absent) if (preg_match('/Contact:\s+([^\s\/]+)\/(?:sips?:)?([^\s]+)\s+([a-zA-Z0-9]+)\s+(\S+)(?:\s+(\S+))?/i', $line, $m)) { $extNum = $m[1]; $uri = $m[2]; $status = $m[4]; $rttRaw = $m[5] ?? null; $isAvail = (stripos($status, 'Avail') !== false && stripos($status, 'Unavail') === false) || stripos($status, 'NonQual') !== false; if ($isAvail) { $onlineCount++; } $seenExts[$extNum] = true; $meta = $contactMeta[$extNum] ?? []; $fullUri = $meta['uri'] ?? ("sip:" . $uri); $userAgent = $meta['user_agent'] ?? ''; // Détection précise du transport utilisé en direct $activeTransport = 'udp'; if (preg_match('/transport=(tls|wss|ws|tcp|udp)/i', $fullUri, $tm)) { $activeTransport = strtolower($tm[1]); } elseif (preg_match('/x-ast-txp=transport-(tls|wss|ws|tcp|udp)/i', $fullUri, $tm)) { $activeTransport = strtolower($tm[1]); } elseif (str_starts_with($fullUri, 'sips:')) { $activeTransport = 'tls'; } $contacts[] = [ 'extension' => $extNum, 'uri' => $fullUri, 'user_agent' => $userAgent, 'active_transport' => $activeTransport, 'status' => $status, 'rtt_ms' => ($rttRaw !== null && is_numeric($rttRaw)) ? round((float)$rttRaw, 1) : null, ]; } } // Filet de sécurité 100% infaillible : Tous les contacts enregistrés dans AstDB (/registrar/contact) // non encore répertoriés (par exemple si qualify est désactivé ou RTT non disponible) $nowTime = time(); foreach ($contactMeta as $extNum => $meta) { if (isset($seenExts[$extNum])) { continue; } $exp = (int)($meta['expiration_time'] ?? 0); if ($exp === 0 || $exp > $nowTime) { $onlineCount++; $fullUri = $meta['uri'] ?? ''; $activeTransport = 'udp'; if (preg_match('/transport=(tls|wss|ws|tcp|udp)/i', $fullUri, $tm)) { $activeTransport = strtolower($tm[1]); } elseif (preg_match('/x-ast-txp=transport-(tls|wss|ws|tcp|udp)/i', $fullUri, $tm)) { $activeTransport = strtolower($tm[1]); } elseif (str_starts_with($fullUri, 'sips:')) { $activeTransport = 'tls'; } $contacts[] = [ 'extension' => (string)$extNum, 'uri' => $fullUri, 'user_agent' => $meta['user_agent'] ?? '', 'active_transport' => $activeTransport, 'status' => 'Avail', 'rtt_ms' => null, ]; } } $rawChannels = $ami->runCommand('core show channels count'); if (preg_match('/(\d+)\s+active\s+call/i', $rawChannels, $cm)) { $activeCalls = (int)$cm[1]; } $rawUptime = $ami->runCommand('core show uptime'); if (preg_match('/System uptime:\s*(.+)/i', $rawUptime, $um)) { $astUptime = trim($um[1]); } $registrations = []; $rawRegs = $ami->runCommand('pjsip show registrations'); foreach (preg_split('/\r?\n/', $rawRegs) ?: [] as $line) { // trunk-ovh-reg/sip:siptrunk.ovh.net:5060 trunk-ovh-auth Registered if (preg_match('/^\s*(trunk-[^\s\/]+)-reg\/(\S+)\s+(\S+)\s+(\S+)/i', $line, $rm)) { $registrations[$rm[1]] = [ 'server_uri' => $rm[2], 'auth' => $rm[3], 'status' => $rm[4], ]; } } } catch (Throwable) { $astUptime = 'Hors-ligne'; $registrations = []; } return [ 'version' => $astVersion, 'online_contacts' => $onlineCount, 'active_calls' => $activeCalls, 'uptime' => $astUptime, 'contacts' => $contacts, 'registrations' => $registrations ?? [], ]; } private static function loadPrevSample(): array { if (!is_readable(self::CACHE_FILE)) { return []; } $raw = @file_get_contents(self::CACHE_FILE); return $raw ? (json_decode($raw, true) ?: []) : []; } private static function savePrevSample(array $sample): void { @file_put_contents(self::CACHE_FILE, json_encode($sample)); } private static function formatDuration(int $seconds): string { $days = intdiv($seconds, 86400); $hours = intdiv($seconds % 86400, 3600); $mins = intdiv($seconds % 3600, 60); if ($days > 0) { return "{$days}j {$hours}h {$mins}m"; } if ($hours > 0) { return "{$hours}h {$mins}m"; } return "{$mins} min"; } }