Add support for generating a signed URL for downloading a file from the daemon

This commit is contained in:
Dane Everitt 2020-04-04 19:54:59 -07:00
parent 15b436d26e
commit be05d2df81
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
7 changed files with 230 additions and 17 deletions

View file

@ -0,0 +1,80 @@
<?php
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
use Lcobucci\JWT\Builder;
use Carbon\CarbonImmutable;
use Illuminate\Support\Str;
use Lcobucci\JWT\Signer\Key;
use Pterodactyl\Models\Backup;
use Pterodactyl\Models\Server;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Illuminate\Http\RedirectResponse;
use Illuminate\Contracts\Routing\ResponseFactory;
use Pterodactyl\Repositories\Wings\DaemonBackupRepository;
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
use Pterodactyl\Http\Requests\Api\Client\Servers\Backups\DownloadBackupRequest;
class DownloadBackupController extends ClientApiController
{
/**
* @var \Pterodactyl\Repositories\Wings\DaemonBackupRepository
*/
private $daemonBackupRepository;
/**
* @var \Illuminate\Contracts\Routing\ResponseFactory
*/
private $responseFactory;
/**
* DownloadBackupController constructor.
*
* @param \Pterodactyl\Repositories\Wings\DaemonBackupRepository $daemonBackupRepository
* @param \Illuminate\Contracts\Routing\ResponseFactory $responseFactory
*/
public function __construct(
DaemonBackupRepository $daemonBackupRepository,
ResponseFactory $responseFactory
) {
parent::__construct();
$this->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);
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Backups;
use Pterodactyl\Models\Backup;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Permission;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
class DownloadBackupRequest extends ClientApiRequest
{
/**
* @return string
*/
public function permission()
{
return Permission::ACTION_BACKUP_DOWNLOAD;
}
/**
* Ensure that this backup belongs to the server that is also present in the
* request.
*
* @return bool
*/
public function resourceExists(): bool
{
/** @var \Pterodactyl\Models\Server|mixed $server */
$server = $this->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;
}
}

View file

@ -26,6 +26,9 @@ class Backup extends Model
const RESOURCE_NAME = 'backup';
const DISK_LOCAL = 'local';
const DISK_AWS_S3 = 's3';
/**
* @var string
*/

View file

@ -0,0 +1,35 @@
<?php
namespace Pterodactyl\Repositories\Wings;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\Server;
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Exception\TransferException;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
class DaemonBackupRepository extends DaemonRepository
{
/**
* Returns a stream of a backup's contents from the Wings instance so that we
* do not need to send the user directly to the Daemon.
*
* @param string $backup
* @return \Psr\Http\Message\ResponseInterface
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function getBackup(string $backup): ResponseInterface
{
Assert::isInstanceOf($this->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);
}
}
}