diff --git a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php index d4554cc71..a69e1bb65 100644 --- a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php @@ -35,4 +35,12 @@ interface ServerRepositoryInterface extends BaseRepositoryInterface * @return \Psr\Http\Message\ResponseInterface */ public function create($id, $overrides = [], $start = false); + + /** + * Update server details on the daemon. + * + * @param array $data + * @return \Psr\Http\Message\ResponseInterface + */ + public function update(array $data); } diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index b068f4cc1..ba41dce6f 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -5,6 +5,8 @@ namespace Pterodactyl\Exceptions; use Exception; use Illuminate\Auth\AuthenticationException; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; +use Prologue\Alerts\Facades\Alert; +use Pterodactyl\Exceptions\Model\DataValidationException; class Handler extends ExceptionHandler { @@ -20,7 +22,9 @@ class Handler extends ExceptionHandler \Illuminate\Database\Eloquent\ModelNotFoundException::class, \Illuminate\Session\TokenMismatchException::class, \Illuminate\Validation\ValidationException::class, - \Pterodactyl\Exceptions\Model\DataValidationException::class, + DisplayException::class, + DisplayValidationException::class, + DataValidationException::class, ]; /** @@ -51,7 +55,7 @@ class Handler extends ExceptionHandler if ($request->expectsJson() || $request->isJson() || $request->is(...config('pterodactyl.json_routes'))) { $exception = $this->prepareException($exception); - if (config('app.debug') || $this->isHttpException($exception)) { + if (config('app.debug') || $this->isHttpException($exception) || $exception instanceof DisplayException) { $displayError = $exception->getMessage(); } else { $displayError = 'An unhandled exception was encountered with this request.'; @@ -64,6 +68,10 @@ class Handler extends ExceptionHandler ], ($this->isHttpException($exception)) ? $exception->getStatusCode() : 500, [], JSON_UNESCAPED_SLASHES); parent::report($exception); + } elseif ($exception instanceof DisplayException) { + Alert::danger($exception->getMessage())->flash(); + + return redirect()->back()->withInput(); } return (isset($response)) ? $response : parent::render($request, $exception); @@ -72,8 +80,8 @@ class Handler extends ExceptionHandler /** * Convert an authentication exception into an unauthenticated response. * - * @param \Illuminate\Http\Request $request - * @param \Illuminate\Auth\AuthenticationException $exception + * @param \Illuminate\Http\Request $request + * @param \Illuminate\Auth\AuthenticationException $exception * @return \Illuminate\Http\Response */ protected function unauthenticated($request, AuthenticationException $exception) diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index 35587cd20..16747c014 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -28,6 +28,7 @@ use Illuminate\Contracts\Config\Repository as ConfigRepository; use Log; use Alert; use Javascript; +use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; @@ -36,18 +37,23 @@ use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; use Pterodactyl\Http\Requests\Admin\ServerFormRequest; use Pterodactyl\Models; +use Pterodactyl\Models\Server; use Illuminate\Http\Request; use GuzzleHttp\Exception\TransferException; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository; -use Pterodactyl\Repositories\ServerRepository; -use Pterodactyl\Repositories\DatabaseRepository; use Pterodactyl\Exceptions\DisplayValidationException; use Pterodactyl\Services\Servers\CreationService; +use Pterodactyl\Services\Servers\DetailsModificationService; class ServersController extends Controller { + /** + * @var \Prologue\Alerts\AlertsMessageBag + */ + protected $alert; + /** * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface */ @@ -73,6 +79,11 @@ class ServersController extends Controller */ protected $databaseHostRepository; + /** + * @var \Pterodactyl\Services\Servers\DetailsModificationService + */ + protected $detailsModificationService; + /** * @var \Pterodactyl\Contracts\Repository\LocationRepositoryInterface */ @@ -99,22 +110,26 @@ class ServersController extends Controller protected $serviceRepository; public function __construct( + AlertsMessageBag $alert, AllocationRepositoryInterface $allocationRepository, ConfigRepository $config, CreationService $service, \Pterodactyl\Services\Database\CreationService $databaseCreationService, DatabaseRepositoryInterface $databaseRepository, DatabaseHostRepository $databaseHostRepository, + DetailsModificationService $detailsModificationService, LocationRepositoryInterface $locationRepository, NodeRepositoryInterface $nodeRepository, ServerRepositoryInterface $repository, ServiceRepositoryInterface $serviceRepository ) { + $this->alert = $alert; $this->allocationRepository = $allocationRepository; $this->config = $config; $this->databaseCreationService = $databaseCreationService; $this->databaseRepository = $databaseRepository; $this->databaseHostRepository = $databaseHostRepository; + $this->detailsModificationService = $detailsModificationService; $this->locationRepository = $locationRepository; $this->nodeRepository = $nodeRepository; $this->repository = $repository; @@ -321,61 +336,40 @@ class ServersController extends Controller /** * Update the details for a server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function setDetails(ServerFormRequest $request, Models\Server $server) + public function setDetails(Request $request, Server $server) { - dd($server); - $repo = new ServerRepository; - try { - $repo->updateDetails($id, array_merge( - $request->only('description'), - $request->intersect([ - 'owner_id', 'name', 'reset_token', - ]) - )); + $this->detailsModificationService->edit($server, $request->only([ + 'owner_id', 'name', 'description', 'reset_token', + ])); - Alert::success('Server details were successfully updated.')->flash(); - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.servers.view.details', $id)->withErrors(json_decode($ex->getMessage()))->withInput(); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception occured while attemping to update this server. This error has been logged.')->flash(); - } + $this->alert->success(trans('admin/server.alerts.details_updated'))->flash(); - return redirect()->route('admin.servers.view.details', $id)->withInput(); + return redirect()->route('admin.servers.view.details', $server->id); } /** * Set the new docker container for a server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function setContainer(Request $request, $id) + public function setContainer(Request $request, Server $server) { - $repo = new ServerRepository; + $this->detailsModificationService->setDockerImage($server, $request->input('docker_image')); + $this->alert->success(trans('admin/server.alerts.docker_image_updated'))->flash(); - try { - $repo->updateContainer($id, $request->intersect('docker_image')); - - Alert::success('Successfully updated this server\'s docker image.')->flash(); - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.servers.view.details', $id)->withErrors(json_decode($ex->getMessage()))->withInput(); - } catch (TransferException $ex) { - Log::warning($ex); - Alert::danger('A TransferException occured while attempting to update the container image. Is the daemon online? This error has been logged.'); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception occured while attemping to update this server\'s docker image. This error has been logged.')->flash(); - } - - return redirect()->route('admin.servers.view.details', $id); + return redirect()->route('admin.servers.view.details', $server->id); } /** diff --git a/app/Http/Requests/Admin/ServerFormRequest.php b/app/Http/Requests/Admin/ServerFormRequest.php index 569de1684..19445b58f 100644 --- a/app/Http/Requests/Admin/ServerFormRequest.php +++ b/app/Http/Requests/Admin/ServerFormRequest.php @@ -36,10 +36,6 @@ class ServerFormRequest extends AdminFormRequest */ public function rules() { - if ($this->method() === 'PATCH') { - return Server::getUpdateRulesForId($this->id); - } - return Server::getCreateRules(); } diff --git a/app/Repositories/Daemon/BaseRepository.php b/app/Repositories/Daemon/BaseRepository.php index 5f6f92b68..c56b2e428 100644 --- a/app/Repositories/Daemon/BaseRepository.php +++ b/app/Repositories/Daemon/BaseRepository.php @@ -88,11 +88,13 @@ class BaseRepository implements BaseRepositoryInterface public function getHttpClient($headers = []) { if (! is_null($this->accessServer)) { - $headers[] = ['X-Access-Server' => $this->getAccessServer()]; + $headers['X-Access-Server'] = $this->getAccessServer(); } if (! is_null($this->accessToken)) { - $headers[] = ['X-Access-Token' => $this->getAccessToken()]; + $headers['X-Access-Token'] = $this->getAccessToken(); + } elseif (! is_null($this->node)) { + $headers['X-Access-Token'] = $this->getNode()->daemonSecret; } return new Client([ diff --git a/app/Repositories/Daemon/ServerRepository.php b/app/Repositories/Daemon/ServerRepository.php index 476f927ba..f398abfa6 100644 --- a/app/Repositories/Daemon/ServerRepository.php +++ b/app/Repositories/Daemon/ServerRepository.php @@ -83,4 +83,14 @@ class ServerRepository extends BaseRepository implements ServerRepositoryInterfa 'json' => $data, ]); } + + /** + * {@inheritdoc} + */ + public function update(array $data) + { + return $this->getHttpClient()->request('PATCH', '/server', [ + 'json' => $data, + ]); + } } diff --git a/app/Services/Servers/CreationService.php b/app/Services/Servers/CreationService.php index 9073dfb1f..290f68a80 100644 --- a/app/Services/Servers/CreationService.php +++ b/app/Services/Servers/CreationService.php @@ -25,7 +25,7 @@ namespace Pterodactyl\Services\Servers; use Ramsey\Uuid\Uuid; -use Illuminate\Database\ConnectionInterface; +use Illuminate\Database\DatabaseManager; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; @@ -46,7 +46,7 @@ class CreationService protected $daemonServerRepository; /** - * @var \Illuminate\Database\ConnectionInterface + * @var \Illuminate\Database\DatabaseManager */ protected $database; @@ -84,35 +84,35 @@ class CreationService * CreationService constructor. * * @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $allocationRepository - * @param \Illuminate\Database\ConnectionInterface $database - * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonServerRepository - * @param \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface $serverVariableRepository + * @param \Illuminate\Database\DatabaseManager $database * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $nodeRepository - * @param \Pterodactyl\Services\Servers\UsernameGenerationService $usernameService + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository + * @param \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface $serverVariableRepository * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $userRepository + * @param \Pterodactyl\Services\Servers\UsernameGenerationService $usernameService * @param \Pterodactyl\Services\Servers\VariableValidatorService $validatorService */ public function __construct( AllocationRepositoryInterface $allocationRepository, - ConnectionInterface $database, - ServerRepositoryInterface $repository, DaemonServerRepositoryInterface $daemonServerRepository, - ServerVariableRepositoryInterface $serverVariableRepository, + DatabaseManager $database, NodeRepositoryInterface $nodeRepository, - UsernameGenerationService $usernameService, + ServerRepositoryInterface $repository, + ServerVariableRepositoryInterface $serverVariableRepository, UserRepositoryInterface $userRepository, + UsernameGenerationService $usernameService, VariableValidatorService $validatorService ) { $this->allocationRepository = $allocationRepository; + $this->daemonServerRepository = $daemonServerRepository; $this->database = $database; - $this->repository = $repository; $this->nodeRepository = $nodeRepository; + $this->repository = $repository; + $this->serverVariableRepository = $serverVariableRepository; $this->userRepository = $userRepository; $this->usernameService = $usernameService; $this->validatorService = $validatorService; - $this->serverVariableRepository = $serverVariableRepository; - $this->daemonServerRepository = $daemonServerRepository; } /** @@ -126,9 +126,6 @@ class CreationService public function create(array $data) { // @todo auto-deployment - $data['user_id'] = 1; - - $node = $this->nodeRepository->find($data['node_id']); $validator = $this->validatorService->setAdmin()->setFields($data['environment'])->validate($data['option_id']); $uniqueShort = bin2hex(random_bytes(4)); @@ -136,7 +133,7 @@ class CreationService $server = $this->repository->create([ 'uuid' => Uuid::uuid4()->toString(), - 'uuidShort' => bin2hex(random_bytes(4)), + 'uuidShort' => $uniqueShort, 'node_id' => $data['node_id'], 'name' => $data['name'], 'description' => $data['description'], @@ -152,7 +149,7 @@ class CreationService 'allocation_id' => $data['allocation_id'], 'service_id' => $data['service_id'], 'option_id' => $data['option_id'], - 'pack_id' => ($data['pack_id'] == 0) ? null : $data['pack_id'], + 'pack_id' => (! isset($data['pack_id']) || $data['pack_id'] == 0) ? null : $data['pack_id'], 'startup' => $data['startup'], 'daemonSecret' => bin2hex(random_bytes(18)), 'image' => $data['docker_image'], @@ -181,9 +178,7 @@ class CreationService $this->serverVariableRepository->insert($records); // Create the server on the daemon & commit it to the database. - $this->daemonServerRepository->setNode($server->node_id) - ->setAccessToken($node->daemonSecret) - ->create($server->id); + $this->daemonServerRepository->setNode($server->node_id)->create($server->id); $this->database->commit(); diff --git a/app/Services/Servers/DetailsModificationService.php b/app/Services/Servers/DetailsModificationService.php new file mode 100644 index 000000000..f1fb3d275 --- /dev/null +++ b/app/Services/Servers/DetailsModificationService.php @@ -0,0 +1,156 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Pterodactyl\Services\Servers; + +use GuzzleHttp\Exception\RequestException; +use Illuminate\Database\DatabaseManager; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\Server; +use Pterodactyl\Repositories\Eloquent\ServerRepository; +use Pterodactyl\Repositories\Daemon\ServerRepository as DaemonServerRepository; + +class DetailsModificationService +{ + /** + * @var \Illuminate\Database\DatabaseManager + */ + protected $database; + + /** + * @var \Pterodactyl\Repositories\Daemon\ServerRepository + */ + protected $daemonServerRepository; + + /** + * @var \Pterodactyl\Repositories\Eloquent\ServerRepository + */ + protected $repository; + + /** + * DetailsModificationService constructor. + * + * @param \Illuminate\Database\DatabaseManager $database + * @param \Pterodactyl\Repositories\Daemon\ServerRepository $daemonServerRepository + * @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository + */ + public function __construct( + DatabaseManager $database, + DaemonServerRepository $daemonServerRepository, + ServerRepository $repository + ) { + $this->database = $database; + $this->daemonServerRepository = $daemonServerRepository; + $this->repository = $repository; + } + + /** + * Update the details for a single server instance. + * + * @param int|\Pterodactyl\Models\Server $server + * @param array $data + * @return bool + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function edit($server, array $data) + { + if (! $server instanceof Server) { + $server = $this->repository->find($server); + } + + $this->database->beginTransaction(); + $currentSecret = $server->daemonSecret; + + if ( + (isset($data['reset_token']) && ! is_null($data['reset_token'])) || + (isset($data['owner_id']) && $data['owner_id'] != $server->owner_id) + ) { + $data['daemonSecret'] = bin2hex(random_bytes(18)); + $shouldUpdate = true; + } + + $this->repository->withoutFresh()->update($server->id, [ + 'owner_id' => array_get($data, 'owner_id') ?? $server->owner_id, + 'name' => array_get($data, 'name') ?? $server->name, + 'description' => array_get($data, 'description') ?? $server->description, + 'daemonSecret' => array_get($data, 'daemonSecret') ?? $server->daemonSecret, + ], true, true); + + // If there are no updates, lets save the changes and return. + if (! isset($shouldUpdate)) { + return $this->database->commit(); + } + + try { + $this->daemonServerRepository->setNode($server->node_id)->setAccessServer($server->uuid)->update([ + 'keys' => [ + (string) $currentSecret => [], + (string) $data['daemonSecret'] => $this->daemonServerRepository::DAEMON_PERMISSIONS, + ], + ]); + + return $this->database->commit(); + } catch (RequestException $exception) { + throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [ + 'code' => $exception->getResponse()->getStatusCode(), + ])); + } + } + + /** + * Update the docker container for a specified server. + * + * @param int|\Pterodactyl\Models\Server $server + * @param string $image + * @return bool + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function setDockerImage($server, $image) + { + if (! $server instanceof Server) { + $server = $this->repository->find($server); + } + + $this->database->beginTransaction(); + $this->repository->withoutFresh()->update($server->id, ['image' => $image]); + + try { + $this->daemonServerRepository->setNode($server->node_id)->setAccessServer($server->uuid)->update([ + 'build' => [ + 'image' => $image, + ], + ]); + + return $this->database->commit(); + } catch (RequestException $exception) { + throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [ + 'code' => $exception->getResponse()->getStatusCode(), + ])); + } + } +} diff --git a/app/Services/Servers/UsernameGenerationService.php b/app/Services/Servers/UsernameGenerationService.php index 10e3382f7..4c3569241 100644 --- a/app/Services/Servers/UsernameGenerationService.php +++ b/app/Services/Servers/UsernameGenerationService.php @@ -47,7 +47,7 @@ class UsernameGenerationService } // Filter the Server Name - $name = trim(preg_replace('/[^\w]+/', '', $name), '_'); + $name = trim(preg_replace('/[^A-Za-z0-9]+/', '', $name), '_'); $name = (strlen($name) < 1) ? str_random(6) : $name; return strtolower(substr($name, 0, 6) . '_' . $unique); diff --git a/app/Services/Servers/VariableValidatorService.php b/app/Services/Servers/VariableValidatorService.php index 1ff3b9cb9..3be004944 100644 --- a/app/Services/Servers/VariableValidatorService.php +++ b/app/Services/Servers/VariableValidatorService.php @@ -27,9 +27,8 @@ namespace Pterodactyl\Services\Servers; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; use Pterodactyl\Exceptions\DisplayValidationException; -use Illuminate\Validation\Factory as ValidationFactory; +use Illuminate\Contracts\Validation\Factory as ValidationFactory; use Pterodactyl\Contracts\Repository\OptionVariableRepositoryInterface; -use Pterodactyl\Exceptions\Services\Servers\RequiredVariableMissingException; class VariableValidatorService { @@ -64,10 +63,18 @@ class VariableValidatorService protected $serverVariableRepository; /** - * @var \Illuminate\Validation\Factory + * @var \Illuminate\Contracts\Validation\Factory */ protected $validator; + /** + * VariableValidatorService constructor. + * + * @param \Pterodactyl\Contracts\Repository\OptionVariableRepositoryInterface $optionVariableRepository + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $serverRepository + * @param \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface $serverVariableRepository + * @param \Illuminate\Contracts\Validation\Factory $validator + */ public function __construct( OptionVariableRepositoryInterface $optionVariableRepository, ServerRepositoryInterface $serverRepository, @@ -121,14 +128,6 @@ class VariableValidatorService } $variables->each(function ($item) { - if (! isset($this->fields[$item->env_variable]) && $item->required) { - if ($item->required) { - throw new RequiredVariableMissingException( - sprintf('Required service option variable %s was missing from this request.', $item->env_variable) - ); - } - } - // Skip doing anything if user is not an admin and variable is not user viewable // or editable. if (! $this->isAdmin && (! $item->user_editable || ! $item->user_viewable)) { @@ -145,7 +144,7 @@ class VariableValidatorService throw new DisplayValidationException(json_encode( collect([ 'notice' => [ - sprintf('There was a validation error with the %s variable.', $item->name), + trans('admin/server.exceptions.bad_variable', ['name' => $item->name]), ], ])->merge($validator->errors()->toArray()) )); diff --git a/composer.json b/composer.json index bd8f49da6..34de41f7f 100644 --- a/composer.json +++ b/composer.json @@ -12,14 +12,14 @@ ], "require": { "php": ">=7.0.0", + "ext-mbstring": "*", + "ext-pdo_mysql": "*", + "ext-zip": "*", "aws/aws-sdk-php": "3.29.7", "barryvdh/laravel-debugbar": "2.4.0", "daneeveritt/login-notifications": "1.0.0", "doctrine/dbal": "2.5.12", "edvinaskrucas/settings": "2.0.0", - "ext-mbstring": "*", - "ext-zip": "*", - "ext-pdo_mysql": "*", "fideloper/proxy": "3.3.3", "guzzlehttp/guzzle": "6.2.3", "igaster/laravel-theme": "1.16.0", @@ -33,6 +33,7 @@ "pragmarx/google2fa": "1.0.1", "predis/predis": "1.1.1", "prologue/alerts": "0.4.1", + "ramsey/uuid": "3.6.1", "s1lentium/iptools": "1.1.0", "sofa/eloquence": "5.4.1", "spatie/laravel-fractal": "4.0.1", diff --git a/composer.lock b/composer.lock index a6afca005..d6b01a579 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "f1afab5cf73088c6034bfb2b13631600", + "content-hash": "48a6ed67ba0a480511075590af7f8eba", "packages": [ { "name": "aws/aws-sdk-php", @@ -282,21 +282,21 @@ }, { "name": "doctrine/annotations", - "version": "v1.4.0", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "54cacc9b81758b14e3ce750f205a393d52339e97" + "reference": "5beebb01b025c94e93686b7a0ed3edae81fe3e7f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/54cacc9b81758b14e3ce750f205a393d52339e97", - "reference": "54cacc9b81758b14e3ce750f205a393d52339e97", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/5beebb01b025c94e93686b7a0ed3edae81fe3e7f", + "reference": "5beebb01b025c94e93686b7a0ed3edae81fe3e7f", "shasum": "" }, "require": { "doctrine/lexer": "1.*", - "php": "^5.6 || ^7.0" + "php": "^7.1" }, "require-dev": { "doctrine/cache": "1.*", @@ -305,7 +305,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4.x-dev" + "dev-master": "1.5.x-dev" } }, "autoload": { @@ -346,37 +346,41 @@ "docblock", "parser" ], - "time": "2017-02-24T16:22:25+00:00" + "time": "2017-07-22T10:58:02+00:00" }, { "name": "doctrine/cache", - "version": "v1.6.1", + "version": "v1.7.0", "source": { "type": "git", "url": "https://github.com/doctrine/cache.git", - "reference": "b6f544a20f4807e81f7044d31e679ccbb1866dc3" + "reference": "53d9518ffeb019c51d542ff60cb578f076d3ff16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/b6f544a20f4807e81f7044d31e679ccbb1866dc3", - "reference": "b6f544a20f4807e81f7044d31e679ccbb1866dc3", + "url": "https://api.github.com/repos/doctrine/cache/zipball/53d9518ffeb019c51d542ff60cb578f076d3ff16", + "reference": "53d9518ffeb019c51d542ff60cb578f076d3ff16", "shasum": "" }, "require": { - "php": "~5.5|~7.0" + "php": "~7.1" }, "conflict": { "doctrine/common": ">2.2,<2.4" }, "require-dev": { - "phpunit/phpunit": "~4.8|~5.0", - "predis/predis": "~1.0", - "satooshi/php-coveralls": "~0.6" + "alcaeus/mongo-php-adapter": "^1.1", + "mongodb/mongodb": "^1.1", + "phpunit/phpunit": "^5.7", + "predis/predis": "~1.0" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.6.x-dev" + "dev-master": "1.7.x-dev" } }, "autoload": { @@ -416,24 +420,24 @@ "cache", "caching" ], - "time": "2016-10-29T11:16:17+00:00" + "time": "2017-07-22T13:00:15+00:00" }, { "name": "doctrine/collections", - "version": "v1.4.0", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba" + "reference": "a01ee38fcd999f34d9bfbcee59dbda5105449cbf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/1a4fb7e902202c33cce8c55989b945612943c2ba", - "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba", + "url": "https://api.github.com/repos/doctrine/collections/zipball/a01ee38fcd999f34d9bfbcee59dbda5105449cbf", + "reference": "a01ee38fcd999f34d9bfbcee59dbda5105449cbf", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": "^7.1" }, "require-dev": { "doctrine/coding-standard": "~0.1@dev", @@ -483,20 +487,20 @@ "collections", "iterator" ], - "time": "2017-01-03T10:49:41+00:00" + "time": "2017-07-22T10:37:32+00:00" }, { "name": "doctrine/common", - "version": "v2.7.2", + "version": "v2.7.3", "source": { "type": "git", "url": "https://github.com/doctrine/common.git", - "reference": "930297026c8009a567ac051fd545bf6124150347" + "reference": "4acb8f89626baafede6ee5475bc5844096eba8a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/common/zipball/930297026c8009a567ac051fd545bf6124150347", - "reference": "930297026c8009a567ac051fd545bf6124150347", + "url": "https://api.github.com/repos/doctrine/common/zipball/4acb8f89626baafede6ee5475bc5844096eba8a9", + "reference": "4acb8f89626baafede6ee5475bc5844096eba8a9", "shasum": "" }, "require": { @@ -556,7 +560,7 @@ "persistence", "spl" ], - "time": "2017-01-13T14:02:13+00:00" + "time": "2017-07-22T08:35:12+00:00" }, { "name": "doctrine/dbal", @@ -631,33 +635,33 @@ }, { "name": "doctrine/inflector", - "version": "v1.1.0", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "90b2128806bfde671b6952ab8bea493942c1fdae" + "reference": "e11d84c6e018beedd929cff5220969a3c6d1d462" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/90b2128806bfde671b6952ab8bea493942c1fdae", - "reference": "90b2128806bfde671b6952ab8bea493942c1fdae", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/e11d84c6e018beedd929cff5220969a3c6d1d462", + "reference": "e11d84c6e018beedd929cff5220969a3c6d1d462", "shasum": "" }, "require": { - "php": ">=5.3.2" + "php": "^7.0" }, "require-dev": { - "phpunit/phpunit": "4.*" + "phpunit/phpunit": "^6.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "1.2.x-dev" } }, "autoload": { - "psr-0": { - "Doctrine\\Common\\Inflector\\": "lib/" + "psr-4": { + "Doctrine\\Common\\Inflector\\": "lib/Doctrine/Common/Inflector" } }, "notification-url": "https://packagist.org/downloads/", @@ -694,7 +698,7 @@ "singularize", "string" ], - "time": "2015-11-06T14:35:42+00:00" + "time": "2017-07-22T12:18:28+00:00" }, { "name": "doctrine/lexer", @@ -2346,16 +2350,16 @@ }, { "name": "psy/psysh", - "version": "v0.8.9", + "version": "v0.8.10", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "58a31cc4404c8f632d8c557bc72056af2d3a83db" + "reference": "7ab97e5a32202585309f3ee35a0c08d2a8e588b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/58a31cc4404c8f632d8c557bc72056af2d3a83db", - "reference": "58a31cc4404c8f632d8c557bc72056af2d3a83db", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/7ab97e5a32202585309f3ee35a0c08d2a8e588b1", + "reference": "7ab97e5a32202585309f3ee35a0c08d2a8e588b1", "shasum": "" }, "require": { @@ -2415,7 +2419,7 @@ "interactive", "shell" ], - "time": "2017-07-06T14:53:52+00:00" + "time": "2017-07-22T15:14:19+00:00" }, { "name": "ramsey/uuid", @@ -2653,16 +2657,16 @@ }, { "name": "spatie/fractalistic", - "version": "2.3.0", + "version": "2.3.1", "source": { "type": "git", "url": "https://github.com/spatie/fractalistic.git", - "reference": "79a48d949bc053a1c60c934f727f5901bf35fa74" + "reference": "2bba98fd266d4691395904be6d981bd09150802f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/fractalistic/zipball/79a48d949bc053a1c60c934f727f5901bf35fa74", - "reference": "79a48d949bc053a1c60c934f727f5901bf35fa74", + "url": "https://api.github.com/repos/spatie/fractalistic/zipball/2bba98fd266d4691395904be6d981bd09150802f", + "reference": "2bba98fd266d4691395904be6d981bd09150802f", "shasum": "" }, "require": { @@ -2700,7 +2704,7 @@ "spatie", "transform" ], - "time": "2017-07-03T08:20:31+00:00" + "time": "2017-07-21T23:08:30+00:00" }, { "name": "spatie/laravel-fractal", @@ -2816,7 +2820,7 @@ }, { "name": "symfony/console", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/console.git", @@ -2885,7 +2889,7 @@ }, { "name": "symfony/css-selector", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", @@ -2938,7 +2942,7 @@ }, { "name": "symfony/debug", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", @@ -2994,7 +2998,7 @@ }, { "name": "symfony/event-dispatcher", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", @@ -3057,7 +3061,7 @@ }, { "name": "symfony/finder", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", @@ -3106,16 +3110,16 @@ }, { "name": "symfony/http-foundation", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "f347a5f561b03db95ed666959db42bbbf429b7e5" + "reference": "e307abe4b79ccbbfdced9b91c132fd128f456bc5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f347a5f561b03db95ed666959db42bbbf429b7e5", - "reference": "f347a5f561b03db95ed666959db42bbbf429b7e5", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e307abe4b79ccbbfdced9b91c132fd128f456bc5", + "reference": "e307abe4b79ccbbfdced9b91c132fd128f456bc5", "shasum": "" }, "require": { @@ -3155,20 +3159,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2017-06-24T09:29:48+00:00" + "time": "2017-07-17T14:07:10+00:00" }, { "name": "symfony/http-kernel", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "33f87c957122cfbd9d90de48698ee074b71106ea" + "reference": "16ceea64d23abddf58797a782ae96a5242282cd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/33f87c957122cfbd9d90de48698ee074b71106ea", - "reference": "33f87c957122cfbd9d90de48698ee074b71106ea", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/16ceea64d23abddf58797a782ae96a5242282cd8", + "reference": "16ceea64d23abddf58797a782ae96a5242282cd8", "shasum": "" }, "require": { @@ -3241,7 +3245,7 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2017-07-05T13:28:15+00:00" + "time": "2017-07-17T19:08:23+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -3412,16 +3416,16 @@ }, { "name": "symfony/process", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "5ab8949b682b1bf9d4511a228b5e045c96758c30" + "reference": "07432804942b9f6dd7b7377faf9920af5f95d70a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/5ab8949b682b1bf9d4511a228b5e045c96758c30", - "reference": "5ab8949b682b1bf9d4511a228b5e045c96758c30", + "url": "https://api.github.com/repos/symfony/process/zipball/07432804942b9f6dd7b7377faf9920af5f95d70a", + "reference": "07432804942b9f6dd7b7377faf9920af5f95d70a", "shasum": "" }, "require": { @@ -3457,11 +3461,11 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2017-07-03T08:12:02+00:00" + "time": "2017-07-13T13:05:09+00:00" }, { "name": "symfony/routing", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", @@ -3539,7 +3543,7 @@ }, { "name": "symfony/translation", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", @@ -3604,16 +3608,16 @@ }, { "name": "symfony/var-dumper", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "9ee920bba1d2ce877496dcafca7cbffff4dbe08a" + "reference": "0f32b62d21991700250fed5109b092949007c5b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9ee920bba1d2ce877496dcafca7cbffff4dbe08a", - "reference": "9ee920bba1d2ce877496dcafca7cbffff4dbe08a", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/0f32b62d21991700250fed5109b092949007c5b3", + "reference": "0f32b62d21991700250fed5109b092949007c5b3", "shasum": "" }, "require": { @@ -3668,7 +3672,7 @@ "debug", "dump" ], - "time": "2017-07-05T13:02:37+00:00" + "time": "2017-07-10T14:18:27+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -3868,16 +3872,16 @@ "packages-dev": [ { "name": "barryvdh/laravel-ide-helper", - "version": "v2.4.0", + "version": "v2.4.1", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-ide-helper.git", - "reference": "87a02ff574f722c685e011f76692ab869ab64f6c" + "reference": "2b1273c45e2f8df7a625563e2283a17c14f02ae8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/87a02ff574f722c685e011f76692ab869ab64f6c", - "reference": "87a02ff574f722c685e011f76692ab869ab64f6c", + "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/2b1273c45e2f8df7a625563e2283a17c14f02ae8", + "reference": "2b1273c45e2f8df7a625563e2283a17c14f02ae8", "shasum": "" }, "require": { @@ -3937,7 +3941,7 @@ "phpstorm", "sublime" ], - "time": "2017-06-16T14:08:59+00:00" + "time": "2017-07-16T00:24:12+00:00" }, { "name": "barryvdh/reflection-docblock", @@ -4052,32 +4056,32 @@ }, { "name": "doctrine/instantiator", - "version": "1.0.5", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", + "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", "shasum": "" }, "require": { - "php": ">=5.3,<8.0-DEV" + "php": "^7.1" }, "require-dev": { "athletic/athletic": "~0.1.8", "ext-pdo": "*", "ext-phar": "*", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "~2.0" + "phpunit/phpunit": "^6.2.3", + "squizlabs/php_codesniffer": "^3.0.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.2.x-dev" } }, "autoload": { @@ -4102,7 +4106,7 @@ "constructor", "instantiate" ], - "time": "2015-06-14T21:17:01+00:00" + "time": "2017-07-22T11:58:36+00:00" }, { "name": "friendsofphp/php-cs-fixer", @@ -4587,22 +4591,22 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "3.1.1", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e" + "reference": "46f7e8bb075036c92695b15a1ddb6971c751e585" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/8331b5efe816ae05461b7ca1e721c01b46bafb3e", - "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/46f7e8bb075036c92695b15a1ddb6971c751e585", + "reference": "46f7e8bb075036c92695b15a1ddb6971c751e585", "shasum": "" }, "require": { "php": ">=5.5", "phpdocumentor/reflection-common": "^1.0@dev", - "phpdocumentor/type-resolver": "^0.2.0", + "phpdocumentor/type-resolver": "^0.4.0", "webmozart/assert": "^1.0" }, "require-dev": { @@ -4628,24 +4632,24 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2016-09-30T07:12:33+00:00" + "time": "2017-07-15T11:38:20+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "0.2.1", + "version": "0.4.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb" + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb", - "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", "shasum": "" }, "require": { - "php": ">=5.5", + "php": "^5.5 || ^7.0", "phpdocumentor/reflection-common": "^1.0" }, "require-dev": { @@ -4675,7 +4679,7 @@ "email": "me@mikevanriel.com" } ], - "time": "2016-11-25T06:54:22+00:00" + "time": "2017-07-14T14:27:02+00:00" }, { "name": "phpspec/prophecy", @@ -5755,7 +5759,7 @@ }, { "name": "symfony/class-loader", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/class-loader.git", @@ -5811,7 +5815,7 @@ }, { "name": "symfony/config", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/config.git", @@ -5873,16 +5877,16 @@ }, { "name": "symfony/filesystem", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "311fa718389efbd8b627c272b9324a62437018cc" + "reference": "427987eb4eed764c3b6e38d52a0f87989e010676" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/311fa718389efbd8b627c272b9324a62437018cc", - "reference": "311fa718389efbd8b627c272b9324a62437018cc", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/427987eb4eed764c3b6e38d52a0f87989e010676", + "reference": "427987eb4eed764c3b6e38d52a0f87989e010676", "shasum": "" }, "require": { @@ -5918,11 +5922,11 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2017-06-24T09:29:48+00:00" + "time": "2017-07-11T07:17:58+00:00" }, { "name": "symfony/stopwatch", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", @@ -5971,7 +5975,7 @@ }, { "name": "symfony/yaml", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", @@ -6083,8 +6087,8 @@ "platform": { "php": ">=7.0.0", "ext-mbstring": "*", - "ext-zip": "*", - "ext-pdo_mysql": "*" + "ext-pdo_mysql": "*", + "ext-zip": "*" }, "platform-dev": [] } diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index be96cd024..e517d5801 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -11,8 +11,36 @@ | */ +\Sofa\Eloquence\Model::unsetEventDispatcher(); + +$factory->define(Pterodactyl\Models\Server::class, function (Faker\Generator $faker) { + return [ + 'id' => $faker->randomNumber(), + 'uuid' => $faker->uuid, + 'uuidShort' => str_random(8), + 'name' => $faker->firstName, + 'description' => implode(' ', $faker->sentences()), + 'skip_scripts' => 0, + 'suspended' => 0, + 'memory' => 512, + 'swap' => 0, + 'disk' => 512, + 'io' => 500, + 'cpu' => 0, + 'oom_disabled' => 0, + 'pack_id' => null, + 'daemonSecret' => $faker->uuid, + 'username' => $faker->userName, + 'sftp_password' => null, + 'installed' => 1, + 'created_at' => \Carbon\Carbon::now(), + 'updated_at' => \Carbon\Carbon::now(), + ]; +}); + $factory->define(Pterodactyl\Models\User::class, function (Faker\Generator $faker) { return [ + 'id' => $faker->randomNumber(), 'external_id' => null, 'uuid' => $faker->uuid, 'username' => $faker->userName, @@ -57,3 +85,26 @@ $factory->define(Pterodactyl\Models\Node::class, function (Faker\Generator $fake 'daemonBase' => '/srv/daemon', ]; }); + +$factory->define(Pterodactyl\Models\ServiceVariable::class, function (Faker\Generator $faker) { + return [ + 'id' => $faker->randomNumber(), + 'name' => $faker->firstName, + 'description' => $faker->sentence(), + 'env_variable' => strtoupper(str_replace(' ', '_', $faker->words(2, true))), + 'default_value' => $faker->colorName, + 'user_viewable' => 0, + 'user_editable' => 0, + 'rules' => 'required|string', + 'created_at' => \Carbon\Carbon::now(), + 'updated_at' => \Carbon\Carbon::now(), + ]; +}); + +$factory->state(Pterodactyl\Models\ServiceVariable::class, 'viewable', function () { + return ['user_viewable' => 1]; +}); + +$factory->state(Pterodactyl\Models\ServiceVariable::class, 'editable', function () { + return ['user_editable' => 1]; +}); diff --git a/resources/lang/en/admin/server.php b/resources/lang/en/admin/server.php new file mode 100644 index 000000000..6ded58801 --- /dev/null +++ b/resources/lang/en/admin/server.php @@ -0,0 +1,34 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +return [ + 'exceptions' => [ + 'bad_variable' => 'There was a validation error with the :name variable.', + 'daemon_exception' => 'There was an exception while attempting to communicate with the daemon resulting in a HTTP/:code response code. This exception has been logged.', + ], + 'alerts' => [ + 'details_updated' => 'Server details have been successfully updated.', + 'docker_image_updated' => 'Successfully changed the default Docker image to use for this server. A reboot is required to apply this change.', + ], +]; diff --git a/resources/themes/pterodactyl/admin/servers/view/details.blade.php b/resources/themes/pterodactyl/admin/servers/view/details.blade.php index 8519a16c2..886df32df 100644 --- a/resources/themes/pterodactyl/admin/servers/view/details.blade.php +++ b/resources/themes/pterodactyl/admin/servers/view/details.blade.php @@ -89,6 +89,7 @@
@@ -102,13 +103,14 @@ diff --git a/routes/admin.php b/routes/admin.php index 039eafe3c..38785ee90 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -110,8 +110,6 @@ Route::group(['prefix' => 'servers'], function () { Route::post('/new', 'ServersController@store'); Route::post('/new/nodes', 'ServersController@nodes')->name('admin.servers.new.nodes'); - Route::post('/view/{id}/details', 'ServersController@setDetails'); - Route::post('/view/{id}/details/container', 'ServersController@setContainer')->name('admin.servers.view.details.container'); Route::post('/view/{id}/build', 'ServersController@updateBuild'); Route::post('/view/{id}/startup', 'ServersController@saveStartup'); Route::post('/view/{id}/database', 'ServersController@newDatabase'); @@ -121,6 +119,8 @@ Route::group(['prefix' => 'servers'], function () { Route::post('/view/{id}/manage/reinstall', 'ServersController@reinstallServer')->name('admin.servers.view.manage.reinstall'); Route::post('/view/{id}/delete', 'ServersController@delete'); + Route::patch('/view/{server}/details', 'ServersController@setDetails'); + Route::patch('/view/{server}/details/container', 'ServersController@setContainer')->name('admin.servers.view.details.container'); Route::patch('/view/{id}/database', 'ServersController@resetDatabasePassword'); Route::delete('/view/{id}/database/{database}/delete', 'ServersController@deleteDatabase')->name('admin.servers.view.database.delete'); diff --git a/tests/Unit/Services/Servers/CreationServiceTest.php b/tests/Unit/Services/Servers/CreationServiceTest.php new file mode 100644 index 000000000..3f1ac7c83 --- /dev/null +++ b/tests/Unit/Services/Servers/CreationServiceTest.php @@ -0,0 +1,224 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Tests\Unit\Services\Servers; + +use Mockery as m; +use phpmock\phpunit\PHPMock; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Services\Servers\CreationService; +use Pterodactyl\Services\Servers\UsernameGenerationService; +use Pterodactyl\Services\Servers\VariableValidatorService; +use Ramsey\Uuid\Uuid; +use Tests\TestCase; +use Illuminate\Database\DatabaseManager; +use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; + +class CreationServiceTest extends TestCase +{ + use PHPMock; + + /** + * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface + */ + protected $allocationRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonServerRepository; + + /** + * @var \Illuminate\Database\DatabaseManager + */ + protected $database; + + /** + * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + */ + protected $nodeRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface + */ + protected $serverVariableRepository; + + /** + * @var \Pterodactyl\Services\Servers\CreationService + */ + protected $service; + + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + */ + protected $userRepository; + + /** + * @var \Pterodactyl\Services\Servers\UsernameGenerationService + */ + protected $usernameService; + + /** + * @var \Pterodactyl\Services\Servers\VariableValidatorService + */ + protected $validatorService; + + /** + * @var \Ramsey\Uuid\Uuid + */ + protected $uuid; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->allocationRepository = m::mock(AllocationRepositoryInterface::class); + $this->daemonServerRepository = m::mock(DaemonServerRepositoryInterface::class); + $this->database = m::mock(DatabaseManager::class); + $this->nodeRepository = m::mock(NodeRepositoryInterface::class); + $this->repository = m::mock(ServerRepositoryInterface::class); + $this->serverVariableRepository = m::mock(ServerVariableRepositoryInterface::class); + $this->userRepository = m::mock(UserRepositoryInterface::class); + $this->usernameService = m::mock(UsernameGenerationService::class); + $this->validatorService = m::mock(VariableValidatorService::class); + $this->uuid = m::mock('overload:Ramsey\Uuid\Uuid'); + + $this->getFunctionMock('\\Pterodactyl\\Services\\Servers', 'bin2hex') + ->expects($this->any())->willReturn('randomstring'); + + $this->getFunctionMock('\\Ramsey\\Uuid\\Uuid', 'uuid4') + ->expects($this->any())->willReturn('s'); + + $this->service = new CreationService( + $this->allocationRepository, + $this->daemonServerRepository, + $this->database, + $this->nodeRepository, + $this->repository, + $this->serverVariableRepository, + $this->userRepository, + $this->usernameService, + $this->validatorService + ); + } + + /** + * Test core functionality of the creation process. + */ + public function testCreateShouldHitAllOfTheNecessaryServicesAndStoreTheServer() + { + $data = [ + 'node_id' => 1, + 'name' => 'SomeName', + 'description' => null, + 'user_id' => 1, + 'memory' => 128, + 'disk' => 128, + 'swap' => 0, + 'io' => 500, + 'cpu' => 0, + 'allocation_id' => 1, + 'allocation_additional' => [2, 3], + 'environment' => [ + 'TEST_VAR_1' => 'var1-value', + ], + 'service_id' => 1, + 'option_id' => 1, + 'startup' => 'startup-param', + 'docker_image' => 'some/image', + ]; + + $this->validatorService->shouldReceive('setAdmin')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('setFields')->with($data['environment'])->once()->andReturnSelf() + ->shouldReceive('validate')->with($data['option_id'])->once()->andReturnSelf(); + + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->uuid->shouldReceive('uuid4')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('toString')->withNoArgs()->once()->andReturn('uuid-0000'); + $this->usernameService->shouldReceive('generate')->with($data['name'], 'randomstring') + ->once()->andReturn('user_name'); + + $this->repository->shouldReceive('create')->with([ + 'uuid' => 'uuid-0000', + 'uuidShort' => 'randomstring', + 'node_id' => $data['node_id'], + 'name' => $data['name'], + 'description' => $data['description'], + 'skip_scripts' => false, + 'suspended' => false, + 'owner_id' => $data['user_id'], + 'memory' => $data['memory'], + 'swap' => $data['swap'], + 'disk' => $data['disk'], + 'io' => $data['io'], + 'cpu' => $data['cpu'], + 'oom_disabled' => false, + 'allocation_id' => $data['allocation_id'], + 'service_id' => $data['service_id'], + 'option_id' => $data['option_id'], + 'pack_id' => null, + 'startup' => $data['startup'], + 'daemonSecret' => 'randomstring', + 'image' => $data['docker_image'], + 'username' => 'user_name', + 'sftp_password' => null, + ])->once()->andReturn((object) [ + 'node_id' => 1, + 'id' => 1, + ]); + + $this->allocationRepository->shouldReceive('assignAllocationsToServer')->with(1, [1, 2, 3]); + $this->validatorService->shouldReceive('getResults')->withNoArgs()->once()->andReturn([[ + 'id' => 1, + 'key' => 'TEST_VAR_1', + 'value' => 'var1-value', + ]]); + + $this->serverVariableRepository->shouldReceive('insert')->with([[ + 'server_id' => 1, + 'variable_id' => 1, + 'variable_value' => 'var1-value', + ]])->once()->andReturnNull(); + $this->daemonServerRepository->shouldReceive('setNode')->with(1)->once()->andReturnSelf() + ->shouldReceive('create')->with(1)->once()->andReturnNull(); + $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $response = $this->service->create($data); + + $this->assertEquals(1, $response->id); + $this->assertEquals(1, $response->node_id); + } +} diff --git a/tests/Unit/Services/Servers/DetailsModificationServiceTest.php b/tests/Unit/Services/Servers/DetailsModificationServiceTest.php new file mode 100644 index 000000000..3ed8208a2 --- /dev/null +++ b/tests/Unit/Services/Servers/DetailsModificationServiceTest.php @@ -0,0 +1,394 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Tests\Unit\Services\Servers; + +use Exception; +use Mockery as m; +use Tests\TestCase; +use phpmock\phpunit\PHPMock; +use Pterodactyl\Models\Server; +use Illuminate\Database\DatabaseManager; +use GuzzleHttp\Exception\RequestException; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Repositories\Eloquent\ServerRepository; +use Pterodactyl\Services\Servers\DetailsModificationService; +use Pterodactyl\Repositories\Daemon\ServerRepository as DaemonServerRepository; + +class DetailsModificationServiceTest extends TestCase +{ + use PHPMock; + + /** + * @var \Illuminate\Database\DatabaseManager + */ + protected $database; + + /** + * @var \Pterodactyl\Repositories\Daemon\ServerRepository + */ + protected $daemonServerRepository; + + /** + * @var \GuzzleHttp\Exception\RequestException + */ + protected $exception; + + /** + * @var \Pterodactyl\Repositories\Eloquent\ServerRepository + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Servers\DetailsModificationService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->database = m::mock(DatabaseManager::class); + $this->exception = m::mock(RequestException::class)->makePartial(); + $this->daemonServerRepository = m::mock(DaemonServerRepository::class); + $this->repository = m::mock(ServerRepository::class); + + $this->getFunctionMock('\\Pterodactyl\\Services\\Servers', 'bin2hex') + ->expects($this->any())->willReturn('randomString'); + + $this->service = new DetailsModificationService( + $this->database, + $this->daemonServerRepository, + $this->repository + ); + } + + /** + * Test basic updating of core variables when a model is provided. + */ + public function testEditShouldSkipDatabaseSearchIfModelIsPassed() + { + $server = factory(Server::class)->make([ + 'owner_id' => 1, + ]); + + $data = ['owner_id' => 1, 'name' => 'New Name', 'description' => 'New Description']; + + $this->repository->shouldNotReceive('find'); + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($server->id, [ + 'owner_id' => $data['owner_id'], + 'name' => $data['name'], + 'description' => $data['description'], + 'daemonSecret' => $server->daemonSecret, + ], true, true)->once()->andReturnNull(); + + $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturn(true); + + $response = $this->service->edit($server, $data); + $this->assertTrue($response); + } + + /** + * Test that repository attempts to find model in database if no model is passed. + */ + public function testEditShouldGetModelFromRepositoryIfNotPassed() + { + $server = factory(Server::class)->make([ + 'owner_id' => 1, + ]); + + $data = ['owner_id' => 1, 'name' => 'New Name', 'description' => 'New Description']; + + $this->repository->shouldReceive('find')->with($server->id)->once()->andReturn($server); + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($server->id, [ + 'owner_id' => $data['owner_id'], + 'name' => $data['name'], + 'description' => $data['description'], + 'daemonSecret' => $server->daemonSecret, + ], true, true)->once()->andReturnNull(); + + $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturn(true); + + $response = $this->service->edit($server->id, $data); + $this->assertTrue($response); + } + + /** + * Test that the daemon secret is reset if the owner id changes. + */ + public function testEditShouldResetDaemonSecretIfOwnerIdIsChanged() + { + $server = factory(Server::class)->make([ + 'owner_id' => 1, + 'node_id' => 1, + ]); + + $data = ['owner_id' => 2, 'name' => 'New Name', 'description' => 'New Description']; + + $this->repository->shouldNotReceive('find'); + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($server->id, [ + 'owner_id' => $data['owner_id'], + 'name' => $data['name'], + 'description' => $data['description'], + 'daemonSecret' => 'randomString', + ], true, true)->once()->andReturnNull(); + + $this->daemonServerRepository->shouldReceive('setNode')->with($server->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($server->uuid)->once()->andReturnSelf() + ->shouldReceive('update')->with([ + 'keys' => [ + $server->daemonSecret => [], + 'randomString' => DaemonServerRepository::DAEMON_PERMISSIONS, + ], + ])->once()->andReturnNull(); + + $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturn(true); + + $response = $this->service->edit($server, $data); + $this->assertTrue($response); + } + + public function testEditShouldResetDaemonSecretIfBooleanValueIsPassed() + { + $server = factory(Server::class)->make([ + 'owner_id' => 1, + 'node_id' => 1, + ]); + + $data = ['owner_id' => 1, 'name' => 'New Name', 'description' => 'New Description', 'reset_token' => true]; + + $this->repository->shouldNotReceive('find'); + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($server->id, [ + 'owner_id' => $data['owner_id'], + 'name' => $data['name'], + 'description' => $data['description'], + 'daemonSecret' => 'randomString', + ], true, true)->once()->andReturnNull(); + + $this->daemonServerRepository->shouldReceive('setNode')->with($server->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($server->uuid)->once()->andReturnSelf() + ->shouldReceive('update')->with([ + 'keys' => [ + $server->daemonSecret => [], + 'randomString' => DaemonServerRepository::DAEMON_PERMISSIONS, + ], + ])->once()->andReturnNull(); + + $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturn(true); + + $response = $this->service->edit($server, $data); + $this->assertTrue($response); + } + + /** + * Test that a displayable exception is thrown if the daemon responds with an error. + */ + public function testEditShouldThrowADisplayableExceptionIfDaemonResponseErrors() + { + $server = factory(Server::class)->make([ + 'owner_id' => 1, + 'node_id' => 1, + ]); + + $data = ['owner_id' => 2, 'name' => 'New Name', 'description' => 'New Description']; + + $this->repository->shouldNotReceive('find'); + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($server->id, [ + 'owner_id' => $data['owner_id'], + 'name' => $data['name'], + 'description' => $data['description'], + 'daemonSecret' => 'randomString', + ], true, true)->once()->andReturnNull(); + + $this->daemonServerRepository->shouldReceive('setNode')->andThrow($this->exception); + $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('getStatusCode')->withNoArgs()->once()->andReturn(400); + + $this->database->shouldNotReceive('commit'); + + try { + $this->service->edit($server, $data); + } catch (Exception $exception) { + $this->assertInstanceOf(DisplayException::class, $exception); + $this->assertEquals( + trans('admin/server.exceptions.daemon_exception', ['code' => 400,]), $exception->getMessage() + ); + } + } + + /** + * Test that an exception not stemming from Guzzle is not thrown as a displayable exception. + * + * @expectedException \Exception + */ + public function testEditShouldNotThrowDisplayableExceptionIfExceptionIsNotThrownByGuzzle() + { + $server = factory(Server::class)->make([ + 'owner_id' => 1, + 'node_id' => 1, + ]); + + $data = ['owner_id' => 2, 'name' => 'New Name', 'description' => 'New Description']; + + $this->repository->shouldNotReceive('find'); + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($server->id, [ + 'owner_id' => $data['owner_id'], + 'name' => $data['name'], + 'description' => $data['description'], + 'daemonSecret' => 'randomString', + ], true, true)->once()->andReturnNull(); + + $this->daemonServerRepository->shouldReceive('setNode')->andThrow(new Exception()); + $this->database->shouldNotReceive('commit'); + + $this->service->edit($server, $data); + } + + /** + * Test that the docker image for a server can be updated if a model is provided. + */ + public function testDockerImageCanBeUpdatedWhenAServerModelIsProvided() + { + $server = factory(Server::class)->make(['node_id' => 1]); + + $this->repository->shouldNotReceive('find'); + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($server->id, [ + 'image' => 'new/image', + ])->once()->andReturnNull(); + + $this->daemonServerRepository->shouldReceive('setNode')->with($server->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($server->uuid)->once()->andReturnSelf() + ->shouldReceive('update')->with([ + 'build' => [ + 'image' => 'new/image', + ], + ])->once()->andReturnNull(); + + $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturn(true); + + $response = $this->service->setDockerImage($server, 'new/image'); + $this->assertTrue($response); + } + + /** + * Test that the docker image for a server can be updated if a model is provided. + */ + public function testDockerImageCanBeUpdatedWhenNoModelIsProvided() + { + $server = factory(Server::class)->make(['node_id' => 1]); + + $this->repository->shouldReceive('find')->with($server->id)->once()->andReturn($server); + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($server->id, [ + 'image' => 'new/image', + ])->once()->andReturnNull(); + + $this->daemonServerRepository->shouldReceive('setNode')->with($server->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($server->uuid)->once()->andReturnSelf() + ->shouldReceive('update')->with([ + 'build' => [ + 'image' => 'new/image', + ], + ])->once()->andReturnNull(); + + $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturn(true); + + $response = $this->service->setDockerImage($server->id, 'new/image'); + $this->assertTrue($response); + } + + /** + * Test that an exception thrown by Guzzle is rendered as a displayable exception. + */ + public function testExceptionThrownByGuzzleWhenSettingDockerImageShouldBeRenderedAsADisplayableException() + { + $server = factory(Server::class)->make(['node_id' => 1]); + + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($server->id, [ + 'image' => 'new/image', + ])->once()->andReturnNull(); + + $this->daemonServerRepository->shouldReceive('setNode')->andThrow($this->exception); + $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('getStatusCode')->withNoArgs()->once()->andReturn(400); + + $this->database->shouldNotReceive('commit'); + + try { + $this->service->setDockerImage($server, 'new/image'); + } catch (Exception $exception) { + $this->assertInstanceOf(DisplayException::class, $exception); + $this->assertEquals( + trans('admin/server.exceptions.daemon_exception', ['code' => 400,]), $exception->getMessage() + ); + } + } + + /** + * Test that an exception not thrown by Guzzle is not transformed to a displayable exception. + * + * @expectedException \Exception + */ + public function testExceptionNotThrownByGuzzleWhenSettingDockerImageShouldNotBeRenderedAsADisplayableException() + { + $server = factory(Server::class)->make(['node_id' => 1]); + + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($server->id, [ + 'image' => 'new/image', + ])->once()->andReturnNull(); + + $this->daemonServerRepository->shouldReceive('setNode')->andThrow(new Exception()); + $this->database->shouldNotReceive('commit'); + + $this->service->setDockerImage($server, 'new/image'); + } +} diff --git a/tests/Unit/Services/Servers/EnvironmentServiceTest.php b/tests/Unit/Services/Servers/EnvironmentServiceTest.php new file mode 100644 index 000000000..7dc5c2719 --- /dev/null +++ b/tests/Unit/Services/Servers/EnvironmentServiceTest.php @@ -0,0 +1,155 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Tests\Unit\Services\Servers; + +use Mockery as m; +use Tests\TestCase; +use Pterodactyl\Models\Server; +use Pterodactyl\Models\Location; +use Pterodactyl\Services\Servers\EnvironmentService; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; + +class EnvironmentServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Servers\EnvironmentService + */ + protected $service; + + /** + * @var \Pterodactyl\Models\Server + */ + protected $server; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->repository = m::mock(ServerRepositoryInterface::class); + $this->server = factory(Server::class)->make([ + 'location' => factory(Location::class)->make(), + ]); + + $this->service = new EnvironmentService($this->repository); + } + + /** + * Test that set environment key function returns an instance of the class. + */ + public function testSettingEnvironmentKeyShouldReturnInstanceOfSelf() + { + $instance = $this->service->setEnvironmentKey('TEST_KEY', function () { + return true; + }); + + $this->assertInstanceOf(EnvironmentService::class, $instance); + } + + /** + * Test that environment defaults are returned by the process function. + */ + public function testProcessShouldReturnDefaultEnvironmentVariablesForAServer() + { + $this->repository->shouldReceive('getVariablesWithValues')->with($this->server->id)->once()->andReturn([ + 'TEST_VARIABLE' => 'Test Variable', + ]); + + $response = $this->service->process($this->server); + + $this->assertEquals(count(EnvironmentService::ENVIRONMENT_CASTS) + 1, count($response), 'Assert response contains correct amount of items.'); + $this->assertTrue(is_array($response), 'Assert that response is an array.'); + + $this->assertArrayHasKey('TEST_VARIABLE', $response); + $this->assertEquals('Test Variable', $response['TEST_VARIABLE']); + + foreach (EnvironmentService::ENVIRONMENT_CASTS as $key => $value) { + $this->assertArrayHasKey($key, $response); + $this->assertEquals(object_get($this->server, $value), $response[$key]); + } + } + + /** + * Test that variables included at run-time are also included. + */ + public function testProcessShouldReturnKeySetAtRuntime() + { + $this->repository->shouldReceive('getVariablesWithValues')->with($this->server->id)->once()->andReturn([]); + + $response = $this->service->setEnvironmentKey('TEST_VARIABLE', function ($server) { + return $server->uuidShort; + })->process($this->server); + + $this->assertTrue(is_array($response), 'Assert response is an array.'); + $this->assertArrayHasKey('TEST_VARIABLE', $response); + $this->assertEquals($this->server->uuidShort, $response['TEST_VARIABLE']); + } + + /** + * Test that duplicate variables provided at run-time override the defaults. + */ + public function testProcessShouldAllowOverwritingDefaultVariablesWithRuntimeProvided() + { + $this->repository->shouldReceive('getVariablesWithValues')->with($this->server->id)->once()->andReturn([]); + + $response = $this->service->setEnvironmentKey('P_SERVER_UUID', function ($server) { + return 'overwritten'; + })->process($this->server); + + $this->assertTrue(is_array($response), 'Assert response is an array.'); + $this->assertArrayHasKey('P_SERVER_UUID', $response); + $this->assertEquals('overwritten', $response['P_SERVER_UUID']); + } + + /** + * Test that function can run when an ID is provided rather than a server model. + */ + public function testProcessShouldAcceptAnIntegerInPlaceOfAServerModel() + { + $this->repository->shouldReceive('find')->with($this->server->id)->once()->andReturn($this->server); + $this->repository->shouldReceive('getVariablesWithValues')->with($this->server->id)->once()->andReturn([]); + + $response = $this->service->process($this->server->id); + + $this->assertTrue(is_array($response), 'Assert that response is an array.'); + } + + /** + * Test that an exception is thrown when no model or valid ID is provided. + * + * @expectedException \InvalidArgumentException + */ + public function testProcessShouldThrowExceptionIfInvalidServerIsProvided() + { + $this->service->process('abcd'); + } +} diff --git a/tests/Unit/Services/Servers/UsernameGenerationServiceTest.php b/tests/Unit/Services/Servers/UsernameGenerationServiceTest.php new file mode 100644 index 000000000..c0d80cd54 --- /dev/null +++ b/tests/Unit/Services/Servers/UsernameGenerationServiceTest.php @@ -0,0 +1,127 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Tests\Unit\Services\Servers; + +use Tests\TestCase; +use phpmock\phpunit\PHPMock; +use Pterodactyl\Services\Servers\UsernameGenerationService; + +class UsernameGenerationServiceTest extends TestCase +{ + use PHPMock; + + /** + * @var UsernameGenerationService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->service = new UsernameGenerationService(); + + $this->getFunctionMock('\\Pterodactyl\\Services\\Servers', 'bin2hex') + ->expects($this->any())->willReturn('dddddddd'); + + $this->getFunctionMock('\\Pterodactyl\\Services\\Servers', 'str_random') + ->expects($this->any())->willReturnCallback(function ($count) { + return str_pad('', $count, 'a'); + }); + } + + /** + * Test that a valid username is returned and is the correct length. + */ + public function testShouldReturnAValidUsernameWithASelfGeneratedIdentifier() + { + $response = $this->service->generate('testname'); + + $this->assertEquals('testna_dddddddd', $response); + } + + /** + * Test that a name and identifier provided returns the expected username. + */ + public function testShouldReturnAValidUsernameWithAnIdentifierProvided() + { + $response = $this->service->generate('testname', 'identifier'); + + $this->assertEquals('testna_identifi', $response); + } + + /** + * Test that the identifier is extended to 8 characters if it is shorter. + */ + public function testShouldExtendIdentifierToBe8CharactersIfItIsShorter() + { + $response = $this->service->generate('testname', 'xyz'); + + $this->assertEquals('testna_xyzaaaaa', $response); + } + + /** + * Test that special characters are removed from the username. + */ + public function testShouldStripSpecialCharactersFromName() + { + $response = $this->service->generate('te!st_n$ame', 'identifier'); + + $this->assertEquals('testna_identifi', $response); + } + + /** + * Test that an empty name is replaced with 6 random characters. + */ + public function testEmptyNamesShouldBeReplacedWithRandomCharacters() + { + $response = $this->service->generate(''); + + $this->assertEquals('aaaaaa_dddddddd', $response); + } + + /** + * Test that a name consisting entirely of special characters is handled. + */ + public function testNameOfOnlySpecialCharactersIsHandledProperly() + { + $response = $this->service->generate('$%#*#(@#(#*$(#!#@'); + + $this->assertEquals('aaaaaa_dddddddd', $response); + } + + /** + * Test that passing a name shorter than 6 characters returns the entire name. + */ + public function testNameShorterThan6CharactersShouldBeRenderedEntirely() + { + $response = $this->service->generate('test', 'identifier'); + + $this->assertEquals('test_identifi', $response); + } +} diff --git a/tests/Unit/Services/Servers/VariableValidatorServiceTest.php b/tests/Unit/Services/Servers/VariableValidatorServiceTest.php new file mode 100644 index 000000000..b2e87cf06 --- /dev/null +++ b/tests/Unit/Services/Servers/VariableValidatorServiceTest.php @@ -0,0 +1,243 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Tests\Unit\Services\Servers; + +use Mockery as m; +use Tests\TestCase; +use Pterodactyl\Models\ServiceVariable; +use Illuminate\Contracts\Validation\Factory; +use Pterodactyl\Exceptions\DisplayValidationException; +use Pterodactyl\Services\Servers\VariableValidatorService; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\OptionVariableRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; + +class VariableValidatorServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Contracts\Repository\OptionVariableRepositoryInterface + */ + protected $optionVariableRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface + */ + protected $serverVariableRepository; + + /** + * @var \Pterodactyl\Services\Servers\VariableValidatorService + */ + protected $service; + + /** + * @var \Illuminate\Validation\Factory + */ + protected $validator; + + /** + * @var \Illuminate\Support\Collection + */ + protected $variables; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->variables = collect( + [ + factory(ServiceVariable::class)->states('editable', 'viewable')->make(), + factory(ServiceVariable::class)->states('viewable')->make(), + factory(ServiceVariable::class)->states('editable')->make(), + factory(ServiceVariable::class)->make(), + ] + ); + + $this->optionVariableRepository = m::mock(OptionVariableRepositoryInterface::class); + $this->serverRepository = m::mock(ServerRepositoryInterface::class); + $this->serverVariableRepository = m::mock(ServerVariableRepositoryInterface::class); + $this->validator = m::mock(Factory::class); + + $this->service = new VariableValidatorService( + $this->optionVariableRepository, + $this->serverRepository, + $this->serverVariableRepository, + $this->validator + ); + } + + /** + * Test that setting fields returns an instance of the class. + */ + public function testSettingFieldsShouldReturnInstanceOfSelf() + { + $response = $this->service->setFields([]); + + $this->assertInstanceOf(VariableValidatorService::class, $response); + } + + /** + * Test that setting administrator value returns an instance of the class. + */ + public function testSettingAdminShouldReturnInstanceOfSelf() + { + $response = $this->service->setAdmin(); + + $this->assertInstanceOf(VariableValidatorService::class, $response); + } + + /** + * Test that getting the results returns an array of values. + */ + public function testGettingResultsReturnsAnArrayOfValues() + { + $response = $this->service->getResults(); + + $this->assertTrue(is_array($response)); + } + + /** + * Test that when no variables are found for an option no data is returned. + */ + public function testEmptyResultSetShouldBeReturnedIfNoVariablesAreFound() + { + $this->optionVariableRepository->shouldReceive('findWhere')->with([['option_id', '=', 1]])->andReturn([]); + + $response = $this->service->validate(1); + + $this->assertInstanceOf(VariableValidatorService::class, $response); + $this->assertTrue(is_array($response->getResults())); + $this->assertEmpty($response->getResults()); + } + + /** + * Test that variables set as user_editable=0 and/or user_viewable=0 are skipped when admin flag is not set. + */ + public function testValidatorShouldNotProcessVariablesSetAsNotUserEditableWhenAdminFlagIsNotPassed() + { + $this->optionVariableRepository->shouldReceive('findWhere')->with([['option_id', '=', 1]])->andReturn($this->variables); + + $this->validator->shouldReceive('make')->with([ + 'variable_value' => 'Test_SomeValue_0', + ], [ + 'variable_value' => $this->variables{0}->rules, + ])->once()->andReturnSelf() + ->shouldReceive('fails')->withNoArgs()->once()->andReturn(false); + + $response = $this->service->setFields([ + $this->variables{0}->env_variable => 'Test_SomeValue_0', + $this->variables{1}->env_variable => 'Test_SomeValue_1', + $this->variables{2}->env_variable => 'Test_SomeValue_2', + $this->variables{3}->env_variable => 'Test_SomeValue_3', + ])->validate(1)->getResults(); + + $this->assertEquals(1, count($response), 'Assert response has a single item in array.'); + $this->assertArrayHasKey('0', $response); + $this->assertArrayHasKey('id', $response[0]); + $this->assertArrayHasKey('key', $response[0]); + $this->assertArrayHasKey('value', $response[0]); + + $this->assertEquals($this->variables{0}->id, $response[0]['id']); + $this->assertEquals($this->variables{0}->env_variable, $response[0]['key']); + $this->assertEquals('Test_SomeValue_0', $response[0]['value']); + } + + /** + * Test that all variables are processed correctly if admin flag is set. + */ + public function testValidatorShouldProcessAllVariablesWhenAdminFlagIsSet() + { + $this->optionVariableRepository->shouldReceive('findWhere')->with([['option_id', '=', 1]])->andReturn($this->variables); + + foreach($this->variables as $key => $variable) { + $this->validator->shouldReceive('make')->with([ + 'variable_value' => 'Test_SomeValue_' . $key, + ], [ + 'variable_value' => $this->variables{$key}->rules, + ])->andReturnSelf() + ->shouldReceive('fails')->withNoArgs()->once()->andReturn(false); + } + + $response = $this->service->setAdmin()->setFields([ + $this->variables{0}->env_variable => 'Test_SomeValue_0', + $this->variables{1}->env_variable => 'Test_SomeValue_1', + $this->variables{2}->env_variable => 'Test_SomeValue_2', + $this->variables{3}->env_variable => 'Test_SomeValue_3', + ])->validate(1)->getResults(); + + $this->assertEquals(4, count($response), 'Assert response has all four items in array.'); + + foreach($response as $key => $values) { + $this->assertArrayHasKey($key, $response); + $this->assertArrayHasKey('id', $response[$key]); + $this->assertArrayHasKey('key', $response[$key]); + $this->assertArrayHasKey('value', $response[$key]); + + $this->assertEquals($this->variables{$key}->id, $response[$key]['id']); + $this->assertEquals($this->variables{$key}->env_variable, $response[$key]['key']); + $this->assertEquals('Test_SomeValue_' . $key, $response[$key]['value']); + } + } + + /** + * Test that a DisplayValidationError is thrown when a variable is not validated. + */ + public function testValidatorShouldThrowExceptionWhenAValidationErrorIsEncountered() + { + $this->optionVariableRepository->shouldReceive('findWhere')->with([['option_id', '=', 1]])->andReturn($this->variables); + + $this->validator->shouldReceive('make')->with([ + 'variable_value' => null, + ], [ + 'variable_value' => $this->variables{0}->rules, + ])->once()->andReturnSelf() + ->shouldReceive('fails')->withNoArgs()->once()->andReturn(true); + + $this->validator->shouldReceive('errors')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('toArray')->withNoArgs()->once()->andReturn([]); + + try { + $this->service->setFields([ + $this->variables{0}->env_variable => null, + ])->validate(1); + } catch (DisplayValidationException $exception) { + $decoded = json_decode($exception->getMessage()); + + $this->assertEquals(0, json_last_error(), 'Assert that response is decodable JSON.'); + $this->assertObjectHasAttribute('notice', $decoded); + $this->assertEquals( + trans('admin/server.exceptions.bad_variable', ['name' => $this->variables{0}->name]), + $decoded->notice[0] + ); + } + } +}