Merge branch 'develop' into matthewpi/security-keys-backport

This commit is contained in:
Matthew Penner 2023-01-17 15:33:53 -07:00
commit f631ac1946
No known key found for this signature in database
1153 changed files with 25099 additions and 37002 deletions

View file

@ -2,7 +2,6 @@
namespace Pterodactyl\Services\Acl\Api;
use ReflectionClass;
use Pterodactyl\Models\ApiKey;
class AdminAcl
@ -63,7 +62,7 @@ class AdminAcl
*/
public static function getResourceList(): array
{
$reflect = new ReflectionClass(__CLASS__);
$reflect = new \ReflectionClass(__CLASS__);
return collect($reflect->getConstants())->filter(function ($value, $key) {
return substr($key, 0, 9) === 'RESOURCE_';

View file

@ -99,7 +99,7 @@ class ActivityLogService
}
/**
* Sets a custom property on the activty log instance.
* Sets a custom property on the activity log instance.
*
* @param string|array $key
* @param mixed $value
@ -115,7 +115,7 @@ class ActivityLogService
}
/**
* Attachs the instance request metadata to the activity log event.
* Attaches the instance request metadata to the activity log event.
*/
public function withRequestMetadata(): self
{

View file

@ -2,7 +2,6 @@
namespace Pterodactyl\Services\Allocations;
use Exception;
use IPTools\Network;
use Pterodactyl\Models\Node;
use Illuminate\Database\ConnectionInterface;
@ -40,23 +39,24 @@ class AssignmentService
*/
public function handle(Node $node, array $data): void
{
$explode = explode('/', $data['allocation_ip']);
$allocationIp = $data['allocation_ip'];
$explode = explode('/', $allocationIp);
if (count($explode) !== 1) {
if (!ctype_digit($explode[1]) || ($explode[1] > self::CIDR_MIN_BITS || $explode[1] < self::CIDR_MAX_BITS)) {
throw new CidrOutOfRangeException();
}
}
$underlying = 'Unknown IP';
try {
// TODO: how should we approach supporting IPv6 with this?
// gethostbyname only supports IPv4, but the alternative (dns_get_record) returns
// an array of records, which is not ideal for this use case, we need a SINGLE
// IP to use, not multiple.
$underlying = gethostbyname($data['allocation_ip']);
$underlying = gethostbyname($allocationIp);
$parsed = Network::parse($underlying);
} catch (Exception $exception) {
/* @noinspection PhpUndefinedVariableInspection */
throw new DisplayException("Could not parse provided allocation IP address ({$underlying}): {$exception->getMessage()}", $exception);
} catch (\Exception $exception) {
throw new DisplayException("Could not parse provided allocation IP address for $allocationIp ($underlying): {$exception->getMessage()}", $exception);
}
$this->connection->beginTransaction();

View file

@ -73,7 +73,10 @@ class DeleteBackupService
/** @var \Pterodactyl\Extensions\Filesystem\S3Filesystem $adapter */
$adapter = $this->manager->adapter(Backup::ADAPTER_AWS_S3);
$adapter->getClient()->deleteObject([
/** @var \Aws\S3\S3Client $client */
$client = $adapter->getClient();
$client->deleteObject([
'Bucket' => $adapter->getBucket(),
'Key' => sprintf('%s/%s.tar.gz', $backup->server->uuid, $backup->uuid),
]);

View file

@ -98,17 +98,17 @@ class InitiateBackupService
// Get the oldest backup the server has that is not "locked" (indicating a backup that should
// never be automatically purged). If we find a backup we will delete it and then continue with
// this process. If no backup is found that can be used an exception is thrown.
/** @var \Pterodactyl\Models\Backup $oldest */
$oldest = $successful->where('is_locked', false)->orderBy('created_at')->first();
if (!$oldest) {
throw new TooManyBackupsException($server->backup_limit);
}
/* @var Backup $oldest */
$this->deleteBackupService->handle($oldest);
}
return $this->connection->transaction(function () use ($server, $name) {
/** @var \Pterodactyl\Models\Backup $backup */
/** @var Backup $backup */
$backup = $this->repository->create([
'server_id' => $server->id,
'uuid' => Uuid::uuid4()->toString(),

View file

@ -3,7 +3,6 @@
namespace Pterodactyl\Services\Databases;
use Exception;
use InvalidArgumentException;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Database;
use Pterodactyl\Helpers\Utilities;
@ -86,7 +85,7 @@ class DatabaseManagementService
// Protect against developer mistakes...
if (empty($data['database']) || !preg_match(self::MATCH_NAME_REGEX, $data['database'])) {
throw new InvalidArgumentException('The database name passed to DatabaseManagementService::handle MUST be prefixed with "s{server_id}_".');
throw new \InvalidArgumentException('The database name passed to DatabaseManagementService::handle MUST be prefixed with "s{server_id}_".');
}
$data = array_merge($data, [
@ -117,14 +116,15 @@ class DatabaseManagementService
return $database;
});
} catch (Exception $exception) {
} catch (\Exception $exception) {
try {
/** @var ?Database $database */
if ($database instanceof Database) {
$this->repository->dropDatabase($database->database);
$this->repository->dropUser($database->username, $database->remote);
$this->repository->flush();
}
} catch (Exception $deletionException) {
} catch (\Exception $deletionException) {
// Do nothing here. We've already encountered an issue before this point so no
// reason to prioritize this error over the initial one.
}

View file

@ -27,21 +27,22 @@ class DeployServerDatabaseService
Assert::notEmpty($data['database'] ?? null);
Assert::notEmpty($data['remote'] ?? null);
$hosts = DatabaseHost::query()->get()->toBase();
if ($hosts->isEmpty()) {
throw new NoSuitableDatabaseHostException();
} else {
$nodeHosts = $hosts->where('node_id', $server->node_id)->toBase();
if ($nodeHosts->isEmpty() && !config('pterodactyl.client_features.databases.allow_random')) {
$databaseHostId = $server->node->database_host_id;
if (is_null($databaseHostId)) {
if (!config('pterodactyl.client_features.databases.allow_random')) {
throw new NoSuitableDatabaseHostException();
}
$hosts = DatabaseHost::query()->get()->toBase();
if ($hosts->isEmpty()) {
throw new NoSuitableDatabaseHostException();
}
$databaseHostId = $hosts->random()->id;
}
return $this->managementService->create($server, [
'database_host_id' => $nodeHosts->isEmpty()
? $hosts->random()->id
: $nodeHosts->random()->id,
'database_host_id' => $databaseHostId,
'database' => DatabaseManagementService::generateUniqueDatabaseName($data['database'], $server->id),
'remote' => $data['remote'],
]);

View file

@ -64,7 +64,7 @@ class AllocationSelectionService
// Ranges are stored in the ports array as an array which can be
// better processed in the repository.
if (preg_match(AssignmentService::PORT_RANGE_REGEX, $port, $matches)) {
if (abs($matches[2] - $matches[1]) > AssignmentService::PORT_RANGE_LIMIT) {
if (abs(intval($matches[2]) - intval($matches[1])) > AssignmentService::PORT_RANGE_LIMIT) {
throw new DisplayException(trans('exceptions.allocations.too_many_ports'));
}

View file

@ -72,18 +72,18 @@ class FindViableNodesService
Assert::integer($this->memory, 'Memory usage must be an int, got %s');
$query = Node::query()->select('nodes.*')
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory')
->selectRaw('IFNULL(SUM(servers.disk), 0) as sum_disk')
->selectRaw('COALESCE(SUM(servers.memory), 0) as sum_memory')
->selectRaw('COALESCE(SUM(servers.disk), 0) as sum_disk')
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
->where('nodes.public', 1);
if (!empty($this->locations)) {
$query = $query->whereIn('nodes.location_id', $this->locations);
$query = $query->whereIn('location_id', $this->locations);
}
$results = $query->groupBy('nodes.id')
->havingRaw('(IFNULL(SUM(servers.memory), 0) + ?) <= (nodes.memory * (1 + (nodes.memory_overallocate / 100)))', [$this->memory])
->havingRaw('(IFNULL(SUM(servers.disk), 0) + ?) <= (nodes.disk * (1 + (nodes.disk_overallocate / 100)))', [$this->disk]);
->havingRaw('(COALESCE(SUM(servers.memory), 0) + ?) <= (nodes.memory * (1.0 + (nodes.memory_overallocate / 100.0)))', [$this->memory])
->havingRaw('(COALESCE(SUM(servers.disk), 0) + ?) <= (nodes.disk * (1.0 + (nodes.disk_overallocate / 100.0)))', [$this->disk]);
if (!is_null($page)) {
$results = $results->paginate($perPage ?? 50, ['*'], 'page', $page);

View file

@ -4,7 +4,6 @@ namespace Pterodactyl\Services\Eggs;
use Illuminate\Support\Arr;
use Pterodactyl\Models\Egg;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Collection;
use Pterodactyl\Exceptions\Service\InvalidFileUploadException;
@ -13,17 +12,10 @@ class EggParserService
/**
* Takes an uploaded file and parses out the egg configuration from within.
*
* @throws \JsonException
* @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException
*/
public function handle(UploadedFile $file): array
public function handle(array $parsed): array
{
if ($file->getError() !== UPLOAD_ERR_OK || !$file->isFile()) {
throw new InvalidFileUploadException('The selected file is not valid and cannot be imported.');
}
/** @var array $parsed */
$parsed = json_decode($file->openFile()->fread($file->getSize()), true, 512, JSON_THROW_ON_ERROR);
if (!in_array(Arr::get($parsed, 'meta.version') ?? '', ['PTDL_v1', 'PTDL_v2'])) {
throw new InvalidFileUploadException('The JSON file provided is not in a format that can be recognized.');
}
@ -46,7 +38,6 @@ class EggParserService
'update_url' => Arr::get($parsed, 'meta.update_url'),
'config_files' => Arr::get($parsed, 'config.files'),
'config_startup' => Arr::get($parsed, 'config.startup'),
'config_logs' => Arr::get($parsed, 'config.logs'),
'config_stop' => Arr::get($parsed, 'config.stop'),
'startup' => Arr::get($parsed, 'startup'),
'script_install' => Arr::get($parsed, 'scripts.installation.script'),

View file

@ -45,7 +45,6 @@ class EggExporterService
'config' => [
'files' => $egg->inherit_config_files,
'startup' => $egg->inherit_config_startup,
'logs' => $egg->inherit_config_logs,
'stop' => $egg->inherit_config_stop,
],
'scripts' => [

View file

@ -6,28 +6,107 @@ use Ramsey\Uuid\Uuid;
use Illuminate\Support\Arr;
use Pterodactyl\Models\Egg;
use Pterodactyl\Models\Nest;
use Symfony\Component\Yaml\Yaml;
use Illuminate\Http\UploadedFile;
use Pterodactyl\Models\EggVariable;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Services\Eggs\EggParserService;
use Symfony\Component\Yaml\Exception\ParseException;
use Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException;
use Pterodactyl\Exceptions\Service\Egg\BadYamlFormatException;
use Pterodactyl\Exceptions\Service\InvalidFileUploadException;
class EggImporterService
{
public function __construct(protected ConnectionInterface $connection, protected EggParserService $parser)
{
public function __construct(
private ConnectionInterface $connection,
private EggParserService $eggParserService
) {
}
/**
* Take an uploaded JSON file and parse it into a new egg.
*
* @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException|\Throwable
* @deprecated use `handleFile` or `handleContent` instead
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException
* @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException
* @throws \Pterodactyl\Exceptions\Service\Egg\BadYamlFormatException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\DisplayException
*/
public function handle(UploadedFile $file, int $nest): Egg
public function handle(UploadedFile $file, int $nestId): Egg
{
$parsed = $this->parser->handle($file);
return $this->handleFile($nestId, $file);
}
/**
* ?
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException
* @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException
* @throws \Pterodactyl\Exceptions\Service\Egg\BadYamlFormatException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\DisplayException
*/
public function handleFile(int $nestId, UploadedFile $file): Egg
{
if ($file->getError() !== UPLOAD_ERR_OK || !$file->isFile()) {
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()));
}
return $this->handleContent($nestId, $file->openFile()->fread($file->getSize()), 'application/json');
}
/**
* ?
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException
* @throws \Pterodactyl\Exceptions\Service\Egg\BadYamlFormatException
* @throws \Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/
public function handleContent(int $nestId, string $content, string $contentType): Egg
{
switch (true) {
case str_starts_with($contentType, 'application/json'):
$parsed = json_decode($content, true);
if (json_last_error() !== 0) {
throw new BadJsonFormatException(trans('exceptions.nest.importer.json_error', ['error' => json_last_error_msg()]));
}
return $this->handleArray($nestId, $parsed);
case str_starts_with($contentType, 'application/yaml'):
try {
$parsed = Yaml::parse($content);
return $this->handleArray($nestId, $parsed);
} catch (ParseException $exception) {
throw new BadYamlFormatException('There was an error while attempting to parse the YAML: ' . $exception->getMessage() . '.');
}
default:
throw new DisplayException('unknown content type');
}
}
/**
* ?
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException
*/
private function handleArray(int $nestId, array $parsed): Egg
{
$parsed = $this->eggParserService->handle($parsed);
/** @var \Pterodactyl\Models\Nest $nest */
$nest = Nest::query()->with('eggs', 'eggs.variables')->findOrFail($nest);
$nest = Nest::query()->with('eggs', 'eggs.variables')->findOrFail($nestId);
return $this->connection->transaction(function () use ($nest, $parsed) {
$egg = (new Egg())->forceFill([
@ -37,7 +116,7 @@ class EggImporterService
'copy_script_from' => null,
]);
$egg = $this->parser->fillFromParsed($egg, $parsed);
$egg = $this->eggParserService->fillFromParsed($egg, $parsed);
$egg->save();
foreach ($parsed['variables'] ?? [] as $variable) {

View file

@ -8,14 +8,18 @@ use Illuminate\Support\Collection;
use Pterodactyl\Models\EggVariable;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Services\Eggs\EggParserService;
use Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException;
use Pterodactyl\Exceptions\Service\InvalidFileUploadException;
class EggUpdateImporterService
{
/**
* EggUpdateImporterService constructor.
*/
public function __construct(protected ConnectionInterface $connection, protected EggParserService $parser)
{
public function __construct(
private ConnectionInterface $connection,
private EggParserService $eggParserService
) {
}
/**
@ -25,10 +29,18 @@ class EggUpdateImporterService
*/
public function handle(Egg $egg, UploadedFile $file): Egg
{
$parsed = $this->parser->handle($file);
if ($file->getError() !== UPLOAD_ERR_OK || !$file->isFile()) {
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()), true);
if (json_last_error() !== 0) {
throw new BadJsonFormatException(trans('exceptions.nest.importer.json_error', ['error' => json_last_error_msg()]));
}
$parsed = $this->eggParserService->handle($parsed);
return $this->connection->transaction(function () use ($egg, $parsed) {
$egg = $this->parser->fillFromParsed($egg, $parsed);
$egg = $this->eggParserService->fillFromParsed($egg, $parsed);
$egg->save();
// Update existing variables or create new ones.

View file

@ -3,11 +3,11 @@
namespace Pterodactyl\Services\Eggs\Variables;
use Illuminate\Support\Str;
use Pterodactyl\Models\Egg;
use Pterodactyl\Models\EggVariable;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Traits\Services\ValidatesValidationRules;
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface;
use Pterodactyl\Exceptions\Service\Egg\Variable\ReservedVariableNameException;
class VariableUpdateService
@ -17,7 +17,7 @@ class VariableUpdateService
/**
* VariableUpdateService constructor.
*/
public function __construct(private EggVariableRepositoryInterface $repository, private ValidationFactory $validator)
public function __construct(private ValidationFactory $validator)
{
}
@ -34,24 +34,21 @@ class VariableUpdateService
* Update a specific egg variable.
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Egg\Variable\ReservedVariableNameException
*/
public function handle(EggVariable $variable, array $data): mixed
public function handle(Egg $egg, array $data): void
{
if (!is_null(array_get($data, 'env_variable'))) {
if (in_array(strtoupper(array_get($data, 'env_variable')), explode(',', EggVariable::RESERVED_ENV_NAMES))) {
throw new ReservedVariableNameException(trans('exceptions.service.variables.reserved_name', ['name' => array_get($data, 'env_variable')]));
}
$search = $this->repository->setColumns('id')->findCountWhere([
['env_variable', '=', $data['env_variable']],
['egg_id', '=', $variable->egg_id],
['id', '!=', $variable->id],
]);
$count = $egg->variables()
->where('egg_variables.env_variable', $data['env_variable'])
->where('egg_variables.id', '!=', $data['id'])
->count();
if ($search > 0) {
if ($count > 0) {
throw new DisplayException(trans('exceptions.service.variables.env_not_unique', ['name' => array_get($data, 'env_variable')]));
}
}
@ -66,13 +63,13 @@ class VariableUpdateService
$options = array_get($data, 'options') ?? [];
return $this->repository->withoutFreshModel()->update($variable->id, [
$egg->variables()->where('egg_variables.id', $data['id'])->update([
'name' => $data['name'] ?? '',
'description' => $data['description'] ?? '',
'env_variable' => $data['env_variable'] ?? '',
'default_value' => $data['default_value'] ?? '',
'user_viewable' => in_array('user_viewable', $options),
'user_editable' => in_array('user_editable', $options),
'user_viewable' => $data['user_viewable'],
'user_editable' => $data['user_editable'],
'rules' => $data['rules'] ?? '',
]);
}

View file

@ -1,117 +0,0 @@
<?php
namespace Pterodactyl\Services\Helpers;
use Illuminate\Support\Arr;
use Illuminate\Filesystem\FilesystemManager;
use Illuminate\Contracts\Filesystem\Filesystem;
use Pterodactyl\Exceptions\ManifestDoesNotExistException;
class AssetHashService
{
/**
* Location of the manifest file generated by gulp.
*/
public const MANIFEST_PATH = './assets/manifest.json';
private Filesystem $filesystem;
protected static mixed $manifest = null;
/**
* AssetHashService constructor.
*/
public function __construct(FilesystemManager $filesystem)
{
$this->filesystem = $filesystem->createLocalDriver(['root' => public_path()]);
}
/**
* Modify a URL to append the asset hash.
*/
public function url(string $resource): string
{
$file = last(explode('/', $resource));
$data = Arr::get($this->manifest(), $file) ?? $file;
return str_replace($file, Arr::get($data, 'src') ?? $file, $resource);
}
/**
* Return the data integrity hash for a resource.
*/
public function integrity(string $resource): string
{
$file = last(explode('/', $resource));
$data = array_get($this->manifest(), $file, $file);
return Arr::get($data, 'integrity') ?? '';
}
/**
* Return a built CSS import using the provided URL.
*/
public function css(string $resource): string
{
$attributes = [
'href' => $this->url($resource),
'rel' => 'stylesheet preload',
'as' => 'style',
'crossorigin' => 'anonymous',
'referrerpolicy' => 'no-referrer',
];
if (config('pterodactyl.assets.use_hash')) {
$attributes['integrity'] = $this->integrity($resource);
}
$output = '<link';
foreach ($attributes as $key => $value) {
$output .= " $key=\"$value\"";
}
return $output . '>';
}
/**
* Return a built JS import using the provided URL.
*/
public function js(string $resource): string
{
$attributes = [
'src' => $this->url($resource),
'crossorigin' => 'anonymous',
];
if (config('pterodactyl.assets.use_hash')) {
$attributes['integrity'] = $this->integrity($resource);
}
$output = '<script';
foreach ($attributes as $key => $value) {
$output .= " $key=\"$value\"";
}
return $output . '></script>';
}
/**
* Get the asset manifest and store it in the cache for quicker lookups.
*/
protected function manifest(): array
{
if (static::$manifest === null) {
self::$manifest = json_decode(
$this->filesystem->get(self::MANIFEST_PATH),
true
);
}
$manifest = static::$manifest;
if ($manifest === null) {
throw new ManifestDoesNotExistException();
}
return $manifest;
}
}

View file

@ -3,46 +3,54 @@
namespace Pterodactyl\Services\Helpers;
use Exception;
use GuzzleHttp\Client;
use Carbon\CarbonImmutable;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Http;
use Illuminate\Contracts\Cache\Repository as CacheRepository;
use Pterodactyl\Exceptions\Service\Helper\CdnVersionFetchingException;
class SoftwareVersionService
{
public const VERSION_CACHE_KEY = 'pterodactyl:versioning_data';
public const GIT_VERSION_CACHE_KEY = 'pterodactyl:git_data';
private static array $result;
/**
* SoftwareVersionService constructor.
*/
public function __construct(
protected CacheRepository $cache,
protected Client $client
) {
public function __construct(private CacheRepository $cache)
{
self::$result = $this->cacheVersionData();
}
/**
* Get the latest version of the panel from the CDN servers.
* Return the current version of the panel that is being used.
*/
public function getPanel(): string
public function getCurrentVersion(): string
{
return config('app.version');
}
/**
* Returns the latest version of the panel from the CDN servers.
*/
public function getLatestPanel(): string
{
return Arr::get(self::$result, 'panel') ?? 'error';
}
/**
* Get the latest version of the daemon from the CDN servers.
* Returns the latest version of the Wings from the CDN servers.
*/
public function getDaemon(): string
public function getLatestWings(): string
{
return Arr::get(self::$result, 'wings') ?? 'error';
}
/**
* Get the URL to the discord server.
* Returns the URL to the discord server.
*/
public function getDiscord(): string
{
@ -50,7 +58,7 @@ class SoftwareVersionService
}
/**
* Get the URL for donations.
* Returns the URL for donations.
*/
public function getDonations(): string
{
@ -62,23 +70,80 @@ class SoftwareVersionService
*/
public function isLatestPanel(): bool
{
if (config('app.version') === 'canary') {
$version = $this->getCurrentVersion();
if ($version === 'canary') {
return true;
}
return version_compare(config('app.version'), $this->getPanel()) >= 0;
return version_compare($version, $this->getLatestPanel()) >= 0;
}
/**
* Determine if a passed daemon version string is the latest.
*/
public function isLatestDaemon(string $version): bool
public function isLatestWings(string $version): bool
{
if ($version === 'develop') {
if ($version === 'develop' || Str::startsWith($version, 'dev-')) {
return true;
}
return version_compare($version, $this->getDaemon()) >= 0;
return version_compare($version, $this->getLatestWings()) >= 0;
}
/**
* ?
*/
public function getVersionData(): array
{
$versionData = $this->versionData();
if ($versionData['is_git']) {
$git = $versionData['version'];
} else {
$git = null;
}
return [
'panel' => [
'current' => $this->getCurrentVersion(),
'latest' => $this->getLatestPanel(),
],
'wings' => [
'latest' => $this->getLatestWings(),
],
'git' => $git,
];
}
/**
* Return version information for the footer.
*/
protected function versionData(): array
{
return $this->cache->remember(self::GIT_VERSION_CACHE_KEY, CarbonImmutable::now()->addSeconds(15), function () {
$configVersion = $this->getCurrentVersion();
if (file_exists(base_path('.git/HEAD'))) {
$head = explode(' ', file_get_contents(base_path('.git/HEAD')));
if (array_key_exists(1, $head)) {
$path = base_path('.git/' . trim($head[1]));
}
}
if (isset($path) && file_exists($path)) {
return [
'version' => substr(file_get_contents($path), 0, 8),
'is_git' => true,
];
}
return [
'version' => $configVersion,
'is_git' => false,
];
});
}
/**
@ -88,10 +153,10 @@ class SoftwareVersionService
{
return $this->cache->remember(self::VERSION_CACHE_KEY, CarbonImmutable::now()->addMinutes(config('pterodactyl.cdn.cache_time', 60)), function () {
try {
$response = $this->client->request('GET', config('pterodactyl.cdn.url'));
$response = Http::get(config('pterodactyl.cdn.url'));
if ($response->getStatusCode() === 200) {
return json_decode($response->getBody(), true);
if ($response->status() === 200) {
return json_decode($response->body(), true);
}
throw new CdnVersionFetchingException();

View file

@ -13,7 +13,7 @@ class NodeCreationService
/**
* NodeCreationService constructor.
*/
public function __construct(private Encrypter $encrypter, protected NodeRepositoryInterface $repository)
public function __construct(protected NodeRepositoryInterface $repository)
{
}
@ -25,7 +25,7 @@ class NodeCreationService
public function handle(array $data): Node
{
$data['uuid'] = Uuid::uuid4()->toString();
$data['daemon_token'] = $this->encrypter->encrypt(Str::random(Node::DAEMON_TOKEN_LENGTH));
$data['daemon_token'] = app(Encrypter::class)->encrypt(Str::random(Node::DAEMON_TOKEN_LENGTH));
$data['daemon_token_id'] = Str::random(Node::DAEMON_TOKEN_ID_LENGTH);
return $this->repository->create($data, true, true);

View file

@ -2,7 +2,6 @@
namespace Pterodactyl\Services\Nodes;
use DateTimeImmutable;
use Carbon\CarbonImmutable;
use Illuminate\Support\Str;
use Pterodactyl\Models\Node;
@ -19,7 +18,7 @@ class NodeJWTService
private ?User $user = null;
private ?DateTimeImmutable $expiresAt;
private ?\DateTimeImmutable $expiresAt;
private ?string $subject = null;
@ -44,7 +43,7 @@ class NodeJWTService
return $this;
}
public function setExpiresAt(DateTimeImmutable $date): self
public function setExpiresAt(\DateTimeImmutable $date): self
{
$this->expiresAt = $date;

View file

@ -27,13 +27,13 @@ class ProcessScheduleService
*/
public function handle(Schedule $schedule, bool $now = false): void
{
/** @var \Pterodactyl\Models\Task $task */
$task = $schedule->tasks()->orderBy('sequence_id')->first();
if (is_null($task)) {
throw new DisplayException('Cannot process schedule for task execution: no tasks are registered.');
}
/* @var \Pterodactyl\Models\Task $task */
$this->connection->transaction(function () use ($schedule, $task) {
$schedule->forceFill([
'is_processing' => true,
@ -56,7 +56,7 @@ class ProcessScheduleService
return;
}
} catch (Exception $exception) {
} catch (\Exception $exception) {
if (!$exception instanceof DaemonConnectionException) {
// If we encountered some exception during this process that wasn't just an
// issue connecting to Wings run the failed sequence for a job. Otherwise we
@ -78,7 +78,7 @@ class ProcessScheduleService
// @see https://github.com/pterodactyl/panel/issues/2550
try {
$this->dispatcher->dispatchNow($job);
} catch (Exception $exception) {
} catch (\Exception $exception) {
$job->failed($exception);
throw $exception;

View file

@ -46,12 +46,12 @@ class BuildModificationService
// If any of these values are passed through in the data array go ahead and set
// them correctly on the server model.
$merge = Arr::only($data, ['oom_disabled', 'memory', 'swap', 'io', 'cpu', 'threads', 'disk', 'allocation_id']);
$merge = Arr::only($data, ['oom_killer', 'memory', 'swap', 'io', 'cpu', 'threads', 'disk', 'allocation_id']);
$server->forceFill(array_merge($merge, [
'database_limit' => Arr::get($data, 'database_limit', 0) ?? null,
'allocation_limit' => Arr::get($data, 'allocation_limit', 0) ?? null,
'backup_limit' => Arr::get($data, 'backup_limit', 0) ?? 0,
'database_limit' => Arr::get($data, 'database_limit', 0) ?? null,
]))->saveOrFail();
return $server->refresh();
@ -88,14 +88,13 @@ class BuildModificationService
// Handle the addition of allocations to this server. Only assign allocations that are not currently
// assigned to a different server, and only allocations on the same node as the server.
if (!empty($data['add_allocations'])) {
$query = Allocation::query()
->where('node_id', $server->node_id)
$query = $server->node->allocations()
->whereIn('id', $data['add_allocations'])
->whereNull('server_id');
// Keep track of all the allocations we're just now adding so that we can use the first
// one to reset the default allocation to.
$freshlyAllocated = $query->pluck('id')->first();
$freshlyAllocated = $query->first()->id ?? null;
$query->update(['server_id' => $server->id, 'notes' => null]);
}

View file

@ -50,7 +50,7 @@ class ServerConfigurationStructureService
],
'suspended' => $server->isSuspended(),
'environment' => $this->environment->handle($server),
'invocation' => $server->startup,
'invocation' => !is_null($server->startup) ? $server->startup : $server->egg->startup,
'skip_egg_scripts' => $server->skip_scripts,
'build' => [
'memory_limit' => $server->memory,
@ -59,22 +59,19 @@ class ServerConfigurationStructureService
'cpu_limit' => $server->cpu,
'threads' => $server->threads,
'disk_space' => $server->disk,
'oom_disabled' => $server->oom_disabled,
// TODO: remove oom_disabled and use oom_killer, this requires a Wings update.
'oom_disabled' => !$server->oom_killer,
'oom_killer' => $server->oom_killer,
],
'container' => [
'image' => $server->image,
// This field is deprecated — use the value in the "build" block.
//
// TODO: remove this key in V2.
'oom_disabled' => $server->oom_disabled,
'requires_rebuild' => false,
],
'allocations' => [
'force_outgoing_ip' => $server->egg->force_outgoing_ip,
'default' => [
'ip' => $server->allocation->ip,
'port' => $server->allocation->port,
],
'force_outgoing_ip' => $server->egg->force_outgoing_ip,
'mappings' => $server->getAllocationMappings(),
],
'mounts' => $server->mounts->map(function (Mount $mount) {
@ -110,7 +107,7 @@ class ServerConfigurationStructureService
return $item->pluck('port');
})->toArray(),
'env' => $this->environment->handle($server),
'oom_disabled' => $server->oom_disabled,
'oom_disabled' => !$server->oom_killer,
'memory' => (int) $server->memory,
'swap' => (int) $server->swap,
'io' => (int) $server->io,

View file

@ -82,7 +82,7 @@ class ServerCreationService
//
// If that connection fails out we will attempt to perform a cleanup by just
// deleting the server itself from the system.
/** @var \Pterodactyl\Models\Server $server */
/** @var Server $server */
$server = $this->connection->transaction(function () use ($data, $eggVariableData) {
// Create the server and assign any additional allocations to it.
$server = $this->createModel($data);
@ -115,7 +115,7 @@ class ServerCreationService
*/
private function configureDeployment(array $data, DeploymentObject $deployment): Allocation
{
/** @var \Illuminate\Support\Collection $nodes */
/** @var Collection $nodes */
$nodes = $this->findViableNodesService->setLocations($deployment->getLocations())
->setDisk(Arr::get($data, 'disk'))
->setMemory(Arr::get($data, 'memory'))
@ -136,7 +136,7 @@ class ServerCreationService
{
$uuid = $this->generateUniqueUuidCombo();
/** @var \Pterodactyl\Models\Server $model */
/** @var Server $model */
$model = $this->repository->create([
'external_id' => Arr::get($data, 'external_id'),
'uuid' => $uuid,
@ -153,7 +153,7 @@ class ServerCreationService
'io' => Arr::get($data, 'io'),
'cpu' => Arr::get($data, 'cpu'),
'threads' => Arr::get($data, 'threads'),
'oom_disabled' => Arr::get($data, 'oom_disabled') ?? true,
'oom_killer' => Arr::get($data, 'oom_killer') ?? false,
'allocation_id' => Arr::get($data, 'allocation_id'),
'nest_id' => Arr::get($data, 'nest_id'),
'egg_id' => Arr::get($data, 'egg_id'),

View file

@ -2,7 +2,6 @@
namespace Pterodactyl\Services\Servers;
use Exception;
use Illuminate\Http\Response;
use Pterodactyl\Models\Server;
use Illuminate\Support\Facades\Log;
@ -61,7 +60,7 @@ class ServerDeletionService
foreach ($server->databases as $database) {
try {
$this->databaseManagementService->delete($database);
} catch (Exception $exception) {
} catch (\Exception $exception) {
if (!$this->force) {
throw $exception;
}

View file

@ -1,27 +0,0 @@
<?php
namespace Pterodactyl\Services\Servers;
use Pterodactyl\Models\Server;
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
class TransferService
{
/**
* TransferService constructor.
*/
public function __construct(
private DaemonServerRepository $daemonServerRepository
) {
}
/**
* Requests an archive from the daemon.
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function requestArchive(Server $server): void
{
$this->daemonServerRepository->setServer($server)->requestArchive();
}
}

View file

@ -58,8 +58,6 @@ class SubuserCreationService
$user = $this->userCreationService->handle([
'email' => $email,
'username' => $username,
'name_first' => 'Server',
'name_last' => 'Subuser',
'root_admin' => false,
]);
}

View file

@ -0,0 +1,189 @@
<?php
namespace Pterodactyl\Services\Telemetry;
use Exception;
use Ramsey\Uuid\Uuid;
use Illuminate\Support\Arr;
use Pterodactyl\Models\Egg;
use Pterodactyl\Models\Nest;
use Pterodactyl\Models\Node;
use Pterodactyl\Models\User;
use Pterodactyl\Models\Mount;
use Pterodactyl\Models\Backup;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Location;
use Illuminate\Support\Facades\DB;
use Pterodactyl\Models\Allocation;
use Illuminate\Support\Facades\Http;
use Pterodactyl\Services\Helpers\SoftwareVersionService;
use Pterodactyl\Repositories\Eloquent\SettingsRepository;
use Pterodactyl\Repositories\Wings\DaemonConfigurationRepository;
class TelemetryCollectionService
{
/**
* TelemetryCollectionService constructor.
*/
public function __construct(
private DaemonConfigurationRepository $daemonConfigurationRepository,
private SettingsRepository $settingsRepository,
private SoftwareVersionService $softwareVersionService
) {
}
/**
* Collects telemetry data and sends it to the Pterodactyl Telemetry Service.
*/
public function __invoke(): void
{
try {
$data = $this->collect();
} catch (Exception) {
return;
}
Http::post('https://telemetry.pterodactyl.io', $data);
}
/**
* Collects telemetry data and returns it as an array.
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/
public function collect(): array
{
$uuid = $this->settingsRepository->get('app:telemetry:uuid');
if (is_null($uuid)) {
$uuid = Uuid::uuid4()->toString();
$this->settingsRepository->set('app:telemetry:uuid', $uuid);
}
$nodes = Node::all()->map(function ($node) {
try {
$info = $this->daemonConfigurationRepository->setNode($node)->getSystemInformation(2);
} catch (Exception) {
return null;
}
return [
'id' => $node->uuid,
'version' => Arr::get($info, 'version', ''),
'docker' => [
'version' => Arr::get($info, 'docker.version', ''),
'cgroups' => [
'driver' => Arr::get($info, 'docker.cgroups.driver', ''),
'version' => Arr::get($info, 'docker.cgroups.version', ''),
],
'containers' => [
'total' => Arr::get($info, 'docker.containers.total', -1),
'running' => Arr::get($info, 'docker.containers.running', -1),
'paused' => Arr::get($info, 'docker.containers.paused', -1),
'stopped' => Arr::get($info, 'docker.containers.stopped', -1),
],
'storage' => [
'driver' => Arr::get($info, 'docker.storage.driver', ''),
'filesystem' => Arr::get($info, 'docker.storage.filesystem', ''),
],
'runc' => [
'version' => Arr::get($info, 'docker.runc.version', ''),
],
],
'system' => [
'architecture' => Arr::get($info, 'system.architecture', ''),
'cpuThreads' => Arr::get($info, 'system.cpu_threads', ''),
'memoryBytes' => Arr::get($info, 'system.memory_bytes', ''),
'kernelVersion' => Arr::get($info, 'system.kernel_version', ''),
'os' => Arr::get($info, 'system.os', ''),
'osType' => Arr::get($info, 'system.os_type', ''),
],
];
})->filter(fn ($node) => !is_null($node))->toArray();
return [
'id' => $uuid,
'panel' => [
'version' => $this->softwareVersionService->getCurrentVersion(),
'phpVersion' => phpversion(),
'drivers' => [
'backup' => [
'type' => config('backups.default'),
],
'cache' => [
'type' => config('cache.default'),
],
'database' => [
'type' => config('database.default'),
'version' => DB::getPdo()->getAttribute(\PDO::ATTR_SERVER_VERSION),
],
],
],
'resources' => [
'allocations' => [
'count' => Allocation::count(),
'used' => Allocation::whereNotNull('server_id')->count(),
],
'backups' => [
'count' => Backup::count(),
'bytes' => Backup::sum('bytes'),
],
'eggs' => [
'count' => Egg::count(),
// Egg UUIDs are generated randomly on import, so there is not a consistent way to
// determine if servers are using default eggs or not.
// 'server_usage' => Egg::all()
// ->flatMap(fn (Egg $egg) => [$egg->uuid => $egg->servers->count()])
// ->filter(fn (int $count) => $count > 0)
// ->toArray(),
],
'locations' => [
'count' => Location::count(),
],
'mounts' => [
'count' => Mount::count(),
],
'nests' => [
'count' => Nest::count(),
// Nest UUIDs are generated randomly on import, so there is not a consistent way to
// determine if servers are using default eggs or not.
// 'server_usage' => Nest::all()
// ->flatMap(fn (Nest $nest) => [$nest->uuid => $nest->eggs->sum(fn (Egg $egg) => $egg->servers->count())])
// ->filter(fn (int $count) => $count > 0)
// ->toArray(),
],
'nodes' => [
'count' => Node::count(),
],
'servers' => [
'count' => Server::count(),
'suspended' => Server::where('status', Server::STATUS_SUSPENDED)->count(),
],
'users' => [
'count' => User::count(),
'admins' => User::where('root_admin', true)->count(),
],
],
'nodes' => $nodes,
];
}
}

View file

@ -2,8 +2,6 @@
namespace Pterodactyl\Services\Users;
use Exception;
use RuntimeException;
use Pterodactyl\Models\User;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
@ -38,8 +36,8 @@ class TwoFactorSetupService
for ($i = 0; $i < $this->config->get('pterodactyl.auth.2fa.bytes', 16); ++$i) {
$secret .= substr(self::VALID_BASE32_CHARACTERS, random_int(0, 31), 1);
}
} catch (Exception $exception) {
throw new RuntimeException($exception->getMessage(), 0, $exception);
} catch (\Exception $exception) {
throw new \RuntimeException($exception->getMessage(), 0, $exception);
}
$this->repository->withoutFreshModel()->update($user->id, [

View file

@ -25,7 +25,7 @@ class UserDeletionService
*
* @throws \Pterodactyl\Exceptions\DisplayException
*/
public function handle(int|User $user): ?bool
public function handle(int|User $user): void
{
if ($user instanceof User) {
$user = $user->id;
@ -36,6 +36,6 @@ class UserDeletionService
throw new DisplayException($this->translator->get('admin/user.exceptions.user_has_servers'));
}
return $this->repository->delete($user);
$this->repository->delete($user);
}
}