Document the PHP api layer and fix what documenting it exposed
Writing down what each endpoint actually guarantees made the places it didn't obvious — shell arguments reaching a crontab or a bash -c unescaped, master.conf written without tmp+rename, and conf edits that could be saved without ever being parsed.
This commit is contained in:
+100
-2
@@ -1,4 +1,95 @@
|
||||
<?php
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
// PURPOSE
|
||||
// Certificate status. Serves the cert panel four ways — the cached results of the last
|
||||
// cert_monitor.sh run, the configured domain list, a live read of NPM's own certificate
|
||||
// store, and an on-demand re-run of the monitor.
|
||||
//
|
||||
// OPERATIONAL MODEL
|
||||
// Two independent sources of truth, deliberately kept separate. cert_monitor.sh checks the
|
||||
// domains as they resolve from outside — the certificate a visitor actually receives. NPM's
|
||||
// API reports what it holds internally. They disagree exactly when something is wrong: a
|
||||
// renewed cert that was never reloaded, or a proxy host pointing at the wrong certificate.
|
||||
// Collapsing them into one number would hide the only case worth catching.
|
||||
//
|
||||
// The default path serves cert_monitor.sh's cache and never checks anything itself. That
|
||||
// script runs on the Sunday report schedule; the run action exists for when someone does
|
||||
// not want to wait for it.
|
||||
//
|
||||
// DESIGN PRINCIPLES
|
||||
// Thresholds come from master.conf, with shipped defaults.
|
||||
// CERT_WARN_DAYS and CERT_CRIT_DAYS are read per request rather than baked in, so the
|
||||
// page and the script that notifies agree on what "critical" means.
|
||||
//
|
||||
// No cache is answered with the domain list, not with an error.
|
||||
// Before the first run there is nothing to report, so the configured domains are
|
||||
// returned marked UNKN. The panel shows what is being watched rather than an empty
|
||||
// state that looks like nothing is configured.
|
||||
//
|
||||
// Already expired sorts with critical, not past it.
|
||||
// A negative day count is CRIT rather than a separate state, and the sort puts the
|
||||
// smallest number first — so the most urgent certificate is always at the top.
|
||||
//
|
||||
// The run action returns the script's output alongside the fresh data.
|
||||
// Capped at 30 lines. When a check fails, the reason is in that output and nowhere in
|
||||
// the structured result.
|
||||
//
|
||||
// OPERATIONAL SAFEGUARDS
|
||||
// The action is chosen from a fixed set, and everything else falls to the cached read.
|
||||
// No part of the request names a script, a domain, or a file. The one script this
|
||||
// endpoint can run is a hardcoded path.
|
||||
//
|
||||
// The script run is externally time-boxed.
|
||||
// `timeout 180` wraps it — set_time_limit() does not cover exec() time on Linux, so
|
||||
// PHP's own limit cannot end a check hung on an unresponsive domain. Exit 124 is
|
||||
// reported as a timeout rather than a generic failure.
|
||||
//
|
||||
// A missing script is reported, not executed.
|
||||
// file_exists() precedes exec(), so a partial deploy returns a named error rather than
|
||||
// a shell failure surfacing as an empty result.
|
||||
//
|
||||
// Every threshold read has a default.
|
||||
// ?: 30 and ?: 7 on the regex captures, so a master.conf mid-edit or missing the keys
|
||||
// still yields coherent statuses instead of comparing every certificate against zero
|
||||
// and reporting the whole estate critical.
|
||||
//
|
||||
// Missing or malformed expiry data is UNKN, never OK.
|
||||
// A certificate whose expires_on is absent or unparseable yields a null day count and
|
||||
// an explicit unknown status. Defaulting it to OK would silently drop a certificate out
|
||||
// of monitoring — the one failure this panel exists to prevent.
|
||||
//
|
||||
// Every cache read degrades.
|
||||
// json_decode with ?: fallbacks throughout, so a truncated cache file yields an empty
|
||||
// result rather than a fatal.
|
||||
//
|
||||
// The NPM path reports its own transport failures.
|
||||
// vv_npm_req() returns an _err key rather than throwing, and that is checked before the
|
||||
// response is treated as a certificate list — so an unreachable NPM is reported as such
|
||||
// instead of rendering as zero certificates.
|
||||
//
|
||||
// Accepted: the run action is reachable over GET.
|
||||
// It re-runs a read-only monitor and writes only its own cache, so repeating it is
|
||||
// harmless. Guarded by the Unraid WebGUI session; see the CSRF note in README-unraid.md.
|
||||
//
|
||||
// REQUEST
|
||||
// GET cached status, or the configured domains when no cache exists
|
||||
// GET|POST ?action=npm live certificate list from NPM's API
|
||||
// GET|POST ?action=domains configured domains and thresholds, no checks run
|
||||
// GET|POST ?action=run re-run cert_monitor.sh, then return its fresh cache
|
||||
//
|
||||
// RESPONSE
|
||||
// default {"ok":true,"checked_at","host","warn_days","crit_days","domains":[…]}
|
||||
// npm {"ok":true,"certs":[{id,nice_name,domain_names,provider,expires,days,status}],
|
||||
// "warn_days","crit_days"}
|
||||
// domains {"ok":true,"host_id","domains":[…],"warn_days","crit_days"}
|
||||
// run {"ok":true,"data":…,"output":[…],"rc":int}
|
||||
// {"ok":false,"error":"NPM request failed"|"cert_monitor.sh not found"|"… timed out …"}
|
||||
//
|
||||
// DEPENDS ON
|
||||
// include/config.php vv_detect_host(), vv_read_conf_raw(), STATE_DIR, SCRIPTS_DIR
|
||||
// include/auth.php vv_npm_req()
|
||||
// Monitors/cert_monitor.sh writes STATE_DIR/cert_status.json
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/config.php';
|
||||
require_once dirname(__DIR__) . '/include/auth.php';
|
||||
@@ -81,8 +172,15 @@ if ($action === 'run') {
|
||||
echo json_encode(['ok' => false, 'error' => 'cert_monitor.sh not found']);
|
||||
exit;
|
||||
}
|
||||
set_time_limit(180);
|
||||
exec('bash ' . escapeshellarg($script) . ' 2>&1', $out, $rc);
|
||||
// set_time_limit() does not cover exec() time on Linux, so the bound has to be external —
|
||||
// cert_monitor.sh reaches out to every configured domain and one unreachable host would
|
||||
// otherwise hold a php-fpm worker open indefinitely.
|
||||
set_time_limit(210);
|
||||
exec('timeout 180 bash ' . escapeshellarg($script) . ' 2>&1', $out, $rc);
|
||||
if ($rc === 124) {
|
||||
echo json_encode(['ok' => false, 'error' => 'cert_monitor.sh timed out after 180s']);
|
||||
exit;
|
||||
}
|
||||
// Read freshly written cache
|
||||
$data = file_exists($cacheFile)
|
||||
? (json_decode(file_get_contents($cacheFile), true) ?: null)
|
||||
|
||||
Reference in New Issue
Block a user