Merge branch 'develop' into feature/file-uploads

This commit is contained in:
Dane Everitt 2020-08-22 18:33:09 -07:00
commit 54f9c5f187
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
136 changed files with 2178 additions and 971 deletions

View file

@ -0,0 +1,51 @@
<?php
namespace Pterodactyl\Console\Commands\Maintenance;
use Carbon\CarbonImmutable;
use InvalidArgumentException;
use Illuminate\Console\Command;
use Pterodactyl\Repositories\Eloquent\BackupRepository;
class PruneOrphanedBackupsCommand extends Command
{
/**
* @var string
*/
protected $signature = 'p:maintenance:prune-backups {--since-minutes=30}';
/**
* @var string
*/
protected $description = 'Marks all backups that have not completed in the last "n" minutes as being failed.';
/**
* @param \Pterodactyl\Repositories\Eloquent\BackupRepository $repository
*/
public function handle(BackupRepository $repository)
{
$since = $this->option('since-minutes');
if (! is_digit($since)) {
throw new InvalidArgumentException('The --since-minutes option must be a valid numeric digit.');
}
$query = $repository->getBuilder()
->whereNull('completed_at')
->whereDate('created_at', '<=', CarbonImmutable::now()->subMinutes($since));
$count = $query->count();
if (! $count) {
$this->info('There are no orphaned backups to be marked as failed.');
return;
}
$this->warn("Marking {$count} backups that have not been marked as completed in the last {$since} minutes as failed.");
$query->update([
'is_successful' => false,
'completed_at' => CarbonImmutable::now(),
'updated_at' => CarbonImmutable::now(),
]);
}
}

View file

@ -22,7 +22,16 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
// Execute scheduled commands for servers every minute, as if there was a normal cron running.
$schedule->command('p:schedule:process')->everyMinute()->withoutOverlapping();
// Every 30 minutes, run the backup pruning command so that any abandoned backups can be removed
// from the UI view for the server.
$schedule->command('p:maintenance:prune-backups', [
'--since-minutes' => '30',
])->everyThirtyMinutes();
// Every day cleanup any internal backups of service files.
$schedule->command('p:maintenance:clean-service-backups')->daily();
}
}

View file

@ -213,6 +213,13 @@ class Handler extends ExceptionHandler
'detail' => 'An error was encountered while processing this request.',
];
if ($exception instanceof ModelNotFoundException || $exception->getPrevious() instanceof ModelNotFoundException) {
// Show a nicer error message compared to the standard "No query results for model"
// response that is normally returned. If we are in debug mode this will get overwritten
// with a more specific error message to help narrow down things.
$error['detail'] = 'The requested resource could not be found on the server.';
}
if (config('app.debug')) {
$error = array_merge($error, [
'detail' => $exception->getMessage(),

View file

@ -2,6 +2,7 @@
namespace Pterodactyl\Exceptions\Http\Connection;
use Illuminate\Support\Arr;
use Illuminate\Http\Response;
use GuzzleHttp\Exception\GuzzleException;
use Pterodactyl\Exceptions\DisplayException;
@ -22,18 +23,34 @@ class DaemonConnectionException extends DisplayException
* @param \GuzzleHttp\Exception\GuzzleException $previous
* @param bool $useStatusCode
*/
public function __construct(GuzzleException $previous, bool $useStatusCode = false)
public function __construct(GuzzleException $previous, bool $useStatusCode = true)
{
/** @var \GuzzleHttp\Psr7\Response|null $response */
$response = method_exists($previous, 'getResponse') ? $previous->getResponse() : null;
if ($useStatusCode) {
$this->statusCode = is_null($response) ? 500 : $response->getStatusCode();
$this->statusCode = is_null($response) ? $this->statusCode : $response->getStatusCode();
}
parent::__construct(trans('admin/server.exceptions.daemon_exception', [
$message = trans('admin/server.exceptions.daemon_exception', [
'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(),
]), $previous, DisplayException::LEVEL_WARNING);
]);
// Attempt to pull the actual error message off the response and return that if it is not
// a 500 level error.
if ($this->statusCode < 500 && ! is_null($response)) {
$body = $response->getBody();
if (is_string($body) || (is_object($body) && method_exists($body, '__toString'))) {
$body = json_decode(is_string($body) ? $body : $body->__toString(), true);
$message = "[Wings Error]: " . Arr::get($body, 'error', $message);
}
}
$level = $this->statusCode >= 500 && $this->statusCode !== 504
? DisplayException::LEVEL_ERROR
: DisplayException::LEVEL_WARNING;
parent::__construct($message, $previous, $level);
}
/**

View file

@ -2,7 +2,6 @@
namespace Pterodactyl\Http\Controllers\Api\Client;
use Pterodactyl\Models\User;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Permission;
use Spatie\QueryBuilder\QueryBuilder;
@ -39,31 +38,27 @@ class ClientController extends ClientApiController
public function index(GetServersRequest $request): array
{
$user = $request->user();
$level = $request->getFilterLevel();
$transformer = $this->getTransformer(ServerTransformer::class);
// Start the query builder and ensure we eager load any requested relationships from the request.
$builder = Server::query()->with($this->getIncludesForTransformer($transformer, ['node']));
$builder = QueryBuilder::for(
Server::query()->with($this->getIncludesForTransformer($transformer, ['node']))
)->allowedFilters('uuid', 'name', 'external_id');
if ($level === User::FILTER_LEVEL_OWNER) {
$builder = $builder->where('owner_id', $request->user()->id);
}
// If set to all, display all servers they can access, including those they access as an
// admin. If set to subuser, only return the servers they can access because they are owner,
// or marked as a subuser of the server.
elseif (($level === User::FILTER_LEVEL_ALL && ! $user->root_admin) || $level === User::FILTER_LEVEL_SUBUSER) {
// Either return all of the servers the user has access to because they are an admin `?type=admin` or
// just return all of the servers the user has access to because they are the owner or a subuser of the
// server.
if ($request->input('type') === 'admin') {
$builder = $user->root_admin
? $builder->whereNotIn('id', $user->accessibleServers()->pluck('id')->all())
// If they aren't an admin but want all the admin servers don't fail the request, just
// make it a query that will never return any results back.
: $builder->whereRaw('1 = 2');
} elseif ($request->input('type') === 'owner') {
$builder = $builder->where('owner_id', $user->id);
} else {
$builder = $builder->whereIn('id', $user->accessibleServers()->pluck('id')->all());
}
// If set to admin, only display the servers a user can access because they are an administrator.
// This means only servers the user would not have access to if they were not an admin (because they
// are not an owner or subuser) are returned.
elseif ($level === User::FILTER_LEVEL_ADMIN && $user->root_admin) {
$builder = $builder->whereNotIn('id', $user->accessibleServers()->pluck('id')->all());
}
$builder = QueryBuilder::for($builder)->allowedFilters(
'uuid', 'name', 'external_id'
);
$servers = $builder->paginate(min($request->query('per_page', 50), 100))->appends($request->query());

View file

@ -5,7 +5,6 @@ namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
use Illuminate\Http\Response;
use Pterodactyl\Models\Server;
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Exception\BadResponseException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Pterodactyl\Repositories\Wings\DaemonCommandRepository;
@ -45,11 +44,13 @@ class CommandController extends ClientApiController
{
try {
$this->repository->setServer($server)->send($request->input('command'));
} catch (RequestException $exception) {
if ($exception instanceof BadResponseException) {
} catch (DaemonConnectionException $exception) {
$previous = $exception->getPrevious();
if ($previous instanceof BadResponseException) {
if (
$exception->getResponse() instanceof ResponseInterface
&& $exception->getResponse()->getStatusCode() === Response::HTTP_BAD_GATEWAY
$previous->getResponse() instanceof ResponseInterface
&& $previous->getResponse()->getStatusCode() === Response::HTTP_BAD_GATEWAY
) {
throw new HttpException(
Response::HTTP_BAD_GATEWAY, 'Server must be online in order to send commands.', $exception
@ -57,7 +58,7 @@ class CommandController extends ClientApiController
}
}
throw new DaemonConnectionException($exception);
throw $exception;
}
return $this->returnNoContent();

View file

@ -6,19 +6,18 @@ use Carbon\CarbonImmutable;
use Illuminate\Http\Response;
use Pterodactyl\Models\Server;
use Illuminate\Http\JsonResponse;
use GuzzleHttp\Exception\TransferException;
use Pterodactyl\Services\Nodes\NodeJWTService;
use Illuminate\Contracts\Routing\ResponseFactory;
use Pterodactyl\Repositories\Wings\DaemonFileRepository;
use Pterodactyl\Transformers\Daemon\FileObjectTransformer;
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\CopyFileRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\ListFilesRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\DeleteFileRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\RenameFileRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\CreateFolderRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\CompressFilesRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\DecompressFilesRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\GetFileContentsRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\WriteFileContentRequest;
@ -69,13 +68,9 @@ class FileController extends ClientApiController
*/
public function directory(ListFilesRequest $request, Server $server): array
{
try {
$contents = $this->fileRepository
->setServer($server)
->getDirectory($request->get('directory') ?? '/');
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception, true);
}
$contents = $this->fileRepository
->setServer($server)
->getDirectory($request->get('directory') ?? '/');
return $this->fractal->collection($contents)
->transformWith($this->getTransformer(FileObjectTransformer::class))
@ -88,7 +83,9 @@ class FileController extends ClientApiController
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\GetFileContentsRequest $request
* @param \Pterodactyl\Models\Server $server
* @return \Illuminate\Http\Response
*
* @throws \Pterodactyl\Exceptions\Http\Server\FileSizeTooLargeException
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function contents(GetFileContentsRequest $request, Server $server): Response
{
@ -139,6 +136,8 @@ class FileController extends ClientApiController
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\WriteFileContentRequest $request
* @param \Pterodactyl\Models\Server $server
* @return \Illuminate\Http\JsonResponse
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function write(WriteFileContentRequest $request, Server $server): JsonResponse
{
@ -156,6 +155,8 @@ class FileController extends ClientApiController
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\CreateFolderRequest $request
* @param \Pterodactyl\Models\Server $server
* @return \Illuminate\Http\JsonResponse
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function create(CreateFolderRequest $request, Server $server): JsonResponse
{
@ -172,6 +173,8 @@ class FileController extends ClientApiController
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\RenameFileRequest $request
* @param \Pterodactyl\Models\Server $server
* @return \Illuminate\Http\JsonResponse
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function rename(RenameFileRequest $request, Server $server): JsonResponse
{
@ -188,6 +191,8 @@ class FileController extends ClientApiController
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\CopyFileRequest $request
* @param \Pterodactyl\Models\Server $server
* @return \Illuminate\Http\JsonResponse
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function copy(CopyFileRequest $request, Server $server): JsonResponse
{
@ -202,9 +207,14 @@ class FileController extends ClientApiController
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\CompressFilesRequest $request
* @param \Pterodactyl\Models\Server $server
* @return array
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function compress(CompressFilesRequest $request, Server $server): array
{
// Allow up to five minutes for this request to process before timing out.
set_time_limit(300);
$file = $this->fileRepository->setServer($server)
->compressFiles(
$request->input('root'), $request->input('files')
@ -215,12 +225,32 @@ class FileController extends ClientApiController
->toArray();
}
/**
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\DecompressFilesRequest $request
* @param \Pterodactyl\Models\Server $server
* @return \Illuminate\Http\JsonResponse
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function decompress(DecompressFilesRequest $request, Server $server): JsonResponse
{
// Allow up to five minutes for this request to process before timing out.
set_time_limit(300);
$this->fileRepository->setServer($server)
->decompressFile($request->input('root'), $request->input('file'));
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
}
/**
* Deletes files or folders for the server in the given root directory.
*
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\DeleteFileRequest $request
* @param \Pterodactyl\Models\Server $server
* @return \Illuminate\Http\JsonResponse
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function delete(DeleteFileRequest $request, Server $server): JsonResponse
{

View file

@ -33,6 +33,8 @@ class PowerController extends ClientApiController
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\SendPowerRequest $request
* @param \Pterodactyl\Models\Server $server
* @return \Illuminate\Http\Response
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function index(SendPowerRequest $request, Server $server): Response
{

View file

@ -0,0 +1,81 @@
<?php
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
use Carbon\CarbonImmutable;
use Pterodactyl\Models\Server;
use Illuminate\Http\JsonResponse;
use Pterodactyl\Services\Servers\VariableValidatorService;
use Pterodactyl\Repositories\Eloquent\ServerVariableRepository;
use Pterodactyl\Transformers\Api\Client\EggVariableTransformer;
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Pterodactyl\Http\Requests\Api\Client\Servers\Startup\UpdateStartupVariableRequest;
class StartupController extends ClientApiController
{
/**
* @var \Pterodactyl\Services\Servers\VariableValidatorService
*/
private $service;
/**
* @var \Pterodactyl\Repositories\Eloquent\ServerVariableRepository
*/
private $repository;
/**
* StartupController constructor.
*
* @param \Pterodactyl\Services\Servers\VariableValidatorService $service
* @param \Pterodactyl\Repositories\Eloquent\ServerVariableRepository $repository
*/
public function __construct(VariableValidatorService $service, ServerVariableRepository $repository)
{
parent::__construct();
$this->service = $service;
$this->repository = $repository;
}
/**
* Updates a single variable for a server.
*
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Startup\UpdateStartupVariableRequest $request
* @param \Pterodactyl\Models\Server $server
* @return array
*
* @throws \Illuminate\Validation\ValidationException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function update(UpdateStartupVariableRequest $request, Server $server)
{
/** @var \Pterodactyl\Models\EggVariable $variable */
$variable = $server->variables()->where('env_variable', $request->input('key'))->first();
if (is_null($variable) || !$variable->user_viewable || !$variable->user_editable) {
throw new BadRequestHttpException(
"The environment variable you are trying to edit [\"{$request->input('key')}\"] does not exist."
);
}
// Revalidate the variable value using the egg variable specific validation rules for it.
$this->validate($request, ['value' => $variable->rules]);
$this->repository->updateOrCreate([
'server_id' => $server->id,
'variable_id' => $variable->id,
], [
'variable_value' => $request->input('value'),
]);
$variable = $variable->refresh();
$variable->server_value = $request->input('value');
return $this->fractal->item($variable)
->transformWith($this->getTransformer(EggVariableTransformer::class))
->toArray();
}
}

View file

@ -3,7 +3,9 @@
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
use Illuminate\Http\Request;
use Pterodactyl\Models\User;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Subuser;
use Illuminate\Http\JsonResponse;
use Pterodactyl\Models\Permission;
use Pterodactyl\Repositories\Eloquent\SubuserRepository;
@ -57,6 +59,21 @@ class SubuserController extends ClientApiController
->toArray();
}
/**
* Returns a single subuser associated with this server instance.
*
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Subusers\GetSubuserRequest $request
* @return array
*/
public function view(GetSubuserRequest $request)
{
$subuser = $request->attributes->get('subuser');
return $this->fractal->item($subuser)
->transformWith($this->getTransformer(SubuserTransformer::class))
->toArray();
}
/**
* Create a new subuser for the given server.
*
@ -84,15 +101,16 @@ class SubuserController extends ClientApiController
* Update a given subuser in the system for the server.
*
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Subusers\UpdateSubuserRequest $request
* @param \Pterodactyl\Models\Server $server
* @return array
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function update(UpdateSubuserRequest $request, Server $server): array
public function update(UpdateSubuserRequest $request): array
{
$subuser = $request->endpointSubuser();
/** @var \Pterodactyl\Models\Subuser $subuser */
$subuser = $request->attributes->get('subuser');
$this->repository->update($subuser->id, [
'permissions' => $this->getDefaultPermissions($request),
]);
@ -106,14 +124,16 @@ class SubuserController extends ClientApiController
* Removes a subusers from a server's assignment.
*
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Subusers\DeleteSubuserRequest $request
* @param \Pterodactyl\Models\Server $server
* @return \Illuminate\Http\JsonResponse
*/
public function delete(DeleteSubuserRequest $request, Server $server)
public function delete(DeleteSubuserRequest $request)
{
$this->repository->delete($request->endpointSubuser()->id);
/** @var \Pterodactyl\Models\Subuser $subuser */
$subuser = $request->attributes->get('subuser');
return JsonResponse::create([], JsonResponse::HTTP_NO_CONTENT);
$this->repository->delete($subuser->id);
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
}
/**

View file

@ -3,6 +3,7 @@
namespace Pterodactyl\Http\Controllers\Api\Remote\Backups;
use Carbon\Carbon;
use Carbon\CarbonImmutable;
use Illuminate\Http\JsonResponse;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Repositories\Eloquent\BackupRepository;
@ -31,25 +32,16 @@ class BackupStatusController extends Controller
* @param \Pterodactyl\Http\Requests\Api\Remote\ReportBackupCompleteRequest $request
* @param string $backup
* @return \Illuminate\Http\JsonResponse
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function __invoke(ReportBackupCompleteRequest $request, string $backup)
{
/** @var \Pterodactyl\Models\Backup $backup */
$backup = $this->repository->findFirstWhere([['uuid', '=', $backup]]);
$this->repository->updateWhere([['uuid', '=', $backup]], [
'is_successful' => $request->input('successful') ? true : false,
'sha256_hash' => $request->input('checksum'),
'bytes' => $request->input('size'),
'completed_at' => CarbonImmutable::now(),
]);
if ($request->input('successful')) {
$this->repository->update($backup->id, [
'sha256_hash' => $request->input('checksum'),
'bytes' => $request->input('size'),
'completed_at' => Carbon::now(),
], true, true);
} else {
$this->repository->delete($backup->id);
}
return JsonResponse::create([], JsonResponse::HTTP_NO_CONTENT);
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace Pterodactyl\Http\Middleware\Api\Client\Server;
use Closure;
use Illuminate\Http\Request;
class SubuserBelongsToServer
{
/**
* Ensure that the user being accessed in the request is a user that is currently assigned
* as a subuser for this server instance. We'll let the requests themselves handle wether or
* not the user making the request can actually modify or delete the subuser record.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
/** @var \Pterodactyl\Models\Server $server */
$server = $request->route()->parameter('server');
/** @var \Pterodactyl\Models\User $user */
$user = $request->route()->parameter('user');
// Don't do anything if there isn't a user present in the request.
if (is_null($user)) {
return $next($request);
}
$request->attributes->set('subuser', $server->subusers()->where('user_id', $user->id)->firstOrFail());
return $next($request);
}
}

View file

@ -3,6 +3,7 @@
namespace Pterodactyl\Http\Middleware\Api\Client;
use Closure;
use Pterodactyl\Models\User;
use Pterodactyl\Models\Backup;
use Pterodactyl\Models\Database;
use Illuminate\Container\Container;
@ -52,6 +53,10 @@ class SubstituteClientApiBindings extends ApiSubstituteBindings
return Backup::query()->where('uuid', $value)->firstOrFail();
});
$this->router->model('user', User::class, function ($value) {
return User::query()->where('uuid', $value)->firstOrFail();
});
return parent::handle($request, $next);
}
}

View file

@ -29,10 +29,6 @@ class DatabaseHostFormRequest extends AdminFormRequest
$this->merge(['node_id' => null]);
}
$this->merge([
'host' => gethostbyname($this->input('host')),
]);
return parent::getValidatorInstance();
}
}

View file

@ -19,6 +19,7 @@ class BaseSettingsFormRequest extends AdminFormRequest
'app:name' => 'required|string|max:255',
'pterodactyl:auth:2fa_required' => 'required|integer|in:0,1,2',
'app:locale' => ['required', 'string', Rule::in(array_keys($this->getAvailableLanguages()))],
'app:analytics' => 'nullable|string',
];
}
@ -31,6 +32,7 @@ class BaseSettingsFormRequest extends AdminFormRequest
'app:name' => 'Company Name',
'pterodactyl:auth:2fa_required' => 'Require 2-Factor Authentication',
'app:locale' => 'Default Language',
'app:analytics' => 'Google Analytics',
];
}
}

View file

@ -17,4 +17,14 @@ class StoreApiKeyRequest extends ClientApiRequest
'allowed_ips.*' => 'ip',
];
}
/**
* @return array|string[]
*/
public function messages()
{
return [
'allowed_ips.*' => 'All of the IP addresses entered must be valid IPv4 addresses.',
];
}
}

View file

@ -2,8 +2,6 @@
namespace Pterodactyl\Http\Requests\Api\Client;
use Pterodactyl\Models\User;
class GetServersRequest extends ClientApiRequest
{
/**
@ -13,28 +11,4 @@ class GetServersRequest extends ClientApiRequest
{
return true;
}
/**
* Return the filtering method for servers when the client base endpoint is requested.
*
* @return int
*/
public function getFilterLevel(): int
{
switch ($this->input('type')) {
case 'all':
return User::FILTER_LEVEL_ALL;
break;
case 'admin':
return User::FILTER_LEVEL_ADMIN;
break;
case 'owner':
return User::FILTER_LEVEL_OWNER;
break;
case 'subuser-of':
default:
return User::FILTER_LEVEL_SUBUSER;
break;
}
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Files;
use Pterodactyl\Models\Permission;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
class DecompressFilesRequest extends ClientApiRequest
{
/**
* Checks that the authenticated user is allowed to create new files for the server. We don't
* rely on the archive permission here as it makes more sense to make sure the user can create
* additional files rather than make an archive.
*
* @return string
*/
public function permission(): string
{
return Permission::ACTION_FILE_CREATE;
}
/**
* @return array
*/
public function rules(): array
{
return [
'root' => 'sometimes|nullable|string',
'file' => 'required|string',
];
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Startup;
use Pterodactyl\Models\Permission;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
class UpdateStartupVariableRequest extends ClientApiRequest
{
/**
* @return string
*/
public function permission()
{
return Permission::ACTION_STARTUP_UPDATE;
}
/**
* The actual validation of the variable's value will happen inside the controller.
*
* @return array|string[]
*/
public function rules(): array
{
return [
'key' => 'required|string',
'value' => 'present|string',
];
}
}

View file

@ -3,12 +3,10 @@
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Subusers;
use Illuminate\Http\Request;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\User;
use Pterodactyl\Exceptions\Http\HttpForbiddenException;
use Pterodactyl\Repositories\Eloquent\SubuserRepository;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Pterodactyl\Services\Servers\GetUserPermissionsService;
abstract class SubuserRequest extends ClientApiRequest
{
@ -30,10 +28,10 @@ abstract class SubuserRequest extends ClientApiRequest
return false;
}
// If there is a subuser present in the URL, validate that it is not the same as the
// current request user. You're not allowed to modify yourself.
if ($this->route()->hasParameter('subuser')) {
if ($this->endpointSubuser()->user_id === $this->user()->id) {
$user = $this->route()->parameter('user');
// Don't allow a user to edit themselves on the server.
if ($user instanceof User) {
if ($user->uuid === $this->user()->uuid) {
return false;
}
}
@ -71,68 +69,14 @@ abstract class SubuserRequest extends ClientApiRequest
// Otherwise, get the current subuser's permission set, and ensure that the
// permissions they are trying to assign are not _more_ than the ones they
// already have.
if (count(array_diff($permissions, $this->currentUserPermissions())) > 0) {
/** @var \Pterodactyl\Models\Subuser|null $subuser */
/** @var \Pterodactyl\Services\Servers\GetUserPermissionsService $service */
$service = $this->container->make(GetUserPermissionsService::class);
if (count(array_diff($permissions, $service->handle($server, $user))) > 0) {
throw new HttpForbiddenException(
'Cannot assign permissions to a subuser that your account does not actively possess.'
);
}
}
/**
* Returns the currently authenticated user's permissions.
*
* @return array
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function currentUserPermissions(): array
{
/** @var \Pterodactyl\Repositories\Eloquent\SubuserRepository $repository */
$repository = $this->container->make(SubuserRepository::class);
/* @var \Pterodactyl\Models\Subuser $model */
try {
$model = $repository->findFirstWhere([
['server_id', $this->route()->parameter('server')->id],
['user_id', $this->user()->id],
]);
} catch (RecordNotFoundException $exception) {
return [];
}
return $model->permissions;
}
/**
* Return the subuser model for the given request which can then be validated. If
* required request parameters are missing a 404 error will be returned, otherwise
* a model exception will be returned if the model is not found.
*
* This returns the subuser based on the endpoint being hit, not the actual subuser
* for the account making the request.
*
* @return \Pterodactyl\Models\Subuser
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function endpointSubuser()
{
/** @var \Pterodactyl\Repositories\Eloquent\SubuserRepository $repository */
$repository = $this->container->make(SubuserRepository::class);
$parameters = $this->route()->parameters();
if (
! isset($parameters['server'], $parameters['server'])
|| ! is_string($parameters['subuser'])
|| ! $parameters['server'] instanceof Server
) {
throw new NotFoundHttpException;
}
return $this->model ?: $this->model = $repository->getUserForServer(
$parameters['server']->id, $parameters['subuser']
);
}
}

View file

@ -37,6 +37,7 @@ class AssetComposer
'enabled' => config('recaptcha.enabled', false),
'siteKey' => config('recaptcha.website_key') ?? '',
],
'analytics' => config('app.analytics') ?? '',
]);
}
}

View file

@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property int $id
* @property int $server_id
* @property int $uuid
* @property bool $is_successful
* @property string $name
* @property string[] $ignored_files
* @property string $disk
@ -44,6 +45,7 @@ class Backup extends Model
*/
protected $casts = [
'id' => 'int',
'is_successful' => 'bool',
'bytes' => 'int',
'ignored_files' => 'array',
];
@ -59,6 +61,7 @@ class Backup extends Model
* @var array
*/
protected $attributes = [
'is_successful' => true,
'sha256_hash' => null,
'bytes' => 0,
];
@ -69,6 +72,7 @@ class Backup extends Model
public static $validationRules = [
'server_id' => 'bail|required|numeric|exists:servers,id',
'uuid' => 'required|uuid',
'is_successful' => 'boolean',
'name' => 'required|string',
'ignored_files' => 'array',
'disk' => 'required|string',

View file

@ -2,6 +2,8 @@
namespace Pterodactyl\Models;
use Pterodactyl\Rules\ResolvesToIPAddress;
class DatabaseHost extends Model
{
/**
@ -51,13 +53,25 @@ class DatabaseHost extends Model
*/
public static $validationRules = [
'name' => 'required|string|max:255',
'host' => 'required|unique:database_hosts,host',
'host' => 'required|string',
'port' => 'required|numeric|between:1,65535',
'username' => 'required|string|max:32',
'password' => 'nullable|string',
'node_id' => 'sometimes|nullable|integer|exists:nodes,id',
];
/**
* @return array
*/
public static function getRules()
{
$rules = parent::getRules();
$rules['host'] = array_merge($rules['host'], [ new ResolvesToIPAddress() ]);
return $rules;
}
/**
* Gets the node associated with a database host.
*

View file

@ -2,6 +2,27 @@
namespace Pterodactyl\Models;
/**
* @property int $id
* @property int $egg_id
* @property string $name
* @property string $description
* @property string $env_variable
* @property string $default_value
* @property bool $user_viewable
* @property bool $user_editable
* @property string $rules
* @property \Carbon\CarbonImmutable $created_at
* @property \Carbon\CarbonImmutable $updated_at
*
* @property bool $required
* @property \Pterodactyl\Models\Egg $egg
* @property \Pterodactyl\Models\ServerVariable $serverVariable
*
* The "server_value" variable is only present on the object if you've loaded this model
* using the server relationship.
* @property string|null $server_value
*/
class EggVariable extends Model
{
/**
@ -17,6 +38,11 @@ class EggVariable extends Model
*/
const RESERVED_ENV_NAMES = 'SERVER_MEMORY,SERVER_IP,SERVER_PORT,ENV,HOME,USER,STARTUP,SERVER_UUID,UUID';
/**
* @var bool
*/
protected $immutableDates = true;
/**
* The table associated with the model.
*
@ -38,8 +64,8 @@ class EggVariable extends Model
*/
protected $casts = [
'egg_id' => 'integer',
'user_viewable' => 'integer',
'user_editable' => 'integer',
'user_viewable' => 'bool',
'user_editable' => 'bool',
];
/**
@ -65,12 +91,19 @@ class EggVariable extends Model
];
/**
* @param $value
* @return bool
*/
public function getRequiredAttribute($value)
public function getRequiredAttribute()
{
return $this->rules === 'required' || str_contains($this->rules, ['required|', '|required']);
return in_array('required', explode('|', $this->rules));
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function egg()
{
return $this->hasOne(Egg::class);
}
/**

View file

@ -55,6 +55,9 @@ class Permission extends Model
const ACTION_FILE_ARCHIVE = 'file.archive';
const ACTION_FILE_SFTP = 'file.sftp';
const ACTION_STARTUP_READ = 'startup.read';
const ACTION_STARTUP_UPDATE = 'startup.update';
const ACTION_SETTINGS_RENAME = 'settings.rename';
const ACTION_SETTINGS_REINSTALL = 'settings.reinstall';
@ -169,8 +172,8 @@ class Permission extends Model
'startup' => [
'description' => 'Permissions that control a user\'s ability to view this server\'s startup parameters.',
'keys' => [
'read' => '',
'update' => '',
'read' => 'Allows a user to view the startup variables for a server.',
'update' => 'Allows a user to modify the startup variables for the server.',
],
],

View file

@ -38,14 +38,14 @@ use Znck\Eloquent\Traits\BelongsToThrough;
* @property \Carbon\Carbon $updated_at
*
* @property \Pterodactyl\Models\User $user
* @property \Pterodactyl\Models\User[]|\Illuminate\Database\Eloquent\Collection $subusers
* @property \Pterodactyl\Models\Subuser[]|\Illuminate\Database\Eloquent\Collection $subusers
* @property \Pterodactyl\Models\Allocation $allocation
* @property \Pterodactyl\Models\Allocation[]|\Illuminate\Database\Eloquent\Collection $allocations
* @property \Pterodactyl\Models\Pack|null $pack
* @property \Pterodactyl\Models\Node $node
* @property \Pterodactyl\Models\Nest $nest
* @property \Pterodactyl\Models\Egg $egg
* @property \Pterodactyl\Models\ServerVariable[]|\Illuminate\Database\Eloquent\Collection $variables
* @property \Pterodactyl\Models\EggVariable[]|\Illuminate\Database\Eloquent\Collection $variables
* @property \Pterodactyl\Models\Schedule[]|\Illuminate\Database\Eloquent\Collection $schedule
* @property \Pterodactyl\Models\Database[]|\Illuminate\Database\Eloquent\Collection $databases
* @property \Pterodactyl\Models\Location $location
@ -270,7 +270,9 @@ class Server extends Model
*/
public function variables()
{
return $this->hasMany(ServerVariable::class);
return $this->hasMany(EggVariable::class, 'egg_id', 'egg_id')
->select(['egg_variables.*', 'server_variables.variable_value as server_value'])
->leftJoin('server_variables', 'server_variables.variable_id', '=', 'egg_variables.id');
}
/**

View file

@ -57,11 +57,6 @@ class User extends Model implements
const USER_LEVEL_USER = 0;
const USER_LEVEL_ADMIN = 1;
const FILTER_LEVEL_ALL = 0;
const FILTER_LEVEL_OWNER = 1;
const FILTER_LEVEL_ADMIN = 2;
const FILTER_LEVEL_SUBUSER = 3;
/**
* The resource name for this model when it is transformed into an
* API representation using fractal.

View file

@ -21,6 +21,7 @@ class SettingsServiceProvider extends ServiceProvider
protected $keys = [
'app:name',
'app:locale',
'app:analytics',
'recaptcha:enabled',
'recaptcha:secret_key',
'recaptcha:website_key',

View file

@ -27,6 +27,7 @@ class BackupRepository extends EloquentRepository
return $this->getBuilder()
->withTrashed()
->where('server_id', $server)
->where('is_successful', true)
->where('created_at', '>=', Carbon::now()->subMinutes($minutes)->toDateTimeString())
->get()
->toBase();

View file

@ -143,6 +143,10 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
*/
public function getVariablesWithValues(int $id, bool $returnAsObject = false)
{
$this->getBuilder()
->with('variables', 'egg.variables')
->findOrFail($id);
try {
$instance = $this->getBuilder()->with('variables', 'egg.variables')->find($id, $this->getColumns());
} catch (ModelNotFoundException $exception) {

View file

@ -18,30 +18,6 @@ class SubuserRepository extends EloquentRepository implements SubuserRepositoryI
return Subuser::class;
}
/**
* Returns a subuser model for the given user and server combination. If no record
* exists an exception will be thrown.
*
* @param int $server
* @param string $uuid
* @return \Pterodactyl\Models\Subuser
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*/
public function getUserForServer(int $server, string $uuid): Subuser
{
/** @var \Pterodactyl\Models\Subuser $model */
$model = $this->getBuilder()
->with('server', 'user')
->select('subusers.*')
->join('users', 'users.id', '=', 'subusers.user_id')
->where('subusers.server_id', $server)
->where('users.uuid', $uuid)
->firstOrFail();
return $model;
}
/**
* Return a subuser with the associated server relationship.
*

View file

@ -5,6 +5,8 @@ namespace Pterodactyl\Repositories\Wings;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\Server;
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Exception\TransferException;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
class DaemonCommandRepository extends DaemonRepository
{
@ -13,16 +15,22 @@ class DaemonCommandRepository extends DaemonRepository
*
* @param string|string[] $command
* @return \Psr\Http\Message\ResponseInterface
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function send($command): ResponseInterface
{
Assert::isInstanceOf($this->server, Server::class);
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/commands', $this->server->uuid),
[
'json' => ['commands' => is_array($command) ? $command : [$command]],
]
);
try {
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/commands', $this->server->uuid),
[
'json' => ['commands' => is_array($command) ? $command : [$command]],
]
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
}
}

View file

@ -5,7 +5,9 @@ namespace Pterodactyl\Repositories\Wings;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\Server;
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Exception\TransferException;
use Pterodactyl\Exceptions\Http\Server\FileSizeTooLargeException;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
class DaemonFileRepository extends DaemonRepository
{
@ -18,17 +20,22 @@ class DaemonFileRepository extends DaemonRepository
*
* @throws \GuzzleHttp\Exception\TransferException
* @throws \Pterodactyl\Exceptions\Http\Server\FileSizeTooLargeException
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function getContent(string $path, int $notLargerThan = null): string
{
Assert::isInstanceOf($this->server, Server::class);
$response = $this->getHttpClient()->get(
sprintf('/api/servers/%s/files/contents', $this->server->uuid),
[
'query' => ['file' => $path],
]
);
try {
$response = $this->getHttpClient()->get(
sprintf('/api/servers/%s/files/contents', $this->server->uuid),
[
'query' => ['file' => $path],
]
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
$length = (int) $response->getHeader('Content-Length')[0] ?? 0;
@ -47,19 +54,23 @@ class DaemonFileRepository extends DaemonRepository
* @param string $content
* @return \Psr\Http\Message\ResponseInterface
*
* @throws \GuzzleHttp\Exception\TransferException
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function putContent(string $path, string $content): ResponseInterface
{
Assert::isInstanceOf($this->server, Server::class);
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/write', $this->server->uuid),
[
'query' => ['file' => $path],
'body' => $content,
]
);
try {
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/write', $this->server->uuid),
[
'query' => ['file' => $path],
'body' => $content,
]
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
}
/**
@ -68,18 +79,22 @@ class DaemonFileRepository extends DaemonRepository
* @param string $path
* @return array
*
* @throws \GuzzleHttp\Exception\TransferException
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function getDirectory(string $path): array
{
Assert::isInstanceOf($this->server, Server::class);
$response = $this->getHttpClient()->get(
sprintf('/api/servers/%s/files/list-directory', $this->server->uuid),
[
'query' => ['directory' => $path],
]
);
try {
$response = $this->getHttpClient()->get(
sprintf('/api/servers/%s/files/list-directory', $this->server->uuid),
[
'query' => ['directory' => $path],
]
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
return json_decode($response->getBody(), true);
}
@ -90,20 +105,26 @@ class DaemonFileRepository extends DaemonRepository
* @param string $name
* @param string $path
* @return \Psr\Http\Message\ResponseInterface
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function createDirectory(string $name, string $path): ResponseInterface
{
Assert::isInstanceOf($this->server, Server::class);
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/create-directory', $this->server->uuid),
[
'json' => [
'name' => urldecode($name),
'path' => urldecode($path),
],
]
);
try {
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/create-directory', $this->server->uuid),
[
'json' => [
'name' => urldecode($name),
'path' => urldecode($path),
],
]
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
}
/**
@ -112,20 +133,26 @@ class DaemonFileRepository extends DaemonRepository
* @param string|null $root
* @param array $files
* @return \Psr\Http\Message\ResponseInterface
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function renameFiles(?string $root, array $files): ResponseInterface
{
Assert::isInstanceOf($this->server, Server::class);
return $this->getHttpClient()->put(
sprintf('/api/servers/%s/files/rename', $this->server->uuid),
[
'json' => [
'root' => $root ?? '/',
'files' => $files,
],
]
);
try {
return $this->getHttpClient()->put(
sprintf('/api/servers/%s/files/rename', $this->server->uuid),
[
'json' => [
'root' => $root ?? '/',
'files' => $files,
],
]
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
}
/**
@ -133,19 +160,25 @@ class DaemonFileRepository extends DaemonRepository
*
* @param string $location
* @return \Psr\Http\Message\ResponseInterface
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function copyFile(string $location): ResponseInterface
{
Assert::isInstanceOf($this->server, Server::class);
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/copy', $this->server->uuid),
[
'json' => [
'location' => urldecode($location),
],
]
);
try {
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/copy', $this->server->uuid),
[
'json' => [
'location' => urldecode($location),
],
]
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
}
/**
@ -154,20 +187,26 @@ class DaemonFileRepository extends DaemonRepository
* @param string|null $root
* @param array $files
* @return \Psr\Http\Message\ResponseInterface
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function deleteFiles(?string $root, array $files): ResponseInterface
{
Assert::isInstanceOf($this->server, Server::class);
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/delete', $this->server->uuid),
[
'json' => [
'root' => $root ?? '/',
'files' => $files,
],
]
);
try {
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/delete', $this->server->uuid),
[
'json' => [
'root' => $root ?? '/',
'files' => $files,
],
]
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
}
/**
@ -176,21 +215,58 @@ class DaemonFileRepository extends DaemonRepository
* @param string|null $root
* @param array $files
* @return array
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function compressFiles(?string $root, array $files): array
{
Assert::isInstanceOf($this->server, Server::class);
$response = $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/compress', $this->server->uuid),
[
'json' => [
'root' => $root ?? '/',
'files' => $files,
],
]
);
try {
$response = $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/compress', $this->server->uuid),
[
'json' => [
'root' => $root ?? '/',
'files' => $files,
],
// Wait for up to 15 minutes for the archive to be completed when calling this endpoint
// since it will likely take quite awhile for large directories.
'timeout' => 60 * 15,
]
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
return json_decode($response->getBody(), true);
}
/**
* Decompresses a given archive file.
*
* @param string|null $root
* @param string $file
* @return \Psr\Http\Message\ResponseInterface
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function decompressFile(?string $root, string $file): ResponseInterface
{
Assert::isInstanceOf($this->server, Server::class);
try {
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/decompress', $this->server->uuid),
[
'json' => [
'root' => $root ?? '/',
'file' => $file,
],
]
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
}
}

View file

@ -5,6 +5,8 @@ namespace Pterodactyl\Repositories\Wings;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\Server;
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Exception\TransferException;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
class DaemonPowerRepository extends DaemonRepository
{
@ -13,14 +15,20 @@ class DaemonPowerRepository extends DaemonRepository
*
* @param string $action
* @return \Psr\Http\Message\ResponseInterface
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function send(string $action): ResponseInterface
{
Assert::isInstanceOf($this->server, Server::class);
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/power', $this->server->uuid),
['json' => ['action' => $action]]
);
try {
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/power', $this->server->uuid),
['json' => ['action' => $action]]
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
}
}

View file

@ -23,7 +23,7 @@ class DaemonServerRepository extends DaemonRepository
sprintf('/api/servers/%s', $this->server->uuid)
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
throw new DaemonConnectionException($exception, false);
}
return json_decode($response->getBody()->__toString(), true);

View file

@ -0,0 +1,49 @@
<?php
namespace Pterodactyl\Rules;
use Illuminate\Contracts\Validation\Rule;
class ResolvesToIPAddress implements Rule
{
/**
* Validate that a given string can correctly resolve to a valid IPv4 address.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value): bool
{
// inet_pton returns false if the value passed through is not a valid IP address, so we'll just
// use that a nice ugly PHP hack to determine if we should pass this off to the gethostbyname
// call below.
$isIP = inet_pton($attribute) !== false;
// If the value received is not an IP address try to look it up using the gethostbyname() call.
// If that returns the same value that we passed in then it means it did not resolve to anything
// and we should fail this validation call.
return $isIP || gethostbyname($value) !== $value;
}
/**
* Return a validation message for use when this rule fails.
*
* @return string
*/
public function message(): string
{
return 'The :attribute must be a valid IPv4 address or hostname that resolves to a valid IPv4 address.';
}
/**
* Convert the rule to a validation string. This is necessary to avoid
* issues with Eloquence which tries to use this rule as a string.
*
* @return string
*/
public function __toString()
{
return 'p_resolves_to_ip_address';
}
}

View file

@ -2,7 +2,6 @@
namespace Pterodactyl\Services\Backups;
use Carbon\Carbon;
use Ramsey\Uuid\Uuid;
use Carbon\CarbonImmutable;
use Webmozart\Assert\Assert;
@ -101,14 +100,14 @@ class InitiateBackupService
public function handle(Server $server, string $name = null): Backup
{
// Do not allow the user to continue if this server is already at its limit.
if (! $server->backup_limit || $server->backups()->count() >= $server->backup_limit) {
if (! $server->backup_limit || $server->backups()->where('is_successful', true)->count() >= $server->backup_limit) {
throw new TooManyBackupsException($server->backup_limit);
}
$previous = $this->repository->getBackupsGeneratedDuringTimespan($server->id, 10);
if ($previous->count() >= 2) {
throw new TooManyRequestsHttpException(
Carbon::now()->diffInSeconds($previous->last()->created_at->addMinutes(10)),
CarbonImmutable::now()->diffInSeconds($previous->last()->created_at->addMinutes(10)),
'Only two backups may be generated within a 10 minute span of time.'
);
}

View file

@ -51,12 +51,29 @@ class EggConfigurationService
);
return [
'startup' => json_decode($server->egg->inherit_config_startup),
'startup' => $this->convertStartupToNewFormat(json_decode($server->egg->inherit_config_startup, true)),
'stop' => $this->convertStopToNewFormat($server->egg->inherit_config_stop),
'configs' => $configs,
];
}
/**
* Convert the "done" variable into an array if it is not currently one.
*
* @param array $startup
* @return array
*/
protected function convertStartupToNewFormat(array $startup)
{
$done = Arr::get($startup, 'done');
return [
'done' => is_string($done) ? [$done] : $done,
'user_interaction' => Arr::get($startup, 'userInteraction') ?? Arr::get($startup, 'user_interaction') ?? [],
'strip_ansi' => Arr::get($startup, 'strip_ansi') ?? false,
];
}
/**
* Converts a legacy stop string into a new generation stop option for a server.
*

View file

@ -30,7 +30,7 @@ class GetUserPermissionsService
}
/** @var \Pterodactyl\Models\Subuser|null $subuserPermissions */
$subuserPermissions = $server->subusers->where('user_id', $user->id)->first();
$subuserPermissions = $server->subusers()->where('user_id', $user->id)->first();
return $subuserPermissions ? $subuserPermissions->permissions : [];
}

View file

@ -0,0 +1,27 @@
<?php
namespace Pterodactyl\Services\Servers;
use Pterodactyl\Models\Server;
class StartupCommandService
{
/**
* Generates a startup command for a given server instance.
*
* @param \Pterodactyl\Models\Server $server
* @return string
*/
public function handle(Server $server): string
{
$find = ['{{SERVER_MEMORY}}', '{{SERVER_IP}}', '{{SERVER_PORT}}'];
$replace = [$server->memory, $server->allocation->ip, $server->allocation->port];
foreach ($server->variables as $variable) {
$find[] = '{{' . $variable->env_variable . '}}';
$replace[] = $variable->user_viewable ? ($variable->server_value ?? $variable->default_value) : '[hidden]';
}
return str_replace($find, $replace, $server->startup);
}
}

View file

@ -1,56 +0,0 @@
<?php
namespace Pterodactyl\Services\Servers;
use Illuminate\Support\Collection;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class StartupCommandViewService
{
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
private $repository;
/**
* StartupCommandViewService constructor.
*
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
*/
public function __construct(ServerRepositoryInterface $repository)
{
$this->repository = $repository;
}
/**
* Generate a startup command for a server and return all of the user-viewable variables
* as well as their assigned values.
*
* @param int $server
* @return \Illuminate\Support\Collection
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function handle(int $server): Collection
{
$response = $this->repository->getVariablesWithValues($server, true);
$server = $this->repository->getPrimaryAllocation($response->server);
$find = ['{{SERVER_MEMORY}}', '{{SERVER_IP}}', '{{SERVER_PORT}}'];
$replace = [$server->memory, $server->getRelation('allocation')->ip, $server->getRelation('allocation')->port];
$variables = $server->getRelation('egg')->getRelation('variables')
->each(function ($variable) use (&$find, &$replace, $response) {
$find[] = '{{' . $variable->env_variable . '}}';
$replace[] = $variable->user_viewable ? $response->data[$variable->env_variable] : '[hidden]';
})->filter(function ($variable) {
return $variable->user_viewable === 1;
});
return collect([
'startup' => str_replace($find, $replace, $server->startup),
'variables' => $variables,
'server_values' => $response->data,
]);
}
}

View file

@ -22,6 +22,7 @@ class BackupTransformer extends BaseClientTransformer
{
return [
'uuid' => $backup->uuid,
'is_successful' => $backup->is_successful,
'name' => $backup->name,
'ignored_files' => $backup->ignored_files,
'sha256_hash' => $backup->sha256_hash,

View file

@ -4,6 +4,7 @@ namespace Pterodactyl\Transformers\Api\Client;
use Pterodactyl\Models\Database;
use League\Fractal\Resource\Item;
use Pterodactyl\Models\Permission;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Contracts\Extensions\HashidsInterface;
@ -65,12 +66,16 @@ class DatabaseTransformer extends BaseClientTransformer
/**
* Include the database password in the request.
*
* @param \Pterodactyl\Models\Database $model
* @return \League\Fractal\Resource\Item
* @param \Pterodactyl\Models\Database $database
* @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource
*/
public function includePassword(Database $model): Item
public function includePassword(Database $database): Item
{
return $this->item($model, function (Database $model) {
if (!$this->getUser()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $database->server)) {
return $this->null();
}
return $this->item($database, function (Database $model) {
return [
'password' => $this->encrypter->decrypt($model->password),
];

View file

@ -0,0 +1,33 @@
<?php
namespace Pterodactyl\Transformers\Api\Client;
use Pterodactyl\Models\EggVariable;
class EggVariableTransformer extends BaseClientTransformer
{
/**
* @return string
*/
public function getResourceName(): string
{
return EggVariable::RESOURCE_NAME;
}
/**
* @param \Pterodactyl\Models\EggVariable $variable
* @return array
*/
public function transform(EggVariable $variable)
{
return [
'name' => $variable->name,
'description' => $variable->description,
'env_variable' => $variable->env_variable,
'default_value' => $variable->default_value,
'server_value' => $variable->server_value,
'is_editable' => $variable->user_editable,
'rules' => $variable->rules,
];
}
}

View file

@ -6,13 +6,17 @@ use Pterodactyl\Models\Egg;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Subuser;
use Pterodactyl\Models\Allocation;
use Pterodactyl\Models\Permission;
use Illuminate\Container\Container;
use Pterodactyl\Models\EggVariable;
use Pterodactyl\Services\Servers\StartupCommandService;
class ServerTransformer extends BaseClientTransformer
{
/**
* @var string[]
*/
protected $defaultIncludes = ['allocations'];
protected $defaultIncludes = ['allocations', 'variables'];
/**
* @var array
@ -36,6 +40,9 @@ class ServerTransformer extends BaseClientTransformer
*/
public function transform(Server $server): array
{
/** @var \Pterodactyl\Services\Servers\StartupCommandService $service */
$service = Container::getInstance()->make(StartupCommandService::class);
return [
'server_owner' => $this->getKey()->user_id === $server->owner_id,
'identifier' => $server->uuidShort,
@ -54,6 +61,7 @@ class ServerTransformer extends BaseClientTransformer
'io' => $server->io,
'cpu' => $server->cpu,
],
'invocation' => $service->handle($server),
'feature_limits' => [
'databases' => $server->database_limit,
'allocations' => $server->allocation_limit,
@ -68,11 +76,16 @@ class ServerTransformer extends BaseClientTransformer
* Returns the allocations associated with this server.
*
* @param \Pterodactyl\Models\Server $server
* @return \League\Fractal\Resource\Collection
* @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource
*
* @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException
*/
public function includeAllocations(Server $server)
{
if (! $this->getUser()->can(Permission::ACTION_ALLOCATION_READ, $server)) {
return $this->null();
}
return $this->collection(
$server->allocations,
$this->makeTransformer(AllocationTransformer::class),
@ -80,6 +93,25 @@ class ServerTransformer extends BaseClientTransformer
);
}
/**
* @param \Pterodactyl\Models\Server $server
* @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource
*
* @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException
*/
public function includeVariables(Server $server)
{
if (! $this->getUser()->can(Permission::ACTION_STARTUP_READ, $server)) {
return $this->null();
}
return $this->collection(
$server->variables->where('user_viewable', true),
$this->makeTransformer(EggVariableTransformer::class),
EggVariable::RESOURCE_NAME
);
}
/**
* Returns the egg associated with this server.
*
@ -96,11 +128,16 @@ class ServerTransformer extends BaseClientTransformer
* Returns the subusers associated with this server.
*
* @param \Pterodactyl\Models\Server $server
* @return \League\Fractal\Resource\Collection
* @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource
*
* @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException
*/
public function includeSubusers(Server $server)
{
if (! $this->getUser()->can(Permission::ACTION_USER_READ, $server)) {
return $this->null();
}
return $this->collection($server->subusers, $this->makeTransformer(SubuserTransformer::class), Subuser::RESOURCE_NAME);
}
}

View file

@ -27,11 +27,11 @@ class StatsTransformer extends BaseClientTransformer
'current_state' => Arr::get($data, 'state', 'stopped'),
'is_suspended' => Arr::get($data, 'suspended', false),
'resources' => [
'memory_bytes' => Arr::get($data, 'resources.memory_bytes', 0),
'cpu_absolute' => Arr::get($data, 'resources.cpu_absolute', 0),
'disk_bytes' => Arr::get($data, 'resources.disk_bytes', 0),
'network_rx_bytes' => Arr::get($data, 'resources.network.rx_bytes', 0),
'network_tx_bytes' => Arr::get($data, 'resources.network.tx_bytes', 0),
'memory_bytes' => Arr::get($data, 'memory_bytes', 0),
'cpu_absolute' => Arr::get($data, 'cpu_absolute', 0),
'disk_bytes' => Arr::get($data, 'disk_bytes', 0),
'network_rx_bytes' => Arr::get($data, 'network.rx_bytes', 0),
'network_tx_bytes' => Arr::get($data, 'network.tx_bytes', 0),
],
];
}