upload.php 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. <?php
  2. // DEPRECATED: The bundled managed backup server is deprecated and unmaintained.
  3. // It is kept for reference only and will be removed in a future release.
  4. // Do not deploy it for new installations.
  5. declare(strict_types=1);
  6. require_once __DIR__ . "/lib.php";
  7. header("Content-Type: application/json; charset=utf-8");
  8. header("Cache-Control: no-store");
  9. header("X-Content-Type-Options: nosniff");
  10. function backupUploadRespond(int $status, array $payload): void
  11. {
  12. http_response_code($status);
  13. echo json_encode(
  14. $payload,
  15. JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,
  16. );
  17. exit;
  18. }
  19. function backupUploadInstanceIsAllowed(string $instance): bool
  20. {
  21. return in_array($instance, backupServerGetSettings()["instances"], true);
  22. }
  23. function backupUploadIsZipFile(string $path): bool
  24. {
  25. $handle = fopen($path, "rb");
  26. if ($handle === false) {
  27. return false;
  28. }
  29. $signature = fread($handle, 4);
  30. fclose($handle);
  31. return $signature === "PK\x03\x04" ||
  32. $signature === "PK\x05\x06" ||
  33. $signature === "PK\x07\x08";
  34. }
  35. function backupUploadChooseFilename(string $clientFilename, string $instanceDir): string
  36. {
  37. $clientFilename = trim($clientFilename);
  38. if ($clientFilename === "") {
  39. $filename = "backup-" . gmdate("Ymd-His") . ".zip";
  40. } elseif (
  41. basename($clientFilename) !== $clientFilename ||
  42. preg_match('/^backup-\d{8}-\d{6}(?:-\d+)?\.zip$/', $clientFilename) !== 1
  43. ) {
  44. throw new RuntimeException("Invalid backup filename.");
  45. } else {
  46. $filename = $clientFilename;
  47. }
  48. $base = substr($filename, 0, -4);
  49. $counter = 2;
  50. while (is_file($instanceDir . DIRECTORY_SEPARATOR . $filename)) {
  51. $filename = $base . "-" . $counter . ".zip";
  52. $counter++;
  53. }
  54. return $filename;
  55. }
  56. if ($_SERVER["REQUEST_METHOD"] !== "POST") {
  57. backupUploadRespond(405, ["success" => false, "error" => "POST required."]);
  58. }
  59. try {
  60. $instance = backupServerValidateInstance((string) ($_POST["instance"] ?? ""));
  61. if (!backupUploadInstanceIsAllowed($instance)) {
  62. throw new RuntimeException("Instance is not allowed.");
  63. }
  64. $file = $_FILES["backup"] ?? null;
  65. if (!is_array($file)) {
  66. throw new RuntimeException("Backup file is missing.");
  67. }
  68. if (($file["error"] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
  69. throw new RuntimeException("Upload failed with error code " . (string) ($file["error"] ?? "unknown") . ".");
  70. }
  71. $tmpName = (string) ($file["tmp_name"] ?? "");
  72. if ($tmpName === "" || !is_uploaded_file($tmpName)) {
  73. throw new RuntimeException("Upload is invalid.");
  74. }
  75. if (!backupUploadIsZipFile($tmpName)) {
  76. throw new RuntimeException("Uploaded file must be a ZIP file.");
  77. }
  78. $instanceDir = backupServerInstanceDir($instance);
  79. backupServerEnsureDirectory($instanceDir);
  80. $requestedFilename = (string) ($_POST["filename"] ?? "");
  81. $clientFilename = $requestedFilename !== "" ? $requestedFilename : (string) ($file["name"] ?? "");
  82. $filename = backupUploadChooseFilename($requestedFilename, $instanceDir);
  83. $targetPath = $instanceDir . DIRECTORY_SEPARATOR . $filename;
  84. if (!move_uploaded_file($tmpName, $targetPath)) {
  85. throw new RuntimeException("Uploaded backup cannot be stored.");
  86. }
  87. @chmod($targetPath, 0664);
  88. $size = filesize($targetPath);
  89. $sha256 = strtolower(hash_file("sha256", $targetPath) ?: "");
  90. if ($size === false || $size <= 0 || !preg_match('/^[a-f0-9]{64}$/', $sha256)) {
  91. @unlink($targetPath);
  92. throw new RuntimeException("Stored backup could not be verified.");
  93. }
  94. $postedSha256 = strtolower(trim((string) ($_POST["sha256"] ?? "")));
  95. if ($postedSha256 !== "" && (!preg_match('/^[a-f0-9]{64}$/', $postedSha256) || $postedSha256 !== $sha256)) {
  96. @unlink($targetPath);
  97. throw new RuntimeException("Backup checksum mismatch.");
  98. }
  99. $index = backupServerReadIndex();
  100. $record = [
  101. "instance" => $instance,
  102. "filename" => $filename,
  103. "client_filename" => basename($clientFilename),
  104. "size" => $size,
  105. "sha256" => $sha256,
  106. "uploaded_at" => date(DATE_ATOM),
  107. "source_ip" => $_SERVER["REMOTE_ADDR"] ?? "unknown",
  108. ];
  109. $index["backups"][] = $record;
  110. backupServerWriteIndex($index["backups"]);
  111. // S3 problems must never fail the upload: the local copy exists, and the
  112. // sync is retried on the next upload or via the management UI.
  113. $s3Enabled = backupS3Enabled();
  114. $s3Result = ["uploaded" => 0, "pending" => 0, "error" => null];
  115. if ($s3Enabled) {
  116. try {
  117. $s3Result = backupServerSyncInstanceS3($instance);
  118. } catch (Throwable $exception) {
  119. $s3Result = ["uploaded" => 0, "pending" => 1, "error" => $exception->getMessage()];
  120. backupServerLog("S3 sync crashed", [
  121. "instance" => $instance,
  122. "error" => $exception->getMessage(),
  123. ]);
  124. }
  125. }
  126. backupServerApplyRetention($instance);
  127. backupUploadRespond(200, [
  128. "success" => true,
  129. "instance" => $instance,
  130. "filename" => $filename,
  131. "size" => $size,
  132. "sha256" => $sha256,
  133. "retention" => backupServerGetSettings()["retention"],
  134. "s3" => [
  135. "enabled" => $s3Enabled,
  136. "uploaded" => $s3Enabled && $s3Result["pending"] === 0,
  137. "pending" => $s3Result["pending"],
  138. ],
  139. ]);
  140. } catch (Throwable $exception) {
  141. backupUploadRespond(400, [
  142. "success" => false,
  143. "error" => $exception->getMessage(),
  144. ]);
  145. }