Merge branch 'develop' into feature/server-mounts

This commit is contained in:
Matthew Penner 2020-07-04 15:20:01 -06:00 committed by GitHub
commit 29876e023b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
166 changed files with 5482 additions and 4130 deletions

View file

@ -38,7 +38,7 @@ class CleanOrphanedApiKeysCommand extends Command
/**
* Delete all orphaned API keys from the database when upgrading from 0.6 to 0.7.
*
* @return null|void
* @return void|null
*/
public function handle()
{

View file

@ -19,7 +19,7 @@ interface SessionRepositoryInterface extends RepositoryInterface
*
* @param int $user
* @param string $session
* @return null|int
* @return int|null
*/
public function deleteUserSession(int $user, string $session);
}

View file

@ -21,7 +21,7 @@ interface TaskRepositoryInterface extends RepositoryInterface
*
* @param int $schedule
* @param int $index
* @return null|\Pterodactyl\Models\Task
* @return \Pterodactyl\Models\Task|null
*/
public function getNextTask(int $schedule, int $index);
}

View file

@ -3,6 +3,7 @@
namespace Pterodactyl\Exceptions;
use Exception;
use Throwable;
use PDOException;
use Psr\Log\LoggerInterface;
use Swift_TransportException;
@ -72,12 +73,12 @@ class Handler extends ExceptionHandler
* services such as AWS Cloudwatch or other monitoring you can replace the
* contents of this function with a call to the parent reporter.
*
* @param \Exception $exception
* @param \Throwable $exception
* @return mixed
*
* @throws \Exception
* @throws \Throwable
*/
public function report(Exception $exception)
public function report(Throwable $exception)
{
if (! config('app.exceptions.report_all', false) && $this->shouldntReport($exception)) {
return null;
@ -103,7 +104,7 @@ class Handler extends ExceptionHandler
return $logger->error($exception);
}
private function generateCleanedExceptionStack(Exception $exception)
private function generateCleanedExceptionStack(Throwable $exception)
{
$cleanedStack = '';
foreach ($exception->getTrace() as $index => $item) {
@ -133,12 +134,12 @@ class Handler extends ExceptionHandler
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Exception $exception
* @param \Throwable $exception
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \Exception
* @throws \Throwable
*/
public function render($request, Exception $exception)
public function render($request, Throwable $exception)
{
$connections = Container::getInstance()->make(Connection::class);
@ -200,11 +201,11 @@ class Handler extends ExceptionHandler
/**
* Return the exception as a JSONAPI representation for use on API requests.
*
* @param \Exception $exception
* @param \Throwable $exception
* @param array $override
* @return array
*/
public static function convertToArray(Exception $exception, array $override = []): array
public static function convertToArray(Throwable $exception, array $override = []): array
{
$error = [
'code' => class_basename($exception),
@ -259,10 +260,10 @@ class Handler extends ExceptionHandler
* Converts an exception into an array to render in the response. Overrides
* Laravel's built-in converter to output as a JSONAPI spec compliant object.
*
* @param \Exception $exception
* @param \Throwable $exception
* @return array
*/
protected function convertExceptionToArray(Exception $exception)
protected function convertExceptionToArray(Throwable $exception)
{
return self::convertToArray($exception);
}

View file

@ -0,0 +1,21 @@
<?php
namespace Pterodactyl\Exceptions\Service;
use Throwable;
use Pterodactyl\Exceptions\DisplayException;
class ServiceLimitExceededException extends DisplayException
{
/**
* Exception thrown when something goes over a defined limit, such as allocated
* ports, tasks, databases, etc.
*
* @param string $message
* @param \Throwable|null $previous
*/
public function __construct(string $message, Throwable $previous = null)
{
parent::__construct($message, $previous, self::LEVEL_WARNING);
}
}

View file

@ -17,10 +17,12 @@ use Prologue\Alerts\AlertsMessageBag;
use GuzzleHttp\Exception\RequestException;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Validation\ValidationException;
use Pterodactyl\Services\Servers\SuspensionService;
use Pterodactyl\Repositories\Eloquent\MountRepository;
use Pterodactyl\Services\Servers\ServerDeletionService;
use Pterodactyl\Services\Servers\ReinstallServerService;
use Pterodactyl\Exceptions\Model\DataValidationException;
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
use Pterodactyl\Services\Servers\BuildModificationService;
use Pterodactyl\Services\Databases\DatabasePasswordService;
@ -284,16 +286,21 @@ class ServersController extends Controller
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Illuminate\Validation\ValidationException
*/
public function updateBuild(Request $request, Server $server)
{
$this->buildModificationService->handle($server, $request->only([
'allocation_id', 'add_allocations', 'remove_allocations',
'memory', 'swap', 'io', 'cpu', 'threads', 'disk',
'database_limit', 'allocation_limit', 'backup_limit', 'oom_disabled',
]));
try {
$this->buildModificationService->handle($server, $request->only([
'allocation_id', 'add_allocations', 'remove_allocations',
'memory', 'swap', 'io', 'cpu', 'threads', 'disk',
'database_limit', 'allocation_limit', 'backup_limit', 'oom_disabled',
]));
} catch (DataValidationException $exception) {
throw new ValidationException($exception->validator);
}
$this->alert->success(trans('admin/server.alerts.build_updated'))->flash();
return redirect()->route('admin.servers.view.build', $server->id);
@ -341,12 +348,12 @@ class ServersController extends Controller
* Creates a new database assigned to a specific server.
*
* @param \Pterodactyl\Http\Requests\Admin\Servers\Databases\StoreServerDatabaseRequest $request
* @param int $server
* @param \Pterodactyl\Models\Server $server
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Exception
* @throws \Throwable
*/
public function newDatabase(StoreServerDatabaseRequest $request, $server)
public function newDatabase(StoreServerDatabaseRequest $request, Server $server)
{
$this->databaseManagementService->create($server, [
'database' => $request->input('database'),
@ -355,7 +362,7 @@ class ServersController extends Controller
'max_connections' => $request->input('max_connections'),
]);
return redirect()->route('admin.servers.view.database', $server)->withInput();
return redirect()->route('admin.servers.view.database', $server->id)->withInput();
}
/**

View file

@ -57,13 +57,12 @@ class DatabaseController extends ApplicationApiController
* server.
*
* @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabasesRequest $request
* @param \Pterodactyl\Models\Server $server
* @return array
*/
public function index(GetServerDatabasesRequest $request): array
public function index(GetServerDatabasesRequest $request, Server $server): array
{
$databases = $this->repository->getDatabasesForServer($request->getModel(Server::class)->id);
return $this->fractal->collection($databases)
return $this->fractal->collection($server->databases)
->transformWith($this->getTransformer(ServerDatabaseTransformer::class))
->toArray();
}
@ -72,11 +71,13 @@ class DatabaseController extends ApplicationApiController
* Return a single server database.
*
* @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabaseRequest $request
* @param \Pterodactyl\Models\Server $server
* @param \Pterodactyl\Models\Database $database
* @return array
*/
public function view(GetServerDatabaseRequest $request): array
public function view(GetServerDatabaseRequest $request, Server $server, Database $database): array
{
return $this->fractal->item($request->getModel(Database::class))
return $this->fractal->item($database)
->transformWith($this->getTransformer(ServerDatabaseTransformer::class))
->toArray();
}
@ -85,29 +86,31 @@ class DatabaseController extends ApplicationApiController
* Reset the password for a specific server database.
*
* @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\ServerDatabaseWriteRequest $request
* @return \Illuminate\Http\Response
* @param \Pterodactyl\Models\Server $server
* @param \Pterodactyl\Models\Database $database
* @return \Illuminate\Http\JsonResponse
*
* @throws \Throwable
*/
public function resetPassword(ServerDatabaseWriteRequest $request): Response
public function resetPassword(ServerDatabaseWriteRequest $request, Server $server, Database $database): JsonResponse
{
$this->databasePasswordService->handle($request->getModel(Database::class));
$this->databasePasswordService->handle($database);
return response('', 204);
return JsonResponse::create([], JsonResponse::HTTP_NO_CONTENT);
}
/**
* Create a new database on the Panel for a given server.
*
* @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\StoreServerDatabaseRequest $request
* @param \Pterodactyl\Models\Server $server
* @return \Illuminate\Http\JsonResponse
*
* @throws \Exception
* @throws \Throwable
*/
public function store(StoreServerDatabaseRequest $request): JsonResponse
public function store(StoreServerDatabaseRequest $request, Server $server): JsonResponse
{
$server = $request->getModel(Server::class);
$database = $this->databaseManagementService->create($server->id, $request->validated());
$database = $this->databaseManagementService->create($server, $request->validated());
return $this->fractal->item($database)
->transformWith($this->getTransformer(ServerDatabaseTransformer::class))
@ -117,7 +120,7 @@ class DatabaseController extends ApplicationApiController
'database' => $database->id,
]),
])
->respond(201);
->respond(Response::HTTP_CREATED);
}
/**

View file

@ -100,39 +100,20 @@ class UserController extends ApplicationApiController
* meta. If there are no errors this is an empty array.
*
* @param \Pterodactyl\Http\Requests\Api\Application\Users\UpdateUserRequest $request
* @param \Pterodactyl\Models\User $user
* @return array
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function update(UpdateUserRequest $request): array
public function update(UpdateUserRequest $request, User $user): array
{
$this->updateService->setUserLevel(User::USER_LEVEL_ADMIN);
$collection = $this->updateService->handle($request->getModel(User::class), $request->validated());
$user = $this->updateService->handle($user, $request->validated());
$errors = [];
if (! empty($collection->get('exceptions'))) {
foreach ($collection->get('exceptions') as $node => $exception) {
/** @var \GuzzleHttp\Exception\RequestException $exception */
/** @var \GuzzleHttp\Psr7\Response|null $response */
$response = method_exists($exception, 'getResponse') ? $exception->getResponse() : null;
$message = trans('admin/server.exceptions.daemon_exception', [
'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(),
]);
$errors[] = ['message' => $message, 'node' => $node];
}
}
$response = $this->fractal->item($collection->get('model'))
$response = $this->fractal->item($user)
->transformWith($this->getTransformer(UserTransformer::class));
if (count($errors) > 0) {
$response->addMeta([
'revocation_errors' => $errors,
]);
}
return $response->toArray();
}

View file

@ -52,16 +52,16 @@ class AccountController extends ClientApiController
* Update the authenticated user's email address.
*
* @param \Pterodactyl\Http\Requests\Api\Client\Account\UpdateEmailRequest $request
* @return \Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function updateEmail(UpdateEmailRequest $request): Response
public function updateEmail(UpdateEmailRequest $request): JsonResponse
{
$this->updateService->handle($request->user(), $request->validated());
return response('', Response::HTTP_CREATED);
return new JsonResponse([], Response::HTTP_NO_CONTENT);
}
/**
@ -74,12 +74,12 @@ class AccountController extends ClientApiController
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function updatePassword(UpdatePasswordRequest $request): \Illuminate\Http\JsonResponse
public function updatePassword(UpdatePasswordRequest $request): JsonResponse
{
$this->updateService->handle($request->user(), $request->validated());
$this->sessionGuard->logoutOtherDevices($request->input('password'));
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
return new JsonResponse([], Response::HTTP_NO_CONTENT);
}
}

View file

@ -103,6 +103,7 @@ class ApiKeyController extends ClientApiController
public function delete(ClientApiRequest $request, string $identifier)
{
$response = $this->repository->deleteWhere([
'key_type' => ApiKey::TYPE_ACCOUNT,
'user_id' => $request->user()->id,
'identifier' => $identifier,
]);

View file

@ -69,9 +69,7 @@ class DatabaseController extends ClientApiController
*/
public function index(GetDatabasesRequest $request, Server $server): array
{
$databases = $this->repository->getDatabasesForServer($server->id);
return $this->fractal->collection($databases)
return $this->fractal->collection($server->databases)
->transformWith($this->getTransformer(DatabaseTransformer::class))
->toArray();
}
@ -83,6 +81,8 @@ class DatabaseController extends ClientApiController
* @param \Pterodactyl\Models\Server $server
* @return array
*
* @throws \Throwable
* @throws \Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException
* @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException
*/
public function store(StoreDatabaseRequest $request, Server $server): array

View file

@ -147,7 +147,7 @@ class ScheduleController extends ClientApiController
{
$this->repository->delete($schedule->id);
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
return new JsonResponse([], Response::HTTP_NO_CONTENT);
}
/**

View file

@ -13,6 +13,7 @@ use Pterodactyl\Exceptions\Http\HttpForbiddenException;
use Pterodactyl\Transformers\Api\Client\TaskTransformer;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
use Pterodactyl\Exceptions\Service\ServiceLimitExceededException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\StoreTaskRequest;
@ -44,11 +45,15 @@ class ScheduleTaskController extends ClientApiController
* @return array
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Service\ServiceLimitExceededException
*/
public function store(StoreTaskRequest $request, Server $server, Schedule $schedule)
{
if ($schedule->server_id !== $server->id) {
throw new NotFoundHttpException;
$limit = config('pterodactyl.client_features.schedules.per_schedule_task_limit', 10);
if ($schedule->tasks()->count() >= $limit) {
throw new ServiceLimitExceededException(
"Schedules may not have more than {$limit} tasks associated with them. Creating this task would put this schedule over the limit."
);
}
$lastTask = $schedule->tasks->last();
@ -58,7 +63,7 @@ class ScheduleTaskController extends ClientApiController
'schedule_id' => $schedule->id,
'sequence_id' => ($lastTask->sequence_id ?? 0) + 1,
'action' => $request->input('action'),
'payload' => $request->input('payload'),
'payload' => $request->input('payload') ?? '',
'time_offset' => $request->input('time_offset'),
]);
@ -87,7 +92,7 @@ class ScheduleTaskController extends ClientApiController
$this->repository->update($task->id, [
'action' => $request->input('action'),
'payload' => $request->input('payload'),
'payload' => $request->input('payload') ?? '',
'time_offset' => $request->input('time_offset'),
]);

View file

@ -55,7 +55,7 @@ class SettingsController extends ClientApiController
'name' => $request->input('name'),
]);
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
return new JsonResponse([], Response::HTTP_NO_CONTENT);
}
/**
@ -71,6 +71,6 @@ class SettingsController extends ClientApiController
{
$this->reinstallServerService->reinstall($server);
return JsonResponse::create([], Response::HTTP_ACCEPTED);
return new JsonResponse([], Response::HTTP_ACCEPTED);
}
}

View file

@ -7,7 +7,6 @@ use Illuminate\Http\Response;
use Pterodactyl\Models\Server;
use Illuminate\Http\JsonResponse;
use Pterodactyl\Models\Permission;
use Illuminate\Contracts\Cache\Repository;
use Pterodactyl\Services\Nodes\NodeJWTService;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
@ -16,11 +15,6 @@ use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
class WebsocketController extends ClientApiController
{
/**
* @var \Illuminate\Contracts\Cache\Repository
*/
private $cache;
/**
* @var \Pterodactyl\Services\Nodes\NodeJWTService
*/
@ -36,16 +30,13 @@ class WebsocketController extends ClientApiController
*
* @param \Pterodactyl\Services\Nodes\NodeJWTService $jwtService
* @param \Pterodactyl\Services\Servers\GetUserPermissionsService $permissionsService
* @param \Illuminate\Contracts\Cache\Repository $cache
*/
public function __construct(
NodeJWTService $jwtService,
GetUserPermissionsService $permissionsService,
Repository $cache
GetUserPermissionsService $permissionsService
) {
parent::__construct();
$this->cache = $cache;
$this->jwtService = $jwtService;
$this->permissionsService = $permissionsService;
}
@ -78,7 +69,7 @@ class WebsocketController extends ClientApiController
$socket = str_replace(['https://', 'http://'], ['wss://', 'ws://'], $server->node->getConnectionAddress());
return JsonResponse::create([
return new JsonResponse([
'data' => [
'token' => $token->__toString(),
'socket' => $socket . sprintf('/api/servers/%s/ws', $server->uuid),

View file

@ -61,11 +61,11 @@ class TwoFactorController extends ClientApiController
*/
public function index(Request $request)
{
if ($request->user()->totp_enabled) {
if ($request->user()->use_totp) {
throw new BadRequestHttpException('Two-factor authentication is already enabled on this account.');
}
return JsonResponse::create([
return new JsonResponse([
'data' => [
'image_url_data' => $this->setupService->handle($request->user()),
],
@ -96,9 +96,14 @@ class TwoFactorController extends ClientApiController
throw new ValidationException($validator);
}
$this->toggleTwoFactorService->handle($request->user(), $request->input('code'), true);
$tokens = $this->toggleTwoFactorService->handle($request->user(), $request->input('code'), true);
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
return new JsonResponse([
'object' => 'recovery_tokens',
'attributes' => [
'tokens' => $tokens,
],
]);
}
/**
@ -124,6 +129,6 @@ class TwoFactorController extends ClientApiController
'use_totp' => false,
]);
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
return new JsonResponse([], Response::HTTP_NO_CONTENT);
}
}

View file

@ -68,10 +68,11 @@ abstract class AbstractLoginController extends Controller
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Contracts\Auth\Authenticatable|null $user
* @param string|null $message
*
* @throws \Pterodactyl\Exceptions\DisplayException
*/
protected function sendFailedLoginResponse(Request $request, Authenticatable $user = null)
protected function sendFailedLoginResponse(Request $request, Authenticatable $user = null, string $message = null)
{
$this->incrementLoginAttempts($request);
$this->fireFailedLoginEvent($user, [
@ -79,7 +80,9 @@ abstract class AbstractLoginController extends Controller
]);
if ($request->route()->named('auth.login-checkpoint')) {
throw new DisplayException(trans('auth.two_factor.checkpoint_failed'));
throw new DisplayException(
$message ?? trans('auth.two_factor.checkpoint_failed')
);
}
throw new DisplayException(trans('auth.failed'));
@ -116,7 +119,7 @@ abstract class AbstractLoginController extends Controller
*/
protected function getField(string $input = null): string
{
return str_contains($input, '@') ? 'email' : 'username';
return ($input && str_contains($input, '@')) ? 'email' : 'username';
}
/**

View file

@ -11,6 +11,7 @@ use Pterodactyl\Http\Requests\Auth\LoginCheckpointRequest;
use Illuminate\Contracts\Cache\Repository as CacheRepository;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Pterodactyl\Repositories\Eloquent\RecoveryTokenRepository;
class LoginCheckpointController extends AbstractLoginController
{
@ -34,6 +35,11 @@ class LoginCheckpointController extends AbstractLoginController
*/
private $encrypter;
/**
* @var \Pterodactyl\Repositories\Eloquent\RecoveryTokenRepository
*/
private $recoveryTokenRepository;
/**
* LoginCheckpointController constructor.
*
@ -42,6 +48,7 @@ class LoginCheckpointController extends AbstractLoginController
* @param \PragmaRX\Google2FA\Google2FA $google2FA
* @param \Illuminate\Contracts\Config\Repository $config
* @param \Illuminate\Contracts\Cache\Repository $cache
* @param \Pterodactyl\Repositories\Eloquent\RecoveryTokenRepository $recoveryTokenRepository
* @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository
*/
public function __construct(
@ -50,6 +57,7 @@ class LoginCheckpointController extends AbstractLoginController
Google2FA $google2FA,
Repository $config,
CacheRepository $cache,
RecoveryTokenRepository $recoveryTokenRepository,
UserRepositoryInterface $repository
) {
parent::__construct($auth, $config);
@ -58,6 +66,7 @@ class LoginCheckpointController extends AbstractLoginController
$this->cache = $cache;
$this->repository = $repository;
$this->encrypter = $encrypter;
$this->recoveryTokenRepository = $recoveryTokenRepository;
}
/**
@ -76,21 +85,35 @@ class LoginCheckpointController extends AbstractLoginController
public function __invoke(LoginCheckpointRequest $request): JsonResponse
{
$token = $request->input('confirmation_token');
$recoveryToken = $request->input('recovery_token');
try {
/** @var \Pterodactyl\Models\User $user */
$user = $this->repository->find($this->cache->get($token, 0));
} catch (RecordNotFoundException $exception) {
return $this->sendFailedLoginResponse($request);
return $this->sendFailedLoginResponse($request, null, 'The authentication token provided has expired, please refresh the page and try again.');
}
$decrypted = $this->encrypter->decrypt($user->totp_secret);
// If we got a recovery token try to find one that matches for the user and then continue
// through the process (and delete the token).
if (! is_null($recoveryToken)) {
foreach ($user->recoveryTokens as $token) {
if (password_verify($recoveryToken, $token->token)) {
$this->recoveryTokenRepository->delete($token->id);
if ($this->google2FA->verifyKey($decrypted, (string) $request->input('authentication_code') ?? '', config('pterodactyl.auth.2fa.window'))) {
$this->cache->delete($token);
return $this->sendLoginResponse($user, $request);
}
}
} else {
$decrypted = $this->encrypter->decrypt($user->totp_secret);
return $this->sendLoginResponse($user, $request);
if ($this->google2FA->verifyKey($decrypted, (string) $request->input('authentication_code') ?? '', config('pterodactyl.auth.2fa.window'))) {
$this->cache->delete($token);
return $this->sendLoginResponse($user, $request);
}
}
return $this->sendFailedLoginResponse($request, $user);
return $this->sendFailedLoginResponse($request, $user, ! empty($recoveryToken) ? 'The recovery token provided is not valid.' : null);
}
}

View file

@ -103,7 +103,7 @@ class LoginController extends AbstractLoginController
$token = Str::random(64);
$this->cache->put($token, $user->id, Chronos::now()->addMinutes(5));
return JsonResponse::create([
return new JsonResponse([
'data' => [
'complete' => false,
'confirmation_token' => $token,

View file

@ -9,6 +9,7 @@ use Pterodactyl\Http\Middleware\TrimStrings;
use Pterodactyl\Http\Middleware\TrustProxies;
use Illuminate\Session\Middleware\StartSession;
use Pterodactyl\Http\Middleware\EncryptCookies;
use Pterodactyl\Http\Middleware\Api\IsValidJson;
use Pterodactyl\Http\Middleware\VerifyCsrfToken;
use Pterodactyl\Http\Middleware\VerifyReCaptcha;
use Pterodactyl\Http\Middleware\AdminAuthenticate;
@ -28,12 +29,8 @@ 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\Server\AuthenticateAsSubuser;
use Pterodactyl\Http\Middleware\Api\Daemon\DaemonAuthenticate;
use Pterodactyl\Http\Middleware\Server\SubuserBelongsToServer;
use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication;
use Pterodactyl\Http\Middleware\Server\DatabaseBelongsToServer;
use Pterodactyl\Http\Middleware\Server\ScheduleBelongsToServer;
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode;
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
use Pterodactyl\Http\Middleware\Api\Client\SubstituteClientApiBindings;
@ -72,7 +69,7 @@ class Kernel extends HttpKernel
RequireTwoFactorAuthentication::class,
],
'api' => [
'throttle:240,1',
IsValidJson::class,
ApiSubstituteBindings::class,
SetSessionDriver::class,
'api..key:' . ApiKey::TYPE_APPLICATION,
@ -80,10 +77,10 @@ class Kernel extends HttpKernel
AuthenticateIPAccess::class,
],
'client-api' => [
'throttle:240,1',
StartSession::class,
SetSessionDriver::class,
AuthenticateSession::class,
IsValidJson::class,
SubstituteClientApiBindings::class,
'api..key:' . ApiKey::TYPE_ACCOUNT,
AuthenticateIPAccess::class,
@ -104,7 +101,6 @@ class Kernel extends HttpKernel
'auth.basic' => AuthenticateWithBasicAuth::class,
'guest' => RedirectIfAuthenticated::class,
'server' => AccessingValidServer::class,
'subuser.auth' => AuthenticateAsSubuser::class,
'admin' => AdminAuthenticate::class,
'csrf' => VerifyCsrfToken::class,
'throttle' => ThrottleRequests::class,
@ -113,14 +109,6 @@ class Kernel extends HttpKernel
'recaptcha' => VerifyReCaptcha::class,
'node.maintenance' => MaintenanceMiddleware::class,
// Server specific middleware (used for authenticating access to resources)
//
// These are only used for individual server authentication, and not global
// actions from other resources. They are defined in the route files.
'server..database' => DatabaseBelongsToServer::class,
'server..subuser' => SubuserBelongsToServer::class,
'server..schedule' => ScheduleBelongsToServer::class,
// API Specific Middleware
'api..key' => AuthenticateKey::class,
];

View file

@ -65,7 +65,7 @@ class AuthenticateServerAccess
}
if ($server->suspended) {
throw new AccessDeniedHttpException('Cannot access a server that is marked as being suspended.');
throw new AccessDeniedHttpException('This server is currenty suspended and the functionality requested is unavailable.');
}
if (! $server->isInstalled()) {

View file

@ -5,8 +5,8 @@ namespace Pterodactyl\Http\Middleware\Api\Daemon;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Repositories\Eloquent\NodeRepository;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
@ -14,10 +14,15 @@ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class DaemonAuthenticate
{
/**
* @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface
* @var \Pterodactyl\Repositories\Eloquent\NodeRepository
*/
private $repository;
/**
* @var \Illuminate\Contracts\Encryption\Encrypter
*/
private $encrypter;
/**
* Daemon routes that this middleware should be skipped on.
*
@ -27,18 +32,13 @@ class DaemonAuthenticate
'daemon.configuration',
];
/**
* @var \Illuminate\Contracts\Encryption\Encrypter
*/
private $encrypter;
/**
* DaemonAuthenticate constructor.
*
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
* @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository
* @param \Pterodactyl\Repositories\Eloquent\NodeRepository $repository
*/
public function __construct(Encrypter $encrypter, NodeRepositoryInterface $repository)
public function __construct(Encrypter $encrypter, NodeRepository $repository)
{
$this->repository = $repository;
$this->encrypter = $encrypter;

View file

@ -0,0 +1,38 @@
<?php
namespace Pterodactyl\Http\Middleware\Api;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class IsValidJson
{
/**
* Throw an exception if the request should be valid JSON data but there is an error while
* parsing the data. This avoids confusing validation errors where every field is flagged and
* it is not immediately clear that there is an issue with the JSON being passed.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
if ($request->isJson() && ! empty($request->getContent())) {
json_decode($request->getContent(), true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new BadRequestHttpException(
sprintf(
'The JSON data passed in the request appears to be malformed. err_code: %d err_message: "%s"',
json_last_error(),
json_last_error_msg()
)
);
}
}
return $next($request);
}
}

View file

@ -1,59 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Http\Middleware\Server;
use Closure;
use Illuminate\Http\Request;
use Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class AuthenticateAsSubuser
{
/**
* @var \Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService
*/
private $keyProviderService;
/**
* SubuserAccessAuthenticate constructor.
*
* @param \Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService $keyProviderService
*/
public function __construct(DaemonKeyProviderService $keyProviderService)
{
$this->keyProviderService = $keyProviderService;
}
/**
* Determine if a subuser has permissions to access a server, if so set their access token.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/
public function handle(Request $request, Closure $next)
{
$server = $request->attributes->get('server');
try {
$token = $this->keyProviderService->handle($server, $request->user());
} catch (RecordNotFoundException $exception) {
throw new AccessDeniedHttpException('This account does not have permission to access this server.');
}
$request->attributes->set('server_token', $token);
return $next($request);
}
}

View file

@ -1,56 +0,0 @@
<?php
namespace Pterodactyl\Http\Middleware\Server;
use Closure;
use Illuminate\Http\Request;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class DatabaseBelongsToServer
{
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface
*/
private $repository;
/**
* DatabaseAccess constructor.
*
* @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository
*/
public function __construct(DatabaseRepositoryInterface $repository)
{
$this->repository = $repository;
}
/**
* Check if a database being requested belongs to the currently loaded server.
* If it does not, throw a 404 error, otherwise continue on with the request
* and set an attribute with the database.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function handle(Request $request, Closure $next)
{
$server = $request->attributes->get('server');
$database = $request->input('database') ?? $request->route()->parameter('database');
if (! is_digit($database)) {
throw new NotFoundHttpException;
}
$database = $this->repository->find($database);
if (is_null($database) || $database->server_id !== $server->id) {
throw new NotFoundHttpException;
}
$request->attributes->set('database', $database);
return $next($request);
}
}

View file

@ -1,60 +0,0 @@
<?php
namespace Pterodactyl\Http\Middleware\Server;
use Closure;
use Illuminate\Http\Request;
use Pterodactyl\Contracts\Extensions\HashidsInterface;
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class ScheduleBelongsToServer
{
/**
* @var \Pterodactyl\Contracts\Extensions\HashidsInterface
*/
private $hashids;
/**
* @var \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface
*/
private $repository;
/**
* TaskAccess constructor.
*
* @param \Pterodactyl\Contracts\Extensions\HashidsInterface $hashids
* @param \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface $repository
*/
public function __construct(HashidsInterface $hashids, ScheduleRepositoryInterface $repository)
{
$this->hashids = $hashids;
$this->repository = $repository;
}
/**
* Determine if a task is assigned to the active server.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function handle(Request $request, Closure $next)
{
$server = $request->attributes->get('server');
$scheduleId = $this->hashids->decodeFirst($request->route()->parameter('schedule'), 0);
$schedule = $this->repository->getScheduleWithTasks($scheduleId);
if ($schedule->server_id !== $server->id) {
throw new NotFoundHttpException;
}
$request->attributes->set('schedule', $schedule);
return $next($request);
}
}

View file

@ -1,67 +0,0 @@
<?php
namespace Pterodactyl\Http\Middleware\Server;
use Closure;
use Illuminate\Http\Request;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Contracts\Extensions\HashidsInterface;
use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class SubuserBelongsToServer
{
/**
* @var \Pterodactyl\Contracts\Extensions\HashidsInterface
*/
private $hashids;
/**
* @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface
*/
private $repository;
/**
* SubuserAccess constructor.
*
* @param \Pterodactyl\Contracts\Extensions\HashidsInterface $hashids
* @param \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface $repository
*/
public function __construct(HashidsInterface $hashids, SubuserRepositoryInterface $repository)
{
$this->hashids = $hashids;
$this->repository = $repository;
}
/**
* Determine if a user has permission to access and modify subuser.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function handle(Request $request, Closure $next)
{
$server = $request->attributes->get('server');
$hash = $request->route()->parameter('subuser', 0);
$subuser = $this->repository->find($this->hashids->decodeFirst($hash, 0));
if (is_null($subuser) || $subuser->server_id !== $server->id) {
throw new NotFoundHttpException;
}
if ($request->method() === 'PATCH') {
if ($subuser->user_id === $request->user()->id) {
throw new DisplayException(trans('exceptions.subusers.editing_self'));
}
}
$request->attributes->set('subuser', $subuser);
return $next($request);
}
}

View file

@ -21,7 +21,7 @@ class StoreNodeRequest extends ApplicationApiRequest
/**
* Validation rules to apply to this request.
*
* @param null|array $rules
* @param array|null $rules
* @return array
*/
public function rules(array $rules = null): array

View file

@ -21,7 +21,7 @@ class StoreScheduleRequest extends ViewScheduleRequest
{
return [
'name' => 'required|string|min:1',
'is_active' => 'boolean',
'is_active' => 'filled|boolean',
'minute' => 'required|string',
'hour' => 'required|string',
'day_of_month' => 'required|string',

View file

@ -25,7 +25,7 @@ class StoreTaskRequest extends ViewScheduleRequest
{
return [
'action' => 'required|in:command,power,backup',
'payload' => 'required_unless:action,backup|string',
'payload' => 'required_unless:action,backup|string|nullable',
'time_offset' => 'required|numeric|min:0|max:900',
'sequence_id' => 'sometimes|required|numeric|min:1',
];

View file

@ -3,8 +3,9 @@
namespace Pterodactyl\Http\Requests\Api\Client\Servers;
use Pterodactyl\Models\Permission;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
class SendCommandRequest extends GetServerRequest
class SendCommandRequest extends ClientApiRequest
{
/**
* Determine if the API user has permission to perform this action.

View file

@ -2,6 +2,7 @@
namespace Pterodactyl\Http\Requests\Auth;
use Illuminate\Validation\Rule;
use Illuminate\Foundation\Http\FormRequest;
class LoginCheckpointRequest extends FormRequest
@ -25,7 +26,20 @@ class LoginCheckpointRequest extends FormRequest
{
return [
'confirmation_token' => 'required|string',
'authentication_code' => 'required|numeric',
'authentication_code' => [
'nullable',
'numeric',
Rule::requiredIf(function () {
return empty($this->input('recovery_token'));
}),
],
'recovery_token' => [
'nullable',
'string',
Rule::requiredIf(function () {
return empty($this->input('authentication_code'));
}),
],
];
}
}

View file

@ -2,6 +2,21 @@
namespace Pterodactyl\Models;
/**
* @property int $id
* @property int $server_id
* @property int $database_host_id
* @property string $database
* @property string $username
* @property string $remote
* @property string $password
* @property int $max_connections
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*
* @property \Pterodactyl\Models\Server $server
* @property \Pterodactyl\Models\DatabaseHost $host
*/
class Database extends Model
{
/**

View file

@ -2,6 +2,16 @@
namespace Pterodactyl\Models;
/**
* @property int $id
* @property string $short
* @property string $long
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*
* @property \Pterodactyl\Models\Node[] $nodes
* @property \Pterodactyl\Models\Server[] $servers
*/
class Location extends Model
{
/**

View file

@ -0,0 +1,39 @@
<?php
namespace Pterodactyl\Models;
/**
* @property int $id
* @property int $user_id
* @property string $token
* @property \Carbon\CarbonImmutable $created_at
*
* @property \Pterodactyl\Models\User $user
*/
class RecoveryToken extends Model
{
/**
* There are no updates to this model, only inserts and deletes.
*/
const UPDATED_AT = null;
/**
* @var bool
*/
protected $immutableDates = true;
/**
* @var string[]
*/
public static $validationRules = [
'token' => 'required|string',
];
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
}

View file

@ -17,7 +17,7 @@ namespace Pterodactyl\Models;
*
* @property \Pterodactyl\Models\Server $server
*/
class ServerTransfer extends Validable
class ServerTransfer extends Model
{
/**
* The resource name for this model when it is transformed into an

View file

@ -90,7 +90,7 @@ class Task extends Model
'schedule_id' => 'required|numeric|exists:schedules,id',
'sequence_id' => 'required|numeric|min:1',
'action' => 'required|string',
'payload' => 'required|string',
'payload' => 'required_unless:action,backup|string',
'time_offset' => 'required|numeric|between:0,900',
'is_queued' => 'boolean',
];

View file

@ -39,6 +39,7 @@ use Pterodactyl\Notifications\SendPasswordReset as ResetPasswordNotification;
* @property \Pterodactyl\Models\ApiKey[]|\Illuminate\Database\Eloquent\Collection $apiKeys
* @property \Pterodactyl\Models\Server[]|\Illuminate\Database\Eloquent\Collection $servers
* @property \Pterodactyl\Models\DaemonKey[]|\Illuminate\Database\Eloquent\Collection $keys
* @property \Pterodactyl\Models\RecoveryToken[]|\Illuminate\Database\Eloquent\Collection $recoveryTokens
*/
class User extends Model implements
AuthenticatableContract,
@ -164,7 +165,7 @@ class User extends Model implements
'name_last' => 'required|string|between:1,255',
'password' => 'sometimes|nullable|string',
'root_admin' => 'boolean',
'language' => 'required|string',
'language' => 'string',
'use_totp' => 'boolean',
'totp_secret' => 'nullable|string',
];
@ -251,4 +252,12 @@ class User extends Model implements
return $this->hasMany(ApiKey::class)
->where('key_type', ApiKey::TYPE_ACCOUNT);
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function recoveryTokens()
{
return $this->hasMany(RecoveryToken::class);
}
}

View file

@ -33,16 +33,22 @@ class RouteServiceProvider extends ServiceProvider
->namespace($this->namespace . '\Auth')
->group(base_path('routes/auth.php'));
Route::middleware(['web', 'csrf', 'auth', 'server', 'subuser.auth', 'node.maintenance'])
Route::middleware(['web', 'csrf', 'auth', 'server', 'node.maintenance'])
->prefix('/api/server/{server}')
->namespace($this->namespace . '\Server')
->group(base_path('routes/server.php'));
Route::middleware(['api'])->prefix('/api/application')
Route::middleware([
sprintf('throttle:%s,%s', config('http.rate_limit.application'), config('http.rate_limit.application_period')),
'api',
])->prefix('/api/application')
->namespace($this->namespace . '\Api\Application')
->group(base_path('routes/api-application.php'));
Route::middleware(['client-api'])->prefix('/api/client')
Route::middleware([
sprintf('throttle:%s,%s', config('http.rate_limit.client'), config('http.rate_limit.client_period')),
'client-api',
])->prefix('/api/client')
->namespace($this->namespace . '\Api\Client')
->group(base_path('routes/api-client.php'));

View file

@ -7,7 +7,7 @@ trait Searchable
/**
* The search term to use when filtering results.
*
* @var null|string
* @var string|null
*/
protected $searchTerm;

View file

@ -140,7 +140,7 @@ class DatabaseRepository extends EloquentRepository implements DatabaseRepositor
*/
public function createUser(string $username, string $remote, string $password, $max_connections): bool
{
if (!$max_connections) {
if (! $max_connections) {
return $this->run(sprintf('CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\'', $username, $remote, $password));
} else {
return $this->run(sprintf('CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\' WITH MAX_USER_CONNECTIONS %s', $username, $remote, $password, $max_connections));

View file

@ -0,0 +1,16 @@
<?php
namespace Pterodactyl\Repositories\Eloquent;
use Pterodactyl\Models\RecoveryToken;
class RecoveryTokenRepository extends EloquentRepository
{
/**
* @return string
*/
public function model()
{
return RecoveryToken::class;
}
}

View file

@ -34,7 +34,7 @@ class SessionRepository extends EloquentRepository implements SessionRepositoryI
*
* @param int $user
* @param string $session
* @return null|int
* @return int|null
*/
public function deleteUserSession(int $user, string $session)
{

View file

@ -41,7 +41,7 @@ class TaskRepository extends EloquentRepository implements TaskRepositoryInterfa
*
* @param int $schedule
* @param int $index
* @return null|\Pterodactyl\Models\Task
* @return \Pterodactyl\Models\Task|null
*/
public function getNextTask(int $schedule, int $index)
{

View file

@ -34,7 +34,7 @@ class DaemonConfigurationRepository extends DaemonRepository
* @return \Psr\Http\Message\ResponseInterface
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function update(?Node $node)
public function update(Node $node)
{
try {
return $this->getHttpClient()->post(

View file

@ -1,87 +0,0 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace Pterodactyl\Services\DaemonKeys;
use Carbon\Carbon;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface;
class DaemonKeyCreationService
{
/**
* @var \Carbon\Carbon
*/
protected $carbon;
/**
* @var \Illuminate\Contracts\Config\Repository
*/
protected $config;
/**
* @var \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface
*/
protected $repository;
/**
* DaemonKeyCreationService constructor.
*
* @param \Carbon\Carbon $carbon
* @param \Illuminate\Contracts\Config\Repository $config
* @param \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface $repository
*/
public function __construct(
Carbon $carbon,
ConfigRepository $config,
DaemonKeyRepositoryInterface $repository
) {
$this->carbon = $carbon;
$this->config = $config;
$this->repository = $repository;
}
/**
* Create a new daemon key to be used when connecting to a daemon.
*
* @param int $server
* @param int $user
* @return string
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/
public function handle(int $server, int $user)
{
$secret = DaemonKeyRepositoryInterface::INTERNAL_KEY_IDENTIFIER . str_random(40);
$this->repository->withoutFreshModel()->create([
'user_id' => $user,
'server_id' => $server,
'secret' => $secret,
'expires_at' => $this->carbon->now()->addMinutes($this->config->get('pterodactyl.api.key_expire_time'))->toDateTimeString(),
]);
return $secret;
}
}

View file

@ -1,121 +0,0 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace Pterodactyl\Services\DaemonKeys;
use Carbon\Carbon;
use Pterodactyl\Models\User;
use Pterodactyl\Models\Server;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface;
use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface;
class DaemonKeyProviderService
{
/**
* @var \Pterodactyl\Services\DaemonKeys\DaemonKeyCreationService
*/
private $keyCreationService;
/**
* @var \Pterodactyl\Services\DaemonKeys\DaemonKeyUpdateService
*/
private $keyUpdateService;
/**
* @var \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface
*/
private $repository;
/**
* @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface
*/
private $subuserRepository;
/**
* GetDaemonKeyService constructor.
*
* @param \Pterodactyl\Services\DaemonKeys\DaemonKeyCreationService $keyCreationService
* @param \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface $repository
* @param \Pterodactyl\Services\DaemonKeys\DaemonKeyUpdateService $keyUpdateService
* @param \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface $subuserRepository
*/
public function __construct(
DaemonKeyCreationService $keyCreationService,
DaemonKeyRepositoryInterface $repository,
DaemonKeyUpdateService $keyUpdateService,
SubuserRepositoryInterface $subuserRepository
) {
$this->keyCreationService = $keyCreationService;
$this->keyUpdateService = $keyUpdateService;
$this->repository = $repository;
$this->subuserRepository = $subuserRepository;
}
/**
* Get the access key for a user on a specific server.
*
* @param \Pterodactyl\Models\Server $server
* @param \Pterodactyl\Models\User $user
* @param bool $updateIfExpired
* @return string
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function handle(Server $server, User $user, $updateIfExpired = true): string
{
try {
$key = $this->repository->findFirstWhere([
['user_id', '=', $user->id],
['server_id', '=', $server->id],
]);
} catch (RecordNotFoundException $exception) {
// If key doesn't exist but we are an admin or the server owner,
// create it.
if ($user->root_admin || $user->id === $server->owner_id) {
return $this->keyCreationService->handle($server->id, $user->id);
}
// Check if user is a subuser for this server. Ideally they should always have
// a record associated with them in the database, but we should still handle
// that potentiality here.
//
// If no subuser is found, a RecordNotFoundException will be thrown, thus handling
// the parent error as well.
$subuser = $this->subuserRepository->findFirstWhere([
['user_id', '=', $user->id],
['server_id', '=', $server->id],
]);
return $this->keyCreationService->handle($subuser->server_id, $subuser->user_id);
}
if (! $updateIfExpired || Carbon::now()->diffInSeconds($key->expires_at, false) > 0) {
return $key->secret;
}
return $this->keyUpdateService->handle($key->id);
}
}

View file

@ -1,88 +0,0 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace Pterodactyl\Services\DaemonKeys;
use Carbon\Carbon;
use Webmozart\Assert\Assert;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface;
class DaemonKeyUpdateService
{
/**
* @var \Carbon\Carbon
*/
protected $carbon;
/**
* @var \Illuminate\Contracts\Config\Repository
*/
protected $config;
/**
* @var \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface
*/
protected $repository;
/**
* DaemonKeyUpdateService constructor.
*
* @param \Carbon\Carbon $carbon
* @param \Illuminate\Contracts\Config\Repository $config
* @param \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface $repository
*/
public function __construct(
Carbon $carbon,
ConfigRepository $config,
DaemonKeyRepositoryInterface $repository
) {
$this->carbon = $carbon;
$this->config = $config;
$this->repository = $repository;
}
/**
* Update a daemon key to expire the previous one.
*
* @param int $key
* @return string
*
* @throws \RuntimeException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function handle($key)
{
Assert::integerish($key, 'First argument passed to handle must be an integer, received %s.');
$secret = DaemonKeyRepositoryInterface::INTERNAL_KEY_IDENTIFIER . str_random(40);
$this->repository->withoutFreshModel()->update($key, [
'secret' => $secret,
'expires_at' => $this->carbon->now()->addMinutes($this->config->get('pterodactyl.api.key_expire_time'))->toDateTimeString(),
]);
return $secret;
}
}

View file

@ -3,19 +3,22 @@
namespace Pterodactyl\Services\Databases;
use Exception;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Database;
use Pterodactyl\Helpers\Utilities;
use Illuminate\Database\DatabaseManager;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Extensions\DynamicDatabaseConnection;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException;
use Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException;
class DatabaseManagementService
{
/**
* @var \Illuminate\Database\DatabaseManager
* @var \Illuminate\Database\ConnectionInterface
*/
private $database;
private $connection;
/**
* @var \Pterodactyl\Extensions\DynamicDatabaseConnection
@ -33,84 +36,113 @@ class DatabaseManagementService
private $repository;
/**
* Determines if the service should validate the user's ability to create an additional
* database for this server. In almost all cases this should be true, but to keep things
* flexible you can also set it to false and create more databases than the server is
* allocated.
*
* @var bool
*/
protected $useRandomHost = false;
protected $validateDatabaseLimit = true;
/**
* CreationService constructor.
*
* @param \Illuminate\Database\DatabaseManager $database
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic
* @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
*/
public function __construct(
DatabaseManager $database,
ConnectionInterface $connection,
DynamicDatabaseConnection $dynamic,
DatabaseRepositoryInterface $repository,
Encrypter $encrypter
) {
$this->database = $database;
$this->connection = $connection;
$this->dynamic = $dynamic;
$this->encrypter = $encrypter;
$this->repository = $repository;
}
/**
* Set wether or not this class should validate that the server has enough slots
* left before creating the new database.
*
* @param bool $validate
* @return $this
*/
public function setValidateDatabaseLimit(bool $validate): self
{
$this->validateDatabaseLimit = $validate;
return $this;
}
/**
* Create a new database that is linked to a specific host.
*
* @param int $server
* @param \Pterodactyl\Models\Server $server
* @param array $data
* @return \Pterodactyl\Models\Database
*
* @throws \Exception
* @throws \Throwable
* @throws \Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException
* @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException
*/
public function create($server, array $data)
public function create(Server $server, array $data)
{
$data['server_id'] = $server;
$data['database'] = sprintf('s%d_%s', $server, $data['database']);
$data['username'] = sprintf('u%d_%s', $server, str_random(10));
$data['password'] = $this->encrypter->encrypt(
Utilities::randomStringWithSpecialCharacters(24)
);
if (! config('pterodactyl.client_features.databases.enabled')) {
throw new DatabaseClientFeatureNotEnabledException;
}
if ($this->validateDatabaseLimit) {
// If the server has a limit assigned and we've already reached that limit, throw back
// an exception and kill the process.
if (! is_null($server->database_limit) && $server->databases()->count() >= $server->database_limit) {
throw new TooManyDatabasesException;
}
}
$data = array_merge($data, [
'server_id' => $server->id,
'database' => sprintf('s%d_%s', $server->id, $data['database']),
'username' => sprintf('u%d_%s', $server->id, str_random(10)),
'password' => $this->encrypter->encrypt(
Utilities::randomStringWithSpecialCharacters(24)
),
]);
$database = null;
$this->database->beginTransaction();
try {
$database = $this->repository->createIfNotExists($data);
$this->dynamic->set('dynamic', $data['database_host_id']);
return $this->connection->transaction(function () use ($data, &$database) {
$database = $this->repository->createIfNotExists($data);
$this->dynamic->set('dynamic', $data['database_host_id']);
$this->repository->createDatabase($database->database);
$this->repository->createUser(
$database->username,
$database->remote,
$this->encrypter->decrypt($database->password),
$database->max_connections
);
$this->repository->assignUserToDatabase(
$database->database,
$database->username,
$database->remote
);
$this->repository->flush();
$this->repository->createDatabase($database->database);
$this->repository->createUser(
$database->username, $database->remote, $this->encrypter->decrypt($database->password), $database->max_connections
);
$this->repository->assignUserToDatabase($database->database, $database->username, $database->remote);
$this->repository->flush();
$this->database->commit();
} catch (Exception $ex) {
return $database;
});
} catch (Exception $exception) {
try {
if (isset($database) && $database instanceof Database) {
if ($database instanceof Database) {
$this->repository->dropDatabase($database->database);
$this->repository->dropUser($database->username, $database->remote);
$this->repository->flush();
}
} catch (Exception $exTwo) {
// ignore an exception
} catch (Exception $exception) {
// Do nothing here. We've already encountered an issue before this point so no
// reason to prioritize this error over the initial one.
}
$this->database->rollBack();
throw $ex;
throw $exception;
}
return $database;
}
/**

View file

@ -6,9 +6,7 @@ use Pterodactyl\Models\Server;
use Pterodactyl\Models\Database;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
use Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException;
use Pterodactyl\Exceptions\Service\Database\NoSuitableDatabaseHostException;
use Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException;
class DeployServerDatabaseService
{
@ -49,20 +47,12 @@ class DeployServerDatabaseService
* @param array $data
* @return \Pterodactyl\Models\Database
*
* @throws \Throwable
* @throws \Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException
* @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException
* @throws \Exception
*/
public function handle(Server $server, array $data): Database
{
if (! config('pterodactyl.client_features.databases.enabled')) {
throw new DatabaseClientFeatureNotEnabledException;
}
$databases = $this->repository->findCountWhere([['server_id', '=', $server->id]]);
if (! is_null($server->database_limit) && $databases >= $server->database_limit) {
throw new TooManyDatabasesException;
}
$allowRandom = config('pterodactyl.client_features.databases.allow_random');
$hosts = $this->databaseHostRepository->setColumns(['id'])->findWhere([
['node_id', '=', $server->node_id],
@ -81,7 +71,7 @@ class DeployServerDatabaseService
$host = $hosts->random();
return $this->managementService->create($server->id, [
return $this->managementService->create($server, [
'database_host_id' => $host->id,
'database' => array_get($data, 'database'),
'remote' => array_get($data, 'remote'),

View file

@ -76,7 +76,16 @@ class EggImporterService
public function handle(UploadedFile $file, int $nest): Egg
{
if ($file->getError() !== UPLOAD_ERR_OK || ! $file->isFile()) {
throw new InvalidFileUploadException(trans('exceptions.nest.importer.file_error'));
throw new InvalidFileUploadException(
sprintf(
'The selected file ["%s"] was not in a valid format to import. (is_file: %s is_valid: %s err_code: %s err: %s)',
$file->getFilename(),
$file->isFile() ? 'true' : 'false',
$file->isValid() ? 'true' : 'false',
$file->getError(),
$file->getErrorMessage()
)
);
}
$parsed = json_decode($file->openFile()->fread($file->getSize()));

View file

@ -57,7 +57,16 @@ class EggUpdateImporterService
public function handle(int $egg, UploadedFile $file)
{
if ($file->getError() !== UPLOAD_ERR_OK || ! $file->isFile()) {
throw new InvalidFileUploadException(trans('exceptions.nest.importer.file_error'));
throw new InvalidFileUploadException(
sprintf(
'The selected file ["%s"] was not in a valid format to import. (is_file: %s is_valid: %s err_code: %s err: %s)',
$file->getFilename(),
$file->isFile() ? 'true' : 'false',
$file->isValid() ? 'true' : 'false',
$file->getError(),
$file->getErrorMessage()
)
);
}
$parsed = json_decode($file->openFile()->fread($file->getSize()));

View file

@ -24,7 +24,7 @@ class AssetHashService
private $application;
/**
* @var null|array
* @var array|null
*/
protected static $manifest;

View file

@ -4,7 +4,7 @@ namespace Pterodactyl\Services\Helpers;
use Exception;
use GuzzleHttp\Client;
use Cake\Chronos\Chronos;
use Carbon\CarbonImmutable;
use Illuminate\Support\Arr;
use Illuminate\Contracts\Cache\Repository as CacheRepository;
use Pterodactyl\Exceptions\Service\Helper\CdnVersionFetchingException;
@ -120,7 +120,7 @@ class SoftwareVersionService
*/
protected function cacheVersionData()
{
return $this->cache->remember(self::VERSION_CACHE_KEY, Chronos::now()->addMinutes(config()->get('pterodactyl.cdn.cache_time', 60)), function () {
return $this->cache->remember(self::VERSION_CACHE_KEY, CarbonImmutable::now()->addMinutes(config()->get('pterodactyl.cdn.cache_time', 60)), function () {
try {
$response = $this->client->request('GET', config()->get('pterodactyl.cdn.url'));

View file

@ -5,7 +5,7 @@ namespace Pterodactyl\Services\Nodes;
use Ramsey\Uuid\Uuid;
use Illuminate\Support\Str;
use Pterodactyl\Models\Node;
use Illuminate\Encryption\Encrypter;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
class NodeCreationService
@ -16,14 +16,14 @@ class NodeCreationService
protected $repository;
/**
* @var \Illuminate\Encryption\Encrypter
* @var \Illuminate\Contracts\Encryption\Encrypter
*/
private $encrypter;
/**
* CreationService constructor.
*
* @param \Illuminate\Encryption\Encrypter $encrypter
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
* @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository
*/
public function __construct(Encrypter $encrypter, NodeRepositoryInterface $repository)

View file

@ -71,8 +71,8 @@ class BuildModificationService
* @return \Pterodactyl\Models\Server
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/
public function handle(Server $server, array $data)
{
@ -91,7 +91,7 @@ class BuildModificationService
}
}
/** @var \Pterodactyl\Models\Server $server */
/* @var \Pterodactyl\Models\Server $server */
$server = $this->repository->withFreshModel()->update($server->id, [
'oom_disabled' => array_get($data, 'oom_disabled'),
'memory' => array_get($data, 'memory'),

View file

@ -4,8 +4,8 @@ namespace Pterodactyl\Services\Servers;
use Pterodactyl\Models\Server;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Repositories\Eloquent\ServerRepository;
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class ReinstallServerService
{
@ -17,27 +17,27 @@ class ReinstallServerService
/**
* @var \Illuminate\Database\ConnectionInterface
*/
private $database;
private $connection;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
* @var \Pterodactyl\Repositories\Eloquent\ServerRepository
*/
private $repository;
/**
* ReinstallService constructor.
*
* @param \Illuminate\Database\ConnectionInterface $database
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
* @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository
*/
public function __construct(
ConnectionInterface $database,
ConnectionInterface $connection,
DaemonServerRepository $daemonServerRepository,
ServerRepositoryInterface $repository
ServerRepository $repository
) {
$this->daemonServerRepository = $daemonServerRepository;
$this->database = $database;
$this->connection = $connection;
$this->repository = $repository;
}
@ -51,14 +51,14 @@ class ReinstallServerService
*/
public function reinstall(Server $server)
{
$this->database->transaction(function () use ($server) {
$this->repository->withoutFreshModel()->update($server->id, [
return $this->connection->transaction(function () use ($server) {
$updated = $this->repository->update($server->id, [
'installed' => Server::STATUS_INSTALLING,
]);
], true, true);
$this->daemonServerRepository->setServer($server)->reinstall();
});
return $server->refresh();
return $updated;
});
}
}

View file

@ -310,8 +310,6 @@ class ServerCreationService
return $allocation->node_id;
}
/** @noinspection PhpDocMissingThrowsInspection */
/**
* Create a unique UUID and UUID-Short combo for a server.
*
@ -319,7 +317,6 @@ class ServerCreationService
*/
private function generateUniqueUuidCombo(): string
{
/** @noinspection PhpUnhandledExceptionInspection */
$uuid = Uuid::uuid4()->toString();
if (! $this->repository->isUniqueUuidCombo($uuid, substr($uuid, 0, 8))) {

View file

@ -2,6 +2,7 @@
namespace Pterodactyl\Services\Servers;
use Exception;
use Psr\Log\LoggerInterface;
use Pterodactyl\Models\Server;
use Illuminate\Database\ConnectionInterface;
@ -109,7 +110,15 @@ class ServerDeletionService
$this->connection->transaction(function () use ($server) {
$this->databaseRepository->setColumns('id')->findWhere([['server_id', '=', $server->id]])->each(function ($item) {
$this->databaseManagementService->delete($item->id);
try {
$this->databaseManagementService->delete($item->id);
} catch (Exception $exception) {
if ($this->force) {
$this->writer->warning($exception);
} else {
throw $exception;
}
}
});
$this->repository->delete($server->id);

View file

@ -3,10 +3,13 @@
namespace Pterodactyl\Services\Users;
use Carbon\Carbon;
use Illuminate\Support\Str;
use Pterodactyl\Models\User;
use PragmaRX\Google2FA\Google2FA;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Pterodactyl\Repositories\Eloquent\RecoveryTokenRepository;
use Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid;
class ToggleTwoFactorService
@ -26,21 +29,37 @@ class ToggleTwoFactorService
*/
private $repository;
/**
* @var \Pterodactyl\Repositories\Eloquent\RecoveryTokenRepository
*/
private $recoveryTokenRepository;
/**
* @var \Illuminate\Database\ConnectionInterface
*/
private $connection;
/**
* ToggleTwoFactorService constructor.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
* @param \PragmaRX\Google2FA\Google2FA $google2FA
* @param \Pterodactyl\Repositories\Eloquent\RecoveryTokenRepository $recoveryTokenRepository
* @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository
*/
public function __construct(
ConnectionInterface $connection,
Encrypter $encrypter,
Google2FA $google2FA,
RecoveryTokenRepository $recoveryTokenRepository,
UserRepositoryInterface $repository
) {
$this->encrypter = $encrypter;
$this->google2FA = $google2FA;
$this->repository = $repository;
$this->recoveryTokenRepository = $recoveryTokenRepository;
$this->connection = $connection;
}
/**
@ -49,32 +68,60 @@ class ToggleTwoFactorService
* @param \Pterodactyl\Models\User $user
* @param string $token
* @param bool|null $toggleState
* @return bool
* @return string[]
*
* @throws \Throwable
* @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
* @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
* @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid
*/
public function handle(User $user, string $token, bool $toggleState = null): bool
public function handle(User $user, string $token, bool $toggleState = null): array
{
$secret = $this->encrypter->decrypt($user->totp_secret);
$isValidToken = $this->google2FA->verifyKey($secret, $token, config()->get('pterodactyl.auth.2fa.window'));
if (! $isValidToken) {
throw new TwoFactorAuthenticationTokenInvalid(
'The token provided is not valid.'
);
throw new TwoFactorAuthenticationTokenInvalid('The token provided is not valid.');
}
$this->repository->withoutFreshModel()->update($user->id, [
'totp_authenticated_at' => Carbon::now(),
'use_totp' => (is_null($toggleState) ? ! $user->use_totp : $toggleState),
]);
return $this->connection->transaction(function () use ($user, $toggleState) {
// Now that we're enabling 2FA on the account, generate 10 recovery tokens for the account
// and store them hashed in the database. We'll return them to the caller so that the user
// can see and save them.
//
// If a user is unable to login with a 2FA token they can provide one of these backup codes
// which will then be marked as deleted from the database and will also bypass 2FA protections
// on their account.
$tokens = [];
if ((! $toggleState && ! $user->use_totp) || $toggleState) {
$inserts = [];
for ($i = 0; $i < 10; $i++) {
$token = Str::random(10);
return true;
$inserts[] = [
'user_id' => $user->id,
'token' => password_hash($token, PASSWORD_DEFAULT),
];
$tokens[] = $token;
}
// Before inserting any new records make sure all of the old ones are deleted to avoid
// any issues or storing an unnecessary number of tokens in the database.
$this->recoveryTokenRepository->deleteWhere(['user_id' => $user->id]);
// Bulk insert the hashed tokens.
$this->recoveryTokenRepository->insert($inserts);
}
$this->repository->withoutFreshModel()->update($user->id, [
'totp_authenticated_at' => Carbon::now(),
'use_totp' => (is_null($toggleState) ? ! $user->use_totp : $toggleState),
]);
return $tokens;
});
}
}

View file

@ -71,7 +71,7 @@ class TwoFactorSetupService
'totp_secret' => $this->encrypter->encrypt($secret),
]);
$company = preg_replace('/\s/', '', $this->config->get('app.name'));
$company = urlencode(preg_replace('/\s/', '', $this->config->get('app.name')));
return sprintf(
'otpauth://totp/%1$s:%2$s?secret=%3$s&issuer=%1$s',

View file

@ -2,6 +2,8 @@
namespace Pterodactyl\Transformers\Api\Application;
use Pterodactyl\Models\Node;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Allocation;
use Pterodactyl\Services\Acl\Api\AdminAcl;
@ -54,10 +56,8 @@ class AllocationTransformer extends BaseTransformer
return $this->null();
}
$allocation->loadMissing('node');
return $this->item(
$allocation->getRelation('node'), $this->makeTransformer(NodeTransformer::class), 'node'
$allocation->node, $this->makeTransformer(NodeTransformer::class), Node::RESOURCE_NAME
);
}
@ -70,14 +70,12 @@ class AllocationTransformer extends BaseTransformer
*/
public function includeServer(Allocation $allocation)
{
if (! $this->authorize(AdminAcl::RESOURCE_SERVERS)) {
if (! $this->authorize(AdminAcl::RESOURCE_SERVERS) || ! $allocation->server) {
return $this->null();
}
$allocation->loadMissing('server');
return $this->item(
$allocation->getRelation('server'), $this->makeTransformer(ServerTransformer::class), 'server'
$allocation->server, $this->makeTransformer(ServerTransformer::class), Server::RESOURCE_NAME
);
}
}