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.
123 lines
6.1 KiB
PHP
123 lines
6.1 KiB
PHP
<?php
|
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
// PURPOSE
|
|
// Standalone rsync scheduling. Records a location and cron expression for one rsync tier so
|
|
// that tier can run on its own schedule when its orchestrator is disabled.
|
|
//
|
|
// OPERATIONAL MODEL
|
|
// Rsync normally runs as a step inside an orchestrator — critical, intermediate, daily,
|
|
// weekly. This exists for the case where someone wants that tier's sync without the rest of
|
|
// the orchestrator's work. vv_cron_rebuild() emits the standalone entry only when the
|
|
// orchestrator is disabled and both a location and a cron are configured, so the two can
|
|
// never both fire: enabling the orchestrator silently takes precedence.
|
|
//
|
|
// Stored in schedule.json under __rsync_<FLAG_NAME>, a namespace deliberately outside the
|
|
// job id space. Everything that iterates the schedule as jobs skips keys with that prefix.
|
|
//
|
|
// The cron is rebuilt immediately on save, so the change takes effect without waiting for
|
|
// another event to regenerate varaverk.cron.
|
|
//
|
|
// DESIGN PRINCIPLES
|
|
// Keyed by flag, not by orchestrator.
|
|
// The tier flag is the stable identity — CRITICAL_RSYNC_ENABLED means the same thing
|
|
// regardless of which orchestrator currently carries that tier. orch_id is stored
|
|
// alongside it purely so the rebuild can check whether that orchestrator is enabled.
|
|
//
|
|
// Configuration only. Nothing here starts a sync; it records when one should start.
|
|
//
|
|
// Empty fields are permitted and mean "not configured".
|
|
// location and cron may both be blank, which is how a standalone entry is cleared —
|
|
// vv_cron_rebuild() requires both to be present before it emits anything.
|
|
//
|
|
// OPERATIONAL SAFEGUARDS
|
|
// POST only, checked before any parameter is read.
|
|
//
|
|
// The flag name is constrained to the rsync namespace.
|
|
// ^[A-Z_]+_RSYNC_ENABLED$ — this endpoint cannot create a schedule key for anything
|
|
// else, so the __rsync_ namespace stays exactly as wide as the tiers it was built for.
|
|
//
|
|
// The cron expression is validated as a crontab injection surface.
|
|
// vv_cron_rebuild() interpolates it directly into a generated crontab line. Only
|
|
// [0-9A-Za-z*,\-/ ] is permitted, and then five whitespace-separated fields are
|
|
// required. The character class is what carries the guarantee: the field-count pattern
|
|
// uses \s, which matches newline, so on its own it would accept a value carrying a
|
|
// second, caller-chosen crontab entry. It was previously not validated at all.
|
|
//
|
|
// The orchestrator id is validated even though it is only ever used as a lookup key.
|
|
// ^[A-Za-z0-9_.\-/]+\.sh$ with an explicit '..' check, so a value that could not name a
|
|
// real script cannot be stored as though it does.
|
|
//
|
|
// The location must be absolute and clean.
|
|
// Leading slash required, '..' rejected, control characters rejected. It reaches the
|
|
// crontab as an escapeshellarg'd --location= token, so validation and escaping are both
|
|
// in place.
|
|
//
|
|
// The rest of the schedule is preserved — the file is loaded, one key replaced, and the
|
|
// whole structure written back.
|
|
//
|
|
// The cron rebuild only runs after a confirmed write, so a failed save cannot regenerate
|
|
// the crontab from a schedule that was not persisted.
|
|
//
|
|
// REQUEST
|
|
// POST flag_name=<TIER>_RSYNC_ENABLED orch_id=<Category/name.sh>
|
|
// location=/absolute/path cron=<5 fields>
|
|
//
|
|
// RESPONSE
|
|
// {"ok":true}
|
|
// {"ok":false,"error":"POST only"|"Invalid flag_name"|"Invalid orch_id"|"Invalid location"
|
|
// |"Invalid cron expression"|"Write failed"}
|
|
//
|
|
// DEPENDS ON
|
|
// include/scheduler.php vv_schedule_load(), vv_schedule_save(), vv_cron_rebuild()
|
|
// Rsync/rsync.sh the script the generated cron entry invokes
|
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
header('Content-Type: application/json');
|
|
require_once dirname(__DIR__) . '/include/config.php';
|
|
require_once dirname(__DIR__) . '/include/scheduler.php';
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
echo json_encode(['ok' => false, 'error' => 'POST only']);
|
|
exit;
|
|
}
|
|
|
|
$flagName = trim($_POST['flag_name'] ?? '');
|
|
$orchId = trim($_POST['orch_id'] ?? '');
|
|
$location = trim($_POST['location'] ?? '');
|
|
$cron = trim($_POST['cron'] ?? '');
|
|
|
|
if (!$flagName || !preg_match('/^[A-Z_]+_RSYNC_ENABLED$/', $flagName)) {
|
|
echo json_encode(['ok' => false, 'error' => 'Invalid flag_name']);
|
|
exit;
|
|
}
|
|
if ($orchId && (str_contains($orchId, '..') || !preg_match('/^[A-Za-z0-9_.\-\/]+\.sh$/', $orchId))) {
|
|
echo json_encode(['ok' => false, 'error' => 'Invalid orch_id']);
|
|
exit;
|
|
}
|
|
if ($location && (!str_starts_with($location, '/') || str_contains($location, '..') || preg_match('/[\x00\n\r]/', $location))) {
|
|
echo json_encode(['ok' => false, 'error' => 'Invalid location']);
|
|
exit;
|
|
}
|
|
// vv_cron_rebuild() interpolates this straight into a crontab line. Restrict to cron-safe
|
|
// characters first — a field-count check alone would accept a value carrying a newline and
|
|
// therefore a second, caller-chosen crontab entry.
|
|
if ($cron && (!preg_match('/^[0-9A-Za-z*,\-\/ ]+$/', $cron) || !preg_match('/^(\S+ +){4}\S+$/', $cron))) {
|
|
echo json_encode(['ok' => false, 'error' => 'Invalid cron expression']);
|
|
exit;
|
|
}
|
|
|
|
$key = '__rsync_' . $flagName;
|
|
$schedule = vv_schedule_load();
|
|
$schedule[$key] = [
|
|
'flag_name' => $flagName,
|
|
'orch_id' => $orchId,
|
|
'location' => $location,
|
|
'cron' => $cron,
|
|
];
|
|
if (!vv_schedule_save($schedule)) {
|
|
echo json_encode(['ok' => false, 'error' => 'Write failed']);
|
|
exit;
|
|
}
|
|
|
|
vv_cron_rebuild($schedule);
|
|
echo json_encode(['ok' => true]);
|