From 67ff67a1bdf8dca58030b7239ca4a30f5d2c09bc Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 22 Sep 2019 15:30:53 -0700 Subject: [PATCH] Add endpoints to return a server's egg configuration --- .../Api/Remote/EggRetrievalController.php | 74 -------- .../Servers/ServerConfigurationController.php | 50 ++++++ app/Models/Egg.php | 37 ++++ app/Services/Eggs/EggConfigurationService.php | 167 ++++++++++++++++-- .../ServerConfigurationStructureService.php | 16 +- .../Api/Application/BaseTransformer.php | 3 +- .../Api/Client/BaseClientTransformer.php | 5 +- .../Api/Client/EggTransformer.php | 30 ++++ .../Api/Client/ServerTransformer.php | 18 ++ routes/api-remote.php | 15 +- 10 files changed, 309 insertions(+), 106 deletions(-) delete mode 100644 app/Http/Controllers/Api/Remote/EggRetrievalController.php create mode 100644 app/Http/Controllers/Api/Remote/Servers/ServerConfigurationController.php create mode 100644 app/Transformers/Api/Client/EggTransformer.php diff --git a/app/Http/Controllers/Api/Remote/EggRetrievalController.php b/app/Http/Controllers/Api/Remote/EggRetrievalController.php deleted file mode 100644 index 30fb7eda3..000000000 --- a/app/Http/Controllers/Api/Remote/EggRetrievalController.php +++ /dev/null @@ -1,74 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Http\Controllers\Api\Remote; - -use Illuminate\Http\JsonResponse; -use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Services\Eggs\EggConfigurationService; -use Pterodactyl\Contracts\Repository\EggRepositoryInterface; - -class EggRetrievalController extends Controller -{ - /** - * @var \Pterodactyl\Services\Eggs\EggConfigurationService - */ - protected $configurationFileService; - - /** - * @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface - */ - protected $repository; - - /** - * OptionUpdateController constructor. - * - * @param \Pterodactyl\Contracts\Repository\EggRepositoryInterface $repository - * @param \Pterodactyl\Services\Eggs\EggConfigurationService $configurationFileService - */ - public function __construct( - EggRepositoryInterface $repository, - EggConfigurationService $configurationFileService - ) { - $this->configurationFileService = $configurationFileService; - $this->repository = $repository; - } - - /** - * Return a JSON array of Eggs and the SHA1 hash of their configuration file. - * - * @return \Illuminate\Http\JsonResponse - */ - public function index(): JsonResponse - { - $eggs = $this->repository->getAllWithCopyAttributes(); - - $response = []; - $eggs->each(function ($egg) use (&$response) { - $response[$egg->uuid] = sha1(json_encode($this->configurationFileService->handle($egg))); - }); - - return response()->json($response); - } - - /** - * Return the configuration file for a single Egg for the Daemon. - * - * @param string $uuid - * @return \Illuminate\Http\JsonResponse - * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function download(string $uuid): JsonResponse - { - $option = $this->repository->getWithCopyAttributes($uuid, 'uuid'); - - return response()->json($this->configurationFileService->handle($option)); - } -} diff --git a/app/Http/Controllers/Api/Remote/Servers/ServerConfigurationController.php b/app/Http/Controllers/Api/Remote/Servers/ServerConfigurationController.php new file mode 100644 index 000000000..eeb157542 --- /dev/null +++ b/app/Http/Controllers/Api/Remote/Servers/ServerConfigurationController.php @@ -0,0 +1,50 @@ +eggConfigurationService = $eggConfigurationService; + $this->repository = $repository; + } + + /** + * @param \Illuminate\Http\Request $request + * @param $uuid + * @return \Illuminate\Http\JsonResponse + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function __invoke(Request $request, $uuid) + { + $server = $this->repository->getByUuid($uuid); + + return JsonResponse::create( + $this->eggConfigurationService->handle($server) + ); + } +} diff --git a/app/Models/Egg.php b/app/Models/Egg.php index 1d13c8023..be02258a1 100644 --- a/app/Models/Egg.php +++ b/app/Models/Egg.php @@ -2,6 +2,43 @@ namespace Pterodactyl\Models; +/** + * @property int $id + * @property string $uuid + * @property int $nest_id + * @property string $author + * @property string $name + * @property string $description + * @property string $docker_image + * @property string|null $config_files + * @property string|null $config_startup + * @property string|null $config_logs + * @property string|null $config_stop + * @property int|null $config_from + * @property string|null $startup + * @property bool $script_is_privileged + * @property string|null $script_install + * @property string $script_entry + * @property string $script_container + * @property int|null $copy_script_from + * @property \Carbon\Carbon $created_at + * @property \Carbon\Carbon $updated_at + * + * @property string|null $copy_script_install + * @property string $copy_script_entry + * @property string $copy_script_container + * @property string|null $inherit_config_files + * @property string|null $inherit_config_startup + * @property string|null $inherit_config_logs + * @property string|null $inherit_config_stop + * + * @property \Pterodactyl\Models\Nest $nest + * @property \Illuminate\Support\Collection|\Pterodactyl\Models\Server[] $servers + * @property \Illuminate\Support\Collection|\Pterodactyl\Models\EggVariable[] $variables + * @property \Illuminate\Support\Collection|\Pterodactyl\Models\Pack[] $packs + * @property \Pterodactyl\Models\Egg|null $scriptFrom + * @property \Pterodactyl\Models\Egg|null $configFrom + */ class Egg extends Validable { /** diff --git a/app/Services/Eggs/EggConfigurationService.php b/app/Services/Eggs/EggConfigurationService.php index a73e3f6a8..185ed460e 100644 --- a/app/Services/Eggs/EggConfigurationService.php +++ b/app/Services/Eggs/EggConfigurationService.php @@ -9,46 +9,189 @@ namespace Pterodactyl\Services\Eggs; +use Illuminate\Support\Arr; +use Illuminate\Support\Str; use Pterodactyl\Models\Egg; +use Pterodactyl\Models\Server; use Pterodactyl\Contracts\Repository\EggRepositoryInterface; +use Pterodactyl\Services\Servers\ServerConfigurationStructureService; class EggConfigurationService { /** * @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface */ - protected $repository; + private $repository; + + /** + * @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService + */ + private $configurationStructureService; /** * EggConfigurationService constructor. * * @param \Pterodactyl\Contracts\Repository\EggRepositoryInterface $repository + * @param \Pterodactyl\Services\Servers\ServerConfigurationStructureService $configurationStructureService */ - public function __construct(EggRepositoryInterface $repository) - { + public function __construct( + EggRepositoryInterface $repository, + ServerConfigurationStructureService $configurationStructureService + ) { $this->repository = $repository; + $this->configurationStructureService = $configurationStructureService; } /** * Return an Egg file to be used by the Daemon. * - * @param int|\Pterodactyl\Models\Egg $egg + * @param \Pterodactyl\Models\Server $server * @return array * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function handle($egg): array + public function handle(Server $server): array { - if (! $egg instanceof Egg) { - $egg = $this->repository->getWithCopyAttributes($egg); + $configs = $this->replacePlaceholders( + $server, json_decode($server->egg->inherit_config_files) + ); + + return [ + 'startup' => json_decode($server->egg->inherit_config_startup), + 'stop' => $this->convertStopToNewFormat($server->egg->inherit_config_stop), + 'configs' => $configs, + ]; + } + + /** + * Converts a legacy stop string into a new generation stop option for a server. + * + * For most eggs, this ends up just being a command sent to the server console, but + * if the stop command is something starting with a caret (^), it will be converted + * into the associated kill signal for the instance. + * + * @param string $stop + * @return array + */ + protected function convertStopToNewFormat(string $stop): array + { + if (! Str::startsWith($stop, '^')) { + return [ + 'type' => 'command', + 'value' => $stop, + ]; + } + + $signal = substr($stop, 1); + if (strtoupper($signal) === 'C') { + return [ + 'type' => 'stop', + 'value' => null, + ]; } return [ - 'startup' => json_decode($egg->inherit_config_startup), - 'stop' => $egg->inherit_config_stop, - 'configs' => json_decode($egg->inherit_config_files), - 'log' => json_decode($egg->inherit_config_logs), - 'query' => 'none', + 'type' => 'signal', + 'value' => strtoupper($signal), ]; } + + /** + * @param \Pterodactyl\Models\Server $server + * @param object $configs + * @return array + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + protected function replacePlaceholders(Server $server, object $configs) + { + // Get the legacy configuration structure for the server so that we + // can property map the egg placeholders to values. + $structure = $this->configurationStructureService->handle($server); + + foreach ($configs as $file => $data) { + foreach ($data->find ?? [] as &$value) { + preg_match('/^{{(?.*)}}$/', $value, $matches); + + if (! $key = $matches['key'] ?? null) { + continue; + } + + // Matched something in {{server.X}} format, now replace that with the actual + // value from the server properties. + // + // The Daemon supports server.X, env.X, and config.X placeholders. + if (! Str::startsWith($key, ['server.', 'env.', 'config.'])) { + continue; + } + + // We don't want to do anything with config keys since the Daemon will need to handle + // that. For example, the Spigot egg uses "config.docker.interface" to identify the Docker + // interface to proxy through, but the Panel would be unaware of that. + if (Str::startsWith($key, 'config.')) { + $value = "{{{$key}}}"; + continue; + } + + // The legacy Daemon would set SERVER_MEMORY, SERVER_IP, and SERVER_PORT with their + // respective values on the Daemon side. Ensure that anything referencing those properly + // replaces them with the matching config value. + switch ($key) { + case 'server.build.env.SERVER_MEMORY': + case 'env.SERVER_MEMORY': + $key = 'server.build.memory'; + break; + case 'server.build.env.SERVER_IP': + case 'env.SERVER_IP': + $key = 'server.build.default.ip'; + break; + case 'server.build.env.SERVER_PORT': + case 'env.SERVER_PORT': + $key = 'server.build.default.port'; + break; + } + + // Replace anything starting with "server." with the value out of the server configuration + // array that used to be created for the old daemon. + if (Str::startsWith($key, 'server.')) { + $value = Arr::get( + $structure, preg_replace('/^server\./', '', $key), '' + ); + continue; + } + + // Finally, replace anything starting with env. with the expected environment + // variable from the server configuration. + $value = Arr::get( + $structure, preg_replace('/^env\./', 'build.env.', $key), '' + ); + } + } + + $response = []; + // Normalize the output of the configuration for the new Wings Daemon to more + // easily ingest, as well as make things more flexible down the road. + foreach ($configs as $file => $data) { + $append = ['file' => $file, 'replace' => []]; + + // I like to think I understand PHP pretty well, but if you don't pass $value + // by reference here, you'll end up with a resursive array loop if the config + // file has two replacements that reference the same item in the configuration + // array for the server. + foreach ($data as $key => &$value) { + if ($key !== 'find') { + $append[$key] = $value; + continue; + } + + foreach ($value as $find => $replace) { + $append['replace'][] = ['match' => $find, 'value' => $replace]; + } + } + + $response[] = $append; + } + + return $response; + } } diff --git a/app/Services/Servers/ServerConfigurationStructureService.php b/app/Services/Servers/ServerConfigurationStructureService.php index a087fad7d..be5ab2b8c 100644 --- a/app/Services/Servers/ServerConfigurationStructureService.php +++ b/app/Services/Servers/ServerConfigurationStructureService.php @@ -14,7 +14,7 @@ use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; class ServerConfigurationStructureService { - const REQUIRED_RELATIONS = ['allocation', 'allocations', 'pack', 'option']; + const REQUIRED_RELATIONS = ['allocation', 'allocations', 'pack', 'egg']; /** * @var \Pterodactyl\Services\Servers\EnvironmentService @@ -43,6 +43,9 @@ class ServerConfigurationStructureService /** * Return a configuration array for a specific server when passed a server model. * + * DO NOT MODIFY THIS FUNCTION. This powers legacy code handling for the new Wings + * daemon, if you modify the structure eggs will break unexpectedly. + * * @param \Pterodactyl\Models\Server $server * @return array * @@ -50,14 +53,7 @@ class ServerConfigurationStructureService */ public function handle(Server $server): array { - if (array_diff(self::REQUIRED_RELATIONS, $server->getRelations())) { - $server = $this->repository->getDataForCreation($server); - } - - $pack = $server->getRelation('pack'); - if (! is_null($pack)) { - $pack = $server->getRelation('pack')->uuid; - } + $server->loadMissing(self::REQUIRED_RELATIONS); return [ 'uuid' => $server->uuid, @@ -80,7 +76,7 @@ class ServerConfigurationStructureService ], 'service' => [ 'egg' => $server->egg->uuid, - 'pack' => $pack, + 'pack' => $server->pack ? $server->pack->uuid : null, 'skip_scripts' => $server->skip_scripts, ], 'rebuild' => false, diff --git a/app/Transformers/Api/Application/BaseTransformer.php b/app/Transformers/Api/Application/BaseTransformer.php index 410943712..563dd086c 100644 --- a/app/Transformers/Api/Application/BaseTransformer.php +++ b/app/Transformers/Api/Application/BaseTransformer.php @@ -8,7 +8,6 @@ use Illuminate\Container\Container; use Illuminate\Database\Eloquent\Model; use League\Fractal\TransformerAbstract; use Pterodactyl\Services\Acl\Api\AdminAcl; -use Pterodactyl\Transformers\Api\Client\BaseClientTransformer; use Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException; /** @@ -93,7 +92,7 @@ abstract class BaseTransformer extends TransformerAbstract $transformer = Container::getInstance()->makeWith($abstract, $parameters); $transformer->setKey($this->getKey()); - if (! $transformer instanceof self || $transformer instanceof BaseClientTransformer) { + if (! $transformer instanceof self) { throw new InvalidTransformerLevelException('Calls to ' . __METHOD__ . ' must return a transformer that is an instance of ' . __CLASS__); } diff --git a/app/Transformers/Api/Client/BaseClientTransformer.php b/app/Transformers/Api/Client/BaseClientTransformer.php index 1278d5ad5..c15e583ec 100644 --- a/app/Transformers/Api/Client/BaseClientTransformer.php +++ b/app/Transformers/Api/Client/BaseClientTransformer.php @@ -5,6 +5,7 @@ namespace Pterodactyl\Transformers\Api\Client; use Pterodactyl\Models\User; use Webmozart\Assert\Assert; use Pterodactyl\Models\Server; +use Illuminate\Container\Container; use Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException; use Pterodactyl\Transformers\Api\Application\BaseTransformer as BaseApplicationTransformer; @@ -63,7 +64,9 @@ abstract class BaseClientTransformer extends BaseApplicationTransformer */ protected function makeTransformer(string $abstract, array $parameters = []) { - $transformer = parent::makeTransformer($abstract, $parameters); + /** @var \Pterodactyl\Transformers\Api\Application\BaseTransformer $transformer */ + $transformer = Container::getInstance()->makeWith($abstract, $parameters); + $transformer->setKey($this->getKey()); if (! $transformer instanceof self) { throw new InvalidTransformerLevelException('Calls to ' . __METHOD__ . ' must return a transformer that is an instance of ' . __CLASS__); diff --git a/app/Transformers/Api/Client/EggTransformer.php b/app/Transformers/Api/Client/EggTransformer.php new file mode 100644 index 000000000..ed5b8c585 --- /dev/null +++ b/app/Transformers/Api/Client/EggTransformer.php @@ -0,0 +1,30 @@ + $egg->uuid, + 'name' => $egg->name, + ]; + } +} diff --git a/app/Transformers/Api/Client/ServerTransformer.php b/app/Transformers/Api/Client/ServerTransformer.php index 64282686a..7805c4bf4 100644 --- a/app/Transformers/Api/Client/ServerTransformer.php +++ b/app/Transformers/Api/Client/ServerTransformer.php @@ -2,10 +2,16 @@ namespace Pterodactyl\Transformers\Api\Client; +use Pterodactyl\Models\Egg; use Pterodactyl\Models\Server; class ServerTransformer extends BaseClientTransformer { + /** + * @var array + */ + protected $availableIncludes = ['egg']; + /** * @return string */ @@ -47,4 +53,16 @@ class ServerTransformer extends BaseClientTransformer ], ]; } + + /** + * Returns the egg associated with this server. + * + * @param \Pterodactyl\Models\Server $server + * @return \League\Fractal\Resource\Item + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException + */ + public function includeEgg(Server $server) + { + return $this->item($server->egg, $this->makeTransformer(EggTransformer::class), Egg::RESOURCE_NAME); + } } diff --git a/routes/api-remote.php b/routes/api-remote.php index 24cc78576..1da85b508 100644 --- a/routes/api-remote.php +++ b/routes/api-remote.php @@ -1,13 +1,10 @@ name('api.remote.authenticate'); -Route::post('/websocket/{token}', 'ValidateWebsocketController')->name('api.remote.authenticate_websocket'); -Route::post('/download-file', 'FileDownloadController@index')->name('api.remote.download_file'); +use Illuminate\Support\Facades\Route; -Route::group(['prefix' => '/eggs'], function () { - Route::get('/', 'EggRetrievalController@index')->name('api.remote.eggs'); - Route::get('/{uuid}', 'EggRetrievalController@download')->name('api.remote.eggs.download'); -}); +Route::get('/authenticate/{token}', 'ValidateKeyController@index'); +Route::post('/websocket/{token}', 'ValidateWebsocketController'); +Route::post('/download-file', 'FileDownloadController@index'); Route::group(['prefix' => '/scripts'], function () { Route::get('/{uuid}', 'EggInstallController@index')->name('api.remote.scripts'); @@ -16,3 +13,7 @@ Route::group(['prefix' => '/scripts'], function () { Route::group(['prefix' => '/sftp'], function () { Route::post('/', 'SftpController@index')->name('api.remote.sftp'); }); + +Route::group(['prefix' => '/servers/{uuid}'], function () { + Route::get('/configuration', 'Servers\ServerConfigurationController'); +});