api(application): v2 backport

This commit is contained in:
Matthew Penner 2022-12-14 17:05:46 -07:00
parent 4cd0bee231
commit 67bf3e342e
No known key found for this signature in database
172 changed files with 2922 additions and 1579 deletions

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

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([

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\InvalidFileUploadException;
use Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException;
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,6 +3,7 @@
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;
@ -34,24 +35,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 +64,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

@ -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 = config()->get('app.version');
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

@ -49,9 +49,9 @@ class BuildModificationService
$merge = Arr::only($data, ['oom_disabled', '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();

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,
@ -63,18 +63,13 @@ class ServerConfigurationStructureService
],
'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) {