📯 tRaNsFeR lOgS 📯

This commit is contained in:
Matthew Penner 2020-12-16 16:55:44 -07:00
parent e6c4a68e4a
commit 5c5e2e24f1
12 changed files with 149 additions and 65 deletions

View file

@ -8,7 +8,6 @@ use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Models\ServerTransfer; use Pterodactyl\Models\ServerTransfer;
use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Servers\TransferService; use Pterodactyl\Services\Servers\TransferService;
use Pterodactyl\Services\Servers\SuspensionService;
use Pterodactyl\Repositories\Eloquent\NodeRepository; use Pterodactyl\Repositories\Eloquent\NodeRepository;
use Pterodactyl\Repositories\Eloquent\ServerRepository; use Pterodactyl\Repositories\Eloquent\ServerRepository;
use Pterodactyl\Repositories\Eloquent\LocationRepository; use Pterodactyl\Repositories\Eloquent\LocationRepository;
@ -42,11 +41,6 @@ class ServerTransferController extends Controller
*/ */
private $nodeRepository; private $nodeRepository;
/**
* @var \Pterodactyl\Services\Servers\SuspensionService
*/
private $suspensionService;
/** /**
* @var \Pterodactyl\Services\Servers\TransferService * @var \Pterodactyl\Services\Servers\TransferService
*/ */
@ -65,7 +59,6 @@ class ServerTransferController extends Controller
* @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository * @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository
* @param \Pterodactyl\Repositories\Eloquent\LocationRepository $locationRepository * @param \Pterodactyl\Repositories\Eloquent\LocationRepository $locationRepository
* @param \Pterodactyl\Repositories\Eloquent\NodeRepository $nodeRepository * @param \Pterodactyl\Repositories\Eloquent\NodeRepository $nodeRepository
* @param \Pterodactyl\Services\Servers\SuspensionService $suspensionService
* @param \Pterodactyl\Services\Servers\TransferService $transferService * @param \Pterodactyl\Services\Servers\TransferService $transferService
* @param \Pterodactyl\Repositories\Wings\DaemonConfigurationRepository $daemonConfigurationRepository * @param \Pterodactyl\Repositories\Wings\DaemonConfigurationRepository $daemonConfigurationRepository
*/ */
@ -75,7 +68,6 @@ class ServerTransferController extends Controller
ServerRepository $repository, ServerRepository $repository,
LocationRepository $locationRepository, LocationRepository $locationRepository,
NodeRepository $nodeRepository, NodeRepository $nodeRepository,
SuspensionService $suspensionService,
TransferService $transferService, TransferService $transferService,
DaemonConfigurationRepository $daemonConfigurationRepository DaemonConfigurationRepository $daemonConfigurationRepository
) { ) {
@ -84,7 +76,6 @@ class ServerTransferController extends Controller
$this->repository = $repository; $this->repository = $repository;
$this->locationRepository = $locationRepository; $this->locationRepository = $locationRepository;
$this->nodeRepository = $nodeRepository; $this->nodeRepository = $nodeRepository;
$this->suspensionService = $suspensionService;
$this->transferService = $transferService; $this->transferService = $transferService;
$this->daemonConfigurationRepository = $daemonConfigurationRepository; $this->daemonConfigurationRepository = $daemonConfigurationRepository;
} }
@ -98,8 +89,7 @@ class ServerTransferController extends Controller
* *
* @throws \Throwable * @throws \Throwable
*/ */
public function transfer(Request $request, Server $server) public function transfer(Request $request, Server $server) {
{
$validatedData = $request->validate([ $validatedData = $request->validate([
'node_id' => 'required|exists:nodes,id', 'node_id' => 'required|exists:nodes,id',
'allocation_id' => 'required|bail|unique:servers|exists:allocations,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. // Check if the selected daemon is online.
$this->daemonConfigurationRepository->setNode($node)->getSystemInformation(); $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. // Create a new ServerTransfer entry.
$transfer = new ServerTransfer; $transfer = new ServerTransfer;

View file

@ -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.'); 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 $token = $this->jwtService
->setExpiresAt(CarbonImmutable::now()->addMinutes(10)) ->setExpiresAt(CarbonImmutable::now()->addMinutes(10))
->setClaims([ ->setClaims([
'user_id' => $request->user()->id, 'user_id' => $request->user()->id,
'server_uuid' => $server->uuid, '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([ return new JsonResponse([
'data' => [ 'data' => [

View file

@ -58,11 +58,6 @@ class ServerTransferController extends Controller
*/ */
private $configurationStructureService; private $configurationStructureService;
/**
* @var \Pterodactyl\Services\Servers\SuspensionService
*/
private $suspensionService;
/** /**
* @var \Psr\Log\LoggerInterface * @var \Psr\Log\LoggerInterface
*/ */
@ -78,7 +73,6 @@ class ServerTransferController extends Controller
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository * @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
* @param \Pterodactyl\Repositories\Wings\DaemonTransferRepository $daemonTransferRepository * @param \Pterodactyl\Repositories\Wings\DaemonTransferRepository $daemonTransferRepository
* @param \Pterodactyl\Services\Servers\ServerConfigurationStructureService $configurationStructureService * @param \Pterodactyl\Services\Servers\ServerConfigurationStructureService $configurationStructureService
* @param \Pterodactyl\Services\Servers\SuspensionService $suspensionService
* @param \Psr\Log\LoggerInterface $writer * @param \Psr\Log\LoggerInterface $writer
*/ */
public function __construct( public function __construct(
@ -89,7 +83,6 @@ class ServerTransferController extends Controller
DaemonServerRepository $daemonServerRepository, DaemonServerRepository $daemonServerRepository,
DaemonTransferRepository $daemonTransferRepository, DaemonTransferRepository $daemonTransferRepository,
ServerConfigurationStructureService $configurationStructureService, ServerConfigurationStructureService $configurationStructureService,
SuspensionService $suspensionService,
LoggerInterface $writer LoggerInterface $writer
) { ) {
$this->connection = $connection; $this->connection = $connection;
@ -99,7 +92,6 @@ class ServerTransferController extends Controller
$this->daemonServerRepository = $daemonServerRepository; $this->daemonServerRepository = $daemonServerRepository;
$this->daemonTransferRepository = $daemonTransferRepository; $this->daemonTransferRepository = $daemonTransferRepository;
$this->configurationStructureService = $configurationStructureService; $this->configurationStructureService = $configurationStructureService;
$this->suspensionService = $suspensionService;
$this->writer = $writer; $this->writer = $writer;
} }
@ -187,9 +179,6 @@ class ServerTransferController extends Controller
// Remove the new allocations. // Remove the new allocations.
$this->allocationRepository->updateWhereIn('id', $allocationIds, ['server_id' => null]); $this->allocationRepository->updateWhereIn('id', $allocationIds, ['server_id' => null]);
// Unsuspend the server.
//$this->suspensionService->toggle($server, 'unsuspend');
return new JsonResponse([], Response::HTTP_NO_CONTENT); return new JsonResponse([], Response::HTTP_NO_CONTENT);
} }
@ -236,7 +225,6 @@ class ServerTransferController extends Controller
// Unsuspend the server // Unsuspend the server
$server->load('node'); $server->load('node');
//$this->suspensionService->toggle($server, $this->suspensionService::ACTION_UNSUSPEND);
return new JsonResponse([], Response::HTTP_NO_CONTENT); return new JsonResponse([], Response::HTTP_NO_CONTENT);
} }

View file

@ -16,6 +16,8 @@ namespace Pterodactyl\Models;
* @property \Carbon\Carbon $updated_at * @property \Carbon\Carbon $updated_at
* *
* @property \Pterodactyl\Models\Server $server * @property \Pterodactyl\Models\Server $server
* @property \Pterodactyl\Models\Node $oldNode
* @property \Pterodactyl\Models\Node $newNode
*/ */
class ServerTransfer extends Model class ServerTransfer extends Model
{ {
@ -78,4 +80,24 @@ class ServerTransfer extends Model
{ {
return $this->belongsTo(Server::class); 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');
}
} }

View file

@ -6,6 +6,7 @@ use Webmozart\Assert\Assert;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Repositories\Wings\DaemonServerRepository; use Pterodactyl\Repositories\Wings\DaemonServerRepository;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
class SuspensionService class SuspensionService
{ {
@ -56,6 +57,11 @@ class SuspensionService
return; 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) { $this->connection->transaction(function () use ($action, $server) {
$server->update([ $server->update([
'suspended' => $action === self::ACTION_SUSPEND, 'suspended' => $action === self::ACTION_SUSPEND,

View file

@ -5,9 +5,13 @@ interface Response {
socket: string; socket: string;
} }
export default (server: string): Promise<Response> => { export default (server: string, transfer: boolean): Promise<Response> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.get(`/api/client/servers/${server}/websocket`) http.get(`/api/client/servers/${server}/websocket`, {
params: {
transfer,
},
})
.then(({ data }) => resolve({ .then(({ data }) => resolve({
token: data.data.token, token: data.data.token,
socket: data.data.socket, socket: data.data.socket,

View file

@ -84,8 +84,6 @@ export default () => {
// Sent by the source node whenever the server was archived successfully. // Sent by the source node whenever the server was archived successfully.
case 'archive': case 'archive':
terminal.writeln(TERMINAL_PRELUDE + 'Server has been archived successfully, attempting connection to target node..\u001b[0m'); 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(() => { useEffect(() => {
if (connected && instance) { if (connected && instance) {
terminal.clear(); // terminal.clear();
instance.addListener('status', handlePowerChangeEvent); instance.addListener('status', handlePowerChangeEvent);
instance.addListener('console output', handleConsoleOutput); instance.addListener('console output', handleConsoleOutput);

View file

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

View file

@ -15,39 +15,14 @@ const reconnectErrors = [
export default () => { export default () => {
let updatingToken = false; let updatingToken = false;
const [ error, setError ] = useState<'connecting' | string>(''); const [ error, setError ] = useState<'connecting' | string>('');
const [ transfer, setTransfer ] = useState<boolean>(false);
const { connected, instance } = ServerContext.useStoreState(state => state.socket); const { connected, instance } = ServerContext.useStoreState(state => state.socket);
const uuid = ServerContext.useStoreState(state => state.server.data?.uuid); const uuid = ServerContext.useStoreState(state => state.server.data?.uuid);
const setServerStatus = ServerContext.useStoreActions(actions => actions.status.setServerStatus); const setServerStatus = ServerContext.useStoreActions(actions => actions.status.setServerStatus);
const { setInstance, setConnectionState } = ServerContext.useStoreActions(actions => actions.socket); const { setInstance, setConnectionState } = ServerContext.useStoreActions(actions => actions.socket);
const updateToken = (uuid: string, socket: Websocket) => { const connect = (uuid: string, transfer = false) => {
if (updatingToken) return; setTransfer(transfer);
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 socket = new Websocket(); 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 => { .then(data => {
// Connect and then set the authentication token. // Connect and then set the authentication token.
socket.setToken(data.token).connect(data.socket); socket.setToken(data.token).connect(data.socket);
@ -85,6 +87,38 @@ export default () => {
setInstance(socket); setInstance(socket);
}) })
.catch(error => console.error(error)); .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 ]); }, [ uuid ]);
return ( return (

View file

@ -42,7 +42,6 @@ export default () => {
setVisible(false); setVisible(false);
}) })
.catch(error => { .catch(error => {
console.log(error);
addError({ key: 'database:create', message: httpErrorToHuman(error) }); addError({ key: 'database:create', message: httpErrorToHuman(error) });
setSubmitting(false); setSubmitting(false);
}); });

View file

@ -1,3 +1,4 @@
import TransferListener from '@/components/server/TransferListener';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom'; import { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom';
import NavigationBar from '@/components/NavigationBar'; import NavigationBar from '@/components/NavigationBar';
@ -128,6 +129,7 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
</SubNavigation> </SubNavigation>
</CSSTransition> </CSSTransition>
<InstallListener/> <InstallListener/>
<TransferListener/>
<WebsocketHandler/> <WebsocketHandler/>
{((installing || transferring) && (!rootAdmin || (rootAdmin && !location.pathname.endsWith(`/server/${id}`)))) ? {((installing || transferring) && (!rootAdmin || (rootAdmin && !location.pathname.endsWith(`/server/${id}`)))) ?
<ScreenBlock <ScreenBlock

View file

@ -72,7 +72,7 @@
<form action="{{ route('admin.servers.view.manage.suspension', $server->id) }}" method="POST"> <form action="{{ route('admin.servers.view.manage.suspension', $server->id) }}" method="POST">
{!! csrf_field() !!} {!! csrf_field() !!}
<input type="hidden" name="action" value="suspend" /> <input type="hidden" name="action" value="suspend" />
<button type="submit" class="btn btn-warning">Suspend Server</button> <button type="submit" class="btn btn-warning @if($server->transfer !== null) disabled @endif">Suspend Server</button>
</form> </form>
</div> </div>
</div> </div>