upload.php 5.4 KB

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