diff --git a/app/Contracts/Repository/AllocationRepositoryInterface.php b/app/Contracts/Repository/AllocationRepositoryInterface.php index acfea56ea..47dfe9475 100644 --- a/app/Contracts/Repository/AllocationRepositoryInterface.php +++ b/app/Contracts/Repository/AllocationRepositoryInterface.php @@ -40,4 +40,21 @@ interface AllocationRepositoryInterface extends RepositoryInterface * @return \Illuminate\Support\Collection */ public function getUniqueAllocationIpsForNode(int $node): Collection; + + /** + * Return all of the allocations that exist for a node that are not currently + * allocated. + * + * @param int $node + * @return array + */ + public function getUnassignedAllocationIds(int $node): array; + + /** + * Get an array of all allocations that are currently assigned to a given server. + * + * @param int $server + * @return array + */ + public function getAssignedAllocationIds(int $server): array; } diff --git a/app/Exceptions/DisplayException.php b/app/Exceptions/DisplayException.php index a74f9be26..e6488f89c 100644 --- a/app/Exceptions/DisplayException.php +++ b/app/Exceptions/DisplayException.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Exceptions; @@ -66,7 +59,7 @@ class DisplayException extends PterodactylException if ($request->expectsJson()) { return response()->json(Handler::convertToArray($this, [ 'detail' => $this->getMessage(), - ]), method_exists($this, 'getStatusCode') ? $this->getStatusCode() : Response::HTTP_INTERNAL_SERVER_ERROR); + ]), method_exists($this, 'getStatusCode') ? $this->getStatusCode() : Response::HTTP_BAD_REQUEST); } app()->make(AlertsMessageBag::class)->danger($this->getMessage())->flash(); diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 450328096..952d94335 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -36,6 +36,8 @@ class Handler extends ExceptionHandler * @var array */ protected $dontFlash = [ + 'token', + 'secret', 'password', 'password_confirmation', ]; diff --git a/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php b/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php index 638948590..1a9cb889e 100644 --- a/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php +++ b/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php @@ -3,31 +3,42 @@ namespace Pterodactyl\Http\Controllers\Api\Application\Servers; use Pterodactyl\Models\Server; +use Pterodactyl\Services\Servers\BuildModificationService; use Pterodactyl\Services\Servers\DetailsModificationService; use Pterodactyl\Transformers\Api\Application\ServerTransformer; use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; use Pterodactyl\Http\Requests\Api\Application\Servers\UpdateServerDetailsRequest; +use Pterodactyl\Http\Requests\Api\Application\Servers\UpdateServerBuildConfigurationRequest; class ServerDetailsController extends ApplicationApiController { + /** + * @var \Pterodactyl\Services\Servers\BuildModificationService + */ + private $buildModificationService; + /** * @var \Pterodactyl\Services\Servers\DetailsModificationService */ - private $modificationService; + private $detailsModificationService; /** * ServerDetailsController constructor. * - * @param \Pterodactyl\Services\Servers\DetailsModificationService $modificationService + * @param \Pterodactyl\Services\Servers\BuildModificationService $buildModificationService + * @param \Pterodactyl\Services\Servers\DetailsModificationService $detailsModificationService */ - public function __construct(DetailsModificationService $modificationService) + public function __construct(BuildModificationService $buildModificationService, DetailsModificationService $detailsModificationService) { parent::__construct(); - $this->modificationService = $modificationService; + $this->buildModificationService = $buildModificationService; + $this->detailsModificationService = $detailsModificationService; } /** + * Update the details for a specific server. + * * @param \Pterodactyl\Http\Requests\Api\Application\Servers\UpdateServerDetailsRequest $request * @param \Pterodactyl\Models\Server $server * @return array @@ -38,7 +49,27 @@ class ServerDetailsController extends ApplicationApiController */ public function details(UpdateServerDetailsRequest $request, Server $server): array { - $server = $this->modificationService->returnUpdatedModel()->handle($server, $request->validated()); + $server = $this->detailsModificationService->returnUpdatedModel()->handle($server, $request->validated()); + + return $this->fractal->item($server) + ->transformWith($this->getTransformer(ServerTransformer::class)) + ->toArray(); + } + + /** + * Update the build details for a specific server. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\UpdateServerBuildConfigurationRequest $request + * @param \Pterodactyl\Models\Server $server + * @return array + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function build(UpdateServerBuildConfigurationRequest $request, Server $server): array + { + $server = $this->buildModificationService->handle($server, $request->validated()); return $this->fractal->item($server) ->transformWith($this->getTransformer(ServerTransformer::class)) diff --git a/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php b/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php new file mode 100644 index 000000000..893ff5ff7 --- /dev/null +++ b/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php @@ -0,0 +1,61 @@ +route()->parameter('server')->id); + + return [ + 'allocation' => $rules['allocation_id'], + 'memory' => $rules['memory'], + 'swap' => $rules['swap'], + 'io' => $rules['io'], + 'cpu' => $rules['cpu'], + 'disk' => $rules['disk'], + 'add_allocations' => 'bail|array', + 'add_allocations.*' => 'integer', + 'remove_allocations' => 'bail|array', + 'remove_allocations.*' => 'integer', + ]; + } + + /** + * Convert the allocation field into the expected format for the service handler. + * + * @return array + */ + public function validated() + { + $data = parent::validated(); + + $data['allocation_id'] = $data['allocation']; + unset($data['allocation']); + + return $data; + } + + /** + * Custom attributes to use in error message responses. + * + * @return array + */ + public function attributes() + { + return [ + 'add_allocations' => 'allocations to add', + 'remove_allocations' => 'allocations to remove', + 'add_allocations.*' => 'allocation to add', + 'remove_allocations.*' => 'allocation to remove', + ]; + } +} diff --git a/app/Repositories/Eloquent/AllocationRepository.php b/app/Repositories/Eloquent/AllocationRepository.php index f4830678d..82fe860d8 100644 --- a/app/Repositories/Eloquent/AllocationRepository.php +++ b/app/Repositories/Eloquent/AllocationRepository.php @@ -2,7 +2,6 @@ namespace Pterodactyl\Repositories\Eloquent; -use Pterodactyl\Models\Node; use Illuminate\Support\Collection; use Pterodactyl\Models\Allocation; use Illuminate\Contracts\Pagination\LengthAwarePaginator; @@ -68,4 +67,31 @@ class AllocationRepository extends EloquentRepository implements AllocationRepos ->orderByRaw('INET_ATON(ip) ASC') ->get($this->getColumns()); } + + /** + * Return all of the allocations that exist for a node that are not currently + * allocated. + * + * @param int $node + * @return array + */ + public function getUnassignedAllocationIds(int $node): array + { + $results = $this->getBuilder()->select('id')->whereNull('server_id')->where('node_id', $node)->get(); + + return $results->pluck('id')->toArray(); + } + + /** + * Get an array of all allocations that are currently assigned to a given server. + * + * @param int $server + * @return array + */ + public function getAssignedAllocationIds(int $server): array + { + $results = $this->getBuilder()->select('id')->where('server_id', $server)->get(); + + return $results->pluck('id')->toArray(); + } } diff --git a/app/Services/Servers/BuildModificationService.php b/app/Services/Servers/BuildModificationService.php index e952941c8..8924b2a04 100644 --- a/app/Services/Servers/BuildModificationService.php +++ b/app/Services/Servers/BuildModificationService.php @@ -2,7 +2,6 @@ namespace Pterodactyl\Services\Servers; -use Illuminate\Log\Writer; use Pterodactyl\Models\Server; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; @@ -10,6 +9,7 @@ use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; +use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; class BuildModificationService @@ -17,105 +17,58 @@ class BuildModificationService /** * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface */ - protected $allocationRepository; - - /** - * @var array - */ - protected $build = []; - - /** - * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface - */ - protected $daemonServerRepository; + private $allocationRepository; /** * @var \Illuminate\Database\ConnectionInterface */ - protected $database; + private $connection; /** - * @var null|int + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface */ - protected $firstAllocationId = null; + private $daemonServerRepository; /** * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface */ - protected $repository; - - /** - * @var \Illuminate\Log\Writer - */ - protected $writer; + private $repository; /** * BuildModificationService constructor. * * @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $allocationRepository - * @param \Illuminate\Database\ConnectionInterface $database + * @param \Illuminate\Database\ConnectionInterface $connection * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonServerRepository * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository - * @param \Illuminate\Log\Writer $writer */ public function __construct( AllocationRepositoryInterface $allocationRepository, - ConnectionInterface $database, + ConnectionInterface $connection, DaemonServerRepositoryInterface $daemonServerRepository, - ServerRepositoryInterface $repository, - Writer $writer + ServerRepositoryInterface $repository ) { $this->allocationRepository = $allocationRepository; $this->daemonServerRepository = $daemonServerRepository; - $this->database = $database; + $this->connection = $connection; $this->repository = $repository; - $this->writer = $writer; - } - - /** - * Set build array parameters. - * - * @param string $key - * @param mixed $value - */ - public function setBuild($key, $value) - { - $this->build[$key] = $value; - } - - /** - * Return the build array or an item out of the build array. - * - * @param string|null $attribute - * @return array|mixed|null - */ - public function getBuild($attribute = null) - { - if (is_null($attribute)) { - return $this->build; - } - - return array_get($this->build, $attribute); } /** * Change the build details for a specified server. * - * @param int|\Pterodactyl\Models\Server $server - * @param array $data + * @param \Pterodactyl\Models\Server $server + * @param array $data + * @return \Pterodactyl\Models\Server * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function handle($server, array $data) + public function handle(Server $server, array $data) { - if (! $server instanceof Server) { - $server = $this->repository->find($server); - } - - $data['allocation_id'] = array_get($data, 'allocation_id', $server->allocation_id); - $this->database->beginTransaction(); + $build = []; + $this->connection->beginTransaction(); $this->processAllocations($server, $data); if (isset($data['allocation_id']) && $data['allocation_id'] != $server->allocation_id) { @@ -128,108 +81,98 @@ class BuildModificationService throw new DisplayException(trans('admin/server.exceptions.default_allocation_not_found')); } - $this->setBuild('default', ['ip' => $allocation->ip, 'port' => $allocation->port]); + $build['default'] = ['ip' => $allocation->ip, 'port' => $allocation->port]; } - $server = $this->repository->update($server->id, [ - 'memory' => (int) array_get($data, 'memory', $server->memory), - 'swap' => (int) array_get($data, 'swap', $server->swap), - 'io' => (int) array_get($data, 'io', $server->io), - 'cpu' => (int) array_get($data, 'cpu', $server->cpu), - 'disk' => (int) array_get($data, 'disk', $server->disk), - 'allocation_id' => array_get($data, 'allocation_id', $server->allocation_id), + $server = $this->repository->withFreshModel()->update($server->id, [ + 'memory' => array_get($data, 'memory'), + 'swap' => array_get($data, 'swap'), + 'io' => array_get($data, 'io'), + 'cpu' => array_get($data, 'cpu'), + 'disk' => array_get($data, 'disk'), + 'allocation_id' => array_get($data, 'allocation_id'), ]); - $allocations = $this->allocationRepository->findWhere([ - ['server_id', '=', $server->id], - ]); + $allocations = $this->allocationRepository->findWhere([['server_id', '=', $server->id]]); - $this->setBuild('memory', (int) $server->memory); - $this->setBuild('swap', (int) $server->swap); - $this->setBuild('io', (int) $server->io); - $this->setBuild('cpu', (int) $server->cpu); - $this->setBuild('disk', (int) $server->disk); - $this->setBuild('ports|overwrite', $allocations->groupBy('ip')->map(function ($item) { + $build['memory'] = (int) $server->memory; + $build['swap'] = (int) $server->swap; + $build['io'] = (int) $server->io; + $build['cpu'] = (int) $server->cpu; + $build['disk'] = (int) $server->disk; + $build['ports|overwrite'] = $allocations->groupBy('ip')->map(function ($item) { return $item->pluck('port'); - })->toArray()); + })->toArray(); try { - $this->daemonServerRepository->setServer($server)->update([ - 'build' => $this->getBuild(), - ]); - - $this->database->commit(); + $this->daemonServerRepository->setServer($server)->update(['build' => $build]); + $this->connection->commit(); } catch (RequestException $exception) { - $response = $exception->getResponse(); - $this->writer->warning($exception); - - throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [ - 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), - ])); + throw new DaemonConnectionException($exception); } + + return $server; } /** - * Process the allocations being assigned in the data and ensure they are available for a server. + * Process the allocations being assigned in the data and ensure they + * are available for a server. * * @param \Pterodactyl\Models\Server $server * @param array $data * * @throws \Pterodactyl\Exceptions\DisplayException */ - public function processAllocations(Server $server, array &$data) + private function processAllocations(Server $server, array &$data) { + $firstAllocationId = null; + if (! array_key_exists('add_allocations', $data) && ! array_key_exists('remove_allocations', $data)) { return; } - // Loop through allocations to add. + // Handle the addition of allocations to this server. if (array_key_exists('add_allocations', $data) && ! empty($data['add_allocations'])) { - $unassigned = $this->allocationRepository->findWhere([ - ['server_id', '=', null], - ['node_id', '=', $server->node_id], - ])->pluck('id')->toArray(); + $unassigned = $this->allocationRepository->getUnassignedAllocationIds($server->node_id); + $updateIds = []; foreach ($data['add_allocations'] as $allocation) { if (! in_array($allocation, $unassigned)) { continue; } - $this->firstAllocationId = $this->firstAllocationId ?? $allocation; - $toUpdate[] = [$allocation]; + $firstAllocationId = $firstAllocationId ?? $allocation; + $updateIds[] = $allocation; } - if (isset($toUpdate)) { - $this->allocationRepository->updateWhereIn('id', $toUpdate, ['server_id' => $server->id]); - unset($toUpdate); + if (! empty($updateIds)) { + $this->allocationRepository->updateWhereIn('id', $updateIds, ['server_id' => $server->id]); } } - // Loop through allocations to remove. + // Handle removal of allocations from this server. if (array_key_exists('remove_allocations', $data) && ! empty($data['remove_allocations'])) { - $assigned = $this->allocationRepository->findWhere([ - ['server_id', '=', $server->id], - ])->pluck('id')->toArray(); + $assigned = $this->allocationRepository->getAssignedAllocationIds($server->id); + $updateIds = []; foreach ($data['remove_allocations'] as $allocation) { if (! in_array($allocation, $assigned)) { continue; } if ($allocation == $data['allocation_id']) { - if (is_null($this->firstAllocationId)) { + if (is_null($firstAllocationId)) { throw new DisplayException(trans('admin/server.exceptions.no_new_default_allocation')); } - $data['allocation_id'] = $this->firstAllocationId; + $data['allocation_id'] = $firstAllocationId; } - $toUpdate[] = [$allocation]; + $updateIds[] = $allocation; } - if (isset($toUpdate)) { - $this->allocationRepository->updateWhereIn('id', $toUpdate, ['server_id' => null]); - unset($toUpdate); + if (! empty($updateIds)) { + $this->allocationRepository->updateWhereIn('id', $updateIds, ['server_id' => null]); } } } diff --git a/routes/api-application.php b/routes/api-application.php index f98b3739b..72576b920 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -73,6 +73,7 @@ Route::group(['prefix' => '/servers'], function () { Route::get('/{server}', 'Servers\ServerController@view')->name('api.application.servers.view'); Route::patch('/{server}/details', 'Servers\ServerDetailsController@details')->name('api.application.servers.details'); + Route::patch('/{server}/build', 'Servers\ServerDetailsController@build')->name('api.application.servers.build'); Route::post('/{server}/suspend', 'Servers\ServerManagementController@suspend')->name('api.application.servers.suspend'); Route::post('/{server}/unsuspend', 'Servers\ServerManagementController@unsuspend')->name('api.application.servers.unsuspend');