| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128 |
- <?php
- declare(strict_types=1);
- // Shared request handling for the token-authenticated api/v1 endpoints.
- //
- // Protocol v1: every request carries the instance id and its secret token in
- // two headers. There is no session, no cookie and no shared password.
- //
- // X-Manage-Instance: example-prod
- // X-Manage-Token: <64 hex chars>
- require_once __DIR__ . "/bootstrap.php";
- require_once __DIR__ . "/instances.php";
- function manageApiSendJson(int $status, array $payload): void
- {
- http_response_code($status);
- header("Content-Type: application/json; charset=utf-8");
- header("Cache-Control: no-store");
- header("X-Content-Type-Options: nosniff");
- echo json_encode(
- $payload,
- JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,
- );
- exit;
- }
- function manageApiFail(int $status, string $error, array $extra = []): void
- {
- manageApiSendJson($status, array_merge([
- "success" => false,
- "error" => $error,
- ], $extra));
- }
- function manageApiRequireMethod(string $method): void
- {
- if (($_SERVER["REQUEST_METHOD"] ?? "") !== $method) {
- header("Allow: " . $method);
- manageApiFail(405, $method . " erforderlich.");
- }
- }
- // Reads a request header regardless of SAPI. Apache with mod_php exposes
- // X-Manage-Token as HTTP_X_MANAGE_TOKEN; some setups only fill getallheaders().
- function manageApiHeader(string $name): string
- {
- $key = "HTTP_" . strtoupper(str_replace("-", "_", $name));
- if (isset($_SERVER[$key])) {
- return trim((string) $_SERVER[$key]);
- }
- if (function_exists("getallheaders")) {
- foreach (getallheaders() ?: [] as $headerName => $value) {
- if (strcasecmp((string) $headerName, $name) === 0) {
- return trim((string) $value);
- }
- }
- }
- return "";
- }
- /**
- * Authenticates the calling instance or terminates the request.
- *
- * Failures are rate-limited per IP and answered with the same generic message,
- * so the endpoint cannot be used to enumerate valid instance ids.
- *
- * @return array the instance record
- */
- function manageApiAuthenticate(): array
- {
- if (!manageRateLimitTryConsume(
- "api-auth",
- (int) MANAGE_API_RATE_LIMIT_MAX,
- (int) MANAGE_API_RATE_LIMIT_WINDOW,
- )) {
- manageApiFail(429, "Zu viele Anfragen. Bitte später erneut versuchen.");
- }
- $id = manageApiHeader("X-Manage-Instance");
- $token = manageApiHeader("X-Manage-Token");
- if ($id === "" || $token === "") {
- manageApiFail(401, "Authentifizierung erforderlich.");
- }
- try {
- $id = manageInstanceValidateId($id);
- } catch (Throwable $exception) {
- manageApiFail(401, "Authentifizierung fehlgeschlagen.");
- }
- $instance = manageInstanceAuthenticate($id, $token);
- if ($instance === null) {
- manageLogError("API authentication failed", ["instance" => $id]);
- manageApiFail(401, "Authentifizierung fehlgeschlagen.");
- }
- if (!$instance["enabled"]) {
- manageApiFail(403, "Diese Instanz ist deaktiviert.");
- }
- // A valid token clears the failure budget for this IP.
- manageRateLimitClearIp("api-auth");
- return $instance;
- }
- // Decodes a JSON request body. Returns an empty array for an empty body so
- // optional payloads do not need a special case at every call site.
- function manageApiReadJsonBody(): array
- {
- $raw = file_get_contents("php://input");
- if (!is_string($raw) || trim($raw) === "") {
- return [];
- }
- $decoded = json_decode($raw, true);
- if (!is_array($decoded)) {
- manageApiFail(400, "Anfrage-Body ist kein gültiges JSON.");
- }
- return $decoded;
- }
|