diff --git a/app/Http/Controllers/Admin/Servers/ServerTransferController.php b/app/Http/Controllers/Admin/Servers/ServerTransferController.php index 62971bd99..dff8444a9 100644 --- a/app/Http/Controllers/Admin/Servers/ServerTransferController.php +++ b/app/Http/Controllers/Admin/Servers/ServerTransferController.php @@ -8,7 +8,6 @@ use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Models\ServerTransfer; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Services\Servers\TransferService; -use Pterodactyl\Services\Servers\SuspensionService; use Pterodactyl\Repositories\Eloquent\NodeRepository; use Pterodactyl\Repositories\Eloquent\ServerRepository; use Pterodactyl\Repositories\Eloquent\LocationRepository; @@ -42,11 +41,6 @@ class ServerTransferController extends Controller */ private $nodeRepository; - /** - * @var \Pterodactyl\Services\Servers\SuspensionService - */ - private $suspensionService; - /** * @var \Pterodactyl\Services\Servers\TransferService */ @@ -65,7 +59,6 @@ class ServerTransferController extends Controller * @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository * @param \Pterodactyl\Repositories\Eloquent\LocationRepository $locationRepository * @param \Pterodactyl\Repositories\Eloquent\NodeRepository $nodeRepository - * @param \Pterodactyl\Services\Servers\SuspensionService $suspensionService * @param \Pterodactyl\Services\Servers\TransferService $transferService * @param \Pterodactyl\Repositories\Wings\DaemonConfigurationRepository $daemonConfigurationRepository */ @@ -75,7 +68,6 @@ class ServerTransferController extends Controller ServerRepository $repository, LocationRepository $locationRepository, NodeRepository $nodeRepository, - SuspensionService $suspensionService, TransferService $transferService, DaemonConfigurationRepository $daemonConfigurationRepository ) { @@ -84,7 +76,6 @@ class ServerTransferController extends Controller $this->repository = $repository; $this->locationRepository = $locationRepository; $this->nodeRepository = $nodeRepository; - $this->suspensionService = $suspensionService; $this->transferService = $transferService; $this->daemonConfigurationRepository = $daemonConfigurationRepository; } @@ -98,8 +89,7 @@ class ServerTransferController extends Controller * * @throws \Throwable */ - public function transfer(Request $request, Server $server) - { + public function transfer(Request $request, Server $server) { $validatedData = $request->validate([ 'node_id' => 'required|exists:nodes,id', 'allocation_id' => 'required|bail|unique:servers|exists:allocations,id', @@ -116,9 +106,6 @@ class ServerTransferController extends Controller // Check if the selected daemon is online. $this->daemonConfigurationRepository->setNode($node)->getSystemInformation(); - // Suspend the server and request an archive to be created. - //$this->suspensionService->toggle($server, 'suspend'); - // Create a new ServerTransfer entry. $transfer = new ServerTransfer; diff --git a/app/Http/Controllers/Api/Client/Servers/WebsocketController.php b/app/Http/Controllers/Api/Client/Servers/WebsocketController.php index f18b47634..5aaf28afd 100644 --- a/app/Http/Controllers/Api/Client/Servers/WebsocketController.php +++ b/app/Http/Controllers/Api/Client/Servers/WebsocketController.php @@ -58,16 +58,32 @@ class WebsocketController extends ClientApiController throw new HttpException(Response::HTTP_FORBIDDEN, 'You do not have permission to connect to this server\'s websocket.'); } + $permissions = $this->permissionsService->handle($server, $user); + + $node = null; + + // Check if there is a transfer query param asking to connect to the target node's websocket. + if ($request->query('transfer', 'false') === 'true') { + // Check if the user has permissions to receive transfer logs. + if (! in_array('admin.websocket.transfer', $permissions)) { + throw new HttpException(Response::HTTP_FORBIDDEN, 'You do not have permission to get transfer logs'); + } + + $node = $server->transfer->newNode; + } else { + $node = $server->node; + } + $token = $this->jwtService ->setExpiresAt(CarbonImmutable::now()->addMinutes(10)) ->setClaims([ 'user_id' => $request->user()->id, 'server_uuid' => $server->uuid, - 'permissions' => $this->permissionsService->handle($server, $user), + 'permissions' => $permissions, ]) - ->handle($server->node, $user->id . $server->uuid); + ->handle($node, $user->id . $server->uuid); - $socket = str_replace(['https://', 'http://'], ['wss://', 'ws://'], $server->node->getConnectionAddress()); + $socket = str_replace(['https://', 'http://'], ['wss://', 'ws://'], $node->getConnectionAddress()); return new JsonResponse([ 'data' => [ diff --git a/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php b/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php index 20f396805..388a1b5b3 100644 --- a/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php +++ b/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php @@ -58,11 +58,6 @@ class ServerTransferController extends Controller */ private $configurationStructureService; - /** - * @var \Pterodactyl\Services\Servers\SuspensionService - */ - private $suspensionService; - /** * @var \Psr\Log\LoggerInterface */ @@ -78,7 +73,6 @@ class ServerTransferController extends Controller * @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( @@ -89,7 +83,6 @@ class ServerTransferController extends Controller DaemonServerRepository $daemonServerRepository, DaemonTransferRepository $daemonTransferRepository, ServerConfigurationStructureService $configurationStructureService, - SuspensionService $suspensionService, LoggerInterface $writer ) { $this->connection = $connection; @@ -99,7 +92,6 @@ class ServerTransferController extends Controller $this->daemonServerRepository = $daemonServerRepository; $this->daemonTransferRepository = $daemonTransferRepository; $this->configurationStructureService = $configurationStructureService; - $this->suspensionService = $suspensionService; $this->writer = $writer; } @@ -187,9 +179,6 @@ class ServerTransferController extends Controller // Remove the new allocations. $this->allocationRepository->updateWhereIn('id', $allocationIds, ['server_id' => null]); - // Unsuspend the server. - //$this->suspensionService->toggle($server, 'unsuspend'); - return new JsonResponse([], Response::HTTP_NO_CONTENT); } @@ -236,7 +225,6 @@ class ServerTransferController extends Controller // Unsuspend the server $server->load('node'); - //$this->suspensionService->toggle($server, $this->suspensionService::ACTION_UNSUSPEND); return new JsonResponse([], Response::HTTP_NO_CONTENT); } diff --git a/app/Models/ServerTransfer.php b/app/Models/ServerTransfer.php index 48f6b8de8..e7bde02f9 100644 --- a/app/Models/ServerTransfer.php +++ b/app/Models/ServerTransfer.php @@ -16,6 +16,8 @@ namespace Pterodactyl\Models; * @property \Carbon\Carbon $updated_at * * @property \Pterodactyl\Models\Server $server + * @property \Pterodactyl\Models\Node $oldNode + * @property \Pterodactyl\Models\Node $newNode */ class ServerTransfer extends Model { @@ -78,4 +80,24 @@ class ServerTransfer extends Model { return $this->belongsTo(Server::class); } + + /** + * Gets the source node associated with a server transfer. + * + * @return \Illuminate\Database\Eloquent\Relations\HasOne + */ + public function oldNode() + { + return $this->hasOne(Node::class, 'id', 'old_node'); + } + + /** + * Gets the target node associated with a server transfer. + * + * @return \Illuminate\Database\Eloquent\Relations\HasOne + */ + public function newNode() + { + return $this->hasOne(Node::class, 'id', 'new_node'); + } } diff --git a/app/Services/Servers/SuspensionService.php b/app/Services/Servers/SuspensionService.php index 200ef9b65..e1a0a805a 100644 --- a/app/Services/Servers/SuspensionService.php +++ b/app/Services/Servers/SuspensionService.php @@ -6,6 +6,7 @@ use Webmozart\Assert\Assert; use Pterodactyl\Models\Server; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Repositories\Wings\DaemonServerRepository; +use Symfony\Component\HttpKernel\Exception\ConflictHttpException; class SuspensionService { @@ -56,6 +57,11 @@ class SuspensionService return; } + // Check if the server is currently being transferred. + if ($server->transfer !== null) { + throw new ConflictHttpException('Server is currently being transferred'); + } + $this->connection->transaction(function () use ($action, $server) { $server->update([ 'suspended' => $action === self::ACTION_SUSPEND, diff --git a/resources/scripts/api/server/getWebsocketToken.ts b/resources/scripts/api/server/getWebsocketToken.ts index da78dfd05..d1e1f24a9 100644 --- a/resources/scripts/api/server/getWebsocketToken.ts +++ b/resources/scripts/api/server/getWebsocketToken.ts @@ -5,9 +5,13 @@ interface Response { socket: string; } -export default (server: string): Promise => { +export default (server: string, transfer: boolean): Promise => { return new Promise((resolve, reject) => { - http.get(`/api/client/servers/${server}/websocket`) + http.get(`/api/client/servers/${server}/websocket`, { + params: { + transfer, + }, + }) .then(({ data }) => resolve({ token: data.data.token, socket: data.data.socket, diff --git a/resources/scripts/components/server/Console.tsx b/resources/scripts/components/server/Console.tsx index 743676d6f..74de72c04 100644 --- a/resources/scripts/components/server/Console.tsx +++ b/resources/scripts/components/server/Console.tsx @@ -84,8 +84,6 @@ export default () => { // Sent by the source node whenever the server was archived successfully. case 'archive': terminal.writeln(TERMINAL_PRELUDE + 'Server has been archived successfully, attempting connection to target node..\u001b[0m'); - // TODO: Get WebSocket information for the target node. - return; } }; @@ -167,7 +165,7 @@ export default () => { useEffect(() => { if (connected && instance) { - terminal.clear(); + // terminal.clear(); instance.addListener('status', handlePowerChangeEvent); instance.addListener('console output', handleConsoleOutput); diff --git a/resources/scripts/components/server/TransferListener.tsx b/resources/scripts/components/server/TransferListener.tsx new file mode 100644 index 000000000..ce9c84793 --- /dev/null +++ b/resources/scripts/components/server/TransferListener.tsx @@ -0,0 +1,28 @@ +import useWebsocketEvent from '@/plugins/useWebsocketEvent'; +import { ServerContext } from '@/state/server'; + +const TransferListener = () => { + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); + const getServer = ServerContext.useStoreActions(actions => actions.server.getServer); + const setServerFromState = ServerContext.useStoreActions(actions => actions.server.setServerFromState); + + // Listen for the installation completion event and then fire off a request to fetch the updated + // server information. This allows the server to automatically become available to the user if they + // just sit on the page. + useWebsocketEvent('transfer status', (status: string) => { + if (status === 'starting') { + setServerFromState(s => ({ ...s, isTransferring: true })); + return; + } + + if (status !== 'success') { + return; + } + + getServer(uuid).catch(error => console.error(error)); + }); + + return null; +}; + +export default TransferListener; diff --git a/resources/scripts/components/server/WebsocketHandler.tsx b/resources/scripts/components/server/WebsocketHandler.tsx index 7a6cfd50a..085cd40f3 100644 --- a/resources/scripts/components/server/WebsocketHandler.tsx +++ b/resources/scripts/components/server/WebsocketHandler.tsx @@ -15,39 +15,14 @@ const reconnectErrors = [ export default () => { let updatingToken = false; const [ error, setError ] = useState<'connecting' | string>(''); + const [ transfer, setTransfer ] = useState(false); const { connected, instance } = ServerContext.useStoreState(state => state.socket); const uuid = ServerContext.useStoreState(state => state.server.data?.uuid); const setServerStatus = ServerContext.useStoreActions(actions => actions.status.setServerStatus); const { setInstance, setConnectionState } = ServerContext.useStoreActions(actions => actions.socket); - const updateToken = (uuid: string, socket: Websocket) => { - if (updatingToken) return; - - updatingToken = true; - getWebsocketToken(uuid) - .then(data => socket.setToken(data.token, true)) - .catch(error => console.error(error)) - .then(() => { - updatingToken = false; - }); - }; - - useEffect(() => { - connected && setError(''); - }, [ connected ]); - - useEffect(() => { - return () => { - instance && instance.close(); - }; - }, [ instance ]); - - useEffect(() => { - // If there is already an instance or there is no server, just exit out of this process - // since we don't need to make a new connection. - if (instance || !uuid) { - return; - } + const connect = (uuid: string, transfer = false) => { + setTransfer(transfer); const socket = new Websocket(); @@ -76,7 +51,34 @@ export default () => { } }); - getWebsocketToken(uuid) + socket.on('transfer status', (status: string) => { + if (status === 'success') { + setTransfer(false); + return; + } + + if (status === 'starting') { + return; + } + + // This doesn't use the `setTransfer` hook as it doesn't want to work properly in this context, + // and causes all kinds of fuckery with the websocket. + let transfer = false; + if (status === 'archived') { + transfer = true; + } + + // Close the current websocket connection. + socket.close(); + + setError('connecting'); + setConnectionState(false); + setInstance(null); + + connect(uuid, transfer); + }); + + getWebsocketToken(uuid, transfer) .then(data => { // Connect and then set the authentication token. socket.setToken(data.token).connect(data.socket); @@ -85,6 +87,38 @@ export default () => { setInstance(socket); }) .catch(error => console.error(error)); + }; + + const updateToken = (uuid: string, socket: Websocket) => { + if (updatingToken) return; + + updatingToken = true; + getWebsocketToken(uuid, transfer) + .then(data => socket.setToken(data.token, true)) + .catch(error => console.error(error)) + .then(() => { + updatingToken = false; + }); + }; + + useEffect(() => { + connected && setError(''); + }, [ connected ]); + + useEffect(() => { + return () => { + instance && instance.close(); + }; + }, [ instance ]); + + useEffect(() => { + // If there is already an instance or there is no server, just exit out of this process + // since we don't need to make a new connection. + if (instance || !uuid) { + return; + } + + connect(uuid); }, [ uuid ]); return ( diff --git a/resources/scripts/components/server/databases/CreateDatabaseButton.tsx b/resources/scripts/components/server/databases/CreateDatabaseButton.tsx index d70faffba..c3b47250f 100644 --- a/resources/scripts/components/server/databases/CreateDatabaseButton.tsx +++ b/resources/scripts/components/server/databases/CreateDatabaseButton.tsx @@ -42,7 +42,6 @@ export default () => { setVisible(false); }) .catch(error => { - console.log(error); addError({ key: 'database:create', message: httpErrorToHuman(error) }); setSubmitting(false); }); diff --git a/resources/scripts/routers/ServerRouter.tsx b/resources/scripts/routers/ServerRouter.tsx index 7405a6e4e..d01f76fcd 100644 --- a/resources/scripts/routers/ServerRouter.tsx +++ b/resources/scripts/routers/ServerRouter.tsx @@ -1,3 +1,4 @@ +import TransferListener from '@/components/server/TransferListener'; import React, { useEffect, useState } from 'react'; import { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom'; import NavigationBar from '@/components/NavigationBar'; @@ -128,6 +129,7 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) + {((installing || transferring) && (!rootAdmin || (rootAdmin && !location.pathname.endsWith(`/server/${id}`)))) ? {!! csrf_field() !!} - +