<?php namespace Pterodactyl\Http\Controllers\Api\Remote\Servers; use Cake\Chronos\Chronos; use Lcobucci\JWT\Builder; 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 Illuminate\Database\ConnectionInterface; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Services\Servers\SuspensionService; use Pterodactyl\Repositories\Eloquent\NodeRepository; 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; class ServerTransferController extends Controller { /** * @var \Illuminate\Database\ConnectionInterface */ private $connection; /** * @var \Pterodactyl\Repositories\Eloquent\ServerRepository */ private $repository; /** * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface */ private $allocationRepository; /** * @var \Pterodactyl\Repositories\Eloquent\NodeRepository */ private $nodeRepository; /** * @var \Pterodactyl\Repositories\Wings\DaemonServerRepository */ private $daemonServerRepository; /** * @var \Pterodactyl\Repositories\Wings\DaemonTransferRepository */ private $daemonTransferRepository; /** * @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService */ private $configurationStructureService; /** * @var \Pterodactyl\Services\Servers\SuspensionService */ private $suspensionService; /** * @var \Psr\Log\LoggerInterface */ private $writer; /** * 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 \Pterodactyl\Services\Servers\SuspensionService $suspensionService * @param \Psr\Log\LoggerInterface $writer */ public function __construct( ConnectionInterface $connection, ServerRepository $repository, AllocationRepositoryInterface $allocationRepository, NodeRepository $nodeRepository, DaemonServerRepository $daemonServerRepository, DaemonTransferRepository $daemonTransferRepository, ServerConfigurationStructureService $configurationStructureService, SuspensionService $suspensionService, LoggerInterface $writer ) { $this->connection = $connection; $this->repository = $repository; $this->allocationRepository = $allocationRepository; $this->nodeRepository = $nodeRepository; $this->daemonServerRepository = $daemonServerRepository; $this->daemonTransferRepository = $daemonTransferRepository; $this->configurationStructureService = $configurationStructureService; $this->suspensionService = $suspensionService; $this->writer = $writer; } /** * The daemon notifies us about the archive status. * * @param \Illuminate\Http\Request $request * @param string $uuid * @return \Illuminate\Http\JsonResponse * * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Throwable */ public function archive(Request $request, string $uuid) { $server = $this->repository->getByUuid($uuid); // Unsuspend the server and don't continue the transfer. if (! $request->input('successful')) { $this->suspensionService->toggle($server, 'unsuspend'); return JsonResponse::create([], Response::HTTP_NO_CONTENT); } $server->node_id = $server->transfer->new_node; $data = $this->configurationStructureService->handle($server); $data['suspended'] = false; $data['service']['skip_scripts'] = true; $allocations = $server->getAllocationMappings(); $data['allocations']['default']['ip'] = array_key_first($allocations); $data['allocations']['default']['port'] = $allocations[$data['allocations']['default']['ip']][0]; $now = Chronos::now(); $signer = new 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())); // 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 { $this->daemonTransferRepository ->setServer($server) ->setNode($this->nodeRepository->find($server->transfer->new_node)) ->notify($server, $data, $server->node, $token->__toString()); } catch (DaemonConnectionException $exception) { throw $exception; } return JsonResponse::create([], Response::HTTP_NO_CONTENT); } /** * The daemon notifies us about a transfer failure. * * @param \Illuminate\Http\Request $request * @param string $uuid * @return \Illuminate\Http\JsonResponse * * @throws \Throwable */ 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); // Remove the new allocations. $this->allocationRepository->updateWhereIn('id', $allocationIds, ['server_id' => null]); // Unsuspend the server. $this->suspensionService->toggle($server, 'unsuspend'); return JsonResponse::create([], Response::HTTP_NO_CONTENT); } /** * The daemon notifies us about a transfer success. * * @param string $uuid * @return \Illuminate\Http\JsonResponse * * @throws \Throwable */ public function success(string $uuid) { $server = $this->repository->getByUuid($uuid); $transfer = $server->transfer; $allocationIds = json_decode($transfer->old_additional_allocations); array_push($allocationIds, $transfer->old_allocation); // Begin a transaction. $this->connection->beginTransaction(); // Remove the old allocations. $this->allocationRepository->updateWhereIn('id', $allocationIds, ['server_id' => null]); // Update the server's allocation_id and node_id. $server->allocation_id = $transfer->new_allocation; $server->node_id = $transfer->new_node; $server->save(); // Mark the transfer as successful. $transfer->successful = true; $transfer->save(); // Commit the transaction. $this->connection->commit(); // Delete the server from the old node try { $this->daemonServerRepository->setServer($server)->delete(); } catch (DaemonConnectionException $exception) { $this->writer->warning($exception); } // Unsuspend the server $server->load('node'); $this->suspensionService->toggle($server, $this->suspensionService::ACTION_UNSUSPEND); return JsonResponse::create([], Response::HTTP_NO_CONTENT); } }