diff --git a/app/Console/Commands/Server/RebuildServerCommand.php b/app/Console/Commands/Server/RebuildServerCommand.php index 562b10bd9..c6b562b06 100644 --- a/app/Console/Commands/Server/RebuildServerCommand.php +++ b/app/Console/Commands/Server/RebuildServerCommand.php @@ -12,12 +12,17 @@ namespace Pterodactyl\Console\Commands\Server; use Webmozart\Assert\Assert; use Illuminate\Console\Command; use GuzzleHttp\Exception\RequestException; -use Pterodactyl\Services\Servers\EnvironmentService; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Services\Servers\ServerConfigurationStructureService; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; class RebuildServerCommand extends Command { + /** + * @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService + */ + protected $configurationStructureService; + /** * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface */ @@ -28,11 +33,6 @@ class RebuildServerCommand extends Command */ protected $description = 'Rebuild a single server, all servers on a node, or all servers on the panel.'; - /** - * @var \Pterodactyl\Services\Servers\EnvironmentService - */ - protected $environmentService; - /** * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface */ @@ -49,18 +49,18 @@ class RebuildServerCommand extends Command * RebuildServerCommand constructor. * * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonRepository - * @param \Pterodactyl\Services\Servers\EnvironmentService $environmentService + * @param \Pterodactyl\Services\Servers\ServerConfigurationStructureService $configurationStructureService * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository */ public function __construct( DaemonServerRepositoryInterface $daemonRepository, - EnvironmentService $environmentService, + ServerConfigurationStructureService $configurationStructureService, ServerRepositoryInterface $repository ) { parent::__construct(); + $this->configurationStructureService = $configurationStructureService; $this->daemonRepository = $daemonRepository; - $this->environmentService = $environmentService; $this->repository = $repository; } @@ -74,19 +74,7 @@ class RebuildServerCommand extends Command $servers->each(function ($server) use ($bar) { $bar->clear(); - $json = [ - 'build' => [ - 'image' => $server->image, - 'env|overwrite' => $this->environmentService->process($server), - ], - 'service' => [ - 'type' => $server->option->service->folder, - 'option' => $server->option->tag, - 'pack' => object_get($server, 'pack.uuid'), - 'skip_scripts' => $server->skip_scripts, - ], - 'rebuild' => true, - ]; + $json = array_merge($this->configurationStructureService->handle($server), ['rebuild' => true]); try { $this->daemonRepository->setNode($server->node_id)->setAccessServer($server->uuid)->update($json); diff --git a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php index c64853b8c..6b7a86d45 100644 --- a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php @@ -9,17 +9,20 @@ namespace Pterodactyl\Contracts\Repository\Daemon; +use Psr\Http\Message\ResponseInterface; + interface ServerRepositoryInterface extends BaseRepositoryInterface { /** * Create a new server on the daemon for the panel. * - * @param int $id + * @param array $structure * @param array $overrides - * @param bool $start * @return \Psr\Http\Message\ResponseInterface + * + * @throws \GuzzleHttp\Exception\RequestException */ - public function create($id, array $overrides = [], $start = false); + public function create(array $structure, array $overrides = []): ResponseInterface; /** * Update server details on the daemon. diff --git a/app/Contracts/Repository/ServiceOptionRepositoryInterface.php b/app/Contracts/Repository/ServiceOptionRepositoryInterface.php index c7acd5a45..310b385ea 100644 --- a/app/Contracts/Repository/ServiceOptionRepositoryInterface.php +++ b/app/Contracts/Repository/ServiceOptionRepositoryInterface.php @@ -10,6 +10,7 @@ namespace Pterodactyl\Contracts\Repository; use Pterodactyl\Models\ServiceOption; +use Illuminate\Database\Eloquent\Collection; interface ServiceOptionRepositoryInterface extends RepositoryInterface { @@ -23,15 +24,21 @@ interface ServiceOptionRepositoryInterface extends RepositoryInterface */ public function getWithVariables(int $id): ServiceOption; + /** + * Return all of the service options and their relations to be used in the daemon API. + * + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getAllWithCopyAttributes(): Collection; + /** * Return a service option with the scriptFrom and configFrom relations loaded onto the model. * - * @param int $id + * @param int|string $value + * @param string $column * @return \Pterodactyl\Models\ServiceOption - * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function getWithCopyAttributes(int $id): ServiceOption; + public function getWithCopyAttributes($value, string $column = 'id'): ServiceOption; /** * Return all of the data needed to export a service. diff --git a/app/Http/Controllers/API/Remote/OptionRetrievalController.php b/app/Http/Controllers/API/Remote/OptionRetrievalController.php new file mode 100644 index 000000000..fdd715ca9 --- /dev/null +++ b/app/Http/Controllers/API/Remote/OptionRetrievalController.php @@ -0,0 +1,74 @@ +. + * + * 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\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Services\Services\Options\OptionConfigurationFileService; + +class OptionRetrievalController extends Controller +{ + /** + * @var \Pterodactyl\Services\Services\Options\OptionConfigurationFileService + */ + protected $configurationFileService; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface + */ + protected $repository; + + /** + * OptionUpdateController constructor. + * + * @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $repository + * @param \Pterodactyl\Services\Services\Options\OptionConfigurationFileService $configurationFileService + */ + public function __construct( + ServiceOptionRepositoryInterface $repository, + OptionConfigurationFileService $configurationFileService + ) { + $this->configurationFileService = $configurationFileService; + $this->repository = $repository; + } + + /** + * Return a JSON array of service options and the SHA1 hash of thier configuration file. + * + * @return \Illuminate\Http\JsonResponse + */ + public function index(): JsonResponse + { + $options = $this->repository->getAllWithCopyAttributes(); + + $response = []; + $options->each(function ($option) use (&$response) { + $response[$option->uuid] = sha1(json_encode($this->configurationFileService->handle($option))); + }); + + return response()->json($response); + } + + /** + * Return the configuration file for a single service option 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/Repositories/Daemon/ServerRepository.php b/app/Repositories/Daemon/ServerRepository.php index c1690eb1a..3515b26e4 100644 --- a/app/Repositories/Daemon/ServerRepository.php +++ b/app/Repositories/Daemon/ServerRepository.php @@ -10,61 +10,29 @@ namespace Pterodactyl\Repositories\Daemon; use Webmozart\Assert\Assert; -use Pterodactyl\Services\Servers\EnvironmentService; +use Psr\Http\Message\ResponseInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface as DatabaseServerRepositoryInterface; class ServerRepository extends BaseRepository implements ServerRepositoryInterface { /** - * {@inheritdoc} + * Create a new server on the daemon for the panel. + * + * @param array $structure + * @param array $overrides + * @return \Psr\Http\Message\ResponseInterface + * + * @throws \GuzzleHttp\Exception\RequestException */ - public function create($id, array $overrides = [], $start = false) + public function create(array $structure, array $overrides = []): ResponseInterface { - Assert::numeric($id, 'First argument passed to create must be numeric, received %s.'); - Assert::boolean($start, 'Third argument passed to create must be boolean, received %s.'); - - $repository = $this->app->make(DatabaseServerRepositoryInterface::class); - $environment = $this->app->make(EnvironmentService::class); - - $server = $repository->getDataForCreation($id); - - $data = [ - 'uuid' => (string) $server->uuid, - 'user' => $server->username, - 'build' => [ - 'default' => [ - 'ip' => $server->allocation->ip, - 'port' => $server->allocation->port, - ], - 'ports' => $server->allocations->groupBy('ip')->map(function ($item) { - return $item->pluck('port'); - })->toArray(), - 'env' => $environment->process($server), - 'memory' => (int) $server->memory, - 'swap' => (int) $server->swap, - 'io' => (int) $server->io, - 'cpu' => (int) $server->cpu, - 'disk' => (int) $server->disk, - 'image' => $server->image, - ], - 'service' => [ - 'type' => $server->option->service->folder, - 'option' => $server->option->tag, - 'pack' => object_get($server, 'pack.uuid'), - 'skip_scripts' => $server->skip_scripts, - ], - 'rebuild' => false, - 'start_on_completion' => $start, - ]; - // Loop through overrides. foreach ($overrides as $key => $value) { - array_set($data, $key, $value); + array_set($structure, $key, $value); } return $this->getHttpClient()->request('POST', 'servers', [ - 'json' => $data, + 'json' => $structure, ]); } diff --git a/app/Repositories/Eloquent/ServerRepository.php b/app/Repositories/Eloquent/ServerRepository.php index 5c86e4a89..2443f6b25 100644 --- a/app/Repositories/Eloquent/ServerRepository.php +++ b/app/Repositories/Eloquent/ServerRepository.php @@ -47,7 +47,7 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt Assert::nullOrIntegerish($server, 'First argument passed to getDataForRebuild must be null or integer, received %s.'); Assert::nullOrIntegerish($node, 'Second argument passed to getDataForRebuild must be null or integer, received %s.'); - $instance = $this->getBuilder()->with('node', 'option.service', 'pack'); + $instance = $this->getBuilder()->with('allocation', 'allocations', 'pack', 'option', 'node'); if (! is_null($server) && is_null($node)) { $instance = $instance->where('id', '=', $server); @@ -111,9 +111,7 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt */ public function getDataForCreation($id) { - $instance = $this->getBuilder()->with('allocation', 'allocations', 'pack', 'option.service') - ->find($id, $this->getColumns()); - + $instance = $this->getBuilder()->with(['allocation', 'allocations', 'pack', 'option'])->find($id, $this->getColumns()); if (! $instance) { throw new RecordNotFoundException(); } diff --git a/app/Repositories/Eloquent/ServiceOptionRepository.php b/app/Repositories/Eloquent/ServiceOptionRepository.php index 5d8bec31d..852726e52 100644 --- a/app/Repositories/Eloquent/ServiceOptionRepository.php +++ b/app/Repositories/Eloquent/ServiceOptionRepository.php @@ -9,7 +9,9 @@ namespace Pterodactyl\Repositories\Eloquent; +use Webmozart\Assert\Assert; use Pterodactyl\Models\ServiceOption; +use Illuminate\Database\Eloquent\Collection; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; @@ -42,18 +44,31 @@ class ServiceOptionRepository extends EloquentRepository implements ServiceOptio return $instance; } + /** + * Return all of the service options and their relations to be used in the daemon API. + * + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getAllWithCopyAttributes(): Collection + { + return $this->getBuilder()->with('scriptFrom', 'configFrom')->get($this->getColumns()); + } + /** * Return a service option with the scriptFrom and configFrom relations loaded onto the model. * - * @param int $id + * @param int|string $value + * @param string $column * @return \Pterodactyl\Models\ServiceOption * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function getWithCopyAttributes(int $id): ServiceOption + public function getWithCopyAttributes($value, string $column = 'id'): ServiceOption { + Assert::true((is_digit($value) || is_string($value)), 'First argument passed to getWithCopyAttributes must be an integer or string, received %s.'); + /** @var \Pterodactyl\Models\ServiceOption $instance */ - $instance = $this->getBuilder()->with('scriptFrom', 'configFrom')->find($id, $this->getColumns()); + $instance = $this->getBuilder()->with('scriptFrom', 'configFrom')->where($column, '=', $value)->first($this->getColumns()); if (! $instance) { throw new RecordNotFoundException; } diff --git a/app/Services/Servers/ServerConfigurationStructureService.php b/app/Services/Servers/ServerConfigurationStructureService.php new file mode 100644 index 000000000..78fee994a --- /dev/null +++ b/app/Services/Servers/ServerConfigurationStructureService.php @@ -0,0 +1,83 @@ +. + * + * This software is licensed under the terms of the MIT license. + * https://opensource.org/licenses/MIT + */ + +namespace Pterodactyl\Services\Servers; + +use Pterodactyl\Models\Server; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; + +class ServerConfigurationStructureService +{ + const REQUIRED_RELATIONS = ['allocation', 'allocations', 'pack', 'option']; + + /** + * @var \Pterodactyl\Services\Servers\EnvironmentService + */ + protected $environment; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $repository; + + /** + * ServerConfigurationStructureService constructor. + * + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository + * @param \Pterodactyl\Services\Servers\EnvironmentService $environment + */ + public function __construct( + ServerRepositoryInterface $repository, + EnvironmentService $environment + ) { + $this->repository = $repository; + $this->environment = $environment; + } + + /** + * @param int|\Pterodactyl\Models\Server $server + * @return array + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle($server): array + { + if (! $server instanceof Server || array_diff(self::REQUIRED_RELATIONS, $server->getRelations())) { + $server = $this->repository->getDataForCreation(is_digit($server) ? $server : $server->id); + } + + return [ + 'uuid' => $server->uuid, + 'user' => $server->username, + 'build' => [ + 'default' => [ + 'ip' => $server->allocation->ip, + 'port' => $server->allocation->port, + ], + 'ports' => $server->allocations->groupBy('ip')->map(function ($item) { + return $item->pluck('port'); + })->toArray(), + 'env' => $this->environment->process($server), + 'memory' => (int) $server->memory, + 'swap' => (int) $server->swap, + 'io' => (int) $server->io, + 'cpu' => (int) $server->cpu, + 'disk' => (int) $server->disk, + 'image' => $server->image, + ], + 'keys' => [], + 'service' => [ + 'option' => $server->option->uuid, + 'pack' => object_get($server, 'pack.uuid'), + 'skip_scripts' => $server->skip_scripts, + ], + 'rebuild' => false, + 'suspended' => (int) $server->suspended, + ]; + } +} diff --git a/app/Services/Servers/ServerCreationService.php b/app/Services/Servers/ServerCreationService.php index 122bac629..6ddf4496a 100644 --- a/app/Services/Servers/ServerCreationService.php +++ b/app/Services/Servers/ServerCreationService.php @@ -29,6 +29,11 @@ class ServerCreationService */ protected $allocationRepository; + /** + * @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService + */ + protected $configurationStructureService; + /** * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface */ @@ -81,6 +86,7 @@ class ServerCreationService * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonServerRepository * @param \Illuminate\Database\DatabaseManager $database * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $nodeRepository + * @param \Pterodactyl\Services\Servers\ServerConfigurationStructureService $configurationStructureService * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository * @param \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface $serverVariableRepository * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $userRepository @@ -93,6 +99,7 @@ class ServerCreationService DaemonServerRepositoryInterface $daemonServerRepository, DatabaseManager $database, NodeRepositoryInterface $nodeRepository, + ServerConfigurationStructureService $configurationStructureService, ServerRepositoryInterface $repository, ServerVariableRepositoryInterface $serverVariableRepository, UserRepositoryInterface $userRepository, @@ -102,6 +109,7 @@ class ServerCreationService ) { $this->allocationRepository = $allocationRepository; $this->daemonServerRepository = $daemonServerRepository; + $this->configurationStructureService = $configurationStructureService; $this->database = $database; $this->nodeRepository = $nodeRepository; $this->repository = $repository; @@ -175,10 +183,11 @@ class ServerCreationService } $this->serverVariableRepository->insert($records); + $structure = $this->configurationStructureService->handle($server->id); // Create the server on the daemon & commit it to the database. try { - $this->daemonServerRepository->setNode($server->node_id)->create($server->id); + $this->daemonServerRepository->setNode($server->node_id)->create($structure, ['start_on_completion' => (bool) $data['start_on_completion']]); $this->database->commit(); } catch (RequestException $exception) { $response = $exception->getResponse(); diff --git a/app/Services/Services/Options/OptionConfigurationFileService.php b/app/Services/Services/Options/OptionConfigurationFileService.php new file mode 100644 index 000000000..c21f435dd --- /dev/null +++ b/app/Services/Services/Options/OptionConfigurationFileService.php @@ -0,0 +1,51 @@ +. + * + * This software is licensed under the terms of the MIT license. + * https://opensource.org/licenses/MIT + */ + +namespace Pterodactyl\Services\Services\Options; + +use Pterodactyl\Models\ServiceOption; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; + +class OptionConfigurationFileService +{ + protected $repository; + + /** + * OptionConfigurationFileService constructor. + * + * @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $repository + */ + public function __construct(ServiceOptionRepositoryInterface $repository) + { + $this->repository = $repository; + } + + /** + * Return a service configuration file to be used by the daemon. + * + * @param int|\Pterodactyl\Models\ServiceOption $option + * @return array + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle($option): array + { + if (! $option instanceof ServiceOption) { + $option = $this->repository->getWithCopyAttributes($option); + } + + return [ + 'startup' => json_decode($option->inherit_config_startup), + 'stop' => $option->inherit_config_stop, + 'configs' => json_decode($option->inherit_config_files), + 'log' => json_decode($option->inherit_config_logs), + 'query' => 'none', + ]; + } +} diff --git a/routes/api-remote.php b/routes/api-remote.php index 54e5d8da4..39a371b2e 100644 --- a/routes/api-remote.php +++ b/routes/api-remote.php @@ -6,4 +6,9 @@ * This software is licensed under the terms of the MIT license. * https://opensource.org/licenses/MIT */ -Route::get('/authenticate/{token}', 'ValidateKeyController@index')->name('post.api.remote.authenticate'); +Route::get('/authenticate/{token}', 'ValidateKeyController@index')->name('api.remote.authenticate'); + +Route::group(['prefix' => '/options'], function () { + Route::get('/', 'OptionRetrievalController@index')->name('api.remote.services'); + Route::get('/{uuid}', 'OptionRetrievalController@download')->name('api.remote.services.download'); +}); diff --git a/routes/daemon.php b/routes/daemon.php index e6d34e971..96dd4e682 100644 --- a/routes/daemon.php +++ b/routes/daemon.php @@ -6,8 +6,6 @@ * This software is licensed under the terms of the MIT license. * https://opensource.org/licenses/MIT */ -Route::get('/services', 'ServiceController@listServices')->name('daemon.services'); -Route::get('/services/pull/{service}/{file}', 'ServiceController@pull')->name('daemon.pull'); Route::get('/packs/pull/{uuid}', 'PackController@pull')->name('daemon.pack.pull'); Route::get('/packs/pull/{uuid}/hash', 'PackController@hash')->name('daemon.pack.hash'); Route::get('/details/option/{server}', 'OptionController@details')->name('daemon.option.details');