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.
95 lines
5.1 KiB
PHP
95 lines
5.1 KiB
PHP
<?php
|
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
// PURPOSE
|
|
// Conf save endpoint. Writes the full text of one configuration file back to
|
|
// Configurations/, for the settings tab's raw editor.
|
|
//
|
|
// OPERATIONAL MODEL
|
|
// Whole-file replacement, not a patch. The editor sends back everything it was given, so a
|
|
// save is a straight overwrite. There is no merge, no per-key update, and no attempt to
|
|
// reconcile with a concurrent edit — last writer wins, which is correct for a single-admin
|
|
// plugin and far more predictable than a partial merge of a bash file.
|
|
//
|
|
// DESIGN PRINCIPLES
|
|
// Which files exist is decided by the host, never by the request.
|
|
// vv_get_conf_files() returns the allowlist for this host — master.conf plus its own
|
|
// host conf on HOST1, its own conf alone elsewhere. The filename is checked for exact
|
|
// membership in that list. This is what keeps sparse checkout honest: HOST2 cannot be
|
|
// asked to write host1.conf, because host1.conf is not in its list.
|
|
//
|
|
// Raw is the point.
|
|
// The structured editor is confform.php. This endpoint exists for the cases that one
|
|
// cannot express — new keys, comments, array literals, bulk edits — so it deliberately
|
|
// does not parse, reformat, or normalise what it is given.
|
|
//
|
|
// OPERATIONAL SAFEGUARDS
|
|
// POST only. Refused before the allowlist is even consulted, so a link or a prefetch can
|
|
// never reach the write path.
|
|
//
|
|
// Exact allowlist membership, not a pattern.
|
|
// in_array() against vv_get_conf_files() — not a regex, not a prefix test, not
|
|
// basename(). A filename that is not literally one of the permitted strings is rejected,
|
|
// which makes traversal and absolute paths unreachable rather than merely filtered.
|
|
//
|
|
// The content is syntax-checked before it can replace a working file.
|
|
// Conf files are sourced by every script in the system. A stray quote saved here would
|
|
// break load_config.sh, and with it every orchestrator, watchdog and fallback path — on
|
|
// a machine whose whole purpose is running unattended. `bash -n` on a private temp copy
|
|
// is the difference between a rejected save and a silent, total outage, so a file that
|
|
// does not parse is refused and the previous version is left untouched.
|
|
//
|
|
// Checked here via vv_conf_syntax_error() only so the editor can show bash's own
|
|
// complaint with a line number. vv_conf_edit() checks again before installing; this one
|
|
// is for the message, not the decision.
|
|
//
|
|
// 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. The whole-file nature
|
|
// of this endpoint is why that matters most here: there is no key to verify, so a clean
|
|
// source is the only assertion available.
|
|
//
|
|
// REQUEST
|
|
// POST file=<allowed conf name> content=<full file text>
|
|
//
|
|
// RESPONSE
|
|
// {"ok":true,"error":null}
|
|
// {"ok":false,"error":"POST only"|"File not permitted"|"Syntax error: …"|"Failed to write file"}
|
|
//
|
|
// DEPENDS ON
|
|
// include/config.php vv_get_conf_files(), CONF_DIR
|
|
// 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';
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
echo json_encode(['ok' => false, 'error' => 'POST only']);
|
|
exit;
|
|
}
|
|
|
|
$file = trim($_POST['file'] ?? '');
|
|
$content = $_POST['content'] ?? '';
|
|
|
|
// Must be an allowed file for this host
|
|
$allowed = vv_get_conf_files();
|
|
if (!$file || !in_array($file, $allowed)) {
|
|
echo json_encode(['ok' => false, 'error' => 'File not permitted']);
|
|
exit;
|
|
}
|
|
|
|
// Every script sources these. A syntax error here takes the whole system down, so the
|
|
// candidate is parsed before it is allowed to replace a working file. 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]);
|
|
exit;
|
|
}
|
|
|
|
$ok = vv_conf_edit($file, fn() => $content, [], ['whole-file']);
|
|
echo json_encode(['ok' => $ok, 'error' => $ok ? null : 'Failed to write file']);
|