From 87371901c0a61a1e6ad1ead1734ca74fbf687aac Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 17 Jan 2021 17:51:09 -0800 Subject: [PATCH] Add base logic to support sending a request to restore a backup for a server --- .../Api/Client/Servers/BackupController.php | 126 +++++++++++++-- .../Servers/DownloadBackupController.php | 144 ------------------ .../Servers/Backups/DeleteBackupRequest.php | 17 --- .../Servers/Backups/DownloadBackupRequest.php | 41 ----- .../Servers/Backups/GetBackupsRequest.php | 17 --- app/Models/Permission.php | 6 +- app/Models/Server.php | 1 + .../Wings/DaemonBackupRepository.php | 31 +++- app/Services/Backups/DownloadLinkService.php | 83 ++++++++++ 9 files changed, 229 insertions(+), 237 deletions(-) delete mode 100644 app/Http/Controllers/Api/Client/Servers/DownloadBackupController.php delete mode 100644 app/Http/Requests/Api/Client/Servers/Backups/DeleteBackupRequest.php delete mode 100644 app/Http/Requests/Api/Client/Servers/Backups/DownloadBackupRequest.php delete mode 100644 app/Http/Requests/Api/Client/Servers/Backups/GetBackupsRequest.php create mode 100644 app/Services/Backups/DownloadLinkService.php diff --git a/app/Http/Controllers/Api/Client/Servers/BackupController.php b/app/Http/Controllers/Api/Client/Servers/BackupController.php index c18aac3ac..3abeaaa1d 100644 --- a/app/Http/Controllers/Api/Client/Servers/BackupController.php +++ b/app/Http/Controllers/Api/Client/Servers/BackupController.php @@ -2,18 +2,21 @@ namespace Pterodactyl\Http\Controllers\Api\Client\Servers; +use Illuminate\Http\Request; use Pterodactyl\Models\Backup; use Pterodactyl\Models\Server; use Pterodactyl\Models\AuditLog; use Illuminate\Http\JsonResponse; +use Pterodactyl\Models\Permission; +use Illuminate\Validation\UnauthorizedException; use Pterodactyl\Services\Backups\DeleteBackupService; -use Pterodactyl\Repositories\Eloquent\BackupRepository; +use Pterodactyl\Services\Backups\DownloadLinkService; use Pterodactyl\Services\Backups\InitiateBackupService; use Pterodactyl\Transformers\Api\Client\BackupTransformer; +use Pterodactyl\Repositories\Wings\DaemonBackupRepository; use Pterodactyl\Http\Controllers\Api\Client\ClientApiController; -use Pterodactyl\Http\Requests\Api\Client\Servers\Backups\GetBackupsRequest; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Pterodactyl\Http\Requests\Api\Client\Servers\Backups\StoreBackupRequest; -use Pterodactyl\Http\Requests\Api\Client\Servers\Backups\DeleteBackupRequest; class BackupController extends ClientApiController { @@ -28,39 +31,51 @@ class BackupController extends ClientApiController private $deleteBackupService; /** - * @var \Pterodactyl\Repositories\Eloquent\BackupRepository + * @var \Pterodactyl\Services\Backups\DownloadLinkService + */ + private $downloadLinkService; + + /** + * @var \Pterodactyl\Repositories\Wings\DaemonBackupRepository */ private $repository; /** * BackupController constructor. * - * @param \Pterodactyl\Repositories\Eloquent\BackupRepository $repository + * @param \Pterodactyl\Repositories\Wings\DaemonBackupRepository $repository * @param \Pterodactyl\Services\Backups\DeleteBackupService $deleteBackupService * @param \Pterodactyl\Services\Backups\InitiateBackupService $initiateBackupService + * @param \Pterodactyl\Services\Backups\DownloadLinkService $downloadLinkService */ public function __construct( - BackupRepository $repository, + DaemonBackupRepository $repository, DeleteBackupService $deleteBackupService, - InitiateBackupService $initiateBackupService + InitiateBackupService $initiateBackupService, + DownloadLinkService $downloadLinkService ) { parent::__construct(); + $this->repository = $repository; $this->initiateBackupService = $initiateBackupService; $this->deleteBackupService = $deleteBackupService; - $this->repository = $repository; + $this->downloadLinkService = $downloadLinkService; } /** * Returns all of the backups for a given server instance in a paginated * result set. * - * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Backups\GetBackupsRequest $request + * @param \Illuminate\Http\Request $request * @param \Pterodactyl\Models\Server $server * @return array */ - public function index(GetBackupsRequest $request, Server $server) + public function index(Request $request, Server $server) { + if (! $request->user()->can(Permission::ACTION_BACKUP_READ, $server)) { + throw new UnauthorizedException; + } + $limit = min($request->query('per_page') ?? 20, 50); return $this->fractal->collection($server->backups()->paginate($limit)) @@ -100,13 +115,17 @@ class BackupController extends ClientApiController /** * Returns information about a single backup. * - * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Backups\GetBackupsRequest $request + * @param \Illuminate\Http\Request $request * @param \Pterodactyl\Models\Server $server * @param \Pterodactyl\Models\Backup $backup * @return array */ - public function view(GetBackupsRequest $request, Server $server, Backup $backup) + public function view(Request $request, Server $server, Backup $backup) { + if (! $request->user()->can(Permission::ACTION_BACKUP_READ, $server)) { + throw new UnauthorizedException; + } + return $this->fractal->item($backup) ->transformWith($this->getTransformer(BackupTransformer::class)) ->toArray(); @@ -116,15 +135,19 @@ class BackupController extends ClientApiController * Deletes a backup from the panel as well as the remote source where it is currently * being stored. * - * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Backups\DeleteBackupRequest $request + * @param \Illuminate\Http\Request $request * @param \Pterodactyl\Models\Server $server * @param \Pterodactyl\Models\Backup $backup * @return \Illuminate\Http\JsonResponse * * @throws \Throwable */ - public function delete(DeleteBackupRequest $request, Server $server, Backup $backup) + public function delete(Request $request, Server $server, Backup $backup) { + if (! $request->user()->can(Permission::ACTION_BACKUP_DELETE, $server)) { + throw new UnauthorizedException; + } + $server->audit(AuditLog::SERVER__BACKUP_DELETED, function (AuditLog $audit) use ($backup) { $audit->metadata = ['backup_uuid' => $backup->uuid]; @@ -133,4 +156,79 @@ class BackupController extends ClientApiController return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); } + + /** + * Download the backup for a given server instance. For daemon local files, the file + * will be streamed back through the Panel. For AWS S3 files, a signed URL will be generated + * which the user is redirected to. + * + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Backup $backup + * @return \Illuminate\Http\JsonResponse + */ + public function download(Request $request, Server $server, Backup $backup) + { + if (! $request->user()->can(Permission::ACTION_BACKUP_DOWNLOAD, $server)) { + throw new UnauthorizedException; + } + + switch ($backup->disk) { + case Backup::ADAPTER_WINGS: + case Backup::ADAPTER_AWS_S3: + return new JsonResponse([ + 'object' => 'signed_url', + 'attributes' => ['url' => ''], + ]); + default: + throw new BadRequestHttpException; + } + } + + /** + * Handles restoring a backup by making a request to the Wings instance telling it + * to begin the process of finding (or downloading) the backup and unpacking it + * over the server files. + * + * If the "truncate" flag is passed through in this request then all of the + * files that currently exist on the server will be deleted before restoring. + * Otherwise the archive will simply be unpacked over the existing files. + * + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Backup $backup + * @return \Illuminate\Http\JsonResponse + * + * @throws \Throwable + */ + public function restore(Request $request, Server $server, Backup $backup) + { + if (! $request->user()->can(Permission::ACTION_BACKUP_RESTORE, $server)) { + throw new UnauthorizedException; + } + + // Cannot restore a backup unless a server is fully installed and not currently + // processing a different backup restoration request. + if (! is_null($server->status)) { + throw new BadRequestHttpException('This server is not currently in a state that allows for a backup to be restored.'); + } + + $server->audit(AuditLog::SERVER__BACKUP_RESTORE_STARTED, function (AuditLog $audit, Server $server) use ($backup, $request) { + $audit->metadata = ['backup_uuid' => $backup->uuid]; + + // If the backup is for an S3 file we need to generate a unique Download link for + // it that will allow Wings to actually access the file. + if ($backup->disk === Backup::ADAPTER_AWS_S3) { + $url = $this->downloadLinkService->handle($backup, $request->user()); + } + + // Update the status right away for the server so that we know not to allow certain + // actions against it via the Panel API. + $server->update(['status' => Server::STATUS_RESTORING_BACKUP]); + + $this->repository->restore($backup, $url ?? null, $request->input('truncate') === 'true'); + }); + + return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); + } } diff --git a/app/Http/Controllers/Api/Client/Servers/DownloadBackupController.php b/app/Http/Controllers/Api/Client/Servers/DownloadBackupController.php deleted file mode 100644 index 4c8b16a25..000000000 --- a/app/Http/Controllers/Api/Client/Servers/DownloadBackupController.php +++ /dev/null @@ -1,144 +0,0 @@ -daemonBackupRepository = $daemonBackupRepository; - $this->responseFactory = $responseFactory; - $this->jwtService = $jwtService; - $this->backupManager = $backupManager; - } - - /** - * Download the backup for a given server instance. For daemon local files, the file - * will be streamed back through the Panel. For AWS S3 files, a signed URL will be generated - * which the user is redirected to. - * - * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Backups\DownloadBackupRequest $request - * @param \Pterodactyl\Models\Server $server - * @param \Pterodactyl\Models\Backup $backup - * @return \Illuminate\Http\JsonResponse - */ - public function __invoke(DownloadBackupRequest $request, Server $server, Backup $backup) - { - switch ($backup->disk) { - case Backup::ADAPTER_WINGS: - $url = $this->getLocalBackupUrl($backup, $server, $request->user()); - break; - case Backup::ADAPTER_AWS_S3: - $url = $this->getS3BackupUrl($backup, $server); - break; - default: - throw new BadRequestHttpException; - } - - return new JsonResponse([ - 'object' => 'signed_url', - 'attributes' => [ - 'url' => $url, - ], - ]); - } - - /** - * Returns a signed URL that allows us to download a file directly out of a non-public - * S3 bucket by using a signed URL. - * - * @param \Pterodactyl\Models\Backup $backup - * @param \Pterodactyl\Models\Server $server - * @return string - */ - protected function getS3BackupUrl(Backup $backup, Server $server) - { - /** @var \League\Flysystem\AwsS3v3\AwsS3Adapter $adapter */ - $adapter = $this->backupManager->adapter(Backup::ADAPTER_AWS_S3); - - $client = $adapter->getClient(); - - $request = $client->createPresignedRequest( - $client->getCommand('GetObject', [ - 'Bucket' => $adapter->getBucket(), - 'Key' => sprintf('%s/%s.tar.gz', $server->uuid, $backup->uuid), - 'ContentType' => 'application/x-gzip', - ]), - CarbonImmutable::now()->addMinutes(5) - ); - - return $request->getUri()->__toString(); - } - - /** - * Returns a download link a backup stored on a wings instance. - * - * @param \Pterodactyl\Models\Backup $backup - * @param \Pterodactyl\Models\Server $server - * @param \Pterodactyl\Models\User $user - * @return string - */ - protected function getLocalBackupUrl(Backup $backup, Server $server, User $user) - { - $token = $this->jwtService - ->setExpiresAt(CarbonImmutable::now()->addMinutes(15)) - ->setClaims([ - 'backup_uuid' => $backup->uuid, - 'server_uuid' => $server->uuid, - ]) - ->handle($server->node, $user->id . $server->uuid); - - return sprintf( - '%s/download/backup?token=%s', - $server->node->getConnectionAddress(), - $token->__toString() - ); - } -} diff --git a/app/Http/Requests/Api/Client/Servers/Backups/DeleteBackupRequest.php b/app/Http/Requests/Api/Client/Servers/Backups/DeleteBackupRequest.php deleted file mode 100644 index 33b68aabd..000000000 --- a/app/Http/Requests/Api/Client/Servers/Backups/DeleteBackupRequest.php +++ /dev/null @@ -1,17 +0,0 @@ -route()->parameter('server'); - /** @var \Pterodactyl\Models\Backup|mixed $backup */ - $backup = $this->route()->parameter('backup'); - - if ($server instanceof Server && $backup instanceof Backup) { - if ($server->exists && $backup->exists && $server->id === $backup->server_id) { - return true; - } - } - - return false; - } -} diff --git a/app/Http/Requests/Api/Client/Servers/Backups/GetBackupsRequest.php b/app/Http/Requests/Api/Client/Servers/Backups/GetBackupsRequest.php deleted file mode 100644 index f938906d1..000000000 --- a/app/Http/Requests/Api/Client/Servers/Backups/GetBackupsRequest.php +++ /dev/null @@ -1,17 +0,0 @@ - [ 'create' => 'Allows a user to create new backups for this server.', 'read' => 'Allows a user to view all backups that exist for this server.', - 'update' => '', 'delete' => 'Allows a user to remove backups from the system.', - 'download' => 'Allows a user to download backups.', + 'download' => 'Allows a user to download a backup for the server. Danger: this allows a user to access all files for the server in the backup.', + 'restore' => 'Allows a user to restore a backup for the server. Danger: this allows the user to delete all of the server files in the process.' ], ], diff --git a/app/Models/Server.php b/app/Models/Server.php index 0f8597224..58e5a9760 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -66,6 +66,7 @@ class Server extends Model const STATUS_INSTALLING = 'installing'; const STATUS_INSTALL_FAILED = 'install_failed'; const STATUS_SUSPENDED = 'suspended'; + const STATUS_RESTORING_BACKUP = 'restoring_backup'; /** * The table associated with the model. diff --git a/app/Repositories/Wings/DaemonBackupRepository.php b/app/Repositories/Wings/DaemonBackupRepository.php index 37156f213..782f39d62 100644 --- a/app/Repositories/Wings/DaemonBackupRepository.php +++ b/app/Repositories/Wings/DaemonBackupRepository.php @@ -2,7 +2,6 @@ namespace Pterodactyl\Repositories\Wings; -use Illuminate\Support\Arr; use Webmozart\Assert\Assert; use Pterodactyl\Models\Backup; use Pterodactyl\Models\Server; @@ -58,6 +57,36 @@ class DaemonBackupRepository extends DaemonRepository } } + /** + * Sends a request to Wings to begin restoring a backup for a server. + * + * @param \Pterodactyl\Models\Backup $backup + * @param string|null $url + * @param bool $truncate + * @return \Psr\Http\Message\ResponseInterface + * + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + */ + public function restore(Backup $backup, string $url = null, bool $truncate = false): ResponseInterface + { + Assert::isInstanceOf($this->server, Server::class); + + try { + return $this->getHttpClient()->post( + sprintf('/api/servers/%s/backup/%s/restore', $this->server->uuid, $backup->uuid), + [ + 'json' => [ + 'adapter' => $backup->disk, + 'truncate_directory' => $truncate, + 'download_url' => $url ?? '', + ], + ] + ); + } catch (TransferException $exception) { + throw new DaemonConnectionException($exception); + } + } + /** * Deletes a backup from the daemon. * diff --git a/app/Services/Backups/DownloadLinkService.php b/app/Services/Backups/DownloadLinkService.php new file mode 100644 index 000000000..f08b96596 --- /dev/null +++ b/app/Services/Backups/DownloadLinkService.php @@ -0,0 +1,83 @@ +backupManager = $backupManager; + $this->jwtService = $jwtService; + } + + /** + * Returns the URL that allows for a backup to be downloaded by an individual + * user, or by the Wings control software. + * + * @param \Pterodactyl\Models\Backup $backup + * @param \Pterodactyl\Models\User $user + * @return string + */ + public function handle(Backup $backup, User $user): string + { + if ($backup->disk === Backup::ADAPTER_AWS_S3) { + return $this->getS3BackupUrl($backup); + } + + $token = $this->jwtService + ->setExpiresAt(CarbonImmutable::now()->addMinutes(15)) + ->setClaims([ + 'backup_uuid' => $backup->uuid, + 'server_uuid' => $backup->server->uuid, + ]) + ->handle($backup->server->node, $user->id . $backup->server->uuid); + + return sprintf('%s/download/backup?token=%s', $backup->server->node->getConnectionAddress(), $token->__toString()); + } + + /** + * Returns a signed URL that allows us to download a file directly out of a non-public + * S3 bucket by using a signed URL. + * + * @param \Pterodactyl\Models\Backup $backup + * @return string + */ + protected function getS3BackupUrl(Backup $backup) + { + /** @var \League\Flysystem\AwsS3v3\AwsS3Adapter $adapter */ + $adapter = $this->backupManager->adapter(Backup::ADAPTER_AWS_S3); + + $request = $adapter->getClient()->createPresignedRequest( + $adapter->getClient()->getCommand('GetObject', [ + 'Bucket' => $adapter->getBucket(), + 'Key' => sprintf('%s/%s.tar.gz', $backup->server->uuid, $backup->uuid), + 'ContentType' => 'application/x-gzip', + ]), + CarbonImmutable::now()->addMinutes(5) + ); + + return $request->getUri()->__toString(); + } +}