| ?mac= | ?ip= | ?device_id= [&port=] * header X-Ops-Token: * → { ok, source:"f", device_model, device_name, version, ssid, frequency, tx_power, mac_ap, * signal, signal_ap, ccq, tx_rate, rx_rate, airmax_capacity_uplink, airmax_capacity_downlink, * connection_time, if_speed_lan0, if_speed_lan1 } * * Anti-SSRF : on ne curl QUE l'IP `manage` d'une ligne `device` réellement provisionnée (jamais une IP brute * fournie par l'appelant sans correspondance en base). */ header('Content-Type: application/json; charset=utf-8'); require __DIR__ . '/ops_secret.php'; // $OPS_TOKEN, $DB_USER, $DB_PASS @include_once __DIR__ . '/facturation/lib/mcrypt.php'; // mcrypt('decrypt', ...) — déchiffre device.pass (repli défaut usine si absent) $DB_HOST = '10.100.80.100'; $DB_NAME = 'gestionclient'; // RADIUS (WPA2) = source AUTORITAIRE de la MAC courante du CPE (auto-MàJ au remplacement, via callingstationid) + // IP de l'AP (nasipaddress). Mêmes creds que F airos_ac.php (le hub OPS n'a PAS le GRANT → passe par ce pont). $RADIUS_HOST = '10.5.2.25'; $RADIUS_DB = 'radiusdb'; $RADIUS_PASS = 'N0HAk4u$'; function out($a, $c = 200) { http_response_code($c); echo json_encode($a, JSON_UNESCAPED_UNICODE); exit; } // ─── auth (mêmes secret + mécanisme que ops_reassign.php / ops_placemarks.php) ─── $tok = $_SERVER['HTTP_X_OPS_TOKEN'] ?? ($_REQUEST['token'] ?? ''); if (!is_string($tok) || !hash_equals($OPS_TOKEN, $tok)) out(['ok' => false, 'error' => 'forbidden'], 403); // ─── airOS status fetch (copié à l'identique de F device_ajax/airos_ac_ajax.php, + timeouts) ─── function airOS_getFile_HTTPS($username, $password, $file, $address) { $ch = curl_init(); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_COOKIEJAR, null); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 6); curl_setopt($ch, CURLOPT_TIMEOUT, 15); curl_setopt($ch, CURLOPT_HTTPHEADER, array('Expect: ')); // airOS >= 8.5 : POST /api/auth (récupère X-CSRF-ID) ; sinon repli /login.cgi curl_setopt($ch, CURLOPT_URL, "https://$address/api/auth"); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, array('username' => $username, 'password' => $password)); curl_setopt($ch, CURLOPT_HEADER, 1); $response = curl_exec($ch); curl_setopt($ch, CURLOPT_HEADER, 0); if (curl_getinfo($ch, CURLINFO_HTTP_CODE) == 200) { preg_match('/X-CSRF-ID: .*/', substr($response, 0, curl_getinfo($ch, CURLINFO_HEADER_SIZE)), $XCSRFID); curl_setopt($ch, CURLOPT_URL, "https://$address/$file"); curl_setopt($ch, CURLOPT_POST, 0); $retfile = curl_exec($ch); curl_setopt($ch, CURLOPT_URL, "https://$address/logout.cgi"); curl_setopt($ch, CURLOPT_HTTPHEADER, array(trim($XCSRFID[0] ?? ''), 'X-AIROS-LUA: 1')); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, array()); curl_exec($ch); } else { curl_setopt($ch, CURLOPT_URL, "https://$address/login.cgi"); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, array('username' => $username, 'password' => $password)); curl_exec($ch); curl_setopt($ch, CURLOPT_URL, "https://$address/$file"); curl_setopt($ch, CURLOPT_POST, 0); $retfile = curl_exec($ch); } curl_close($ch); return $retfile; } function sec2t($n) { if (!is_numeric($n) || $n <= 0) return ''; $n = (int)$n; return sprintf('%dd %02d:%02d:%02d', intdiv($n, 86400), intdiv($n % 86400, 3600), intdiv($n % 3600, 60), $n % 60); } // ─── résout la ligne device (creds + IP de gestion) : par sn, puis mac, puis manage=ip, puis id ─── $serial = trim($_REQUEST['serial'] ?? $_REQUEST['sn'] ?? ''); $mac = trim($_REQUEST['mac'] ?? ''); $ip = trim($_REQUEST['ip'] ?? ''); $device_id = (int)($_REQUEST['device_id'] ?? 0); $db = @new mysqli($DB_HOST, $DB_USER, $DB_PASS, $DB_NAME); if ($db->connect_errno) out(['ok' => false, 'error' => 'db_connect'], 500); $db->set_charset('utf8'); $row = null; $fetch1 = function ($sql, $type, $val) use ($db) { $st = $db->prepare($sql); if (!$st) return null; $st->bind_param($type, $val); $st->execute(); return $st->get_result()->fetch_assoc() ?: null; }; $cols = 'id, category, manage, port, user, pass, sn, mac, model'; if ($device_id) $row = $fetch1("SELECT $cols FROM device WHERE id=? LIMIT 1", 'i', $device_id); if (!$row && $serial !== '') $row = $fetch1("SELECT $cols FROM device WHERE sn=? AND manage<>'' LIMIT 1", 's', $serial); if (!$row && $mac !== '') $row = $fetch1("SELECT $cols FROM device WHERE mac=? AND manage<>'' LIMIT 1", 's', $mac); if (!$row && $ip !== '') $row = $fetch1("SELECT $cols FROM device WHERE manage=? LIMIT 1", 's', $ip); if (!$row) out(['ok' => false, 'error' => 'device introuvable (serial/mac/ip/device_id)'], 404); if (stripos((string)$row['category'], 'cambium') !== false) out(['ok' => false, 'error' => 'Cambium non supporté par ce pont (API différente)'], 200); $mip = trim((string)$row['manage']); if ($mip === '') out(['ok' => false, 'error' => 'aucune IP de gestion pour cet appareil'], 200); $port = (int)($_REQUEST['port'] ?? 0); if ($port <= 0) $port = (int)$row['port']; if ($port <= 0) $port = 2196; $addr = $mip . ':' . $port; $user = trim((string)$row['user']); $pwd = ''; if (trim((string)$row['pass']) !== '' && function_exists('mcrypt')) { $pwd = @mcrypt('decrypt', $row['pass']); } if ($user === '' || $pwd === '' || $pwd === false) { $user = 'admin'; $pwd = 'N0HAk4u$'; } // repli = défaut usine F $j = airOS_getFile_HTTPS($user, $pwd, 'status.cgi', $addr); $s = $j ? json_decode($j, true) : null; if (!is_array($s) || !isset($s['host'])) out(['ok' => false, 'error' => 'CPE injoignable ou réponse invalide', 'addr' => $addr], 200); $idx = array('1x', '2x', '2x', '4x', '4x', '6x', '6x', '8x', '8x'); $w = $s['wireless'] ?? array(); $sta = $w['sta'][0] ?? array(); $host = $s['host'] ?? array(); $eth0 = null; $eth1 = null; foreach (($s['interfaces'] ?? array()) as $if) { if (($if['ifname'] ?? '') === 'eth0') $eth0 = $if['status'] ?? array(); if (($if['ifname'] ?? '') === 'eth1') $eth1 = $if['status'] ?? array(); } $lan = function ($st) { return ($st && !empty($st['plugged'])) ? (($st['speed'] ?? '?') . ' Mbps' . (!empty($st['duplex']) ? ' [Full]' : ' [Half]')) : 'unplugged'; }; // signal : v8 = wireless.sta[0].signal ; v6 (NanoStation) = wireless.signal (top-niveau). $signal = $sta['signal'] ?? ($w['signal'] ?? null); // CCQ (Transmit) = 0-100 % ; certains firmwares exposent un champ hors échelle → on masque l'aberrant. $ccq = $sta['ccq'] ?? ($w['ccq'] ?? null); if ($ccq !== null && (!is_numeric($ccq) || $ccq < 0 || $ccq > 100)) $ccq = null; // RADIUS (WPA2) = MAC COURANTE du CPE (callingstationid, auto-MàJ au remplacement d'équipement) + IP de l'AP // (nasipaddress, fiable). On résout le radius_user via le service de l'appareil, puis la dernière session radacct. $radius_user = null; $radius_mac = null; $ap_ip = null; $stR = $db->prepare("SELECT radius_user FROM service WHERE device_id=? AND radius_user<>'' ORDER BY (status=1) DESC, id DESC LIMIT 1"); if ($stR) { $stR->bind_param('i', $row['id']); $stR->execute(); $rr = $stR->get_result()->fetch_assoc(); if ($rr) $radius_user = $rr['radius_user']; } if ($radius_user) { $rdb = @new mysqli($RADIUS_HOST, $DB_USER, $RADIUS_PASS, $RADIUS_DB); if ($rdb && !$rdb->connect_errno) { $stx = $rdb->prepare("SELECT callingstationid, nasipaddress FROM radacct WHERE username=? ORDER BY radacctid DESC LIMIT 1"); if ($stx) { $stx->bind_param('s', $radius_user); $stx->execute(); $rx = $stx->get_result()->fetch_assoc(); if ($rx) { $cm = strtoupper(trim(str_replace('-', ':', (string)$rx['callingstationid']))); if (preg_match('/^([0-9A-F]{2}:){5}[0-9A-F]{2}$/', $cm)) $radius_mac = $cm; $nip = trim((string)$rx['nasipaddress']); if ($nip !== '' && $nip !== '0.0.0.0') $ap_ip = $nip; } } $rdb->close(); } } // Repli AP IP si RADIUS muet : MAC radio de l'AP → ligne device (best-effort). if ($ap_ip === null) { $apmac = $w['apmac'] ?? ''; if ($apmac !== '') { $st = $db->prepare("SELECT manage FROM device WHERE (mac=? OR mac=?) AND manage<>'' LIMIT 1"); if ($st) { $macNC = str_replace(array(':', '-'), '', $apmac); $st->bind_param('ss', $apmac, $macNC); $st->execute(); $r2 = $st->get_result()->fetch_assoc(); if ($r2) $ap_ip = $r2['manage']; } } } $eth0b = $eth0 ?: array(); out(array( 'ok' => true, 'source' => 'f', 'device_model' => $host['devmodel'] ?? null, 'device_name' => $host['hostname'] ?? null, 'version' => $host['fwversion'] ?? null, 'ssid' => $w['essid'] ?? null, 'frequency' => $w['frequency'] ?? null, 'tx_power' => $w['txpower'] ?? null, 'mac_ap' => $w['apmac'] ?? null, 'ap_ip' => $ap_ip, 'radius_user' => $radius_user, 'radius_mac' => $radius_mac, 'signal' => $signal, 'signal_ap' => $sta['remote']['signal'] ?? null, 'ccq' => $ccq, 'tx_rate' => $idx[$w['tx_idx'] ?? -1] ?? null, 'rx_rate' => $idx[$w['rx_idx'] ?? -1] ?? null, 'airmax_capacity_uplink' => isset($sta['airmax']['uplink_capacity']) ? round($sta['airmax']['uplink_capacity'] / 1024, 2) : null, 'airmax_capacity_downlink' => isset($sta['airmax']['downlink_capacity']) ? round($sta['airmax']['downlink_capacity'] / 1024, 2) : null, 'connection_time' => sec2t($host['uptime'] ?? 0), 'if_speed_lan0' => $lan($eth0), 'if_speed_lan1' => $lan($eth1), // Compteurs d'octets eth0 (LAN client) → débit LIVE calculé côté OPS (Δoctets/Δs × 8). eth0 rx = upload, tx = download. 'eth_rx_bytes' => isset($eth0b['rx_bytes']) ? (float) $eth0b['rx_bytes'] : null, 'eth_tx_bytes' => isset($eth0b['tx_bytes']) ? (float) $eth0b['tx_bytes'] : null, 'ts' => round(microtime(true) * 1000), ));