Simplify logic when a server is in an unsupported state
This commit is contained in:
parent
be26921fcc
commit
e30a765071
10 changed files with 76 additions and 294 deletions
30
app/Exceptions/Http/Server/ServerStateConflictException.php
Normal file
30
app/Exceptions/Http/Server/ServerStateConflictException.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Exceptions\Http\Server;
|
||||
|
||||
use Throwable;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
|
||||
|
||||
class ServerStateConflictException extends ConflictHttpException
|
||||
{
|
||||
/**
|
||||
* Exception thrown when the server is in an unsupported state for API access or
|
||||
* certain operations within the codebase.
|
||||
*/
|
||||
public function __construct(Server $server, Throwable $previous = null)
|
||||
{
|
||||
$message = 'This server is currently in an unsupported state, please try again later.';
|
||||
if ($server->isSuspended()) {
|
||||
$message = 'This server is currently suspended and the functionality requested is unavailable.';
|
||||
} elseif (!$server->isInstalled()) {
|
||||
$message = 'This server has not yet completed its installation process, please try again later.';
|
||||
} elseif ($server->status === Server::STATUS_RESTORING_BACKUP) {
|
||||
$message = 'This server is currently restoring from a backup, please try again later.';
|
||||
} elseif (!is_null($server->transfer)) {
|
||||
$message = 'This server is currently being transferred to a new machine, please try again later.';
|
||||
}
|
||||
|
||||
parent::__construct($message, $previous);
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Exceptions\Http\Server;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
class ServerTransferringException extends HttpException
|
||||
{
|
||||
/**
|
||||
* ServerTransferringException constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(Response::HTTP_CONFLICT, 'This server is currently being transferred to a new machine, please try again laster.');
|
||||
}
|
||||
}
|
|
@ -12,8 +12,6 @@ use Pterodactyl\Exceptions\Http\HttpForbiddenException;
|
|||
use Pterodactyl\Repositories\Eloquent\ServerRepository;
|
||||
use Pterodactyl\Services\Servers\GetUserPermissionsService;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Pterodactyl\Exceptions\Http\Server\ServerTransferringException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Pterodactyl\Http\Requests\Api\Remote\SftpAuthenticationFormRequest;
|
||||
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
|
||||
|
||||
|
@ -98,16 +96,7 @@ class SftpAuthenticationController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
// Prevent SFTP access to servers that are being transferred.
|
||||
if (!is_null($server->transfer)) {
|
||||
throw new ServerTransferringException();
|
||||
}
|
||||
|
||||
// Remember, for security purposes, only reveal the existence of the server to people that
|
||||
// have provided valid credentials, and have permissions to know about it.
|
||||
if ($server->isSuspended() || !$server->isInstalled()) {
|
||||
throw new BadRequestHttpException('Server is not installed or is currently suspended.');
|
||||
}
|
||||
$server->validateCurrentState();
|
||||
|
||||
return new JsonResponse([
|
||||
'server' => $server->uuid,
|
||||
|
|
|
@ -28,7 +28,6 @@ use Pterodactyl\Http\Middleware\Api\AuthenticateIPAccess;
|
|||
use Pterodactyl\Http\Middleware\Api\ApiSubstituteBindings;
|
||||
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
|
||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||
use Pterodactyl\Http\Middleware\Server\AccessingValidServer;
|
||||
use Pterodactyl\Http\Middleware\Api\Daemon\DaemonAuthenticate;
|
||||
use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication;
|
||||
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode;
|
||||
|
@ -106,7 +105,6 @@ class Kernel extends HttpKernel
|
|||
'auth' => Authenticate::class,
|
||||
'auth.basic' => AuthenticateWithBasicAuth::class,
|
||||
'guest' => RedirectIfAuthenticated::class,
|
||||
'server' => AccessingValidServer::class,
|
||||
'admin' => AdminAuthenticate::class,
|
||||
'csrf' => VerifyCsrfToken::class,
|
||||
'throttle' => ThrottleRequests::class,
|
||||
|
|
|
@ -6,10 +6,8 @@ use Closure;
|
|||
use Illuminate\Http\Request;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Pterodactyl\Exceptions\Http\Server\ServerTransferringException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Pterodactyl\Exceptions\Http\Server\ServerStateConflictException;
|
||||
|
||||
class AuthenticateServerAccess
|
||||
{
|
||||
|
@ -60,23 +58,17 @@ class AuthenticateServerAccess
|
|||
}
|
||||
}
|
||||
|
||||
if ($server->suspended && !$request->routeIs('api:client:server.resources')) {
|
||||
throw new BadRequestHttpException('This server is currently suspended and the functionality requested is unavailable.');
|
||||
}
|
||||
|
||||
// Still allow users to get information about their server if it is installing or being transferred.
|
||||
try {
|
||||
$server->validateCurrentState();
|
||||
} catch (ServerStateConflictException $exception) {
|
||||
// Still allow users to get information about their server if it is installing or
|
||||
// being transferred.
|
||||
if (!$request->routeIs('api:client:server.view')) {
|
||||
if (!$server->isInstalled()) {
|
||||
// Throw an exception for all server routes; however if the user is an admin and requesting the
|
||||
// server details, don't throw the exception for them.
|
||||
if (!$user->root_admin || ($user->root_admin && !$request->routeIs($this->except))) {
|
||||
throw new ConflictHttpException('Server has not completed the installation process.');
|
||||
if ($server->isSuspended() && !$request->routeIs('api:client:server.resources')) {
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_null($server->transfer)) {
|
||||
if (!$user->root_admin || ($user->root_admin && !$request->routeIs($this->except))) {
|
||||
throw new ServerTransferringException();
|
||||
if (!$user->root_admin || !$request->routeIs($this->except)) {
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Middleware\Server;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Contracts\Routing\ResponseFactory;
|
||||
use Illuminate\Contracts\Config\Repository as ConfigRepository;
|
||||
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
|
||||
use Pterodactyl\Exceptions\Http\Server\ServerTransferringException;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
|
||||
class AccessingValidServer
|
||||
{
|
||||
/**
|
||||
* @var \Illuminate\Contracts\Config\Repository
|
||||
*/
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* @var \Illuminate\Contracts\Routing\ResponseFactory
|
||||
*/
|
||||
private $response;
|
||||
|
||||
/**
|
||||
* AccessingValidServer constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
ConfigRepository $config,
|
||||
ResponseFactory $response,
|
||||
ServerRepositoryInterface $repository
|
||||
) {
|
||||
$this->config = $config;
|
||||
$this->repository = $repository;
|
||||
$this->response = $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a given user has permission to access a server.
|
||||
*
|
||||
* @return \Illuminate\Http\Response|mixed
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$attributes = $request->route()->parameter('server');
|
||||
$isApiRequest = $request->expectsJson() || $request->is(...$this->config->get('pterodactyl.json_routes', []));
|
||||
$server = $this->repository->getByUuid($attributes instanceof Server ? $attributes->uuid : $attributes);
|
||||
|
||||
if ($server->isSuspended()) {
|
||||
if ($isApiRequest) {
|
||||
throw new AccessDeniedHttpException('Server is suspended and cannot be accessed.');
|
||||
}
|
||||
|
||||
return $this->response->view('errors.suspended', [], 403);
|
||||
}
|
||||
|
||||
// Servers can have install statuses other than 1 or 0, so don't check
|
||||
// for a bool-type operator here.
|
||||
if (!$server->isInstalled()) {
|
||||
if ($isApiRequest) {
|
||||
throw new ConflictHttpException('Server is still completing the installation process.');
|
||||
}
|
||||
|
||||
return $this->response->view('errors.installing', [], 409);
|
||||
}
|
||||
|
||||
if (!is_null($server->transfer)) {
|
||||
if ($isApiRequest) {
|
||||
throw new ServerTransferringException();
|
||||
}
|
||||
|
||||
return $this->response->view('errors.transferring', [], 409);
|
||||
}
|
||||
|
||||
// Add server to the request attributes. This will replace sessions
|
||||
// as files are updated.
|
||||
$request->attributes->set('server', $server);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ use Closure;
|
|||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
use Znck\Eloquent\Traits\BelongsToThrough;
|
||||
use Pterodactyl\Exceptions\Http\Server\ServerStateConflictException;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
|
@ -371,4 +372,23 @@ class Server extends Model
|
|||
{
|
||||
return $this->hasMany(AuditLog::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the server is currently in a user-accessible state. If not, an
|
||||
* exception is raised. This should be called whenever something needs to make
|
||||
* sure the server is not in a weird state that should block user access.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Http\Server\ServerStateConflictException
|
||||
*/
|
||||
public function validateCurrentState()
|
||||
{
|
||||
if (
|
||||
$this->isSuspended() ||
|
||||
!$this->isInstalled() ||
|
||||
$this->status === self::STATUS_RESTORING_BACKUP ||
|
||||
!is_null($this->transfer)
|
||||
) {
|
||||
throw new ServerStateConflictException($this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ use Webmozart\Assert\Assert;
|
|||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
|
||||
use Pterodactyl\Exceptions\Http\Server\ServerTransferringException;
|
||||
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
|
||||
|
||||
class SuspensionService
|
||||
{
|
||||
|
@ -55,7 +55,7 @@ class SuspensionService
|
|||
|
||||
// Check if the server is currently being transferred.
|
||||
if (!is_null($server->transfer)) {
|
||||
throw new ServerTransferringException();
|
||||
throw new ConflictHttpException('Cannot toggle suspension status on a server that is currently being transferred.');
|
||||
}
|
||||
|
||||
$this->connection->transaction(function () use ($action, $server, $isSuspending) {
|
||||
|
|
|
@ -111,17 +111,19 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
|||
</span>
|
||||
</div>
|
||||
:
|
||||
server.isInstalling ?
|
||||
(server.isTransferring || server.status) ?
|
||||
<div css={tw`flex-1 text-center`}>
|
||||
<span css={tw`bg-neutral-500 rounded px-2 py-1 text-neutral-100 text-xs`}>
|
||||
Installing
|
||||
</span>
|
||||
</div>
|
||||
{server.isTransferring ?
|
||||
'Transferring'
|
||||
:
|
||||
server.isTransferring ?
|
||||
<div css={tw`flex-1 text-center`}>
|
||||
<span css={tw`bg-neutral-500 rounded px-2 py-1 text-neutral-100 text-xs`}>
|
||||
Transferring
|
||||
server.status === 'installing' ? 'Installing' : (
|
||||
server.status === 'restoring_backup' ?
|
||||
'Restoring Backup'
|
||||
:
|
||||
'Unavailable'
|
||||
)
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
:
|
||||
|
|
|
@ -1,140 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Tests\Unit\Http\Middleware\Server;
|
||||
|
||||
use Mockery as m;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Contracts\Config\Repository;
|
||||
use Illuminate\Contracts\Routing\ResponseFactory;
|
||||
use Pterodactyl\Http\Middleware\Server\AccessingValidServer;
|
||||
use Pterodactyl\Tests\Unit\Http\Middleware\MiddlewareTestCase;
|
||||
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
|
||||
class AccessingValidServerTest extends MiddlewareTestCase
|
||||
{
|
||||
/**
|
||||
* @var \Illuminate\Contracts\Config\Repository|\Mockery\Mock
|
||||
*/
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* @var \Illuminate\Contracts\Routing\ResponseFactory|\Mockery\Mock
|
||||
*/
|
||||
private $response;
|
||||
|
||||
/**
|
||||
* Setup tests.
|
||||
*/
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->config = m::mock(Repository::class);
|
||||
$this->repository = m::mock(ServerRepositoryInterface::class);
|
||||
$this->response = m::mock(ResponseFactory::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that an exception is thrown if the request is an API request and the server is suspended.
|
||||
*/
|
||||
public function testExceptionIsThrownIfServerIsSuspended()
|
||||
{
|
||||
$this->expectException(AccessDeniedHttpException::class);
|
||||
$this->expectExceptionMessage('Server is suspended and cannot be accessed.');
|
||||
|
||||
$model = Server::factory()->make(['suspended' => 1]);
|
||||
|
||||
$this->request->shouldReceive('route->parameter')->with('server')->once()->andReturn('123456');
|
||||
$this->request->shouldReceive('expectsJson')->withNoArgs()->once()->andReturn(true);
|
||||
|
||||
$this->repository->shouldReceive('getByUuid')->with('123456')->once()->andReturn($model);
|
||||
|
||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that an exception is thrown if the request is an API request and the server is not installed.
|
||||
*/
|
||||
public function testExceptionIsThrownIfServerIsNotInstalled()
|
||||
{
|
||||
$this->expectException(ConflictHttpException::class);
|
||||
$this->expectExceptionMessage('Server is still completing the installation process.');
|
||||
|
||||
$model = Server::factory()->make(['installed' => 0]);
|
||||
|
||||
$this->request->shouldReceive('route->parameter')->with('server')->once()->andReturn('123456');
|
||||
$this->request->shouldReceive('expectsJson')->withNoArgs()->once()->andReturn(true);
|
||||
|
||||
$this->repository->shouldReceive('getByUuid')->with('123456')->once()->andReturn($model);
|
||||
|
||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the correct error pages are rendered depending on the status of the server.
|
||||
*
|
||||
* @dataProvider viewDataProvider
|
||||
*/
|
||||
public function testCorrectErrorPagesAreRendered(Server $model, string $page, int $httpCode)
|
||||
{
|
||||
$this->request->shouldReceive('route->parameter')->with('server')->once()->andReturn('123456');
|
||||
$this->request->shouldReceive('expectsJson')->withNoArgs()->once()->andReturn(false);
|
||||
$this->config->shouldReceive('get')->with('pterodactyl.json_routes', [])->once()->andReturn([]);
|
||||
$this->request->shouldReceive('is')->with(...[])->once()->andReturn(false);
|
||||
|
||||
$this->repository->shouldReceive('getByUuid')->with('123456')->once()->andReturn($model);
|
||||
$this->response->shouldReceive('view')->with($page, [], $httpCode)->once()->andReturn(true);
|
||||
|
||||
$response = $this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
||||
$this->assertTrue($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the full middleware works correctly.
|
||||
*/
|
||||
public function testValidServerProcess()
|
||||
{
|
||||
$model = Server::factory()->make();
|
||||
|
||||
$this->request->shouldReceive('route->parameter')->with('server')->once()->andReturn('123456');
|
||||
$this->request->shouldReceive('expectsJson')->withNoArgs()->once()->andReturn(false);
|
||||
$this->config->shouldReceive('get')->with('pterodactyl.json_routes', [])->once()->andReturn([]);
|
||||
$this->request->shouldReceive('is')->with(...[])->once()->andReturn(false);
|
||||
|
||||
$this->repository->shouldReceive('getByUuid')->with('123456')->once()->andReturn($model);
|
||||
|
||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
||||
$this->assertRequestHasAttribute('server');
|
||||
$this->assertRequestAttributeEquals($model, 'server');
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide test data that checks that the correct view is returned for each model type.
|
||||
*/
|
||||
public function viewDataProvider(): array
|
||||
{
|
||||
// Without this we are unable to instantiate the factory builders for some reason.
|
||||
$this->refreshApplication();
|
||||
|
||||
return [
|
||||
[Server::factory()->make(['suspended' => 1]), 'errors.suspended', 403],
|
||||
[Server::factory()->make(['installed' => 0]), 'errors.installing', 409],
|
||||
[Server::factory()->make(['installed' => 2]), 'errors.installing', 409],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance of the middleware using mocked dependencies.
|
||||
*/
|
||||
private function getMiddleware(): AccessingValidServer
|
||||
{
|
||||
return new AccessingValidServer($this->config, $this->response, $this->repository);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue