From be05d2df81cf85c2bd9444a86bd685294bae649c Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 4 Apr 2020 19:54:59 -0700 Subject: [PATCH] Add support for generating a signed URL for downloading a file from the daemon --- .../Servers/DownloadBackupController.php | 80 ++++++++++++++++++ .../Client/SubstituteClientApiBindings.php | 5 ++ .../Servers/Backups/DownloadBackupRequest.php | 41 ++++++++++ app/Models/Backup.php | 3 + .../Wings/DaemonBackupRepository.php | 35 ++++++++ .../components/server/backups/BackupRow.tsx | 82 +++++++++++++++---- routes/api-client.php | 1 + 7 files changed, 230 insertions(+), 17 deletions(-) create mode 100644 app/Http/Controllers/Api/Client/Servers/DownloadBackupController.php create mode 100644 app/Http/Requests/Api/Client/Servers/Backups/DownloadBackupRequest.php create mode 100644 app/Repositories/Wings/DaemonBackupRepository.php diff --git a/app/Http/Controllers/Api/Client/Servers/DownloadBackupController.php b/app/Http/Controllers/Api/Client/Servers/DownloadBackupController.php new file mode 100644 index 000000000..22c7ea6fb --- /dev/null +++ b/app/Http/Controllers/Api/Client/Servers/DownloadBackupController.php @@ -0,0 +1,80 @@ +daemonBackupRepository = $daemonBackupRepository; + $this->responseFactory = $responseFactory; + } + + /** + * 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\RedirectResponse + */ + public function __invoke(DownloadBackupRequest $request, Server $server, Backup $backup) + { + $signer = new Sha256; + $now = CarbonImmutable::now(); + + $token = (new Builder)->issuedBy(config('app.url')) + ->permittedFor($server->node->getConnectionAddress()) + ->identifiedBy(hash('sha256', $request->user()->id . $server->uuid), true) + ->issuedAt($now->getTimestamp()) + ->canOnlyBeUsedAfter($now->subMinutes(5)->getTimestamp()) + ->expiresAt($now->addMinutes(15)->getTimestamp()) + ->withClaim('unique_id', Str::random(16)) + ->withClaim('backup_uuid', $backup->uuid) + ->withClaim('server_uuid', $server->uuid) + ->getToken($signer, new Key($server->node->daemonSecret)); + + $location = sprintf( + '%s/download/backup?token=%s', + $server->node->getConnectionAddress(), + $token->__toString() + ); + + return RedirectResponse::create($location); + } +} diff --git a/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php b/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php index 40c26c538..81ed6b401 100644 --- a/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php +++ b/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Http\Middleware\Api\Client; use Closure; +use Pterodactyl\Models\Backup; use Illuminate\Container\Container; use Pterodactyl\Contracts\Extensions\HashidsInterface; use Pterodactyl\Http\Middleware\Api\ApiSubstituteBindings; @@ -55,6 +56,10 @@ class SubstituteClientApiBindings extends ApiSubstituteBindings } }); + $this->router->model('backup', Backup::class, function ($value) { + return Backup::query()->where('uuid', $value)->firstOrFail(); + }); + return parent::handle($request, $next); } } diff --git a/app/Http/Requests/Api/Client/Servers/Backups/DownloadBackupRequest.php b/app/Http/Requests/Api/Client/Servers/Backups/DownloadBackupRequest.php new file mode 100644 index 000000000..cfa38e688 --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/Backups/DownloadBackupRequest.php @@ -0,0 +1,41 @@ +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/Models/Backup.php b/app/Models/Backup.php index 56be90f87..6259f11c6 100644 --- a/app/Models/Backup.php +++ b/app/Models/Backup.php @@ -26,6 +26,9 @@ class Backup extends Model const RESOURCE_NAME = 'backup'; + const DISK_LOCAL = 'local'; + const DISK_AWS_S3 = 's3'; + /** * @var string */ diff --git a/app/Repositories/Wings/DaemonBackupRepository.php b/app/Repositories/Wings/DaemonBackupRepository.php new file mode 100644 index 000000000..d95573d1f --- /dev/null +++ b/app/Repositories/Wings/DaemonBackupRepository.php @@ -0,0 +1,35 @@ +server, Server::class); + + try { + return $this->getHttpClient()->get( + sprintf('/api/servers/%s/backup/%s', $this->server->uuid, $backup), + ['stream' => true] + ); + } catch (TransferException $exception) { + throw new DaemonConnectionException($exception); + } + } +} diff --git a/resources/scripts/components/server/backups/BackupRow.tsx b/resources/scripts/components/server/backups/BackupRow.tsx index ec98cc885..406372e12 100644 --- a/resources/scripts/components/server/backups/BackupRow.tsx +++ b/resources/scripts/components/server/backups/BackupRow.tsx @@ -1,28 +1,67 @@ -import React from 'react'; +import React, { useState } from 'react'; import { ServerBackup } from '@/api/server/backups/getServerBackups'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faArchive } from '@fortawesome/free-solid-svg-icons/faArchive'; import format from 'date-fns/format'; -import distanceInWordsToNow from 'date-fns/distance_in_words_to_now' +import distanceInWordsToNow from 'date-fns/distance_in_words_to_now'; import Spinner from '@/components/elements/Spinner'; import { faCloudDownloadAlt } from '@fortawesome/free-solid-svg-icons/faCloudDownloadAlt'; +import Modal, { RequiredModalProps } from '@/components/elements/Modal'; +import { bytesToHuman } from '@/helpers'; +import Can from '@/components/elements/Can'; +import { join } from "path"; +import useServer from '@/plugins/useServer'; interface Props { backup: ServerBackup; className?: string; } +const DownloadModal = ({ checksum, ...props }: RequiredModalProps & { checksum: string }) => ( + +

Verify file checksum

+

+ The SHA256 checksum of this file is: +

+
+            {checksum}
+        
+
+); + export default ({ backup, className }: Props) => { + const { uuid } = useServer(); + const [ visible, setVisible ] = useState(false); + return (
+ {visible && + setVisible(false)} + checksum={backup.sha256Hash} + /> + }
- + {backup.completedAt ? + + : + + }
-

{backup.name}

-

{backup.uuid}

+

+ {backup.name} + {backup.completedAt && + {bytesToHuman(backup.bytes)} + } +

+

+ {backup.uuid} +

-
+

{

Created

-
- {!backup.completedAt ? -
- -
- : - - - - } -
+ + +
); }; diff --git a/routes/api-client.php b/routes/api-client.php index d0619949f..f68d61d8c 100644 --- a/routes/api-client.php +++ b/routes/api-client.php @@ -91,6 +91,7 @@ Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateServ Route::get('/', 'Servers\BackupController@index'); Route::post('/', 'Servers\BackupController@store'); Route::get('/{backup}', 'Servers\BackupController@view'); + Route::get('/{backup}/download', 'Servers\DownloadBackupController'); Route::post('/{backup}', 'Servers\BackupController@update'); Route::delete('/{backup}', 'Servers\BackupController@delete'); });