From 2ee08a1a3dc0ead5a5355777b766d0b28fe01f69 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 24 Dec 2020 10:10:40 -0800 Subject: [PATCH] Update logic for server transfer controller --- .../Servers/ServerTransferController.php | 199 ++++++++---------- app/Models/ServerTransfer.php | 14 +- app/Services/Eggs/EggConfigurationService.php | 2 +- app/Services/Nodes/NodeJWTService.php | 29 ++- .../ServerConfigurationStructureService.php | 31 ++- ..._24_092449_make_allocation_fields_json.php | 34 +++ 6 files changed, 178 insertions(+), 131 deletions(-) create mode 100644 database/migrations/2020_12_24_092449_make_allocation_fields_json.php diff --git a/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php b/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php index 32c56e98a..cc097a637 100644 --- a/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php +++ b/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php @@ -3,20 +3,19 @@ namespace Pterodactyl\Http\Controllers\Api\Remote\Servers; use Cake\Chronos\Chronos; -use Lcobucci\JWT\Builder; +use Illuminate\Support\Arr; use Illuminate\Http\Request; -use Lcobucci\JWT\Signer\Key; -use Psr\Log\LoggerInterface; use Illuminate\Http\Response; use Illuminate\Http\JsonResponse; -use Lcobucci\JWT\Signer\Hmac\Sha256; +use Pterodactyl\Models\Allocation; +use Illuminate\Support\Facades\Log; +use Pterodactyl\Models\ServerTransfer; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Repositories\Eloquent\NodeRepository; +use Pterodactyl\Services\Nodes\NodeJWTService; use Pterodactyl\Repositories\Eloquent\ServerRepository; use Pterodactyl\Repositories\Wings\DaemonServerRepository; use Pterodactyl\Repositories\Wings\DaemonTransferRepository; -use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; use Pterodactyl\Services\Servers\ServerConfigurationStructureService; @@ -32,16 +31,6 @@ class ServerTransferController extends Controller */ private $repository; - /** - * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface - */ - private $allocationRepository; - - /** - * @var \Pterodactyl\Repositories\Eloquent\NodeRepository - */ - private $nodeRepository; - /** * @var \Pterodactyl\Repositories\Wings\DaemonServerRepository */ @@ -58,40 +47,34 @@ class ServerTransferController extends Controller private $configurationStructureService; /** - * @var \Psr\Log\LoggerInterface + * @var \Pterodactyl\Services\Nodes\NodeJWTService */ - private $writer; + private $jwtService; /** * ServerTransferController constructor. * * @param \Illuminate\Database\ConnectionInterface $connection * @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository - * @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $allocationRepository - * @param \Pterodactyl\Repositories\Eloquent\NodeRepository $nodeRepository * @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository * @param \Pterodactyl\Repositories\Wings\DaemonTransferRepository $daemonTransferRepository * @param \Pterodactyl\Services\Servers\ServerConfigurationStructureService $configurationStructureService - * @param \Psr\Log\LoggerInterface $writer + * @param \Pterodactyl\Services\Nodes\NodeJWTService $jwtService */ public function __construct( ConnectionInterface $connection, ServerRepository $repository, - AllocationRepositoryInterface $allocationRepository, - NodeRepository $nodeRepository, DaemonServerRepository $daemonServerRepository, DaemonTransferRepository $daemonTransferRepository, ServerConfigurationStructureService $configurationStructureService, - LoggerInterface $writer + NodeJWTService $jwtService ) { $this->connection = $connection; $this->repository = $repository; - $this->allocationRepository = $allocationRepository; - $this->nodeRepository = $nodeRepository; $this->daemonServerRepository = $daemonServerRepository; $this->daemonTransferRepository = $daemonTransferRepository; $this->configurationStructureService = $configurationStructureService; - $this->writer = $writer; + $this->jwtService = $jwtService; } /** @@ -101,7 +84,6 @@ class ServerTransferController extends Controller * @param string $uuid * @return \Illuminate\Http\JsonResponse * - * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Throwable */ @@ -111,62 +93,43 @@ class ServerTransferController extends Controller // Unsuspend the server and don't continue the transfer. if (! $request->input('successful')) { - $transfer = $server->transfer; - - $transfer->successful = false; - $transfer->saveOrFail(); - - $allocationIds = json_decode($transfer->new_additional_allocations); - array_push($allocationIds, $transfer->new_allocation); - - // Release the reserved allocations. - $this->allocationRepository->updateWhereIn('id', $allocationIds, ['server_id' => null]); - - return new JsonResponse([], Response::HTTP_NO_CONTENT); + return $this->processFailedTransfer($server->transfer); } - $server->node_id = $server->transfer->new_node; - - $data = $this->configurationStructureService->handle($server); - $data['suspended'] = false; - $data['service']['skip_scripts'] = true; + // We want to generate a new configuration using the new node_id value from the + // transfer, and not the old node value. + $data = $this->configurationStructureService->handle($server, [ + 'node_id' => $server->transfer->new_node, + ]); $allocations = $server->getAllocationMappings(); - $data['allocations']['default']['ip'] = array_key_first($allocations); - $data['allocations']['default']['port'] = $allocations[$data['allocations']['default']['ip']][0]; + $primary = array_key_first($allocations); + Arr::set($data, 'allocations.default.ip', $primary); + Arr::set($data, 'allocations.default.port', $allocations[$primary][0]); + Arr::set($data, 'service.skip_scripts', true); + Arr::set($data, 'suspended', false); - $now = Chronos::now(); - $signer = new Sha256; + $this->connection->transaction(function () use ($data, $server) { + // This token is used by the new node the server is being transfered to. It allows + // that node to communicate with the old node during the process to initiate the + // actual file transfer. + $token = $this->jwtService + ->setExpiresAt(Chronos::now()->addMinutes(15)) + ->setSubject($server->uuid) + ->handle($server->node, $server->uuid, 'sha256'); - $token = (new Builder)->issuedBy(config('app.url')) - ->permittedFor($server->node->getConnectionAddress()) - ->identifiedBy(hash('sha256', $server->uuid), true) - ->issuedAt($now->getTimestamp()) - ->canOnlyBeUsedAfter($now->getTimestamp()) - ->expiresAt($now->addMinutes(15)->getTimestamp()) - ->relatedTo($server->uuid, true) - ->getToken($signer, new Key($server->node->getDecryptedKey())); - - // Update the archived field on the transfer to make clients connect to the websocket - // on the new node to be able to receive transfer logs. - $server->transfer->forceFill([ - 'archived' => true, - ])->saveOrFail(); - - // On the daemon transfer repository, make sure to set the node after the server - // because setServer() tells the repository to use the server's node and not the one - // we want to specify. - try { - /** @var \Pterodactyl\Models\Node $newNode */ - $newNode = $this->nodeRepository->find($server->transfer->new_node); + // Update the archived field on the transfer to make clients connect to the websocket + // on the new node to be able to receive transfer logs. + $server->transfer->forceFill(['archived' => true])->saveOrFail(); + // On the daemon transfer repository, make sure to set the node after the server + // because setServer() tells the repository to use the server's node and not the one + // we want to specify. $this->daemonTransferRepository ->setServer($server) - ->setNode($newNode) + ->setNode($server->transfer->newNode) ->notify($server, $data, $server->node, $token->__toString()); - } catch (DaemonConnectionException $exception) { - throw $exception; - } + }); return new JsonResponse([], Response::HTTP_NO_CONTENT); } @@ -182,25 +145,8 @@ class ServerTransferController extends Controller public function failure(string $uuid) { $server = $this->repository->getByUuid($uuid); - $transfer = $server->transfer; - $allocationIds = json_decode($transfer->new_additional_allocations); - array_push($allocationIds, $transfer->new_allocation); - - // Begin a transaction. - $this->connection->beginTransaction(); - - // Mark the transfer as unsuccessful. - $transfer->successful = false; - $transfer->saveOrFail(); - - // Remove the new allocations. - $this->allocationRepository->updateWhereIn('id', $allocationIds, ['server_id' => null]); - - // Commit the transaction. - $this->connection->commit(); - - return new JsonResponse([], Response::HTTP_NO_CONTENT); + return $this->processFailedTransfer($server->transfer); } /** @@ -216,34 +162,63 @@ class ServerTransferController extends Controller $server = $this->repository->getByUuid($uuid); $transfer = $server->transfer; - $allocationIds = json_decode($transfer->old_additional_allocations); - array_push($allocationIds, $transfer->old_allocation); + /** @var \Pterodactyl\Models\Server $server */ + $server = $this->connection->transaction(function () use ($server, $transfer) { + $allocations = [$transfer->old_allocation]; + if (! empty($transfer->old_additional_allocations)) { + array_push($allocations, $transfer->old_additional_allocations); + } - // Begin a transaction. - $this->connection->beginTransaction(); + // Remove the old allocations for the server and re-assign the server to the new + // primary allocation and node. + Allocation::query()->whereIn('id', $allocations)->update(['server_id' => null]); + $server->update([ + 'allocation_id' => $transfer->new_allocation, + 'node_id' => $transfer->new_node, + ]); - // Remove the old allocations. - $this->allocationRepository->updateWhereIn('id', $allocationIds, ['server_id' => null]); + $server = $server->fresh(); + $server->transfer->update(['successful' => true]); - // Update the server's allocation_id and node_id. - $server->allocation_id = $transfer->new_allocation; - $server->node_id = $transfer->new_node; - $server->saveOrFail(); + return $server; + }); - // Mark the transfer as successful. - $transfer->successful = true; - $transfer->saveOrFail(); - - // Commit the transaction. - $this->connection->commit(); - - // Delete the server from the old node + // Delete the server from the old node making sure to point it to the old node so + // that we do not delete it from the new node the server was transfered to. try { - $this->daemonServerRepository->setServer($server)->delete(); + $this->daemonServerRepository + ->setServer($server) + ->setNode($transfer->oldNode) + ->delete(); } catch (DaemonConnectionException $exception) { - $this->writer->warning($exception); + Log::warning($exception, ['transfer_id' => $server->transfer->id]); } return new JsonResponse([], Response::HTTP_NO_CONTENT); } + + /** + * Release all of the reserved allocations for this transfer and mark it as failed in + * the database. + * + * @param \Pterodactyl\Models\ServerTransfer $transfer + * @return \Illuminate\Http\JsonResponse + * + * @throws \Throwable + */ + protected function processFailedTransfer(ServerTransfer $transfer) + { + $this->connection->transaction(function () use (&$transfer) { + $transfer->forceFill(['successful' => false])->saveOrFail(); + + $allocations = [$transfer->new_allocation]; + if (! empty($transfer->new_additional_allocations)) { + array_push($allocations, $transfer->new_additional_allocations); + } + + Allocation::query()->whereIn('id', $allocations)->update(['server_id' => null]); + }); + + return new JsonResponse([], Response::HTTP_NO_CONTENT); + } } diff --git a/app/Models/ServerTransfer.php b/app/Models/ServerTransfer.php index cb95fefc7..1af9ab7e4 100644 --- a/app/Models/ServerTransfer.php +++ b/app/Models/ServerTransfer.php @@ -9,8 +9,8 @@ namespace Pterodactyl\Models; * @property int $new_node * @property int $old_allocation * @property int $new_allocation - * @property string $old_additional_allocations - * @property string $new_additional_allocations + * @property array|null $old_additional_allocations + * @property array|null $new_additional_allocations * @property bool|null $successful * @property bool $archived * @property \Carbon\Carbon $created_at @@ -53,8 +53,8 @@ class ServerTransfer extends Model 'new_node' => 'int', 'old_allocation' => 'int', 'new_allocation' => 'int', - 'old_additional_allocations' => 'string', - 'new_additional_allocations' => 'string', + 'old_additional_allocations' => 'array', + 'new_additional_allocations' => 'array', 'successful' => 'bool', 'archived' => 'bool', ]; @@ -68,8 +68,10 @@ class ServerTransfer extends Model 'new_node' => 'required|numeric', 'old_allocation' => 'required|numeric', 'new_allocation' => 'required|numeric', - 'old_additional_allocations' => 'nullable', - 'new_additional_allocations' => 'nullable', + 'old_additional_allocations' => 'nullable|array', + 'old_additional_allocations.*' => 'numeric', + 'new_additional_allocations' => 'nullable|array', + 'new_additional_allocations.*' => 'numeric', 'successful' => 'sometimes|nullable|boolean', ]; diff --git a/app/Services/Eggs/EggConfigurationService.php b/app/Services/Eggs/EggConfigurationService.php index 6dbb469ba..4d1db4e2e 100644 --- a/app/Services/Eggs/EggConfigurationService.php +++ b/app/Services/Eggs/EggConfigurationService.php @@ -102,7 +102,7 @@ class EggConfigurationService { // Get the legacy configuration structure for the server so that we // can property map the egg placeholders to values. - $structure = $this->configurationStructureService->handle($server, true); + $structure = $this->configurationStructureService->handle($server, [], true); $response = []; // Normalize the output of the configuration for the new Wings Daemon to more diff --git a/app/Services/Nodes/NodeJWTService.php b/app/Services/Nodes/NodeJWTService.php index 85332a6bc..7c359efe8 100644 --- a/app/Services/Nodes/NodeJWTService.php +++ b/app/Services/Nodes/NodeJWTService.php @@ -22,6 +22,11 @@ class NodeJWTService */ private $expiresAt; + /** + * @var string|null + */ + private $subject; + /** * Set the claims to include in this JWT. * @@ -35,6 +40,10 @@ class NodeJWTService return $this; } + /** + * @param \DateTimeInterface $date + * @return $this + */ public function setExpiresAt(DateTimeInterface $date) { $this->expiresAt = $date->getTimestamp(); @@ -42,20 +51,32 @@ class NodeJWTService return $this; } + /** + * @param string $subject + * @return $this + */ + public function setSubject(string $subject) + { + $this->subject = $subject; + + return $this; + } + /** * Generate a new JWT for a given node. * * @param \Pterodactyl\Models\Node $node * @param string|null $identifiedBy + * @param string $algo * @return \Lcobucci\JWT\Token */ - public function handle(Node $node, string $identifiedBy) + public function handle(Node $node, string $identifiedBy, string $algo = 'md5') { $signer = new Sha256; $builder = (new Builder)->issuedBy(config('app.url')) ->permittedFor($node->getConnectionAddress()) - ->identifiedBy(md5($identifiedBy), true) + ->identifiedBy(hash($algo, $identifiedBy), true) ->issuedAt(CarbonImmutable::now()->getTimestamp()) ->canOnlyBeUsedAfter(CarbonImmutable::now()->subMinutes(5)->getTimestamp()); @@ -63,6 +84,10 @@ class NodeJWTService $builder = $builder->expiresAt($this->expiresAt); } + if (!empty($this->subject)) { + $builder = $builder->relatedTo($this->subject, true); + } + foreach ($this->claims as $key => $value) { $builder = $builder->withClaim($key, $value); } diff --git a/app/Services/Servers/ServerConfigurationStructureService.php b/app/Services/Servers/ServerConfigurationStructureService.php index dc2a4bfb2..790e9ecc1 100644 --- a/app/Services/Servers/ServerConfigurationStructureService.php +++ b/app/Services/Servers/ServerConfigurationStructureService.php @@ -29,14 +29,25 @@ class ServerConfigurationStructureService * daemon, if you modify the structure eggs will break unexpectedly. * * @param \Pterodactyl\Models\Server $server + * @param array $override * @param bool $legacy deprecated * @return array */ - public function handle(Server $server, bool $legacy = false): array + public function handle(Server $server, array $override = [], bool $legacy = false): array { - return $legacy ? - $this->returnLegacyFormat($server) - : $this->returnCurrentFormat($server); + $clone = $server; + // If any overrides have been set on this call make sure to update them on the + // cloned instance so that the configuration generated uses them. + if (!empty($override)) { + $clone = $server->fresh(); + foreach ($override as $key => $value) { + $clone->setAttribute($key, $value); + } + } + + return $legacy + ? $this->returnLegacyFormat($clone) + : $this->returnCurrentFormat($clone); } /** @@ -105,12 +116,12 @@ class ServerConfigurationStructureService })->toArray(), 'env' => $this->environment->handle($server), 'oom_disabled' => $server->oom_disabled, - 'memory' => (int) $server->memory, - 'swap' => (int) $server->swap, - 'io' => (int) $server->io, - 'cpu' => (int) $server->cpu, + 'memory' => (int)$server->memory, + 'swap' => (int)$server->swap, + 'io' => (int)$server->io, + 'cpu' => (int)$server->cpu, 'threads' => $server->threads, - 'disk' => (int) $server->disk, + 'disk' => (int)$server->disk, 'image' => $server->image, ], 'service' => [ @@ -118,7 +129,7 @@ class ServerConfigurationStructureService 'skip_scripts' => $server->skip_scripts, ], 'rebuild' => false, - 'suspended' => (int) $server->suspended, + 'suspended' => (int)$server->suspended, ]; } } diff --git a/database/migrations/2020_12_24_092449_make_allocation_fields_json.php b/database/migrations/2020_12_24_092449_make_allocation_fields_json.php new file mode 100644 index 000000000..0f1ff554f --- /dev/null +++ b/database/migrations/2020_12_24_092449_make_allocation_fields_json.php @@ -0,0 +1,34 @@ +json('old_additional_allocations')->nullable()->change(); + $table->json('new_additional_allocations')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('server_transfers', function (Blueprint $table) { + $table->string('old_additional_allocations')->nullable()->change(); + $table->string('new_additional_allocations')->nullable()->change(); + }); + } +}