api.php 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. <?php
  2. declare(strict_types=1);
  3. // Shared request handling for the token-authenticated api/v1 endpoints.
  4. //
  5. // Protocol v1: every request carries the instance id and its secret token in
  6. // two headers. There is no session, no cookie and no shared password.
  7. //
  8. // X-Manage-Instance: psa-prod
  9. // X-Manage-Token: <64 hex chars>
  10. require_once __DIR__ . "/bootstrap.php";
  11. require_once __DIR__ . "/instances.php";
  12. function manageApiSendJson(int $status, array $payload): void
  13. {
  14. http_response_code($status);
  15. header("Content-Type: application/json; charset=utf-8");
  16. header("Cache-Control: no-store");
  17. header("X-Content-Type-Options: nosniff");
  18. echo json_encode(
  19. $payload,
  20. JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,
  21. );
  22. exit;
  23. }
  24. function manageApiFail(int $status, string $error, array $extra = []): void
  25. {
  26. manageApiSendJson($status, array_merge([
  27. "success" => false,
  28. "error" => $error,
  29. ], $extra));
  30. }
  31. function manageApiRequireMethod(string $method): void
  32. {
  33. if (($_SERVER["REQUEST_METHOD"] ?? "") !== $method) {
  34. header("Allow: " . $method);
  35. manageApiFail(405, $method . " erforderlich.");
  36. }
  37. }
  38. // Reads a request header regardless of SAPI. Apache with mod_php exposes
  39. // X-Manage-Token as HTTP_X_MANAGE_TOKEN; some setups only fill getallheaders().
  40. function manageApiHeader(string $name): string
  41. {
  42. $key = "HTTP_" . strtoupper(str_replace("-", "_", $name));
  43. if (isset($_SERVER[$key])) {
  44. return trim((string) $_SERVER[$key]);
  45. }
  46. if (function_exists("getallheaders")) {
  47. foreach (getallheaders() ?: [] as $headerName => $value) {
  48. if (strcasecmp((string) $headerName, $name) === 0) {
  49. return trim((string) $value);
  50. }
  51. }
  52. }
  53. return "";
  54. }
  55. /**
  56. * Authenticates the calling instance or terminates the request.
  57. *
  58. * Failures are rate-limited per IP and answered with the same generic message,
  59. * so the endpoint cannot be used to enumerate valid instance ids.
  60. *
  61. * @return array the instance record
  62. */
  63. function manageApiAuthenticate(): array
  64. {
  65. if (!manageRateLimitTryConsume(
  66. "api-auth",
  67. (int) MANAGE_API_RATE_LIMIT_MAX,
  68. (int) MANAGE_API_RATE_LIMIT_WINDOW,
  69. )) {
  70. manageApiFail(429, "Zu viele Anfragen. Bitte später erneut versuchen.");
  71. }
  72. $id = manageApiHeader("X-Manage-Instance");
  73. $token = manageApiHeader("X-Manage-Token");
  74. if ($id === "" || $token === "") {
  75. manageApiFail(401, "Authentifizierung erforderlich.");
  76. }
  77. try {
  78. $id = manageInstanceValidateId($id);
  79. } catch (Throwable $exception) {
  80. manageApiFail(401, "Authentifizierung fehlgeschlagen.");
  81. }
  82. $instance = manageInstanceAuthenticate($id, $token);
  83. if ($instance === null) {
  84. manageLogError("API authentication failed", ["instance" => $id]);
  85. manageApiFail(401, "Authentifizierung fehlgeschlagen.");
  86. }
  87. if (!$instance["enabled"]) {
  88. manageApiFail(403, "Diese Instanz ist deaktiviert.");
  89. }
  90. // A valid token clears the failure budget for this IP.
  91. manageRateLimitClearIp("api-auth");
  92. return $instance;
  93. }
  94. // Decodes a JSON request body. Returns an empty array for an empty body so
  95. // optional payloads do not need a special case at every call site.
  96. function manageApiReadJsonBody(): array
  97. {
  98. $raw = file_get_contents("php://input");
  99. if (!is_string($raw) || trim($raw) === "") {
  100. return [];
  101. }
  102. $decoded = json_decode($raw, true);
  103. if (!is_array($decoded)) {
  104. manageApiFail(400, "Anfrage-Body ist kein gültiges JSON.");
  105. }
  106. return $decoded;
  107. }