Backfill (scripts/backfill-wireless-equip.js): mirror F wireless CPEs (airos_ac/airosm/cambium) into OPS Service Equipment, resolving customer + service_location via device→service→delivery_id→Service Location(legacy_delivery_id), dedup by MAC-or-IP, serial=MAC when F sn is null, undo manifest. Park was already ~99% synced — created the 2 genuinely-missing (incl. James A. Bryson) + flagged devices without service/MAC. Verified: Bryson EQP-0000100001 → live LiteBeam 5AC signal -74/-72, airMAX 93/67. Signal interval coloring (F parity): wifiSigColor (airOS: >=-60 green … <-78 red) and fibreRxColor (Rx -8..-25 green, -25..-28 orange, else red) color the live signal tiles (wireless + fibre). Assistant analysis (check_signal advice) kept. Device + AP links in service details: CPE management GUI link (Interface → https://ip) in the wireless panel; AP link when its mgmt IP resolves (ops_airos.php resolves the AP by radio MAC in F's device table, best-effort → apIp/ap_ip), else shows the AP MAC. Hub normalizeAiros passes apIp through. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
160 lines
8.3 KiB
PHP
160 lines
8.3 KiB
PHP
<?php
|
|
/**
|
|
* ops_airos.php — Pont LECTURE Ops → signal LIVE d'un CPE sans-fil (airOS/Cambium) via status.cgi.
|
|
* Déployé SUR facturation.targo.ca (seul hôte qui joint le réseau des CPE). Gaté X-Ops-Token (= ops_reassign.php).
|
|
* Read-only : ne fait qu'interroger status.cgi du CPE. Port du device_ajax/airos_ac_ajax.php de F.
|
|
*
|
|
* GET/POST ?serial=<sn> | ?mac=<mac> | ?ip=<manage> | ?device_id=<id> [&port=]
|
|
* header X-Ops-Token: <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';
|
|
|
|
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;
|
|
|
|
// IP de gestion de l'AP (borne) : résous la MAC radio de l'AP → ligne device correspondante (best-effort ; permet
|
|
// un lien direct vers l'AP dans OPS). La MAC radio ≠ toujours device.mac → si non trouvé, ap_ip reste null.
|
|
$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) { $macNoColon = str_replace(array(':', '-'), '', $apmac); $st->bind_param('ss', $apmac, $macNoColon); $st->execute(); $r2 = $st->get_result()->fetch_assoc(); if ($r2) $ap_ip = $r2['manage']; }
|
|
}
|
|
|
|
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,
|
|
'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),
|
|
));
|