Route every conf writer through the guarded path

Eleven call sites wrote master.conf with tmp+rename and nothing else — no backup, no
parse check, no audit — including the two toggles the UI uses most and the raw editor
that installs a whole hand-edited file.
This commit is contained in:
Gmer4Lfe
2026-08-09 19:07:28 -04:00
parent d9f917ecef
commit 67eabdc17c
10 changed files with 174 additions and 121 deletions
+18 -21
View File
@@ -45,9 +45,13 @@
// The temp copy is created with tempnam() and always removed, so a rejected save cannot
// leave a stray file beside the real conf for a script to source.
//
// The real write is atomic.
// vv_write_conf_raw() writes .vv.tmp and rename()s, so a script sourcing the conf during
// the save reads either the old file or the new one, never a half-written one.
// The write goes through the one guarded conf path.
// vv_conf_edit() takes an exclusive lock, copies the previous file into CONF_BACKUP_DIR,
// re-checks the syntax, installs via .vv.tmp + rename() so a concurrent reader sees the
// old file or the new one but never a half-written one, then sources the installed file
// to prove it still loads and restores the backup if it does not. That backup matters
// more here than anywhere else: this endpoint replaces a whole hand-edited file, and the
// confs are gitignored, so before it existed a bad paste had nothing to go back to.
//
// The push only happens after a confirmed write.
// Guarded on $written, so a failed save cannot distribute a stale or partly-written
@@ -68,11 +72,13 @@
// {"ok":false,"error":"Not allowed"|"Syntax error: …"|"Method not allowed"}
//
// DEPENDS ON
// include/config.php vv_get_conf_files(), vv_read_conf_raw(), vv_write_conf_raw(),
// vv_push_master_conf(), vv_push_setup_state()
// include/config.php vv_get_conf_files(), vv_read_conf_raw(),
// vv_push_master_conf(), vv_push_setup_state()
// include/confform.php vv_conf_syntax_error(), vv_conf_edit()
// ═══════════════════════════════════════════════════════════════════════════════════════════════
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';
require_once dirname(__DIR__) . '/include/confform.php';
$allowed = vv_get_conf_files();
@@ -95,24 +101,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
// Every script sources these, and master.conf is pushed to every partner from here — a
// syntax error saved through this endpoint would propagate the outage across the mesh.
$check = tempnam(sys_get_temp_dir(), 'vvconf');
if ($check !== false) {
file_put_contents($check, $content);
$out = []; $rc = 0;
exec('bash -n ' . escapeshellarg($check) . ' 2>&1', $out, $rc);
@unlink($check);
if ($rc !== 0) {
$msg = implode(' ', array_filter(array_map('trim', $out)));
echo json_encode([
'ok' => false,
'error' => 'Syntax error: ' . str_replace($check, $file, $msg ?: 'conf does not parse'),
'push' => [],
]);
exit;
}
// Checked here as well as inside vv_conf_edit() so the editor can show bash's own complaint;
// the write path only knows whether to proceed, not what to tell the person typing.
$syntax = vv_conf_syntax_error($content, $file);
if ($syntax !== null) {
echo json_encode(['ok' => false, 'error' => 'Syntax error: ' . $syntax, 'push' => []]);
exit;
}
$written = vv_write_conf_raw($file, $content);
$written = vv_conf_edit($file, fn() => $content, [], ['whole-file']);
$push = [];
if ($written && $file === 'master.conf') {
$push = vv_push_master_conf();