'Origin not allowed']); exit; } } // ── HMAC secret ─────────────────────────────────────────────────────────────── // Auto-generated on first run, stored outside webroot in data/secret.key function get_hmac_secret(): string { $secretFile = __DIR__ . '/../data/secret.key'; if (file_exists($secretFile)) { return trim(file_get_contents($secretFile)); } $secret = bin2hex(random_bytes(32)); $dir = dirname($secretFile); if (!is_dir($dir)) mkdir($dir, 0750, true); file_put_contents($secretFile, $secret, LOCK_EX); chmod($secretFile, 0600); return $secret; } // ── Rate limiting ───────────────────────────────────────────────────────────── // Returns false when limit exceeded, true otherwise function check_rate_limit(string $action, int $limit, int $window_seconds): bool { $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; $rateDir = __DIR__ . '/../data/rate/'; if (!is_dir($rateDir)) @mkdir($rateDir, 0755, true); $rateFile = $rateDir . $action . '_' . md5($ip) . '.json'; $now = time(); $times = []; if (file_exists($rateFile)) { $times = json_decode(file_get_contents($rateFile), true) ?: []; $times = array_values(array_filter($times, fn($t) => $t > $now - $window_seconds)); } if (count($times) >= $limit) return false; $times[] = $now; file_put_contents($rateFile, json_encode($times), LOCK_EX); return true; } // ── Atomic JSON read/write ──────────────────────────────────────────────────── // Returns [file_handle, data_array] — caller must call write_json_locked() to finish function read_json_locked(string $file): array { $dir = dirname($file); if (!is_dir($dir)) mkdir($dir, 0750, true); $fp = fopen($file, 'c+'); flock($fp, LOCK_EX); $size = filesize($file); $data = ($size > 0) ? (json_decode(fread($fp, $size), true) ?: []) : []; return [$fp, $data]; } function write_json_locked($fp, array $data): void { ftruncate($fp, 0); rewind($fp); fwrite($fp, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); flock($fp, LOCK_UN); fclose($fp); }