get(call_index_key(base_call($callsign))); $calls = (is_string($u) && $u !== '') ? acct_all_calls($u) : [$callsign]; } else { json_out(['ok' => false, 'error' => 'missing_callsign'], 400); } if (!$calls) json_out(['ok' => true, 'nodes' => (object)[]]); // Parse paths from svxreflector.conf $conf_lines = @file('/etc/svxlink/svxreflector.conf', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: []; $pki_dir = '/etc/svxlink/pki'; $pending_dir = ''; $certs_dir = ''; foreach ($conf_lines as $line) { $line = trim($line); if (str_starts_with($line, 'CERT_PKI_DIR=')) $pki_dir = substr($line, strlen('CERT_PKI_DIR=')); if (str_starts_with($line, 'CERT_CA_PENDING_CSRS_DIR=')) $pending_dir = substr($line, strlen('CERT_CA_PENDING_CSRS_DIR=')); if (str_starts_with($line, 'CERT_CA_CERTS_DIR=')) $certs_dir = substr($line, strlen('CERT_CA_CERTS_DIR=')); } if ($pending_dir === '') $pending_dir = $pki_dir . '/pending_csrs'; if ($certs_dir === '') $certs_dir = $pki_dir . '/certs'; // Renewal count for a node, via the per-email counter (cert email, else CSR email). function node_renewals(string $cs): int { $e = node_cert_email($cs); if ($e === '') $e = node_csr_email($cs); return $e === '' ? 0 : cert_email_count($e); } // Fetch reflector status $ch = curl_init('http://127.0.0.1:8098/api/status'); curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5, CURLOPT_CONNECTTIMEOUT => 2]); $json = curl_exec($ch); curl_close($ch); $data = json_decode($json ?: '', true); $all_nodes = (is_array($data) && isset($data['nodes'])) ? $data['nodes'] : []; $r = redis(); $matched = []; foreach ($all_nodes as $node_call => $node_info) { // Only LOCAL client nodes — remote trunk nodes belong to other reflectors. if (!empty($node_info['remoteTrunk'])) continue; $sysop = strtoupper(trim($node_info['sysop'] ?? '')); $match = ($sysop !== '' && in_array($sysop, $calls, true)) || calls_match_node($calls, (string)$node_call); // Also match if one of the account's callsigns is a node admin if (!$match) { $admins = $r->sMembers(rkey("node_admins:" . strtoupper($node_call))); if (is_array($admins)) { foreach ($calls as $c) { if (in_array($c, $admins, true)) { $match = true; break; } } } } if ($match) { // Node admins (sysop always included) $stored = $r->sMembers(rkey("node_admins:" . strtoupper($node_call))); if (!is_array($stored)) $stored = []; $all_admins = $stored; if ($sysop !== '' && !in_array($sysop, $all_admins, true)) array_unshift($all_admins, $sysop); sort($all_admins); $node_info['node_admins'] = $all_admins; // Cert status $node_info['has_cert'] = is_file(rtrim($certs_dir, '/') . '/' . $node_call . '.crt'); $node_info['has_pending_csr'] = is_file(rtrim($pending_dir, '/') . '/' . $node_call . '.csr'); $node_info['renewals'] = node_renewals($node_call); $matched[$node_call] = $node_info; } } // --- Also include OFFLINE nodes for this callsign (not currently connected) --- // Password nodes from the reflector conf (via the scoped helper) and cert nodes // from the PKI, matched by base callsign or node-admin membership. Mirrors the // profile's node list so admins can manage admins even when a node is down. $add_offline = function (string $nc, array $extra) use (&$matched, $r, $calls, $certs_dir, $pending_dir) { $nc = strtoupper(trim($nc)); if ($nc === '' || isset($matched[$nc])) return; $base = explode('-', $nc)[0]; $admins = $r->sMembers(rkey("node_admins:$nc")); if (!is_array($admins)) $admins = []; if (!calls_match_node($calls, $nc)) { $isadm = false; foreach ($calls as $c) { if (in_array($c, $admins, true)) { $isadm = true; break; } } if (!$isadm) return; } $all_admins = $admins; if ($base !== '' && !in_array($base, $all_admins, true)) array_unshift($all_admins, $base); sort($all_admins); $matched[$nc] = $extra + [ 'offline' => true, 'sysop' => $base, 'node_admins' => $all_admins, 'has_cert' => is_file(rtrim($certs_dir, '/') . "/$nc.crt"), 'has_pending_csr' => is_file(rtrim($pending_dir, '/') . "/$nc.csr"), 'renewals' => node_renewals($nc), ]; }; // Password nodes (offline included) from the reflector conf via the scoped helper $pw_out = []; @exec('sudo /usr/local/sbin/svxreflector-users list 2>/dev/null', $pw_out); foreach ($pw_out as $pn) $add_offline($pn, ['password' => true]); // Cert nodes (offline included) from the PKI foreach (glob(rtrim($certs_dir, '/') . '/*.crt') ?: [] as $f) { $cn = basename($f, '.crt'); if (str_starts_with($cn, 'svxreflector_') || str_starts_with($cn, 'SvxReflector_')) continue; $add_offline($cn, ['cert' => true]); } json_out(['ok' => true, 'nodes' => $matched]);