Probe every domain every minute, and keep it out of its own traffic numbers

NPM's access log only describes hosts somebody visited; the host most likely to be quietly broken
is the one nobody does. Probes carry a User-Agent npm_access_stats.sh drops — unmarked, this
monitor would be fifty thousand requests a day in the very logs it reports on.
This commit is contained in:
Gmer4Lfe
2026-08-15 20:28:30 -04:00
parent c0ace5a0ca
commit 1001c25487
9 changed files with 534 additions and 5 deletions
+35 -1
View File
@@ -102,7 +102,7 @@ require_once dirname(__DIR__) . '/include/auth.php';
// panels every stack carries rather than gated to one.
const VV_AUTH_ACTION_PANEL = [
// GET
'npm_proxies' => 'proxies', 'npm_certs' => 'proxies', 'npm_stats' => 'proxies',
'npm_proxies' => 'proxies', 'npm_certs' => 'proxies', 'npm_stats' => 'proxies', 'npm_uptime' => 'proxies',
'lldap_users' => 'users', 'lldap_groups' => 'users', 'lldap_avatar' => 'users',
'authelia_rules' => 'acl',
// POST
@@ -115,6 +115,16 @@ const VV_AUTH_ACTION_PANEL = [
'authelia_save' => 'acl',
];
// Same windowing rule as uptime_probe.php: buckets are keyed by time, so "the last N" is a key
// sort rather than an assumption that every period produced a sample.
function vv_uptime_window_api(array $buckets, int $n): ?float {
if (!$buckets) return null;
krsort($buckets);
$u = $t = 0;
foreach (array_slice($buckets, 0, $n, true) as $b) { $u += $b['u'] ?? 0; $t += $b['t'] ?? 0; }
return $t > 0 ? round($u / $t * 100, 2) : null;
}
function vv_auth_action_allowed(string $action): bool {
$panel = VV_AUTH_ACTION_PANEL[$action] ?? null;
// Unmapped actions are left to the existing "Unknown action" answer rather than being refused
@@ -161,6 +171,30 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
exit;
}
// Per-domain uptime, written by Tools/uptime_probe.sh every minute. Keyed by hostname rather
// than proxy id, because that is what was probed.
if ($action === 'npm_uptime') {
$f = rtrim(defined('DB_DIR') ? DB_DIR : (DATA_DIR . '/db'), '/') . '/uptime.json';
$u = is_file($f) ? (json_decode((string) @file_get_contents($f), true) ?: []) : [];
$out = [];
foreach ($u['domains'] ?? [] as $dom => $r) {
// Only what the row draws. The hourly and daily buckets are dozens of entries per
// domain and the page shows three percentages and a strip.
$out[$dom] = [
'state' => $r['state'] ?? null,
'samples' => array_slice($r['samples'] ?? [], -60),
'h1' => vv_uptime_window_api($r['hours'] ?? [], 1),
'h24' => vv_uptime_window_api($r['hours'] ?? [], 24),
'd30' => vv_uptime_window_api($r['days'] ?? [], 30),
'last_change' => $r['last_change'] ?? null,
'last_detail' => $r['last_detail'] ?? null,
'last_ms' => $r['last_ms'] ?? null,
];
}
echo json_encode(['ok' => true, 'domains' => $out, 'last_pass' => $u['last_pass'] ?? null]);
exit;
}
$result = match ($action) {
'npm_proxies' => vv_npm_list_proxies(),
'npm_certs' => ['ok' => true, 'certs' => vv_npm_list_certs()],