Stripped from the internal repo: per-box deploy trees, Go services (nodelist/astmeta/proxy/svxdr), deploy/sync scripts and all secrets. Config templates for nginx, PHP-FPM, Redis and tmpfiles live in install/ with CHANGEME placeholders. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
131 lines
5.3 KiB
PHP
131 lines
5.3 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
require __DIR__ . '/lib.php';
|
|
|
|
$uuid = require_admin(2);
|
|
// Preferred: the account uuid, so all of the account's callsigns (primary +
|
|
// admin-added extra/club calls) are matched. 'callsign' kept as fallback.
|
|
$acct_uuid = trim($_GET['uuid'] ?? '');
|
|
$callsign = strtoupper(trim($_GET['callsign'] ?? ''));
|
|
if ($acct_uuid !== '') {
|
|
$calls = acct_all_calls($acct_uuid);
|
|
} elseif ($callsign !== '') {
|
|
$u = redis()->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]);
|