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

@ -8,5 +8,8 @@ indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
[.*yml]
indent_size = 2
[*.md]
trim_trailing_whitespace = false

View File

@ -2,13 +2,13 @@ APP_ENV=testing
APP_DEBUG=true
APP_KEY=SomeRandomString3232RandomString
APP_THEME=pterodactyl
APP_TIMEZONE=UTC
APP_TIMEZONE=America/Los_Angeles
APP_URL=http://localhost/
TESTING_DB_HOST=127.0.0.1
TESTING_DB_DATABASE=travis
TESTING_DB_DATABASE=panel_test
TESTING_DB_USERNAME=root
TESTING_DB_PASSWORD=""
TESTING_DB_PASSWORD=
CACHE_DRIVER=array
SESSION_DRIVER=array

2
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,2 @@
#github: [DaneEveritt]
custom: ["https://paypal.me/PterodactylSoftware"]

64
.github/workflows/tests.yml vendored Normal file
View File

@ -0,0 +1,64 @@
name: tests
on:
push:
pull_request:
jobs:
integration_tests:
if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')"
runs-on: ubuntu-latest
services:
mysql:
image: mysql:5.7
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: panel_test
ports:
- 3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
strategy:
fail-fast: true
matrix:
php: [7.3, 7.4]
name: PHP ${{ matrix.php }}
steps:
- name: checkout
uses: actions/checkout@v2
- name: get cache directory
id: composer-cache
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: cache dependencies
uses: actions/cache@v2
with:
path: |
~/.php_cs.cache
${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-cache-${{ matrix.php }}-${{ hashFiles('**.composer.lock') }}
restore-keys: |
${{ runner.os }}-cache-${{ matrix.php }}-
- name: setup
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: cli, openssl, gd, mysql, pdo, mbstring, tokenizer, bcmath, xml, curl, zip
tools: composer:v1
coverage: none
- name: configure
run: cp .env.ci .env
- name: install dependencies
run: composer install --prefer-dist --no-interaction --no-progress
- name: run cs-fixer
run: vendor/bin/php-cs-fixer fix --dry-run --diff --diff-format=udiff
continue-on-error: true
- name: execute unit tests
run: vendor/bin/phpunit --bootstrap bootstrap/app.php tests/Unit
if: ${{ always() }}
env:
DB_CONNECTION: testing
TESTING_DB_HOST: UNIT_NO_DB
- name: execute integration tests
run: vendor/bin/phpunit tests/Integration
if: ${{ always() }}
env:
TESTING_DB_PORT: ${{ job.services.mysql.ports[3306] }}
TESTING_DB_USERNAME: root

1
.gitignore vendored
View File

@ -32,3 +32,4 @@ coverage.xml
resources/lang/locales.js
resources/assets/pterodactyl/scripts/helpers/ziggy.js
resources/assets/scripts/helpers/ziggy.js
.phpunit.result.cache

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
);
}
}

View File

@ -17,12 +17,22 @@ $kernel->bootstrap();
$output = new ConsoleOutput;
if (config('database.default') !== 'testing') {
$output->writeln(PHP_EOL . '<error>Cannot run test process against non-testing database.</error>');
$output->writeln(PHP_EOL . '<error>Environment is currently pointed at: "' . config('database.default') . '".</error>');
exit(1);
}
/*
* Perform database migrations and reseeding before continuing with
* running the tests.
*/
$output->writeln(PHP_EOL . '<comment>Refreshing database for Integration tests...</comment>');
$kernel->call('migrate:fresh', ['--database' => 'testing']);
if (! env('SKIP_MIGRATIONS')) {
$output->writeln(PHP_EOL . '<info>Refreshing database for Integration tests...</info>');
$kernel->call('migrate:fresh', ['--database' => 'testing']);
$output->writeln('<comment>Seeding database for Integration tests...</comment>' . PHP_EOL);
$kernel->call('db:seed', ['--database' => 'testing']);
$output->writeln('<info>Seeding database for Integration tests...</info>' . PHP_EOL);
$kernel->call('db:seed', ['--database' => 'testing']);
} else {
$output->writeln(PHP_EOL . '<comment>Skipping database migrations...</comment>' . PHP_EOL);
}

View File

@ -11,7 +11,7 @@
}
],
"require": {
"php": ">=7.2",
"php": "^7.2",
"ext-mbstring": "*",
"ext-pdo_mysql": "*",
"ext-zip": "*",
@ -23,9 +23,10 @@
"guzzlehttp/guzzle": "^6.5",
"hashids/hashids": "^4.0",
"laracasts/utilities": "^3.1",
"laravel/framework": "^6.18",
"laravel/framework": "^7.17",
"laravel/helpers": "^1.2",
"laravel/tinker": "^1.0",
"laravel/tinker": "^2.4",
"laravel/ui": "^2.0",
"lcobucci/jwt": "^3.3",
"league/flysystem-aws-s3-v3": "^1.0",
"league/flysystem-memory": "^1.0",
@ -33,22 +34,23 @@
"pragmarx/google2fa": "^5.0",
"predis/predis": "^1.1",
"prologue/alerts": "^0.4",
"psy/psysh": "^0.10.4",
"s1lentium/iptools": "^1.1",
"spatie/laravel-fractal": "^5.7",
"staudenmeir/belongs-to-through": "^2.9",
"staudenmeir/belongs-to-through": "^2.10",
"symfony/yaml": "^4.4",
"webmozart/assert": "^1.7"
"webmozart/assert": "^1.9"
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.2",
"barryvdh/laravel-ide-helper": "^2.6",
"codedungeon/phpunit-result-printer": "0.25.1",
"friendsofphp/php-cs-fixer": "^2.16.1",
"fzaninotto/faker": "^1.9.1",
"laravel/dusk": "^5.11",
"mockery/mockery": "^1.0",
"barryvdh/laravel-debugbar": "^3.3",
"barryvdh/laravel-ide-helper": "^2.7",
"codedungeon/phpunit-result-printer": "^0.28.0",
"friendsofphp/php-cs-fixer": "2.16.1",
"fzaninotto/faker": "^1.9",
"laravel/dusk": "^6.3",
"mockery/mockery": "^1.4",
"php-mock/php-mock-phpunit": "^2.6",
"phpunit/phpunit": "^7"
"phpunit/phpunit": "^8.5"
},
"autoload": {
"classmap": [

2909
composer.lock generated

File diff suppressed because it is too large Load Diff

21
config/http.php Normal file
View File

@ -0,0 +1,21 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| API Rate Limits
|--------------------------------------------------------------------------
|
| Defines the rate limit for the number of requests per minute that can be
| executed against both the client and internal (application) APIs over the
| defined period (by default, 1 minute).
|
*/
'rate_limit' => [
'client_period' => 1,
'client' => env('APP_API_CLIENT_RATELIMIT', 240),
'application_period' => 1,
'application' => env('APP_API_APPLICATION_RATELIMIT', 240),
],
];

View File

@ -3,7 +3,6 @@
use Monolog\Handler\StreamHandler;
return [
/*
|--------------------------------------------------------------------------
| Default Log Channel
@ -77,5 +76,4 @@ return [
'level' => 'debug',
],
],
];

View File

@ -162,6 +162,11 @@ return [
'enabled' => env('PTERODACTYL_CLIENT_DATABASES_ENABLED', true),
'allow_random' => env('PTERODACTYL_CLIENT_DATABASES_ALLOW_RANDOM', true),
],
'schedules' => [
// The total number of tasks that can exist for any given schedule at once.
'per_schedule_task_limit' => 10,
],
],
/*
@ -218,5 +223,7 @@ return [
|
| 'P_SERVER_CREATED_AT' => 'created_at'
*/
'environment_variables' => [],
'environment_variables' => [
'P_SERVER_ALLOCATION_LIMIT' => 'allocation_limit',
],
];

View File

@ -20,9 +20,7 @@ use Pterodactyl\Models\ApiKey;
$factory->define(Pterodactyl\Models\Server::class, function (Faker $faker) {
return [
'id' => $faker->unique()->randomNumber(),
'node_id' => $faker->randomNumber(),
'uuid' => $faker->unique()->uuid,
'uuid' => Uuid::uuid4()->toString(),
'uuidShort' => str_random(8),
'name' => $faker->firstName,
'description' => implode(' ', $faker->sentences()),
@ -34,9 +32,6 @@ $factory->define(Pterodactyl\Models\Server::class, function (Faker $faker) {
'io' => 500,
'cpu' => 0,
'oom_disabled' => 0,
'allocation_id' => $faker->randomNumber(),
'nest_id' => $faker->randomNumber(),
'egg_id' => $faker->randomNumber(),
'pack_id' => null,
'installed' => 1,
'database_limit' => null,
@ -50,7 +45,6 @@ $factory->define(Pterodactyl\Models\User::class, function (Faker $faker) {
static $password;
return [
'id' => $faker->unique()->randomNumber(),
'external_id' => $faker->unique()->isbn10,
'uuid' => $faker->uuid,
'username' => $faker->userName,
@ -74,15 +68,13 @@ $factory->state(Pterodactyl\Models\User::class, 'admin', function () {
$factory->define(Pterodactyl\Models\Location::class, function (Faker $faker) {
return [
'id' => $faker->unique()->randomNumber(),
'short' => $faker->unique()->domainWord,
'short' => Str::random(8),
'long' => $faker->catchPhrase,
];
});
$factory->define(Pterodactyl\Models\Node::class, function (Faker $faker) {
return [
'id' => $faker->unique()->randomNumber(),
'uuid' => Uuid::uuid4()->toString(),
'public' => true,
'name' => $faker->firstName,
@ -95,7 +87,7 @@ $factory->define(Pterodactyl\Models\Node::class, function (Faker $faker) {
'disk_overallocate' => 0,
'upload_size' => 100,
'daemon_token_id' => Str::random(Node::DAEMON_TOKEN_ID_LENGTH),
'daemon_token' => Str::random(Node::DAEMON_TOKEN_LENGTH),
'daemon_token' => encrypt(Str::random(Node::DAEMON_TOKEN_LENGTH)),
'daemonListen' => 8080,
'daemonSFTP' => 2022,
'daemonBase' => '/var/lib/pterodactyl/volumes',
@ -104,7 +96,6 @@ $factory->define(Pterodactyl\Models\Node::class, function (Faker $faker) {
$factory->define(Pterodactyl\Models\Nest::class, function (Faker $faker) {
return [
'id' => $faker->unique()->randomNumber(),
'uuid' => $faker->unique()->uuid,
'author' => 'testauthor@example.com',
'name' => $faker->word,
@ -114,9 +105,7 @@ $factory->define(Pterodactyl\Models\Nest::class, function (Faker $faker) {
$factory->define(Pterodactyl\Models\Egg::class, function (Faker $faker) {
return [
'id' => $faker->unique()->randomNumber(),
'uuid' => $faker->unique()->uuid,
'nest_id' => $faker->unique()->randomNumber(),
'name' => $faker->name,
'description' => implode(' ', $faker->sentences(3)),
'startup' => 'java -jar test.jar',
@ -125,7 +114,6 @@ $factory->define(Pterodactyl\Models\Egg::class, function (Faker $faker) {
$factory->define(Pterodactyl\Models\EggVariable::class, function (Faker $faker) {
return [
'id' => $faker->unique()->randomNumber(),
'name' => $faker->firstName,
'description' => $faker->sentence(),
'env_variable' => strtoupper(str_replace(' ', '_', $faker->words(2, true))),
@ -146,8 +134,6 @@ $factory->state(Pterodactyl\Models\EggVariable::class, 'editable', function () {
$factory->define(Pterodactyl\Models\Pack::class, function (Faker $faker) {
return [
'id' => $faker->unique()->randomNumber(),
'egg_id' => $faker->randomNumber(),
'uuid' => $faker->uuid,
'name' => $faker->word,
'description' => null,
@ -159,17 +145,11 @@ $factory->define(Pterodactyl\Models\Pack::class, function (Faker $faker) {
});
$factory->define(Pterodactyl\Models\Subuser::class, function (Faker $faker) {
return [
'id' => $faker->unique()->randomNumber(),
'user_id' => $faker->randomNumber(),
'server_id' => $faker->randomNumber(),
];
return [];
});
$factory->define(Pterodactyl\Models\Allocation::class, function (Faker $faker) {
return [
'id' => $faker->unique()->randomNumber(),
'node_id' => $faker->randomNumber(),
'ip' => $faker->ipv4,
'port' => $faker->randomNumber(5),
];
@ -177,13 +157,11 @@ $factory->define(Pterodactyl\Models\Allocation::class, function (Faker $faker) {
$factory->define(Pterodactyl\Models\DatabaseHost::class, function (Faker $faker) {
return [
'id' => $faker->unique()->randomNumber(),
'name' => $faker->colorName,
'host' => $faker->unique()->ipv4,
'port' => 3306,
'username' => $faker->colorName,
'password' => Crypt::encrypt($faker->word),
'node_id' => $faker->randomNumber(),
];
});
@ -191,9 +169,6 @@ $factory->define(Pterodactyl\Models\Database::class, function (Faker $faker) {
static $password;
return [
'id' => $faker->unique()->randomNumber(),
'server_id' => $faker->randomNumber(),
'database_host_id' => $faker->randomNumber(),
'database' => str_random(10),
'username' => str_random(10),
'remote' => '%',
@ -205,16 +180,12 @@ $factory->define(Pterodactyl\Models\Database::class, function (Faker $faker) {
$factory->define(Pterodactyl\Models\Schedule::class, function (Faker $faker) {
return [
'id' => $faker->unique()->randomNumber(),
'server_id' => $faker->randomNumber(),
'name' => $faker->firstName(),
];
});
$factory->define(Pterodactyl\Models\Task::class, function (Faker $faker) {
return [
'id' => $faker->unique()->randomNumber(),
'schedule_id' => $faker->randomNumber(),
'sequence_id' => $faker->randomNumber(1),
'action' => 'command',
'payload' => 'test command',
@ -225,9 +196,6 @@ $factory->define(Pterodactyl\Models\Task::class, function (Faker $faker) {
$factory->define(Pterodactyl\Models\DaemonKey::class, function (Faker $faker) {
return [
'id' => $faker->unique()->randomNumber(),
'server_id' => $faker->randomNumber(),
'user_id' => $faker->randomNumber(),
'secret' => 'i_' . str_random(40),
'expires_at' => \Carbon\Carbon::now()->addMinutes(10)->toDateTimeString(),
];
@ -237,8 +205,6 @@ $factory->define(Pterodactyl\Models\ApiKey::class, function (Faker $faker) {
static $token;
return [
'id' => $faker->unique()->randomNumber(),
'user_id' => $faker->randomNumber(),
'key_type' => ApiKey::TYPE_APPLICATION,
'identifier' => str_random(Pterodactyl\Models\ApiKey::IDENTIFIER_LENGTH),
'token' => $token ?: $token = encrypt(str_random(Pterodactyl\Models\ApiKey::KEY_LENGTH)),

View File

@ -23,6 +23,5 @@ class DeleteTaskWhenParentServerIsDeleted extends Migration
*/
public function down()
{
//
}
}

View File

@ -1,8 +1,8 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AllowNullableDescriptions extends Migration
{

View File

@ -1,8 +1,8 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddMaxConnectionsColumn extends Migration
{

View File

@ -1,8 +1,8 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddBackupLimitToServers extends Migration
{

View File

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateUserRecoveryTokensTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('recovery_tokens', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('user_id');
$table->string('token');
$table->timestamp('created_at')->nullable();
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('recovery_tokens');
}
}

View File

@ -112,14 +112,16 @@ class EggSeeder extends Seeder
$files = $this->filesystem->allFiles(database_path('seeds/eggs/' . kebab_case($nest->name)));
$this->command->alert('Updating Eggs for Nest: ' . $nest->name);
collect($files)->each(function ($file) use ($nest) {
Collection::make($files)->each(function ($file) use ($nest) {
/* @var \Symfony\Component\Finder\SplFileInfo $file */
$decoded = json_decode($file->getContents());
if (json_last_error() !== JSON_ERROR_NONE) {
return $this->command->error('JSON decode exception for ' . $file->getFilename() . ': ' . json_last_error_msg());
$this->command->error('JSON decode exception for ' . $file->getFilename() . ': ' . json_last_error_msg());
return;
}
$file = new UploadedFile($file->getPathname(), $file->getFilename(), 'application/json', $file->getSize());
$file = new UploadedFile($file->getPathname(), $file->getFilename(), 'application/json');
try {
$egg = $this->repository->setColumns('id')->findFirstWhere([
@ -130,11 +132,11 @@ class EggSeeder extends Seeder
$this->updateImporterService->handle($egg->id, $file);
return $this->command->info('Updated ' . $decoded->name);
$this->command->info('Updated ' . $decoded->name);
} catch (RecordNotFoundException $exception) {
$this->importerService->handle($file, $nest->id);
return $this->command->comment('Created ' . $decoded->name);
$this->command->comment('Created ' . $decoded->name);
}
});

View File

@ -36,7 +36,7 @@
"name": "Server Version",
"description": "Version of Mumble Server to download and use.",
"env_variable": "MUMBLE_VERSION",
"default_value": "1.2.19",
"default_value": "1.3.1",
"user_viewable": 1,
"user_editable": 1,
"rules": "required|regex:\/^([0-9_\\.-]{5,8})$\/"

View File

@ -27,6 +27,7 @@
#terminal > .cmd {
padding: 1px 0;
word-wrap: break-word;
white-space: pre-wrap;
}
#terminal_input {

View File

@ -1,9 +1,7 @@
import http from '@/api/http';
export default (code: string): Promise<void> => {
return new Promise((resolve, reject) => {
http.post('/api/client/account/two-factor', { code })
.then(() => resolve())
.catch(reject);
});
export default async (code: string): Promise<string[]> => {
const { data } = await http.post('/api/client/account/two-factor', { code });
return data.attributes.tokens;
};

View File

@ -1,13 +1,14 @@
import http from '@/api/http';
import { LoginResponse } from '@/api/auth/login';
export default (token: string, code: string): Promise<LoginResponse> => {
export default (token: string, code: string, recoveryToken?: string): Promise<LoginResponse> => {
return new Promise((resolve, reject) => {
http.post('/auth/login/checkpoint', {
// eslint-disable-next-line @typescript-eslint/camelcase
/* eslint-disable @typescript-eslint/camelcase */
confirmation_token: token,
// eslint-disable-next-line @typescript-eslint/camelcase
authentication_code: code,
recovery_token: (recoveryToken && recoveryToken.length > 0) ? recoveryToken : undefined,
/* eslint-enable @typescript-eslint/camelcase */
})
.then(response => resolve({
complete: response.data.data.complete,

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import { Link, RouteComponentProps } from 'react-router-dom';
import loginCheckpoint from '@/api/auth/loginCheckpoint';
import { httpErrorToHuman } from '@/api/http';
@ -14,6 +14,7 @@ import Field from '@/components/elements/Field';
interface Values {
code: string;
recoveryCode: '',
}
type OwnProps = RouteComponentProps<{}, StaticContext, { token?: string }>
@ -24,7 +25,8 @@ type Props = OwnProps & {
}
const LoginCheckpointContainer = () => {
const { isSubmitting } = useFormikContext<Values>();
const { isSubmitting, setFieldValue } = useFormikContext<Values>();
const [ isMissingDevice, setIsMissingDevice ] = useState(false);
return (
<LoginFormContainer
@ -34,10 +36,14 @@ const LoginCheckpointContainer = () => {
<div className={'mt-6'}>
<Field
light={true}
name={'code'}
title={'Authentication Code'}
description={'Enter the two-factor token generated by your device.'}
type={'number'}
name={isMissingDevice ? 'recoveryCode' : 'code'}
title={isMissingDevice ? 'Recovery Code' : 'Authentication Code'}
description={
isMissingDevice
? 'Enter one of the recovery codes generated when you setup 2-Factor authentication on this account in order to continue.'
: 'Enter the two-factor token generated by your device.'
}
type={isMissingDevice ? 'text' : 'number'}
autoFocus={true}
/>
</div>
@ -54,6 +60,18 @@ const LoginCheckpointContainer = () => {
}
</button>
</div>
<div className={'mt-6 text-center'}>
<span
onClick={() => {
setFieldValue('code', '');
setFieldValue('recoveryCode', '');
setIsMissingDevice(s => !s);
}}
className={'cursor-pointer text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700'}
>
{!isMissingDevice ? 'I\'ve Lost My Device' : 'I Have My Device'}
</span>
</div>
<div className={'mt-6 text-center'}>
<Link
to={'/auth/login'}
@ -67,10 +85,9 @@ const LoginCheckpointContainer = () => {
};
const EnhancedForm = withFormik<Props, Values>({
handleSubmit: ({ code }, { setSubmitting, props: { addError, clearFlashes, location } }) => {
handleSubmit: ({ code, recoveryCode }, { setSubmitting, props: { addError, clearFlashes, location } }) => {
clearFlashes();
console.log(location.state.token, code);
loginCheckpoint(location.state?.token || '', code)
loginCheckpoint(location.state?.token || '', code, recoveryCode)
.then(response => {
if (response.complete) {
// @ts-ignore
@ -89,11 +106,7 @@ const EnhancedForm = withFormik<Props, Values>({
mapPropsToValues: () => ({
code: '',
}),
validationSchema: object().shape({
code: string().required('An authentication code must be provided.')
.length(6, 'Authentication code must be 6 digits in length.'),
recoveryCode: '',
}),
})(LoginCheckpointContainer);

View File

@ -52,6 +52,8 @@ export default ({ server, className }: { server: Server; className: string | und
alarms.memory = isAlarmState(stats.memoryUsageInBytes, server.limits.memory);
alarms.disk = server.limits.disk === 0 ? false : isAlarmState(stats.diskUsageInBytes, server.limits.disk);
}
const disklimit = server.limits.disk != 0 ? bytesToHuman(server.limits.disk * 1000 * 1000) : "Unlimited";
const memorylimit = server.limits.memory != 0 ? bytesToHuman(server.limits.memory * 1000 * 1000) : "Unlimited";
return (
<Link to={`/server/${server.id}`} className={`grey-row-box cursor-pointer ${className}`}>
@ -127,7 +129,7 @@ export default ({ server, className }: { server: Server; className: string | und
{bytesToHuman(stats.memoryUsageInBytes)}
</p>
</div>
<p className={'text-xs text-neutral-600 text-center mt-1'}>of {bytesToHuman(server.limits.memory * 1000 * 1000)}</p>
<p className={'text-xs text-neutral-600 text-center mt-1'}>of {memorylimit}</p>
</div>
<div className={'flex-1 ml-4'}>
<div className={'flex justify-center'}>
@ -147,9 +149,7 @@ export default ({ server, className }: { server: Server; className: string | und
{bytesToHuman(stats.diskUsageInBytes)}
</p>
</div>
<p className={'text-xs text-neutral-600 text-center mt-1'}>
of {bytesToHuman(server.limits.disk * 1000 * 1000)}
</p>
<p className={'text-xs text-neutral-600 text-center mt-1'}>of {disklimit}</p>
</div>
</React.Fragment>
}

View File

@ -2,21 +2,22 @@ import React, { useEffect, useState } from 'react';
import Modal, { RequiredModalProps } from '@/components/elements/Modal';
import { Form, Formik, FormikHelpers } from 'formik';
import { object, string } from 'yup';
import Field from '@/components/elements/Field';
import getTwoFactorTokenUrl from '@/api/account/getTwoFactorTokenUrl';
import enableAccountTwoFactor from '@/api/account/enableAccountTwoFactor';
import FlashMessageRender from '@/components/FlashMessageRender';
import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationStore } from '@/state';
import { httpErrorToHuman } from '@/api/http';
import FlashMessageRender from '@/components/FlashMessageRender';
import Field from '@/components/elements/Field';
interface Values {
code: string;
}
export default ({ ...props }: RequiredModalProps) => {
export default ({ onDismissed, ...props }: RequiredModalProps) => {
const [ token, setToken ] = useState('');
const [ loading, setLoading ] = useState(true);
const [ recoveryTokens, setRecoveryTokens ] = useState<string[]>([]);
const updateUserData = useStoreActions((actions: Actions<ApplicationStore>) => actions.user.updateUserData);
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
@ -27,22 +28,30 @@ export default ({ ...props }: RequiredModalProps) => {
.then(setToken)
.catch(error => {
console.error(error);
addError({ message: httpErrorToHuman(error), key: 'account:two-factor' });
});
}, []);
const submit = ({ code }: Values, { setSubmitting }: FormikHelpers<Values>) => {
clearFlashes('account:two-factor');
enableAccountTwoFactor(code)
.then(() => {
updateUserData({ useTotp: true });
props.onDismissed();
.then(tokens => {
setRecoveryTokens(tokens);
})
.catch(error => {
console.error(error);
addError({ message: httpErrorToHuman(error), key: 'account:two-factor' });
setSubmitting(false);
});
})
.then(() => setSubmitting(false));
};
const dismiss = () => {
if (recoveryTokens.length > 0) {
updateUserData({ useTotp: true });
}
onDismissed();
};
return (
@ -58,47 +67,73 @@ export default ({ ...props }: RequiredModalProps) => {
{({ isSubmitting, isValid }) => (
<Modal
{...props}
onDismissed={dismiss}
dismissable={!isSubmitting}
showSpinnerOverlay={loading || isSubmitting}
closeOnEscape={!recoveryTokens}
closeOnBackground={!recoveryTokens}
>
<Form className={'mb-0'}>
<FlashMessageRender className={'mb-6'} byKey={'account:two-factor'}/>
<div className={'flex flex-wrap'}>
<div className={'w-full md:flex-1'}>
<div className={'w-32 h-32 md:w-64 md:h-64 bg-neutral-600 p-2 rounded mx-auto'}>
{!token || !token.length ?
<img
src={'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='}
className={'w-64 h-64 rounded'}
{recoveryTokens.length > 0 ?
<>
<h2 className={'mb-4'}>Two-factor authentication enabled</h2>
<p className={'text-neutral-300'}>
Two-factor authentication has been enabled on your account. Should you loose access to
this device you'll need to use on of the codes displayed below in order to access your
account.
</p>
<p className={'text-neutral-300 mt-4'}>
<strong>These codes will not be displayed again.</strong> Please take note of them now
by storing them in a secure repository such as a password manager.
</p>
<pre className={'mt-4 rounded font-mono bg-neutral-900 p-4'}>
{recoveryTokens.map(token => <code key={token} className={'block mb-1'}>{token}</code>)}
</pre>
<div className={'text-right'}>
<button className={'mt-6 btn btn-lg btn-primary'} onClick={dismiss}>
Close
</button>
</div>
</>
:
<Form className={'mb-0'}>
<FlashMessageRender className={'mb-6'} byKey={'account:two-factor'}/>
<div className={'flex flex-wrap'}>
<div className={'w-full md:flex-1'}>
<div className={'w-32 h-32 md:w-64 md:h-64 bg-neutral-600 p-2 rounded mx-auto'}>
{!token || !token.length ?
<img
src={'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='}
className={'w-64 h-64 rounded'}
/>
:
<img
src={`https://api.qrserver.com/v1/create-qr-code/?size=500x500&data=${token}`}
onLoad={() => setLoading(false)}
className={'w-full h-full shadow-none rounded-0'}
/>
}
</div>
</div>
<div className={'w-full mt-6 md:mt-0 md:flex-1 md:flex md:flex-col'}>
<div className={'flex-1'}>
<Field
id={'code'}
name={'code'}
type={'text'}
title={'Code From Authenticator'}
description={'Enter the code from your authenticator device after scanning the QR image.'}
autoFocus={!loading}
/>
:
<img
src={`https://api.qrserver.com/v1/create-qr-code/?size=500x500&data=${token}`}
onLoad={() => setLoading(false)}
className={'w-full h-full shadow-none rounded-0'}
/>
}
</div>
<div className={'mt-6 md:mt-0 text-right'}>
<button className={'btn btn-primary btn-sm'} disabled={!isValid}>
Setup
</button>
</div>
</div>
</div>
<div className={'w-full mt-6 md:mt-0 md:flex-1 md:flex md:flex-col'}>
<div className={'flex-1'}>
<Field
id={'code'}
name={'code'}
type={'text'}
title={'Code From Authenticator'}
description={'Enter the code from your authenticator device after scanning the QR image.'}
autoFocus={!loading}
/>
</div>
<div className={'mt-6 md:mt-0 text-right'}>
<button className={'btn btn-primary btn-sm'} disabled={!isValid}>
Setup
</button>
</div>
</div>
</div>
</Form>
</Form>
}
</Modal>
)}
</Formik>

View File

@ -81,6 +81,9 @@ export default () => {
};
}, [ instance, connected ]);
const disklimit = server.limits.disk != 0 ? bytesToHuman(server.limits.disk * 1000 * 1000) : "Unlimited";
const memorylimit = server.limits.memory != 0 ? bytesToHuman(server.limits.memory * 1000 * 1000) : "Unlimited";
return (
<PageContentBlock className={'flex'}>
<div className={'w-1/4'}>
@ -112,8 +115,8 @@ export default () => {
className={'mr-1'}
/>
&nbsp;{bytesToHuman(memory)}
<span className={'text-neutral-500'}> / {bytesToHuman(server.limits.memory * 1000 * 1000)}</span>
</p>
<span className={'text-neutral-500'}> / {memorylimit}</span>
</p>
<p className={'text-xs mt-2'}>
<FontAwesomeIcon
icon={faHdd}
@ -121,7 +124,7 @@ export default () => {
className={'mr-1'}
/>
&nbsp;{bytesToHuman(disk)}
<span className={'text-neutral-500'}> / {bytesToHuman(server.limits.disk * 1000 * 1000)}</span>
<span className={'text-neutral-500'}> / {disklimit}</span>
</p>
</TitledGreyBox>
{!server.isInstalling ?

View File

@ -12,7 +12,7 @@ import { ServerContext } from '@/state/server';
import PageContentBlock from '@/components/elements/PageContentBlock';
export default () => {
const { uuid } = useServer();
const { uuid, featureLimits } = useServer();
const { addError, clearFlashes } = useFlash();
const [ loading, setLoading ] = useState(true);
@ -50,10 +50,22 @@ export default () => {
/>)}
</div>
}
{featureLimits.backups === 0 &&
<p className="text-center text-sm text-neutral-400">
Backups cannot be created for this server.
</p>
}
<Can action={'backup.create'}>
{(featureLimits.backups > 0 && backups.length > 0) &&
<p className="text-center text-xs text-neutral-400 mt-2">
{backups.length} of {featureLimits.backups} backups have been created for this server.
</p>
}
{featureLimits.backups > 0 && featureLimits.backups !== backups.length &&
<div className={'mt-6 flex justify-end'}>
<CreateBackupButton/>
</div>
}
</Can>
</PageContentBlock>
);

View File

@ -59,7 +59,12 @@ export default () => {
</p>
}
<Can action={'database.create'}>
{featureLimits.databases > 0 &&
{(featureLimits.databases > 0 && databases.length > 0) &&
<p className="text-center text-xs text-neutral-400 mt-2">
{databases.length} of {featureLimits.databases} databases have been allocated to this server.
</p>
}
{featureLimits.databases > 0 && featureLimits.databases !== databases.length &&
<div className={'mt-6 flex justify-end'}>
<CreateDatabaseButton/>
</div>

View File

@ -110,7 +110,7 @@ export default () => {
fetchContent={value => {
fetchFileContent = value;
}}
onContentSaved={() => null}
onContentSaved={() => save()}
/>
</div>
<div className={'flex justify-end mt-4'}>

View File

@ -25,10 +25,10 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
.filter(directory => !!directory)
.map((directory, index, dirs) => {
if (!withinFileEditor && index === dirs.length - 1) {
return { name: directory };
return { name: decodeURIComponent(directory) };
}
return { name: directory, path: `/${dirs.slice(0, index + 1).join('/')}` };
return { name: decodeURIComponent(directory), path: `/${dirs.slice(0, index + 1).join('/')}` };
});
return (
@ -57,7 +57,7 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
}
{file &&
<React.Fragment>
<span className={'px-1 text-neutral-300'}>{file}</span>
<span className={'px-1 text-neutral-300'}>{decodeURIComponent(file)}</span>
</React.Fragment>
}
</div>

View File

@ -70,7 +70,12 @@ export default () => {
/>
<p className={'text-xs mt-2 text-neutral-400'}>
<span className={'text-neutral-200'}>This directory will be created as</span>
&nbsp;/home/container/<span className={'text-cyan-200'}>{join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, '')}</span>
&nbsp;/home/container/
<span className={'text-cyan-200'}>
{decodeURIComponent(
join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, ''),
)}
</span>
</p>
<div className={'flex justify-end'}>
<button className={'btn btn-sm btn-primary mt-8'}>

View File

@ -14,12 +14,26 @@ import Can from '@/components/elements/Can';
import useServer from '@/plugins/useServer';
import useFlash from '@/plugins/useFlash';
import { ServerContext } from '@/state/server';
import { faFileArchive } from '@fortawesome/free-solid-svg-icons/faFileArchive';
interface Props {
schedule: Schedule;
task: Task;
}
const getActionDetails = (action: string): [ string, any ] => {
switch (action) {
case 'command':
return ['Send Command', faCode];
case 'power':
return ['Send Power Action', faToggleOn];
case 'backup':
return ['Create Backup', faFileArchive];
default:
return ['Unknown Action', faCode];
}
};
export default ({ schedule, task }: Props) => {
const { uuid } = useServer();
const { clearFlashes, addError } = useFlash();
@ -43,6 +57,8 @@ export default ({ schedule, task }: Props) => {
});
};
const [ title, icon ] = getActionDetails(task.action);
return (
<div className={'flex items-center bg-neutral-700 border border-neutral-600 mb-2 px-6 py-4 rounded'}>
<SpinnerOverlay visible={isLoading} fixed={true} size={'large'}/>
@ -56,14 +72,19 @@ export default ({ schedule, task }: Props) => {
onDismissed={() => setVisible(false)}
onConfirmed={() => onConfirmDeletion()}
/>
<FontAwesomeIcon icon={task.action === 'command' ? faCode : faToggleOn} className={'text-lg text-white'}/>
<FontAwesomeIcon icon={icon} className={'text-lg text-white'}/>
<div className={'flex-1'}>
<p className={'ml-6 text-neutral-300 mb-2 uppercase text-xs'}>
{task.action === 'command' ? 'Send command' : 'Send power action'}
<p className={'ml-6 text-neutral-300 uppercase text-xs'}>
{title}
</p>
<code className={'ml-6 font-mono bg-neutral-800 rounded py-1 px-2 text-sm'}>
{task.payload}
</code>
{task.payload &&
<div className={'ml-6 mt-2'}>
{task.action === 'backup' && <p className={'text-xs uppercase text-neutral-400 mb-1'}>Ignoring files & folders:</p>}
<div className={'font-mono bg-neutral-800 rounded py-1 px-2 text-sm w-auto whitespace-pre inline-block'}>
{task.payload}
</div>
</div>
}
</div>
{task.sequenceId > 1 &&
<div className={'mr-6'}>

View File

@ -71,7 +71,10 @@ const TaskDetailsForm = ({ isEditingTask }: { isEditingTask: boolean }) => {
:
<div>
<label className={'input-dark-label'}>Ignored Files</label>
<FormikFieldWrapper name={'payload'}>
<FormikFieldWrapper
name={'payload'}
description={'Optional. Include the files and folders to be excluded in this backup. By default, the contents of your .pteroignore file will be used.'}
>
<FormikField as={'textarea'} name={'payload'} className={'input-dark h-32'}/>
</FormikFieldWrapper>
</div>

View File

@ -48,7 +48,7 @@ const files: ServerFileStore = {
}),
setDirectory: action((state, payload) => {
state.directory = cleanDirectoryPath(payload)
state.directory = cleanDirectoryPath(payload);
}),
};

View File

@ -123,6 +123,7 @@ select.input:not(.appearance-none) {
select.input-dark:not(.appearance-none) {
@apply .bg-neutral-600 .border-neutral-500 .text-neutral-200;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='%23C3D1DF' d='M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z'/%3e%3c/svg%3e ");
background-color: hsl(220deg 21% 16%);
&:hover:not(:disabled), &:focus {
@apply .border-neutral-400;

View File

@ -27,3 +27,39 @@ code.clean {
@apply .mt-4;
}
}
::-webkit-scrollbar {
background: none;
width: 16px;
height: 16px;
}
::-webkit-scrollbar-thumb {
border: solid 0 rgb(0 0 0 / 0%);
border-right-width: 4px;
border-left-width: 4px;
-webkit-border-radius: 9px 4px;
-webkit-box-shadow: inset 0 0 0 1px hsl(211, 10%, 53%), inset 0 0 0 4px hsl(209deg 18% 30%);
}
::-webkit-scrollbar-track-piece {
margin: 4px 0;
}
::-webkit-scrollbar-thumb:horizontal {
border-right-width: 0;
border-left-width: 0;
border-top-width: 4px;
border-bottom-width: 4px;
-webkit-border-radius: 4px 9px;
}
::-webkit-scrollbar-thumb:hover {
-webkit-box-shadow:
inset 0 0 0 1px hsl(212, 92%, 43%),
inset 0 0 0 4px hsl(212, 92%, 43%);
}
::-webkit-scrollbar-corner {
background: transparent;
}

View File

@ -74,7 +74,7 @@
swal({
type: 'success',
title: 'Token created.',
text: '<p>To auto-configure your node run the following command:<br /><small><pre>cd /etc/pterodactyl && ./wings configure --panel-url {{ config('app.url') }} --token ' + data.token + ' --node ' + data.node + '{{ config('app.debug') ? ' --allow-insecure' : '' }}</pre></small></p>',
text: '<p>To auto-configure your node run the following command:<br /><small><pre>cd /etc/pterodactyl && sudo ./wings configure --panel-url {{ config('app.url') }} --token ' + data.token + ' --node ' + data.node + '{{ config('app.debug') ? ' --allow-insecure' : '' }}</pre></small></p>',
html: true
})
}).fail(function () {

View File

@ -176,6 +176,8 @@
<input type="text" id="pMemory" name="memory" class="form-control" value="{{ old('memory') }}" />
<span class="input-group-addon">MB</span>
</div>
<p class="text-muted small">The maximum amount of memory allowed for this container. Setting this to <code>0</code> will allow unlimited memory in a container.</p>
</div>
<div class="form-group col-xs-6">
@ -185,21 +187,18 @@
<input type="text" id="pSwap" name="swap" class="form-control" value="{{ old('swap', 0) }}" />
<span class="input-group-addon">MB</span>
</div>
<p class="text-muted small">Setting this to <code>0</code> will disable swap space on this server. Setting to <code>-1</code> will allow unlimited swap.</p>
</div>
</div>
<div class="box-footer no-border no-pad-top no-pad-bottom">
<p class="text-muted small">If you do not want to assign swap space to a server, simply put <code>0</code> for the value, or <code>-1</code> to allow unlimited swap space. If you want to disable memory limiting on a server, simply enter <code>0</code> into the memory field.<p>
</div>
<div class="box-body row">
<div class="form-group col-xs-6">
<label for="pDisk">Disk Space</label>
<div class="input-group">
<input type="text" id="pDisk" name="disk" class="form-control" value="{{ old('disk') }}" />
<span class="input-group-addon">MB</span>
</div>
<p class="text-muted small">This server will not be allowed to boot if it is using more than this amount of space. If a server goes over this limit while running it will be safely stopped and locked until enough space is available. Set to <code>0</code> to allow unlimited disk usage.</p>
</div>
<div class="form-group col-xs-6">

Some files were not shown because too many files have changed in this diff Show More