Initial commit - NexusPBX IP Telephony Platform

This commit is contained in:
sorcier committed 2026-11-02 02:24:47 +01:00
commit c5de8d0c71
25 files changed
+18123

No files matched your search

+403
View File
@@ -0,0 +1,403 @@
<?php
declare(strict_types=1);
namespace App;
use PDO;
use Throwable;
require_once __DIR__ . '/FirewallManager.php';
require_once __DIR__ . '/AmiClient.php';
class SystemMetrics
{
private const CACHE_FILE = __DIR__ . '/../storage/metrics_prev.json';
public static function collect(PDO $db): array
{
$now = microtime(true);
$prev = self::loadPrevSample();
// 1. CPU (/proc/stat + /proc/loadavg + /proc/cpuinfo)
$cpuStat = self::readCpuStat();
$cpuPercent = 0.0;
if (isset($prev['cpu'])) {
$dTotal = $cpuStat['total'] - $prev['cpu']['total'];
$dIdle = $cpuStat['idle'] - $prev['cpu']['idle'];
if ($dTotal > 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']);
$isF2bRunning = !empty($fwStatus['is_running']) && (($lines[3] ?? '') === 'active');
$fwEnabled = !empty($fwStatus['settings']['firewall_enabled']) && $isF2bRunning;
$adminPort = $fwStatus['settings']['admin_https_port'] ?? 8443;
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 ? 'Règles actives & Isolation 80' : '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;
}
}
}
foreach (preg_split('/\r?\n/', $rawContacts) ?: [] as $line) {
// Contact: 101/sip:101@10.10.167.162:5060 5dd9a21be8 Avail 9.234
// or with -nan: Contact: 102/sip:102@10.10.168.228:5060;x-ast-txp=trans bb0686ce80 Unavail -nan
if (preg_match('/Contact:\s+([^\s\/]+)\/sip:([^\s]+)\s+([a-f0-9]+)\s+(\S+)\s+(-?[0-9.]+|nan|-nan)/i', $line, $m)) {
$extNum = $m[1];
$uri = $m[2];
$status = $m[4];
$isAvail = (stripos($status, 'Avail') !== false && stripos($status, 'Unavail') === false) || stripos($status, 'NonQual') !== false;
if ($isAvail) {
$onlineCount++;
}
$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' => is_numeric($m[5]) ? round((float)$m[5], 1) : 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";
}
}