From 97dc0519d66f82bc8fe71de96d185ec5e20b383f Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Wed, 18 Oct 2017 22:32:19 -0500 Subject: [PATCH 01/11] Add database management back to front-end and begin some refactoring Here we go again boys... --- .../DatabaseRepositoryInterface.php | 65 +++++--- .../Controllers/Admin/DatabaseController.php | 83 ++++++---- .../Controllers/Admin/ServersController.php | 21 ++- .../Controllers/Server/DatabaseController.php | 71 +++++++++ app/Http/Kernel.php | 13 +- .../Server/DatabaseBelongsToServer.php | 51 ++++++ ...Access.php => ScheduleBelongsToServer.php} | 2 +- ...rAccess.php => SubuserBelongsToServer.php} | 2 +- app/Http/Middleware/ServerAuthenticate.php | 5 + .../Middleware/SubuserAccessAuthenticate.php | 1 + .../Server/ServerDataComposer.php | 27 ++-- .../Eloquent/DatabaseRepository.php | 96 +++++++----- app/Services/Database/DatabaseHostService.php | 148 ------------------ .../DatabaseManagementService.php | 68 ++------ .../Databases/DatabasePasswordService.php | 86 ++++++++++ .../Databases/Hosts/HostCreationService.php | 92 +++++++++++ .../Databases/Hosts/HostDeletionService.php | 53 +++++++ .../Databases/Hosts/HostUpdateService.php | 96 ++++++++++++ .../Servers/ServerDeletionService.php | 6 +- .../Controllers/JavascriptInjection.php | 20 ++- public/js/laroute.js | 2 +- resources/lang/en/server.php | 2 +- .../admin/databases/view.blade.php | 5 +- .../admin/servers/view/database.blade.php | 7 +- .../pterodactyl/layouts/master.blade.php | 14 +- .../index.blade.php} | 85 ++++++---- routes/admin.php | 1 + routes/server.php | 41 ++--- .../Admin/DatabaseControllerTest.php | 5 +- .../Database/DatabaseHostServiceTest.php | 5 +- .../DatabaseManagementServiceTest.php | 4 +- .../Servers/ServerDeletionServiceTest.php | 4 +- 32 files changed, 774 insertions(+), 407 deletions(-) create mode 100644 app/Http/Controllers/Server/DatabaseController.php create mode 100644 app/Http/Middleware/Server/DatabaseBelongsToServer.php rename app/Http/Middleware/Server/{ScheduleAccess.php => ScheduleBelongsToServer.php} (98%) rename app/Http/Middleware/Server/{SubuserAccess.php => SubuserBelongsToServer.php} (98%) delete mode 100644 app/Services/Database/DatabaseHostService.php rename app/Services/{Database => Databases}/DatabaseManagementService.php (67%) create mode 100644 app/Services/Databases/DatabasePasswordService.php create mode 100644 app/Services/Databases/Hosts/HostCreationService.php create mode 100644 app/Services/Databases/Hosts/HostDeletionService.php create mode 100644 app/Services/Databases/Hosts/HostUpdateService.php rename resources/themes/pterodactyl/server/{settings/databases.blade.php => databases/index.blade.php} (55%) diff --git a/app/Contracts/Repository/DatabaseRepositoryInterface.php b/app/Contracts/Repository/DatabaseRepositoryInterface.php index ca5379df1..1e90d0e04 100644 --- a/app/Contracts/Repository/DatabaseRepositoryInterface.php +++ b/app/Contracts/Repository/DatabaseRepositoryInterface.php @@ -9,8 +9,35 @@ namespace Pterodactyl\Contracts\Repository; +use Illuminate\Support\Collection; + interface DatabaseRepositoryInterface extends RepositoryInterface { + const DEFAULT_CONNECTION_NAME = 'dynamic'; + + /** + * Set the connection name to execute statements against. + * + * @param string $connection + * @return $this + */ + public function setConnection(string $connection); + + /** + * Return the connection to execute statements aganist. + * + * @return string + */ + public function getConnection(): string; + + /** + * Return all of the databases belonging to a server. + * + * @param int $server + * @return \Illuminate\Support\Collection + */ + public function getDatabasesForServer(int $server): Collection; + /** * Create a new database if it does not already exist on the host with * the provided details. @@ -26,58 +53,52 @@ interface DatabaseRepositoryInterface extends RepositoryInterface /** * Create a new database on a given connection. * - * @param string $database - * @param null|string $connection + * @param string $database * @return bool */ - public function createDatabase($database, $connection = null); + public function createDatabase($database); /** * Create a new database user on a given connection. * - * @param string $username - * @param string $remote - * @param string $password - * @param null|string $connection + * @param string $username + * @param string $remote + * @param string $password * @return bool */ - public function createUser($username, $remote, $password, $connection = null); + public function createUser($username, $remote, $password); /** * Give a specific user access to a given database. * - * @param string $database - * @param string $username - * @param string $remote - * @param null|string $connection + * @param string $database + * @param string $username + * @param string $remote * @return bool */ - public function assignUserToDatabase($database, $username, $remote, $connection = null); + public function assignUserToDatabase($database, $username, $remote); /** * Flush the privileges for a given connection. * - * @param null|string $connection * @return mixed */ - public function flush($connection = null); + public function flush(); /** * Drop a given database on a specific connection. * - * @param string $database - * @param null|string $connection + * @param string $database * @return bool */ - public function dropDatabase($database, $connection = null); + public function dropDatabase($database); /** * Drop a given user on a specific connection. * - * @param string $username - * @param string $remote - * @param null|string $connection + * @param string $username + * @param string $remote * @return mixed */ - public function dropUser($username, $remote, $connection = null); + public function dropUser($username, $remote); } diff --git a/app/Http/Controllers/Admin/DatabaseController.php b/app/Http/Controllers/Admin/DatabaseController.php index 9eac33f69..02271d699 100644 --- a/app/Http/Controllers/Admin/DatabaseController.php +++ b/app/Http/Controllers/Admin/DatabaseController.php @@ -9,11 +9,15 @@ namespace Pterodactyl\Http\Controllers\Admin; -use Pterodactyl\Models\DatabaseHost; +use PDOException; +use Illuminate\View\View; +use Illuminate\Http\RedirectResponse; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Services\Database\DatabaseHostService; +use Pterodactyl\Services\Databases\Hosts\HostUpdateService; use Pterodactyl\Http\Requests\Admin\DatabaseHostFormRequest; +use Pterodactyl\Services\Databases\Hosts\HostCreationService; +use Pterodactyl\Services\Databases\Hosts\HostDeletionService; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; @@ -22,41 +26,57 @@ class DatabaseController extends Controller /** * @var \Prologue\Alerts\AlertsMessageBag */ - protected $alert; + private $alert; + + /** + * @var \Pterodactyl\Services\Databases\Hosts\HostCreationService + */ + private $creationService; + + /** + * @var \Pterodactyl\Services\Databases\Hosts\HostDeletionService + */ + private $deletionService; /** * @var \Pterodactyl\Contracts\Repository\LocationRepositoryInterface */ - protected $locationRepository; + private $locationRepository; /** * @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface */ - protected $repository; + private $repository; /** - * @var \Pterodactyl\Services\Database\DatabaseHostService + * @var \Pterodactyl\Services\Databases\Hosts\HostUpdateService */ - protected $service; + private $updateService; /** * DatabaseController constructor. * * @param \Prologue\Alerts\AlertsMessageBag $alert * @param \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface $repository - * @param \Pterodactyl\Services\Database\DatabaseHostService $service + * @param \Pterodactyl\Services\Databases\Hosts\HostCreationService $creationService + * @param \Pterodactyl\Services\Databases\Hosts\HostDeletionService $deletionService + * @param \Pterodactyl\Services\Databases\Hosts\HostUpdateService $updateService * @param \Pterodactyl\Contracts\Repository\LocationRepositoryInterface $locationRepository */ public function __construct( AlertsMessageBag $alert, DatabaseHostRepositoryInterface $repository, - DatabaseHostService $service, + HostCreationService $creationService, + HostDeletionService $deletionService, + HostUpdateService $updateService, LocationRepositoryInterface $locationRepository ) { $this->alert = $alert; + $this->creationService = $creationService; + $this->deletionService = $deletionService; $this->repository = $repository; - $this->service = $service; $this->locationRepository = $locationRepository; + $this->updateService = $updateService; } /** @@ -64,7 +84,7 @@ class DatabaseController extends Controller * * @return \Illuminate\View\View */ - public function index() + public function index(): View { return view('admin.databases.index', [ 'locations' => $this->locationRepository->getAllWithNodes(), @@ -80,7 +100,7 @@ class DatabaseController extends Controller * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function view($host) + public function view($host): View { return view('admin.databases.view', [ 'locations' => $this->locationRepository->getAllWithNodes(), @@ -94,42 +114,41 @@ class DatabaseController extends Controller * @param \Pterodactyl\Http\Requests\Admin\DatabaseHostFormRequest $request * @return \Illuminate\Http\RedirectResponse * - * @throws \Throwable + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function create(DatabaseHostFormRequest $request) + public function create(DatabaseHostFormRequest $request): RedirectResponse { try { - $host = $this->service->create($request->normalize()); - $this->alert->success('Successfully created a new database host on the system.')->flash(); - - return redirect()->route('admin.databases.view', $host->id); - } catch (\PDOException $ex) { + $host = $this->creationService->handle($request->normalize()); + } catch (PDOException $ex) { $this->alert->danger($ex->getMessage())->flash(); + + return redirect()->route('admin.databases'); } - return redirect()->route('admin.databases'); + $this->alert->success('Successfully created a new database host on the system.')->flash(); + + return redirect()->route('admin.databases.view', $host->id); } /** * Handle updating database host. * * @param \Pterodactyl\Http\Requests\Admin\DatabaseHostFormRequest $request - * @param \Pterodactyl\Models\DatabaseHost $host + * @param int $host * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function update(DatabaseHostFormRequest $request, DatabaseHost $host) + public function update(DatabaseHostFormRequest $request, int $host): RedirectResponse { - if ($request->input('action') === 'delete') { - return $this->delete($host); - } - try { - $host = $this->service->update($host->id, $request->normalize()); + $host = $this->updateService->handle($host, $request->normalize()); $this->alert->success('Database host was updated successfully.')->flash(); - } catch (\PDOException $ex) { + } catch (PDOException $ex) { $this->alert->danger($ex->getMessage())->flash(); } @@ -139,14 +158,14 @@ class DatabaseController extends Controller /** * Handle request to delete a database host. * - * @param \Pterodactyl\Models\DatabaseHost $host + * @param int $host * @return \Illuminate\Http\RedirectResponse * - * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException */ - public function delete(DatabaseHost $host) + public function delete(int $host): RedirectResponse { - $this->service->delete($host->id); + $this->deletionService->handle($host); $this->alert->success('The requested database host has been deleted from the system.')->flash(); return redirect()->route('admin.databases'); diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index 5867d4788..a10c56904 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -22,12 +22,13 @@ use Pterodactyl\Services\Servers\ServerDeletionService; use Pterodactyl\Services\Servers\ReinstallServerService; use Pterodactyl\Services\Servers\ContainerRebuildService; use Pterodactyl\Services\Servers\BuildModificationService; -use Pterodactyl\Services\Database\DatabaseManagementService; +use Pterodactyl\Services\Databases\DatabasePasswordService; use Pterodactyl\Services\Servers\DetailsModificationService; use Pterodactyl\Services\Servers\StartupModificationService; use Pterodactyl\Contracts\Repository\NestRepositoryInterface; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository; +use Pterodactyl\Services\Databases\DatabaseManagementService; use Illuminate\Contracts\Config\Repository as ConfigRepository; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; @@ -67,10 +68,15 @@ class ServersController extends Controller protected $databaseRepository; /** - * @var \Pterodactyl\Services\Database\DatabaseManagementService + * @var \Pterodactyl\Services\Databases\DatabaseManagementService */ protected $databaseManagementService; + /** + * @var \Pterodactyl\Services\Databases\DatabasePasswordService + */ + protected $databasePasswordService; + /** * @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface */ @@ -135,7 +141,8 @@ class ServersController extends Controller * @param \Illuminate\Contracts\Config\Repository $config * @param \Pterodactyl\Services\Servers\ContainerRebuildService $containerRebuildService * @param \Pterodactyl\Services\Servers\ServerCreationService $service - * @param \Pterodactyl\Services\Database\DatabaseManagementService $databaseManagementService + * @param \Pterodactyl\Services\Databases\DatabaseManagementService $databaseManagementService + * @param \Pterodactyl\Services\Databases\DatabasePasswordService $databasePasswordService * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $databaseRepository * @param \Pterodactyl\Repositories\Eloquent\DatabaseHostRepository $databaseHostRepository * @param \Pterodactyl\Services\Servers\ServerDeletionService $deletionService @@ -156,6 +163,7 @@ class ServersController extends Controller ContainerRebuildService $containerRebuildService, ServerCreationService $service, DatabaseManagementService $databaseManagementService, + DatabasePasswordService $databasePasswordService, DatabaseRepositoryInterface $databaseRepository, DatabaseHostRepository $databaseHostRepository, ServerDeletionService $deletionService, @@ -173,9 +181,10 @@ class ServersController extends Controller $this->buildModificationService = $buildModificationService; $this->config = $config; $this->containerRebuildService = $containerRebuildService; - $this->databaseManagementService = $databaseManagementService; - $this->databaseRepository = $databaseRepository; $this->databaseHostRepository = $databaseHostRepository; + $this->databaseManagementService = $databaseManagementService; + $this->databasePasswordService = $databasePasswordService; + $this->databaseRepository = $databaseRepository; $this->detailsModificationService = $detailsModificationService; $this->deletionService = $deletionService; $this->locationRepository = $locationRepository; @@ -609,7 +618,7 @@ class ServersController extends Controller ['id', '=', $request->input('database')], ]); - $this->databaseManagementService->changePassword($database->id, str_random(20)); + $this->databasePasswordService->handle($database, str_random(20)); return response('', 204); } diff --git a/app/Http/Controllers/Server/DatabaseController.php b/app/Http/Controllers/Server/DatabaseController.php new file mode 100644 index 000000000..15fb96c01 --- /dev/null +++ b/app/Http/Controllers/Server/DatabaseController.php @@ -0,0 +1,71 @@ +passwordService = $passwordService; + $this->repository = $repository; + } + + /** + * Render the database listing for a server. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\View\View + */ + public function index(Request $request): View + { + $server = $request->attributes->get('server'); + $this->injectJavascript(); + + return view('server.databases.index', [ + 'databases' => $this->repository->getDatabasesForServer($server->id), + ]); + } + + /** + * Handle a request to update the password for a specific database. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function update(Request $request): JsonResponse + { + $password = str_random(20); + $this->passwordService->handle($request->attributes->get('database'), $password); + + return response()->json(['password' => $password]); + } +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index b1812a9d3..22e90a903 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -5,6 +5,9 @@ namespace Pterodactyl\Http; use Pterodactyl\Http\Middleware\DaemonAuthenticate; use Illuminate\Foundation\Http\Kernel as HttpKernel; use Illuminate\Routing\Middleware\SubstituteBindings; +use Pterodactyl\Http\Middleware\Server\SubuserBelongsToServer; +use Pterodactyl\Http\Middleware\Server\DatabaseBelongsToServer; +use Pterodactyl\Http\Middleware\Server\ScheduleBelongsToServer; class Kernel extends HttpKernel { @@ -63,7 +66,6 @@ class Kernel extends HttpKernel 'guest' => \Pterodactyl\Http\Middleware\RedirectIfAuthenticated::class, 'server' => \Pterodactyl\Http\Middleware\ServerAuthenticate::class, 'subuser.auth' => \Pterodactyl\Http\Middleware\SubuserAccessAuthenticate::class, - 'subuser' => \Pterodactyl\Http\Middleware\Server\SubuserAccess::class, 'admin' => \Pterodactyl\Http\Middleware\AdminAuthenticate::class, 'daemon-old' => DaemonAuthenticate::class, 'csrf' => \Pterodactyl\Http\Middleware\VerifyCsrfToken::class, @@ -71,6 +73,13 @@ class Kernel extends HttpKernel 'can' => \Illuminate\Auth\Middleware\Authorize::class, 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 'recaptcha' => \Pterodactyl\Http\Middleware\VerifyReCaptcha::class, - 'schedule' => \Pterodactyl\Http\Middleware\Server\ScheduleAccess::class, + + // Server specific middleware (used for authenticating access to resources) + // + // These are only used for individual server authentication, and not gloabl + // actions from other resources. They are defined in the route files. + 'server..database' => DatabaseBelongsToServer::class, + 'server..subuser' => SubuserBelongsToServer::class, + 'server..schedule' => ScheduleBelongsToServer::class, ]; } diff --git a/app/Http/Middleware/Server/DatabaseBelongsToServer.php b/app/Http/Middleware/Server/DatabaseBelongsToServer.php new file mode 100644 index 000000000..bc31c29c8 --- /dev/null +++ b/app/Http/Middleware/Server/DatabaseBelongsToServer.php @@ -0,0 +1,51 @@ +repository = $repository; + } + + /** + * Check if a database being requested belongs to the currently loaded server. + * If it does not, throw a 404 error, otherwise continue on with the request + * and set an attribute with the database. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @return mixed + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle(Request $request, Closure $next) + { + $server = $request->attributes->get('server'); + + $database = $this->repository->find($request->input('database')); + if ($database->server_id !== $server->id) { + throw new NotFoundHttpException; + } + + $request->attributes->set('database', $database); + + return $next($request); + } +} diff --git a/app/Http/Middleware/Server/ScheduleAccess.php b/app/Http/Middleware/Server/ScheduleBelongsToServer.php similarity index 98% rename from app/Http/Middleware/Server/ScheduleAccess.php rename to app/Http/Middleware/Server/ScheduleBelongsToServer.php index b54b07d47..145429f8c 100644 --- a/app/Http/Middleware/Server/ScheduleAccess.php +++ b/app/Http/Middleware/Server/ScheduleBelongsToServer.php @@ -14,7 +14,7 @@ use Illuminate\Contracts\Session\Session; use Pterodactyl\Contracts\Extensions\HashidsInterface; use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface; -class ScheduleAccess +class ScheduleBelongsToServer { /** * @var \Pterodactyl\Contracts\Extensions\HashidsInterface diff --git a/app/Http/Middleware/Server/SubuserAccess.php b/app/Http/Middleware/Server/SubuserBelongsToServer.php similarity index 98% rename from app/Http/Middleware/Server/SubuserAccess.php rename to app/Http/Middleware/Server/SubuserBelongsToServer.php index 85ebe2640..b18620f51 100644 --- a/app/Http/Middleware/Server/SubuserAccess.php +++ b/app/Http/Middleware/Server/SubuserBelongsToServer.php @@ -15,7 +15,7 @@ use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; -class SubuserAccess +class SubuserBelongsToServer { /** * @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface diff --git a/app/Http/Middleware/ServerAuthenticate.php b/app/Http/Middleware/ServerAuthenticate.php index 7df3d110a..538382f23 100644 --- a/app/Http/Middleware/ServerAuthenticate.php +++ b/app/Http/Middleware/ServerAuthenticate.php @@ -105,8 +105,13 @@ class ServerAuthenticate } // Store the server in the session. + // @todo remove from session. use request attributes. $this->session->now('server_data.model', $server); + // Add server to the request attributes. This will replace sessions + // as files are updated. + $request->attributes->set('server', $server); + return $next($request); } } diff --git a/app/Http/Middleware/SubuserAccessAuthenticate.php b/app/Http/Middleware/SubuserAccessAuthenticate.php index fd4b8cf4d..30a906884 100644 --- a/app/Http/Middleware/SubuserAccessAuthenticate.php +++ b/app/Http/Middleware/SubuserAccessAuthenticate.php @@ -60,6 +60,7 @@ class SubuserAccessAuthenticate try { $token = $this->keyProviderService->handle($server->id, $request->user()->id); $this->session->now('server_data.token', $token); + $request->attributes->set('server_token', $token); } catch (RecordNotFoundException $exception) { throw new AuthenticationException('This account does not have permission to access this server.'); } diff --git a/app/Http/ViewComposers/Server/ServerDataComposer.php b/app/Http/ViewComposers/Server/ServerDataComposer.php index 7da647587..9e1858645 100644 --- a/app/Http/ViewComposers/Server/ServerDataComposer.php +++ b/app/Http/ViewComposers/Server/ServerDataComposer.php @@ -1,32 +1,25 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Http\ViewComposers\Server; use Illuminate\View\View; -use Illuminate\Contracts\Session\Session; +use Illuminate\Http\Request; class ServerDataComposer { /** - * @var \Illuminate\Contracts\Session\Session + * @var \Illuminate\Http\Request */ - protected $session; + protected $request; /** * ServerDataComposer constructor. * - * @param \Illuminate\Contracts\Session\Session $session + * @param \Illuminate\Http\Request $request */ - public function __construct(Session $session) + public function __construct(Request $request) { - $this->session = $session; + $this->request = $request; } /** @@ -36,10 +29,10 @@ class ServerDataComposer */ public function compose(View $view) { - $data = $this->session->get('server_data'); + $server = $this->request->get('server'); - $view->with('server', array_get($data, 'model')); - $view->with('node', object_get($data['model'], 'node')); - $view->with('daemon_token', array_get($data, 'token')); + $view->with('server', $server); + $view->with('node', object_get($server, 'node')); + $view->with('daemon_token', $this->request->get('server_token')); } } diff --git a/app/Repositories/Eloquent/DatabaseRepository.php b/app/Repositories/Eloquent/DatabaseRepository.php index de3ff65bf..f030812b5 100644 --- a/app/Repositories/Eloquent/DatabaseRepository.php +++ b/app/Repositories/Eloquent/DatabaseRepository.php @@ -10,6 +10,7 @@ namespace Pterodactyl\Repositories\Eloquent; use Pterodactyl\Models\Database; +use Illuminate\Support\Collection; use Illuminate\Foundation\Application; use Illuminate\Database\DatabaseManager; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; @@ -17,6 +18,11 @@ use Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException; class DatabaseRepository extends EloquentRepository implements DatabaseRepositoryInterface { + /** + * @var string + */ + protected $connection = self::DEFAULT_CONNECTION_NAME; + /** * @var \Illuminate\Database\DatabaseManager */ @@ -45,6 +51,40 @@ class DatabaseRepository extends EloquentRepository implements DatabaseRepositor return Database::class; } + /** + * Set the connection name to execute statements against. + * + * @param string $connection + * @return $this + */ + public function setConnection(string $connection) + { + $this->connection = $connection; + + return $this; + } + + /** + * Return the connection to execute statements aganist. + * + * @return string + */ + public function getConnection(): string + { + return $this->connection; + } + + /** + * Return all of the databases belonging to a server. + * + * @param int $server + * @return \Illuminate\Support\Collection + */ + public function getDatabasesForServer(int $server): Collection + { + return $this->getBuilder()->where('server_id', $server)->get($this->getColumns()); + } + /** * {@inheritdoc} * @return bool|\Illuminate\Database\Eloquent\Model @@ -67,80 +107,64 @@ class DatabaseRepository extends EloquentRepository implements DatabaseRepositor /** * {@inheritdoc} */ - public function createDatabase($database, $connection = null) + public function createDatabase($database) { - return $this->runStatement( - sprintf('CREATE DATABASE IF NOT EXISTS `%s`', $database), - $connection - ); + return $this->runStatement(sprintf('CREATE DATABASE IF NOT EXISTS `%s`', $database)); } /** * {@inheritdoc} */ - public function createUser($username, $remote, $password, $connection = null) + public function createUser($username, $remote, $password) { - return $this->runStatement( - sprintf('CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\'', $username, $remote, $password), - $connection - ); + return $this->runStatement(sprintf('CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\'', $username, $remote, $password)); } /** * {@inheritdoc} */ - public function assignUserToDatabase($database, $username, $remote, $connection = null) + public function assignUserToDatabase($database, $username, $remote) { - return $this->runStatement( - sprintf( - 'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX, EXECUTE ON `%s`.* TO `%s`@`%s`', - $database, - $username, - $remote - ), - $connection - ); + return $this->runStatement(sprintf( + 'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX, EXECUTE ON `%s`.* TO `%s`@`%s`', + $database, + $username, + $remote + )); } /** * {@inheritdoc} */ - public function flush($connection = null) + public function flush() { - return $this->runStatement('FLUSH PRIVILEGES', $connection); + return $this->runStatement('FLUSH PRIVILEGES'); } /** * {@inheritdoc} */ - public function dropDatabase($database, $connection = null) + public function dropDatabase($database) { - return $this->runStatement( - sprintf('DROP DATABASE IF EXISTS `%s`', $database), - $connection - ); + return $this->runStatement(sprintf('DROP DATABASE IF EXISTS `%s`', $database)); } /** * {@inheritdoc} */ - public function dropUser($username, $remote, $connection = null) + public function dropUser($username, $remote) { - return $this->runStatement( - sprintf('DROP USER IF EXISTS `%s`@`%s`', $username, $remote), - $connection - ); + return $this->runStatement(sprintf('DROP USER IF EXISTS `%s`@`%s`', $username, $remote)); } /** * Run the provided statement against the database on a given connection. * - * @param string $statement - * @param null|string $connection + * @param string $statement * @return bool */ - protected function runStatement($statement, $connection = null) + protected function runStatement($statement) { - return $this->database->connection($connection)->statement($statement); + return $this->database->connection($this->getConnection())->statement($statement); } } diff --git a/app/Services/Database/DatabaseHostService.php b/app/Services/Database/DatabaseHostService.php deleted file mode 100644 index cb7f1f9f3..000000000 --- a/app/Services/Database/DatabaseHostService.php +++ /dev/null @@ -1,148 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Services\Database; - -use Illuminate\Database\DatabaseManager; -use Pterodactyl\Exceptions\DisplayException; -use Illuminate\Contracts\Encryption\Encrypter; -use Pterodactyl\Extensions\DynamicDatabaseConnection; -use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; -use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; - -class DatabaseHostService -{ - /** - * @var \Illuminate\Database\DatabaseManager - */ - protected $database; - - /** - * @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface - */ - protected $databaseRepository; - - /** - * @var \Pterodactyl\Extensions\DynamicDatabaseConnection - */ - protected $dynamic; - - /** - * @var \Illuminate\Contracts\Encryption\Encrypter - */ - protected $encrypter; - - /** - * @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface - */ - protected $repository; - - /** - * DatabaseHostService constructor. - * - * @param \Illuminate\Database\DatabaseManager $database - * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $databaseRepository - * @param \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface $repository - * @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic - * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter - */ - public function __construct( - DatabaseManager $database, - DatabaseRepositoryInterface $databaseRepository, - DatabaseHostRepositoryInterface $repository, - DynamicDatabaseConnection $dynamic, - Encrypter $encrypter - ) { - $this->database = $database; - $this->databaseRepository = $databaseRepository; - $this->dynamic = $dynamic; - $this->encrypter = $encrypter; - $this->repository = $repository; - } - - /** - * Create a new database host and persist it to the database. - * - * @param array $data - * @return \Pterodactyl\Models\DatabaseHost - * - * @throws \Throwable - * @throws \PDOException - */ - public function create(array $data) - { - $this->database->beginTransaction(); - - $host = $this->repository->create([ - 'password' => $this->encrypter->encrypt(array_get($data, 'password')), - 'name' => array_get($data, 'name'), - 'host' => array_get($data, 'host'), - 'port' => array_get($data, 'port'), - 'username' => array_get($data, 'username'), - 'max_databases' => null, - 'node_id' => array_get($data, 'node_id'), - ]); - - // Check Access - $this->dynamic->set('dynamic', $host); - $this->database->connection('dynamic')->select('SELECT 1 FROM dual'); - - $this->database->commit(); - - return $host; - } - - /** - * Update a database host and persist to the database. - * - * @param int $id - * @param array $data - * @return mixed - * - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function update($id, array $data) - { - $this->database->beginTransaction(); - - if (! empty(array_get($data, 'password'))) { - $data['password'] = $this->encrypter->encrypt($data['password']); - } else { - unset($data['password']); - } - - $host = $this->repository->update($id, $data); - - $this->dynamic->set('dynamic', $host); - $this->database->connection('dynamic')->select('SELECT 1 FROM dual'); - - $this->database->commit(); - - return $host; - } - - /** - * Delete a database host if it has no active databases attached to it. - * - * @param int $id - * @return bool|null - * - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function delete($id) - { - $count = $this->databaseRepository->findCountWhere([['database_host_id', '=', $id]]); - if ($count > 0) { - throw new DisplayException(trans('exceptions.databases.delete_has_databases')); - } - - return $this->repository->delete($id); - } -} diff --git a/app/Services/Database/DatabaseManagementService.php b/app/Services/Databases/DatabaseManagementService.php similarity index 67% rename from app/Services/Database/DatabaseManagementService.php rename to app/Services/Databases/DatabaseManagementService.php index 21d01809e..845ee6282 100644 --- a/app/Services/Database/DatabaseManagementService.php +++ b/app/Services/Databases/DatabaseManagementService.php @@ -7,7 +7,7 @@ * https://opensource.org/licenses/MIT */ -namespace Pterodactyl\Services\Database; +namespace Pterodactyl\Services\Databases; use Illuminate\Database\DatabaseManager; use Illuminate\Contracts\Encryption\Encrypter; @@ -79,28 +79,26 @@ class DatabaseManagementService $database = $this->repository->createIfNotExists($data); $this->dynamic->set('dynamic', $data['database_host_id']); - $this->repository->createDatabase($database->database, 'dynamic'); + $this->repository->createDatabase($database->database); $this->repository->createUser( $database->username, $database->remote, - $this->encrypter->decrypt($database->password), - 'dynamic' + $this->encrypter->decrypt($database->password) ); $this->repository->assignUserToDatabase( $database->database, $database->username, - $database->remote, - 'dynamic' + $database->remote ); - $this->repository->flush('dynamic'); + $this->repository->flush(); $this->database->commit(); } catch (\Exception $ex) { try { if (isset($database)) { - $this->repository->dropDatabase($database->database, 'dynamic'); - $this->repository->dropUser($database->username, $database->remote, 'dynamic'); - $this->repository->flush('dynamic'); + $this->repository->dropDatabase($database->database); + $this->repository->dropUser($database->username, $database->remote); + $this->repository->flush(); } } catch (\Exception $exTwo) { // ignore an exception @@ -113,62 +111,22 @@ class DatabaseManagementService return $database; } - /** - * Change the password for a specific user and database combination. - * - * @param int $id - * @param string $password - * @return bool - * - * @throws \Exception - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function changePassword($id, $password) - { - $database = $this->repository->find($id); - $this->dynamic->set('dynamic', $database->database_host_id); - - $this->database->beginTransaction(); - - try { - $updated = $this->repository->withoutFresh()->update($id, [ - 'password' => $this->encrypter->encrypt($password), - ]); - - $this->repository->dropUser($database->username, $database->remote, 'dynamic'); - $this->repository->createUser($database->username, $database->remote, $password, 'dynamic'); - $this->repository->assignUserToDatabase( - $database->database, - $database->username, - $database->remote, - 'dynamic' - ); - $this->repository->flush('dynamic'); - - $this->database->commit(); - } catch (\Exception $ex) { - $this->database->rollBack(); - throw $ex; - } - - return $updated; - } - /** * Delete a database from the given host server. * * @param int $id * @return bool|null + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function delete($id) { $database = $this->repository->find($id); $this->dynamic->set('dynamic', $database->database_host_id); - $this->repository->dropDatabase($database->database, 'dynamic'); - $this->repository->dropUser($database->username, $database->remote, 'dynamic'); - $this->repository->flush('dynamic'); + $this->repository->dropDatabase($database->database); + $this->repository->dropUser($database->username, $database->remote); + $this->repository->flush(); return $this->repository->delete($id); } diff --git a/app/Services/Databases/DatabasePasswordService.php b/app/Services/Databases/DatabasePasswordService.php new file mode 100644 index 000000000..4f6443ced --- /dev/null +++ b/app/Services/Databases/DatabasePasswordService.php @@ -0,0 +1,86 @@ +connection = $connection; + $this->dynamic = $dynamic; + $this->encrypter = $encrypter; + $this->repository = $repository; + } + + /** + * Updates a password for a given database. + * + * @param \Pterodactyl\Models\Database|int $database + * @param string $password + * @return bool + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle($database, string $password): bool + { + if (! $database instanceof Database) { + $database = $this->repository->find($database); + } + + $this->dynamic->set('dynamic', $database->database_host_id); + $this->connection->beginTransaction(); + + $updated = $this->repository->withoutFresh()->update($database->id, [ + 'password' => $this->encrypter->encrypt($password), + ]); + + $this->repository->dropUser($database->username, $database->remote); + $this->repository->createUser($database->username, $database->remote, $password); + $this->repository->assignUserToDatabase($database->database, $database->username, $database->remote); + $this->repository->flush(); + + unset($password); + $this->connection->commit(); + + return $updated; + } +} diff --git a/app/Services/Databases/Hosts/HostCreationService.php b/app/Services/Databases/Hosts/HostCreationService.php new file mode 100644 index 000000000..15b32ea04 --- /dev/null +++ b/app/Services/Databases/Hosts/HostCreationService.php @@ -0,0 +1,92 @@ +connection = $connection; + $this->databaseManager = $databaseManager; + $this->dynamic = $dynamic; + $this->encrypter = $encrypter; + $this->repository = $repository; + } + + /** + * Create a new database host on the Panel. + * + * @param array $data + * @return \Pterodactyl\Models\DatabaseHost + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle(array $data): DatabaseHost + { + $this->connection->beginTransaction(); + + $host = $this->repository->create([ + 'password' => $this->encrypter->encrypt(array_get($data, 'password')), + 'name' => array_get($data, 'name'), + 'host' => array_get($data, 'host'), + 'port' => array_get($data, 'port'), + 'username' => array_get($data, 'username'), + 'max_databases' => null, + 'node_id' => array_get($data, 'node_id'), + ]); + + // Confirm access using the provided credentials before saving data. + $this->dynamic->set('dynamic', $host); + $this->databaseManager->connection('dynamic')->select('SELECT 1 FROM dual'); + $this->connection->commit(); + + return $host; + } +} diff --git a/app/Services/Databases/Hosts/HostDeletionService.php b/app/Services/Databases/Hosts/HostDeletionService.php new file mode 100644 index 000000000..b69c8dcf9 --- /dev/null +++ b/app/Services/Databases/Hosts/HostDeletionService.php @@ -0,0 +1,53 @@ +databaseRepository = $databaseRepository; + $this->repository = $repository; + } + + /** + * Delete a specified host from the Panel if no databases are + * attached to it. + * + * @param int $host + * @return int + * + * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException + */ + public function handle(int $host): int + { + $count = $this->databaseRepository->findCountWhere([['database_host_id', '=', $host]]); + if ($count > 0) { + throw new HasActiveServersException(trans('exceptions.databases.delete_has_databases')); + } + + return $this->repository->delete($host); + } +} diff --git a/app/Services/Databases/Hosts/HostUpdateService.php b/app/Services/Databases/Hosts/HostUpdateService.php new file mode 100644 index 000000000..5f4b19b31 --- /dev/null +++ b/app/Services/Databases/Hosts/HostUpdateService.php @@ -0,0 +1,96 @@ +. + * + * This software is licensed under the terms of the MIT license. + * https://opensource.org/licenses/MIT + */ + +namespace Pterodactyl\Services\Databases\Hosts; + +use Pterodactyl\Models\DatabaseHost; +use Illuminate\Database\DatabaseManager; +use Illuminate\Database\ConnectionInterface; +use Illuminate\Contracts\Encryption\Encrypter; +use Pterodactyl\Extensions\DynamicDatabaseConnection; +use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; + +class HostUpdateService +{ + /** + * @var \Illuminate\Database\ConnectionInterface + */ + private $connection; + + /** + * @var \Illuminate\Database\DatabaseManager + */ + private $databaseManager; + + /** + * @var \Pterodactyl\Extensions\DynamicDatabaseConnection + */ + private $dynamic; + + /** + * @var \Illuminate\Contracts\Encryption\Encrypter + */ + private $encrypter; + + /** + * @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface + */ + private $repository; + + /** + * DatabaseHostService constructor. + * + * @param \Illuminate\Database\ConnectionInterface $connection + * @param \Illuminate\Database\DatabaseManager $databaseManager + * @param \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface $repository + * @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter + */ + public function __construct( + ConnectionInterface $connection, + DatabaseManager $databaseManager, + DatabaseHostRepositoryInterface $repository, + DynamicDatabaseConnection $dynamic, + Encrypter $encrypter + ) { + $this->connection = $connection; + $this->databaseManager = $databaseManager; + $this->dynamic = $dynamic; + $this->encrypter = $encrypter; + $this->repository = $repository; + } + + /** + * Update a database host and persist to the database. + * + * @param int $hostId + * @param array $data + * @return mixed + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle(int $hostId, array $data): DatabaseHost + { + if (! empty(array_get($data, 'password'))) { + $data['password'] = $this->encrypter->encrypt($data['password']); + } else { + unset($data['password']); + } + + $this->connection->beginTransaction(); + $host = $this->repository->update($hostId, $data); + + $this->dynamic->set('dynamic', $host); + $this->databaseManager->connection('dynamic')->select('SELECT 1 FROM dual'); + $this->connection->commit(); + + return $host; + } +} diff --git a/app/Services/Servers/ServerDeletionService.php b/app/Services/Servers/ServerDeletionService.php index 836df7e4f..1129a187c 100644 --- a/app/Services/Servers/ServerDeletionService.php +++ b/app/Services/Servers/ServerDeletionService.php @@ -14,7 +14,7 @@ use Pterodactyl\Models\Server; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Services\Database\DatabaseManagementService; +use Pterodactyl\Services\Databases\DatabaseManagementService; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; @@ -32,7 +32,7 @@ class ServerDeletionService protected $daemonServerRepository; /** - * @var \Pterodactyl\Services\Database\DatabaseManagementService + * @var \Pterodactyl\Services\Databases\DatabaseManagementService */ protected $databaseManagementService; @@ -62,7 +62,7 @@ class ServerDeletionService * @param \Illuminate\Database\ConnectionInterface $connection * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonServerRepository * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $databaseRepository - * @param \Pterodactyl\Services\Database\DatabaseManagementService $databaseManagementService + * @param \Pterodactyl\Services\Databases\DatabaseManagementService $databaseManagementService * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository * @param \Illuminate\Log\Writer $writer */ diff --git a/app/Traits/Controllers/JavascriptInjection.php b/app/Traits/Controllers/JavascriptInjection.php index 5063a50f2..cce577819 100644 --- a/app/Traits/Controllers/JavascriptInjection.php +++ b/app/Traits/Controllers/JavascriptInjection.php @@ -10,25 +10,23 @@ namespace Pterodactyl\Traits\Controllers; use Javascript; +use Illuminate\Http\Request; trait JavascriptInjection { - /** - * @var \Illuminate\Contracts\Session\Session - */ - protected $session; - /** * Injects server javascript into the page to be used by other services. * - * @param array $args - * @param bool $overwrite - * @return mixed + * @param array $args + * @param bool $overwrite + * @param \Illuminate\Http\Request|null $request + * @return array */ - public function injectJavascript($args = [], $overwrite = false) + public function injectJavascript($args = [], $overwrite = false, Request $request = null) { - $server = $this->session->get('server_data.model'); - $token = $this->session->get('server_data.token'); + $request = $request ?? app()->make(Request::class); + $server = $request->attributes->get('server'); + $token = $request->attributes->get('server_token'); $response = array_merge([ 'server' => [ diff --git a/public/js/laroute.js b/public/js/laroute.js index e6e9db4c7..deec07500 100644 --- a/public/js/laroute.js +++ b/public/js/laroute.js @@ -6,7 +6,7 @@ absolute: false, rootUrl: 'http://pterodactyl.app', - routes : [{"host":null,"methods":["GET","HEAD"],"uri":"api\/user","name":"api.user","action":"Pterodactyl\Http\Controllers\API\User\CoreController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/user\/server\/{server}","name":"api.user.server","action":"Pterodactyl\Http\Controllers\API\User\ServerController@index"},{"host":null,"methods":["POST"],"uri":"api\/user\/server\/{server}\/power","name":"api.user.server.power","action":"Pterodactyl\Http\Controllers\API\User\ServerController@power"},{"host":null,"methods":["POST"],"uri":"api\/user\/server\/{server}\/command","name":"api.user.server.command","action":"Pterodactyl\Http\Controllers\API\User\ServerController@command"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/admin","name":null,"action":"Pterodactyl\Http\Controllers\API\Admin\CoreController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/admin\/servers","name":null,"action":"Pterodactyl\Http\Controllers\API\Admin\ServerController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/admin\/servers\/{id}","name":null,"action":"Pterodactyl\Http\Controllers\API\Admin\ServerController@view"},{"host":null,"methods":["POST"],"uri":"api\/admin\/servers","name":null,"action":"Pterodactyl\Http\Controllers\API\Admin\ServerController@store"},{"host":null,"methods":["PUT"],"uri":"api\/admin\/servers\/{id}\/details","name":null,"action":"Pterodactyl\Http\Controllers\API\Admin\ServerController@details"},{"host":null,"methods":["PUT"],"uri":"api\/admin\/servers\/{id}\/container","name":null,"action":"Pterodactyl\Http\Controllers\API\Admin\ServerController@container"},{"host":null,"methods":["PUT"],"uri":"api\/admin\/servers\/{id}\/build","name":null,"action":"Pterodactyl\Http\Controllers\API\Admin\ServerController@build"},{"host":null,"methods":["PUT"],"uri":"api\/admin\/servers\/{id}\/startup","name":null,"action":"Pterodactyl\Http\Controllers\API\Admin\ServerController@startup"},{"host":null,"methods":["PATCH"],"uri":"api\/admin\/servers\/{id}\/install","name":null,"action":"Pterodactyl\Http\Controllers\API\Admin\ServerController@install"},{"host":null,"methods":["PATCH"],"uri":"api\/admin\/servers\/{id}\/rebuild","name":null,"action":"Pterodactyl\Http\Controllers\API\Admin\ServerController@rebuild"},{"host":null,"methods":["PATCH"],"uri":"api\/admin\/servers\/{id}\/suspend","name":null,"action":"Pterodactyl\Http\Controllers\API\Admin\ServerController@suspend"},{"host":null,"methods":["DELETE"],"uri":"api\/admin\/servers\/{id}","name":null,"action":"Pterodactyl\Http\Controllers\API\Admin\ServerController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/admin\/locations","name":null,"action":"Pterodactyl\Http\Controllers\API\Admin\LocationController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/admin\/nodes","name":null,"action":"Pterodactyl\Http\Controllers\API\Admin\NodeController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/admin\/nodes\/{id}","name":null,"action":"Pterodactyl\Http\Controllers\API\Admin\NodeController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/admin\/nodes\/{id}\/config","name":null,"action":"Pterodactyl\Http\Controllers\API\Admin\NodeController@viewConfig"},{"host":null,"methods":["POST"],"uri":"api\/admin\/nodes","name":null,"action":"Pterodactyl\Http\Controllers\API\Admin\NodeController@store"},{"host":null,"methods":["DELETE"],"uri":"api\/admin\/nodes\/{id}","name":null,"action":"Pterodactyl\Http\Controllers\API\Admin\NodeController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/admin\/users","name":null,"action":"Pterodactyl\Http\Controllers\API\Admin\UserController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/admin\/users\/{id}","name":null,"action":"Pterodactyl\Http\Controllers\API\Admin\UserController@view"},{"host":null,"methods":["POST"],"uri":"api\/admin\/users","name":null,"action":"Pterodactyl\Http\Controllers\API\Admin\UserController@store"},{"host":null,"methods":["PUT"],"uri":"api\/admin\/users\/{id}","name":null,"action":"Pterodactyl\Http\Controllers\API\Admin\UserController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/admin\/users\/{id}","name":null,"action":"Pterodactyl\Http\Controllers\API\Admin\UserController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/admin\/services","name":null,"action":"Pterodactyl\Http\Controllers\API\Admin\ServiceController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/admin\/services\/{id}","name":null,"action":"Pterodactyl\Http\Controllers\API\Admin\ServiceController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"\/","name":"index","action":"Pterodactyl\Http\Controllers\Base\IndexController@getIndex"},{"host":null,"methods":["GET","HEAD"],"uri":"status\/{server}","name":"index.status","action":"Pterodactyl\Http\Controllers\Base\IndexController@status"},{"host":null,"methods":["GET","HEAD"],"uri":"account","name":"account","action":"Pterodactyl\Http\Controllers\Base\AccountController@index"},{"host":null,"methods":["POST"],"uri":"account","name":null,"action":"Pterodactyl\Http\Controllers\Base\AccountController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"account\/api","name":"account.api","action":"Pterodactyl\Http\Controllers\Base\APIController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"account\/api\/new","name":"account.api.new","action":"Pterodactyl\Http\Controllers\Base\APIController@create"},{"host":null,"methods":["POST"],"uri":"account\/api\/new","name":null,"action":"Pterodactyl\Http\Controllers\Base\APIController@store"},{"host":null,"methods":["DELETE"],"uri":"account\/api\/revoke\/{key}","name":"account.api.revoke","action":"Pterodactyl\Http\Controllers\Base\APIController@revoke"},{"host":null,"methods":["GET","HEAD"],"uri":"account\/security","name":"account.security","action":"Pterodactyl\Http\Controllers\Base\SecurityController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"account\/security\/revoke\/{id}","name":"account.security.revoke","action":"Pterodactyl\Http\Controllers\Base\SecurityController@revoke"},{"host":null,"methods":["PUT"],"uri":"account\/security\/totp","name":"account.security.totp","action":"Pterodactyl\Http\Controllers\Base\SecurityController@generateTotp"},{"host":null,"methods":["POST"],"uri":"account\/security\/totp","name":"account.security.totp.set","action":"Pterodactyl\Http\Controllers\Base\SecurityController@setTotp"},{"host":null,"methods":["DELETE"],"uri":"account\/security\/totp","name":"account.security.totp.disable","action":"Pterodactyl\Http\Controllers\Base\SecurityController@disableTotp"},{"host":null,"methods":["GET","HEAD"],"uri":"admin","name":"admin.index","action":"Pterodactyl\Http\Controllers\Admin\BaseController@getIndex"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/locations","name":"admin.locations","action":"Pterodactyl\Http\Controllers\Admin\LocationController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/locations\/view\/{location}","name":"admin.locations.view","action":"Pterodactyl\Http\Controllers\Admin\LocationController@view"},{"host":null,"methods":["POST"],"uri":"admin\/locations","name":null,"action":"Pterodactyl\Http\Controllers\Admin\LocationController@create"},{"host":null,"methods":["PATCH"],"uri":"admin\/locations\/view\/{location}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\LocationController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/databases","name":"admin.databases","action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/databases\/view\/{host}","name":"admin.databases.view","action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@view"},{"host":null,"methods":["POST"],"uri":"admin\/databases","name":null,"action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@create"},{"host":null,"methods":["PATCH"],"uri":"admin\/databases\/view\/{host}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/settings","name":"admin.settings","action":"Pterodactyl\Http\Controllers\Admin\BaseController@getSettings"},{"host":null,"methods":["POST"],"uri":"admin\/settings","name":null,"action":"Pterodactyl\Http\Controllers\Admin\BaseController@postSettings"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/users","name":"admin.users","action":"Pterodactyl\Http\Controllers\Admin\UserController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/users\/accounts.json","name":"admin.users.json","action":"Pterodactyl\Http\Controllers\Admin\UserController@json"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/users\/new","name":"admin.users.new","action":"Pterodactyl\Http\Controllers\Admin\UserController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/users\/view\/{user}","name":"admin.users.view","action":"Pterodactyl\Http\Controllers\Admin\UserController@view"},{"host":null,"methods":["POST"],"uri":"admin\/users\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\UserController@store"},{"host":null,"methods":["PATCH"],"uri":"admin\/users\/view\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\UserController@update"},{"host":null,"methods":["DELETE"],"uri":"admin\/users\/view\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\UserController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers","name":"admin.servers","action":"Pterodactyl\Http\Controllers\Admin\ServersController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/new","name":"admin.servers.new","action":"Pterodactyl\Http\Controllers\Admin\ServersController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}","name":"admin.servers.view","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewIndex"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/details","name":"admin.servers.view.details","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewDetails"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/build","name":"admin.servers.view.build","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewBuild"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/startup","name":"admin.servers.view.startup","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewStartup"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/database","name":"admin.servers.view.database","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewDatabase"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/manage","name":"admin.servers.view.manage","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewManage"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/delete","name":"admin.servers.view.delete","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewDelete"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@store"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/new\/nodes","name":"admin.servers.new.nodes","action":"Pterodactyl\Http\Controllers\Admin\ServersController@nodes"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/build","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@updateBuild"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/startup","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@saveStartup"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/database","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@newDatabase"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/manage\/toggle","name":"admin.servers.view.manage.toggle","action":"Pterodactyl\Http\Controllers\Admin\ServersController@toggleInstall"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/manage\/rebuild","name":"admin.servers.view.manage.rebuild","action":"Pterodactyl\Http\Controllers\Admin\ServersController@rebuildContainer"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/manage\/suspension","name":"admin.servers.view.manage.suspension","action":"Pterodactyl\Http\Controllers\Admin\ServersController@manageSuspension"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/manage\/reinstall","name":"admin.servers.view.manage.reinstall","action":"Pterodactyl\Http\Controllers\Admin\ServersController@reinstallServer"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/delete","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@delete"},{"host":null,"methods":["PATCH"],"uri":"admin\/servers\/view\/{server}\/details","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@setDetails"},{"host":null,"methods":["PATCH"],"uri":"admin\/servers\/view\/{server}\/details\/container","name":"admin.servers.view.details.container","action":"Pterodactyl\Http\Controllers\Admin\ServersController@setContainer"},{"host":null,"methods":["PATCH"],"uri":"admin\/servers\/view\/{server}\/database","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@resetDatabasePassword"},{"host":null,"methods":["DELETE"],"uri":"admin\/servers\/view\/{server}\/database\/{database}\/delete","name":"admin.servers.view.database.delete","action":"Pterodactyl\Http\Controllers\Admin\ServersController@deleteDatabase"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes","name":"admin.nodes","action":"Pterodactyl\Http\Controllers\Admin\NodesController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/new","name":"admin.nodes.new","action":"Pterodactyl\Http\Controllers\Admin\NodesController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}","name":"admin.nodes.view","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewIndex"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/settings","name":"admin.nodes.view.settings","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewSettings"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/configuration","name":"admin.nodes.view.configuration","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewConfiguration"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/allocation","name":"admin.nodes.view.allocation","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewAllocation"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/servers","name":"admin.nodes.view.servers","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewServers"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/settings\/token","name":"admin.nodes.view.configuration.token","action":"Pterodactyl\Http\Controllers\Admin\NodesController@setToken"},{"host":null,"methods":["POST"],"uri":"admin\/nodes\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\NodesController@store"},{"host":null,"methods":["POST"],"uri":"admin\/nodes\/view\/{node}\/allocation","name":null,"action":"Pterodactyl\Http\Controllers\Admin\NodesController@createAllocation"},{"host":null,"methods":["POST"],"uri":"admin\/nodes\/view\/{node}\/allocation\/remove","name":"admin.nodes.view.allocation.removeBlock","action":"Pterodactyl\Http\Controllers\Admin\NodesController@allocationRemoveBlock"},{"host":null,"methods":["POST"],"uri":"admin\/nodes\/view\/{node}\/allocation\/alias","name":"admin.nodes.view.allocation.setAlias","action":"Pterodactyl\Http\Controllers\Admin\NodesController@allocationSetAlias"},{"host":null,"methods":["PATCH"],"uri":"admin\/nodes\/view\/{node}\/settings","name":null,"action":"Pterodactyl\Http\Controllers\Admin\NodesController@updateSettings"},{"host":null,"methods":["DELETE"],"uri":"admin\/nodes\/view\/{node}\/delete","name":"admin.nodes.view.delete","action":"Pterodactyl\Http\Controllers\Admin\NodesController@delete"},{"host":null,"methods":["DELETE"],"uri":"admin\/nodes\/view\/{node}\/allocation\/remove\/{allocation}","name":"admin.nodes.view.allocation.removeSingle","action":"Pterodactyl\Http\Controllers\Admin\NodesController@allocationRemoveSingle"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/services","name":"admin.services","action":"Pterodactyl\Http\Controllers\Admin\ServiceController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/services\/new","name":"admin.services.new","action":"Pterodactyl\Http\Controllers\Admin\ServiceController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/services\/view\/{service}","name":"admin.services.view","action":"Pterodactyl\Http\Controllers\Admin\ServiceController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/services\/view\/{service}\/functions","name":"admin.services.view.functions","action":"Pterodactyl\Http\Controllers\Admin\ServiceController@viewFunctions"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/services\/option\/new","name":"admin.services.option.new","action":"Pterodactyl\Http\Controllers\Admin\OptionController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/services\/option\/{option}","name":"admin.services.option.view","action":"Pterodactyl\Http\Controllers\Admin\OptionController@viewConfiguration"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/services\/option\/{option}\/variables","name":"admin.services.option.variables","action":"Pterodactyl\Http\Controllers\Admin\VariableController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/services\/option\/{option}\/scripts","name":"admin.services.option.scripts","action":"Pterodactyl\Http\Controllers\Admin\OptionController@viewScripts"},{"host":null,"methods":["POST"],"uri":"admin\/services\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServiceController@store"},{"host":null,"methods":["POST"],"uri":"admin\/services\/option\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\OptionController@store"},{"host":null,"methods":["POST"],"uri":"admin\/services\/option\/{option}\/variables","name":null,"action":"Pterodactyl\Http\Controllers\Admin\VariableController@store"},{"host":null,"methods":["PATCH"],"uri":"admin\/services\/view\/{service}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServiceController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/services\/view\/{service}\/functions","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServiceController@updateFunctions"},{"host":null,"methods":["PATCH"],"uri":"admin\/services\/option\/{option}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\OptionController@editConfiguration"},{"host":null,"methods":["PATCH"],"uri":"admin\/services\/option\/{option}\/scripts","name":null,"action":"Pterodactyl\Http\Controllers\Admin\OptionController@updateScripts"},{"host":null,"methods":["PATCH"],"uri":"admin\/services\/option\/{option}\/variables\/{variable}","name":"admin.services.option.variables.edit","action":"Pterodactyl\Http\Controllers\Admin\VariableController@update"},{"host":null,"methods":["DELETE"],"uri":"admin\/services\/view\/{service}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServiceController@destroy"},{"host":null,"methods":["DELETE"],"uri":"admin\/services\/option\/{option}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\OptionController@destroy"},{"host":null,"methods":["DELETE"],"uri":"admin\/services\/option\/{option}\/variables\/{variable}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\VariableController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/packs","name":"admin.packs","action":"Pterodactyl\Http\Controllers\Admin\PackController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/packs\/new","name":"admin.packs.new","action":"Pterodactyl\Http\Controllers\Admin\PackController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/packs\/new\/template","name":"admin.packs.new.template","action":"Pterodactyl\Http\Controllers\Admin\PackController@newTemplate"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/packs\/view\/{pack}","name":"admin.packs.view","action":"Pterodactyl\Http\Controllers\Admin\PackController@view"},{"host":null,"methods":["POST"],"uri":"admin\/packs\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\PackController@store"},{"host":null,"methods":["POST"],"uri":"admin\/packs\/view\/{pack}\/export\/{files?}","name":"admin.packs.view.export","action":"Pterodactyl\Http\Controllers\Admin\PackController@export"},{"host":null,"methods":["PATCH"],"uri":"admin\/packs\/view\/{pack}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\PackController@update"},{"host":null,"methods":["DELETE"],"uri":"admin\/packs\/view\/{pack}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\PackController@destroy"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/logout","name":"auth.logout","action":"Pterodactyl\Http\Controllers\Auth\LoginController@logout"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/login","name":"auth.login","action":"Pterodactyl\Http\Controllers\Auth\LoginController@showLoginForm"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/login\/totp","name":"auth.totp","action":"Pterodactyl\Http\Controllers\Auth\LoginController@totp"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/password","name":"auth.password","action":"Pterodactyl\Http\Controllers\Auth\ForgotPasswordController@showLinkRequestForm"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/password\/reset\/{token}","name":"auth.reset","action":"Pterodactyl\Http\Controllers\Auth\ResetPasswordController@showResetForm"},{"host":null,"methods":["POST"],"uri":"auth\/login","name":null,"action":"Pterodactyl\Http\Controllers\Auth\LoginController@login"},{"host":null,"methods":["POST"],"uri":"auth\/login\/totp","name":null,"action":"Pterodactyl\Http\Controllers\Auth\LoginController@totpCheckpoint"},{"host":null,"methods":["POST"],"uri":"auth\/password","name":null,"action":"Pterodactyl\Http\Controllers\Auth\ForgotPasswordController@sendResetLinkEmail"},{"host":null,"methods":["POST"],"uri":"auth\/password\/reset","name":"auth.reset.post","action":"Pterodactyl\Http\Controllers\Auth\ResetPasswordController@reset"},{"host":null,"methods":["POST"],"uri":"auth\/password\/reset\/{token}","name":null,"action":"Pterodactyl\Http\Controllers\Auth\ForgotPasswordController@sendResetLinkEmail"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}","name":"server.index","action":"Pterodactyl\Http\Controllers\Server\ConsoleController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/console","name":"server.console","action":"Pterodactyl\Http\Controllers\Server\ConsoleController@console"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/settings\/databases","name":"server.settings.databases","action":"Pterodactyl\Http\Controllers\Server\ServerController@getDatabases"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/settings\/sftp","name":"server.settings.sftp","action":"Pterodactyl\Http\Controllers\Server\ServerController@getSFTP"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/settings\/startup","name":"server.settings.startup","action":"Pterodactyl\Http\Controllers\Server\ServerController@getStartup"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/settings\/allocation","name":"server.settings.allocation","action":"Pterodactyl\Http\Controllers\Server\ServerController@getAllocation"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/settings\/sftp","name":null,"action":"Pterodactyl\Http\Controllers\Server\ServerController@postSettingsSFTP"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/settings\/startup","name":null,"action":"Pterodactyl\Http\Controllers\Server\ServerController@postSettingsStartup"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/files","name":"server.files.index","action":"Pterodactyl\Http\Controllers\Server\Files\FileActionsController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/files\/add","name":"server.files.add","action":"Pterodactyl\Http\Controllers\Server\Files\FileActionsController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/files\/edit\/{file}","name":"server.files.edit","action":"Pterodactyl\Http\Controllers\Server\Files\FileActionsController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/files\/download\/{file}","name":"server.files.edit","action":"Pterodactyl\Http\Controllers\Server\Files\DownloadController@index"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/files\/directory-list","name":"server.files.directory-list","action":"Pterodactyl\Http\Controllers\Server\Files\RemoteRequestController@directory"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/files\/save","name":"server.files.save","action":"Pterodactyl\Http\Controllers\Server\Files\RemoteRequestController@store"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/users","name":"server.subusers","action":"Pterodactyl\Http\Controllers\Server\SubuserController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/users\/new","name":"server.subusers.new","action":"Pterodactyl\Http\Controllers\Server\SubuserController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/users\/view\/{subuser}","name":"server.subusers.view","action":"Pterodactyl\Http\Controllers\Server\SubuserController@view"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/users\/new","name":null,"action":"Pterodactyl\Http\Controllers\Server\SubuserController@store"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/users\/view\/{subuser}","name":null,"action":"Pterodactyl\Http\Controllers\Server\SubuserController@update"},{"host":null,"methods":["DELETE"],"uri":"server\/{server}\/users\/view\/{subuser}\/delete","name":"server.subusers.delete","action":"Pterodactyl\Http\Controllers\Server\SubuserController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/schedules","name":"server.schedules","action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/schedules\/new","name":"server.schedules.new","action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/schedules\/view\/{schedule}","name":"server.schedules.view","action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@view"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/schedules\/new","name":null,"action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@store"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/schedules\/view\/{schedule}","name":null,"action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@update"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/schedules\/view\/{schedule}\/toggle","name":"server.schedules.toggle","action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskToggleController@index"},{"host":null,"methods":["DELETE"],"uri":"server\/{server}\/schedules\/view\/{schedule}\/delete","name":"server.schedules.delete","action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@delete"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/ajax\/settings\/reset-database-password","name":"server.ajax.reset-database-password","action":"Pterodactyl\Http\Controllers\Server\AjaxController@postResetDatabasePassword"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/remote\/authenticate\/{token}","name":"post.api.remote.authenticate","action":"Pterodactyl\Http\Controllers\API\Remote\ValidateKeyController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"daemon\/services","name":"daemon.services","action":"Pterodactyl\Http\Controllers\Daemon\ServiceController@listServices"},{"host":null,"methods":["GET","HEAD"],"uri":"daemon\/services\/pull\/{service}\/{file}","name":"daemon.pull","action":"Pterodactyl\Http\Controllers\Daemon\ServiceController@pull"},{"host":null,"methods":["GET","HEAD"],"uri":"daemon\/packs\/pull\/{uuid}","name":"daemon.pack.pull","action":"Pterodactyl\Http\Controllers\Daemon\PackController@pull"},{"host":null,"methods":["GET","HEAD"],"uri":"daemon\/packs\/pull\/{uuid}\/hash","name":"daemon.pack.hash","action":"Pterodactyl\Http\Controllers\Daemon\PackController@hash"},{"host":null,"methods":["GET","HEAD"],"uri":"daemon\/details\/option\/{server}","name":"daemon.option.details","action":"Pterodactyl\Http\Controllers\Daemon\OptionController@details"},{"host":null,"methods":["GET","HEAD"],"uri":"daemon\/configure\/{token}","name":"daemon.configuration","action":"Pterodactyl\Http\Controllers\Daemon\ActionController@configuration"},{"host":null,"methods":["POST"],"uri":"daemon\/download","name":"daemon.download","action":"Pterodactyl\Http\Controllers\Daemon\ActionController@authenticateDownload"},{"host":null,"methods":["POST"],"uri":"daemon\/install","name":"daemon.install","action":"Pterodactyl\Http\Controllers\Daemon\ActionController@markInstall"},{"host":null,"methods":["GET","HEAD"],"uri":"_debugbar\/open","name":"debugbar.openhandler","action":"Barryvdh\Debugbar\Controllers\OpenHandlerController@handle"},{"host":null,"methods":["GET","HEAD"],"uri":"_debugbar\/clockwork\/{id}","name":"debugbar.clockwork","action":"Barryvdh\Debugbar\Controllers\OpenHandlerController@clockwork"},{"host":null,"methods":["GET","HEAD"],"uri":"_debugbar\/assets\/stylesheets","name":"debugbar.assets.css","action":"Barryvdh\Debugbar\Controllers\AssetController@css"},{"host":null,"methods":["GET","HEAD"],"uri":"_debugbar\/assets\/javascript","name":"debugbar.assets.js","action":"Barryvdh\Debugbar\Controllers\AssetController@js"}], + routes : [{"host":null,"methods":["GET","HEAD"],"uri":"\/","name":"index","action":"Pterodactyl\Http\Controllers\Base\IndexController@getIndex"},{"host":null,"methods":["GET","HEAD"],"uri":"status\/{server}","name":"index.status","action":"Pterodactyl\Http\Controllers\Base\IndexController@status"},{"host":null,"methods":["GET","HEAD"],"uri":"account","name":"account","action":"Pterodactyl\Http\Controllers\Base\AccountController@index"},{"host":null,"methods":["POST"],"uri":"account","name":null,"action":"Pterodactyl\Http\Controllers\Base\AccountController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"account\/api","name":"account.api","action":"Pterodactyl\Http\Controllers\Base\APIController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"account\/api\/new","name":"account.api.new","action":"Pterodactyl\Http\Controllers\Base\APIController@create"},{"host":null,"methods":["POST"],"uri":"account\/api\/new","name":null,"action":"Pterodactyl\Http\Controllers\Base\APIController@store"},{"host":null,"methods":["DELETE"],"uri":"account\/api\/revoke\/{key}","name":"account.api.revoke","action":"Pterodactyl\Http\Controllers\Base\APIController@revoke"},{"host":null,"methods":["GET","HEAD"],"uri":"account\/security","name":"account.security","action":"Pterodactyl\Http\Controllers\Base\SecurityController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"account\/security\/revoke\/{id}","name":"account.security.revoke","action":"Pterodactyl\Http\Controllers\Base\SecurityController@revoke"},{"host":null,"methods":["PUT"],"uri":"account\/security\/totp","name":"account.security.totp","action":"Pterodactyl\Http\Controllers\Base\SecurityController@generateTotp"},{"host":null,"methods":["POST"],"uri":"account\/security\/totp","name":"account.security.totp.set","action":"Pterodactyl\Http\Controllers\Base\SecurityController@setTotp"},{"host":null,"methods":["DELETE"],"uri":"account\/security\/totp","name":"account.security.totp.disable","action":"Pterodactyl\Http\Controllers\Base\SecurityController@disableTotp"},{"host":null,"methods":["GET","HEAD"],"uri":"admin","name":"admin.index","action":"Pterodactyl\Http\Controllers\Admin\BaseController@getIndex"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/locations","name":"admin.locations","action":"Pterodactyl\Http\Controllers\Admin\LocationController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/locations\/view\/{location}","name":"admin.locations.view","action":"Pterodactyl\Http\Controllers\Admin\LocationController@view"},{"host":null,"methods":["POST"],"uri":"admin\/locations","name":null,"action":"Pterodactyl\Http\Controllers\Admin\LocationController@create"},{"host":null,"methods":["PATCH"],"uri":"admin\/locations\/view\/{location}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\LocationController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/databases","name":"admin.databases","action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/databases\/view\/{host}","name":"admin.databases.view","action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@view"},{"host":null,"methods":["POST"],"uri":"admin\/databases","name":null,"action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@create"},{"host":null,"methods":["PATCH"],"uri":"admin\/databases\/view\/{host}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@update"},{"host":null,"methods":["DELETE"],"uri":"admin\/databases\/view\/{host}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/settings","name":"admin.settings","action":"Pterodactyl\Http\Controllers\Admin\BaseController@getSettings"},{"host":null,"methods":["POST"],"uri":"admin\/settings","name":null,"action":"Pterodactyl\Http\Controllers\Admin\BaseController@postSettings"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/users","name":"admin.users","action":"Pterodactyl\Http\Controllers\Admin\UserController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/users\/accounts.json","name":"admin.users.json","action":"Pterodactyl\Http\Controllers\Admin\UserController@json"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/users\/new","name":"admin.users.new","action":"Pterodactyl\Http\Controllers\Admin\UserController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/users\/view\/{user}","name":"admin.users.view","action":"Pterodactyl\Http\Controllers\Admin\UserController@view"},{"host":null,"methods":["POST"],"uri":"admin\/users\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\UserController@store"},{"host":null,"methods":["PATCH"],"uri":"admin\/users\/view\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\UserController@update"},{"host":null,"methods":["DELETE"],"uri":"admin\/users\/view\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\UserController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers","name":"admin.servers","action":"Pterodactyl\Http\Controllers\Admin\ServersController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/new","name":"admin.servers.new","action":"Pterodactyl\Http\Controllers\Admin\ServersController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}","name":"admin.servers.view","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewIndex"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/details","name":"admin.servers.view.details","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewDetails"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/build","name":"admin.servers.view.build","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewBuild"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/startup","name":"admin.servers.view.startup","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewStartup"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/database","name":"admin.servers.view.database","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewDatabase"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/manage","name":"admin.servers.view.manage","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewManage"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/delete","name":"admin.servers.view.delete","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewDelete"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@store"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/new\/nodes","name":"admin.servers.new.nodes","action":"Pterodactyl\Http\Controllers\Admin\ServersController@nodes"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/build","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@updateBuild"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/startup","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@saveStartup"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/database","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@newDatabase"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/manage\/toggle","name":"admin.servers.view.manage.toggle","action":"Pterodactyl\Http\Controllers\Admin\ServersController@toggleInstall"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/manage\/rebuild","name":"admin.servers.view.manage.rebuild","action":"Pterodactyl\Http\Controllers\Admin\ServersController@rebuildContainer"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/manage\/suspension","name":"admin.servers.view.manage.suspension","action":"Pterodactyl\Http\Controllers\Admin\ServersController@manageSuspension"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/manage\/reinstall","name":"admin.servers.view.manage.reinstall","action":"Pterodactyl\Http\Controllers\Admin\ServersController@reinstallServer"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/delete","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@delete"},{"host":null,"methods":["PATCH"],"uri":"admin\/servers\/view\/{server}\/details","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@setDetails"},{"host":null,"methods":["PATCH"],"uri":"admin\/servers\/view\/{server}\/details\/container","name":"admin.servers.view.details.container","action":"Pterodactyl\Http\Controllers\Admin\ServersController@setContainer"},{"host":null,"methods":["PATCH"],"uri":"admin\/servers\/view\/{server}\/database","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@resetDatabasePassword"},{"host":null,"methods":["DELETE"],"uri":"admin\/servers\/view\/{server}\/database\/{database}\/delete","name":"admin.servers.view.database.delete","action":"Pterodactyl\Http\Controllers\Admin\ServersController@deleteDatabase"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes","name":"admin.nodes","action":"Pterodactyl\Http\Controllers\Admin\NodesController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/new","name":"admin.nodes.new","action":"Pterodactyl\Http\Controllers\Admin\NodesController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}","name":"admin.nodes.view","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewIndex"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/settings","name":"admin.nodes.view.settings","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewSettings"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/configuration","name":"admin.nodes.view.configuration","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewConfiguration"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/allocation","name":"admin.nodes.view.allocation","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewAllocation"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/servers","name":"admin.nodes.view.servers","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewServers"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/settings\/token","name":"admin.nodes.view.configuration.token","action":"Pterodactyl\Http\Controllers\Admin\NodesController@setToken"},{"host":null,"methods":["POST"],"uri":"admin\/nodes\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\NodesController@store"},{"host":null,"methods":["POST"],"uri":"admin\/nodes\/view\/{node}\/allocation","name":null,"action":"Pterodactyl\Http\Controllers\Admin\NodesController@createAllocation"},{"host":null,"methods":["POST"],"uri":"admin\/nodes\/view\/{node}\/allocation\/remove","name":"admin.nodes.view.allocation.removeBlock","action":"Pterodactyl\Http\Controllers\Admin\NodesController@allocationRemoveBlock"},{"host":null,"methods":["POST"],"uri":"admin\/nodes\/view\/{node}\/allocation\/alias","name":"admin.nodes.view.allocation.setAlias","action":"Pterodactyl\Http\Controllers\Admin\NodesController@allocationSetAlias"},{"host":null,"methods":["PATCH"],"uri":"admin\/nodes\/view\/{node}\/settings","name":null,"action":"Pterodactyl\Http\Controllers\Admin\NodesController@updateSettings"},{"host":null,"methods":["DELETE"],"uri":"admin\/nodes\/view\/{node}\/delete","name":"admin.nodes.view.delete","action":"Pterodactyl\Http\Controllers\Admin\NodesController@delete"},{"host":null,"methods":["DELETE"],"uri":"admin\/nodes\/view\/{node}\/allocation\/remove\/{allocation}","name":"admin.nodes.view.allocation.removeSingle","action":"Pterodactyl\Http\Controllers\Admin\NodesController@allocationRemoveSingle"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests","name":"admin.nests","action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/new","name":"admin.nests.new","action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/view\/{nest}","name":"admin.nests.view","action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/egg\/new","name":"admin.nests.egg.new","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/egg\/{egg}","name":"admin.nests.egg.view","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/egg\/{egg}\/export","name":"admin.nests.egg.export","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggShareController@export"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/egg\/{egg}\/variables","name":"admin.nests.egg.variables","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggVariableController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/egg\/{egg}\/scripts","name":"admin.nests.egg.scripts","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggScriptController@index"},{"host":null,"methods":["POST"],"uri":"admin\/nests\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@store"},{"host":null,"methods":["POST"],"uri":"admin\/nests\/import","name":"admin.nests.egg.import","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggShareController@import"},{"host":null,"methods":["POST"],"uri":"admin\/nests\/egg\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggController@store"},{"host":null,"methods":["POST"],"uri":"admin\/nests\/egg\/{egg}\/variables","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggVariableController@store"},{"host":null,"methods":["PUT"],"uri":"admin\/nests\/egg\/{egg}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggShareController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/nests\/view\/{nest}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/nests\/egg\/{egg}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/nests\/egg\/{egg}\/scripts","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggScriptController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/nests\/egg\/{egg}\/variables\/{variable}","name":"admin.nests.egg.variables.edit","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggVariableController@update"},{"host":null,"methods":["DELETE"],"uri":"admin\/nests\/view\/{nest}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@destroy"},{"host":null,"methods":["DELETE"],"uri":"admin\/nests\/egg\/{egg}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggController@destroy"},{"host":null,"methods":["DELETE"],"uri":"admin\/nests\/egg\/{egg}\/variables\/{variable}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggVariableController@destroy"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/packs","name":"admin.packs","action":"Pterodactyl\Http\Controllers\Admin\PackController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/packs\/new","name":"admin.packs.new","action":"Pterodactyl\Http\Controllers\Admin\PackController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/packs\/new\/template","name":"admin.packs.new.template","action":"Pterodactyl\Http\Controllers\Admin\PackController@newTemplate"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/packs\/view\/{pack}","name":"admin.packs.view","action":"Pterodactyl\Http\Controllers\Admin\PackController@view"},{"host":null,"methods":["POST"],"uri":"admin\/packs\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\PackController@store"},{"host":null,"methods":["POST"],"uri":"admin\/packs\/view\/{pack}\/export\/{files?}","name":"admin.packs.view.export","action":"Pterodactyl\Http\Controllers\Admin\PackController@export"},{"host":null,"methods":["PATCH"],"uri":"admin\/packs\/view\/{pack}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\PackController@update"},{"host":null,"methods":["DELETE"],"uri":"admin\/packs\/view\/{pack}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\PackController@destroy"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/logout","name":"auth.logout","action":"Pterodactyl\Http\Controllers\Auth\LoginController@logout"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/login","name":"auth.login","action":"Pterodactyl\Http\Controllers\Auth\LoginController@showLoginForm"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/login\/totp","name":"auth.totp","action":"Pterodactyl\Http\Controllers\Auth\LoginController@totp"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/password","name":"auth.password","action":"Pterodactyl\Http\Controllers\Auth\ForgotPasswordController@showLinkRequestForm"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/password\/reset\/{token}","name":"auth.reset","action":"Pterodactyl\Http\Controllers\Auth\ResetPasswordController@showResetForm"},{"host":null,"methods":["POST"],"uri":"auth\/login","name":null,"action":"Pterodactyl\Http\Controllers\Auth\LoginController@login"},{"host":null,"methods":["POST"],"uri":"auth\/login\/totp","name":null,"action":"Pterodactyl\Http\Controllers\Auth\LoginController@totpCheckpoint"},{"host":null,"methods":["POST"],"uri":"auth\/password","name":null,"action":"Pterodactyl\Http\Controllers\Auth\ForgotPasswordController@sendResetLinkEmail"},{"host":null,"methods":["POST"],"uri":"auth\/password\/reset","name":"auth.reset.post","action":"Pterodactyl\Http\Controllers\Auth\ResetPasswordController@reset"},{"host":null,"methods":["POST"],"uri":"auth\/password\/reset\/{token}","name":null,"action":"Pterodactyl\Http\Controllers\Auth\ForgotPasswordController@sendResetLinkEmail"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}","name":"server.index","action":"Pterodactyl\Http\Controllers\Server\ConsoleController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/console","name":"server.console","action":"Pterodactyl\Http\Controllers\Server\ConsoleController@console"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/settings\/sftp","name":"server.settings.sftp","action":"Pterodactyl\Http\Controllers\Server\ServerController@getSFTP"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/settings\/startup","name":"server.settings.startup","action":"Pterodactyl\Http\Controllers\Server\ServerController@getStartup"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/settings\/allocation","name":"server.settings.allocation","action":"Pterodactyl\Http\Controllers\Server\ServerController@getAllocation"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/settings\/sftp","name":null,"action":"Pterodactyl\Http\Controllers\Server\ServerController@postSettingsSFTP"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/settings\/startup","name":null,"action":"Pterodactyl\Http\Controllers\Server\ServerController@postSettingsStartup"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/databases","name":"server.databases.index","action":"Pterodactyl\Http\Controllers\Server\DatabaseController@index"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/databases\/password","name":"server.databases.password","action":"Pterodactyl\Http\Controllers\Server\DatabaseController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/files","name":"server.files.index","action":"Pterodactyl\Http\Controllers\Server\Files\FileActionsController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/files\/add","name":"server.files.add","action":"Pterodactyl\Http\Controllers\Server\Files\FileActionsController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/files\/edit\/{file}","name":"server.files.edit","action":"Pterodactyl\Http\Controllers\Server\Files\FileActionsController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/files\/download\/{file}","name":"server.files.edit","action":"Pterodactyl\Http\Controllers\Server\Files\DownloadController@index"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/files\/directory-list","name":"server.files.directory-list","action":"Pterodactyl\Http\Controllers\Server\Files\RemoteRequestController@directory"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/files\/save","name":"server.files.save","action":"Pterodactyl\Http\Controllers\Server\Files\RemoteRequestController@store"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/users","name":"server.subusers","action":"Pterodactyl\Http\Controllers\Server\SubuserController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/users\/new","name":"server.subusers.new","action":"Pterodactyl\Http\Controllers\Server\SubuserController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/users\/view\/{subuser}","name":"server.subusers.view","action":"Pterodactyl\Http\Controllers\Server\SubuserController@view"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/users\/new","name":null,"action":"Pterodactyl\Http\Controllers\Server\SubuserController@store"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/users\/view\/{subuser}","name":null,"action":"Pterodactyl\Http\Controllers\Server\SubuserController@update"},{"host":null,"methods":["DELETE"],"uri":"server\/{server}\/users\/view\/{subuser}\/delete","name":"server.subusers.delete","action":"Pterodactyl\Http\Controllers\Server\SubuserController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/schedules","name":"server.schedules","action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/schedules\/new","name":"server.schedules.new","action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/schedules\/view\/{schedule}","name":"server.schedules.view","action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@view"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/schedules\/new","name":null,"action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@store"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/schedules\/view\/{schedule}","name":null,"action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@update"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/schedules\/view\/{schedule}\/toggle","name":"server.schedules.toggle","action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskToggleController@index"},{"host":null,"methods":["DELETE"],"uri":"server\/{server}\/schedules\/view\/{schedule}\/delete","name":"server.schedules.delete","action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/remote\/authenticate\/{token}","name":"api.remote.authenticate","action":"Pterodactyl\Http\Controllers\API\Remote\ValidateKeyController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/remote\/eggs","name":"api.remote.eggs","action":"Pterodactyl\Http\Controllers\API\Remote\EggRetrievalController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/remote\/eggs\/{uuid}","name":"api.remote.eggs.download","action":"Pterodactyl\Http\Controllers\API\Remote\EggRetrievalController@download"},{"host":null,"methods":["GET","HEAD"],"uri":"daemon\/packs\/pull\/{uuid}","name":"daemon.pack.pull","action":"Pterodactyl\Http\Controllers\Daemon\PackController@pull"},{"host":null,"methods":["GET","HEAD"],"uri":"daemon\/packs\/pull\/{uuid}\/hash","name":"daemon.pack.hash","action":"Pterodactyl\Http\Controllers\Daemon\PackController@hash"},{"host":null,"methods":["GET","HEAD"],"uri":"daemon\/details\/option\/{server}","name":"daemon.option.details","action":"Pterodactyl\Http\Controllers\Daemon\OptionController@details"},{"host":null,"methods":["GET","HEAD"],"uri":"daemon\/configure\/{token}","name":"daemon.configuration","action":"Pterodactyl\Http\Controllers\Daemon\ActionController@configuration"},{"host":null,"methods":["POST"],"uri":"daemon\/download","name":"daemon.download","action":"Pterodactyl\Http\Controllers\Daemon\ActionController@authenticateDownload"},{"host":null,"methods":["POST"],"uri":"daemon\/install","name":"daemon.install","action":"Pterodactyl\Http\Controllers\Daemon\ActionController@markInstall"},{"host":null,"methods":["GET","HEAD"],"uri":"_debugbar\/open","name":"debugbar.openhandler","action":"Barryvdh\Debugbar\Controllers\OpenHandlerController@handle"},{"host":null,"methods":["GET","HEAD"],"uri":"_debugbar\/clockwork\/{id}","name":"debugbar.clockwork","action":"Barryvdh\Debugbar\Controllers\OpenHandlerController@clockwork"},{"host":null,"methods":["GET","HEAD"],"uri":"_debugbar\/assets\/stylesheets","name":"debugbar.assets.css","action":"Barryvdh\Debugbar\Controllers\AssetController@css"},{"host":null,"methods":["GET","HEAD"],"uri":"_debugbar\/assets\/javascript","name":"debugbar.assets.js","action":"Barryvdh\Debugbar\Controllers\AssetController@js"}], prefix: '', route : function (name, parameters, route) { diff --git a/resources/lang/en/server.php b/resources/lang/en/server.php index 0aa414fd2..ec570e4a8 100644 --- a/resources/lang/en/server.php +++ b/resources/lang/en/server.php @@ -302,7 +302,7 @@ return [ 'database' => [ 'header' => 'Databases', 'header_sub' => 'All databases available for this server.', - 'your_dbs' => 'Your Databases', + 'your_dbs' => 'Configured Databases', 'host' => 'MySQL Host', 'reset_password' => 'Reset Password', 'no_dbs' => 'There are no databases listed for this server.', diff --git a/resources/themes/pterodactyl/admin/databases/view.blade.php b/resources/themes/pterodactyl/admin/databases/view.blade.php index 65296e578..427fafb82 100644 --- a/resources/themes/pterodactyl/admin/databases/view.blade.php +++ b/resources/themes/pterodactyl/admin/databases/view.blade.php @@ -79,9 +79,8 @@ diff --git a/resources/themes/pterodactyl/admin/servers/view/database.blade.php b/resources/themes/pterodactyl/admin/servers/view/database.blade.php index c76d1fbba..6c556137d 100644 --- a/resources/themes/pterodactyl/admin/servers/view/database.blade.php +++ b/resources/themes/pterodactyl/admin/servers/view/database.blade.php @@ -40,6 +40,9 @@
+
+ Database passwords can be viewed when visiting this server on the front-end. +

Active Databases

@@ -128,7 +131,7 @@ }, function () { $.ajax({ method: 'DELETE', - url: Router.route('admin.servers.view.database.delete', { id: '{{ $server->id }}', database: self.data('id') }), + url: Router.route('admin.servers.view.database.delete', { server: '{{ $server->id }}', database: self.data('id') }), headers: { 'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content') }, }).done(function () { self.parent().parent().slideUp(); @@ -149,7 +152,7 @@ $(this).addClass('disabled').find('i').addClass('fa-spin'); $.ajax({ type: 'PATCH', - url: Router.route('admin.servers.view.database', { id: '{{ $server->id }}' }), + url: Router.route('admin.servers.view.database', { server: '{{ $server->id }}' }), headers: { 'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content') }, data: { database: $(this).data('id') }, }).done(function (data) { diff --git a/resources/themes/pterodactyl/layouts/master.blade.php b/resources/themes/pterodactyl/layouts/master.blade.php index 102d45fa9..651686a33 100644 --- a/resources/themes/pterodactyl/layouts/master.blade.php +++ b/resources/themes/pterodactyl/layouts/master.blade.php @@ -155,6 +155,17 @@ @endcan + @can('view-databases', $server) +
  • + + @lang('navigation.server.databases') + +
  • + @endcan @if(Gate::allows('view-startup', $server) || Gate::allows('view-sftp', $server) || Gate::allows('view-databases', $server) || Gate::allows('view-allocation', $server))
  • @lang('navigation.server.startup_parameters')
  • @endcan - @can('view-databases', $server) -
  • @lang('navigation.server.databases')
  • - @endcan @endif diff --git a/resources/themes/pterodactyl/server/settings/databases.blade.php b/resources/themes/pterodactyl/server/databases/index.blade.php similarity index 55% rename from resources/themes/pterodactyl/server/settings/databases.blade.php rename to resources/themes/pterodactyl/server/databases/index.blade.php index e2c400da1..6ba24c1ad 100644 --- a/resources/themes/pterodactyl/server/settings/databases.blade.php +++ b/resources/themes/pterodactyl/server/databases/index.blade.php @@ -25,6 +25,11 @@

    @lang('server.config.database.your_dbs')

    + @if(auth()->user()->root_admin) + + @endif
    @if(count($databases) > 0)
    @@ -41,7 +46,14 @@ {{ $database->database }} {{ $database->username }} - {{ Crypt::decrypt($database->password) }} + + + •••••••• + + + {{ $database->host->host }}:{{ $database->host->port }} @can('reset-db-password', $server) @@ -55,7 +67,7 @@
    @else
    -
    +
    @lang('server.config.database.no_dbs') @if(Auth::user()->root_admin === 1) @endsection diff --git a/routes/admin.php b/routes/admin.php index da1451cb9..9332b82de 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -38,6 +38,7 @@ Route::group(['prefix' => 'databases'], function () { Route::post('/', 'DatabaseController@create'); Route::patch('/view/{host}', 'DatabaseController@update'); + Route::delete('/view/{host}', 'DatabaseController@delete'); }); /* diff --git a/routes/server.php b/routes/server.php index c0d04dd39..6386b6618 100644 --- a/routes/server.php +++ b/routes/server.php @@ -18,7 +18,6 @@ Route::get('/console', 'ConsoleController@console')->name('server.console'); | */ Route::group(['prefix' => 'settings'], function () { - Route::get('/databases', 'ServerController@getDatabases')->name('server.settings.databases'); Route::get('/sftp', 'ServerController@getSFTP')->name('server.settings.sftp'); Route::get('/startup', 'ServerController@getStartup')->name('server.settings.startup'); Route::get('/allocation', 'ServerController@getAllocation')->name('server.settings.allocation'); @@ -27,6 +26,20 @@ Route::group(['prefix' => 'settings'], function () { Route::post('/startup', 'ServerController@postSettingsStartup'); }); +/* +|-------------------------------------------------------------------------- +| Server Database Controller Routes +|-------------------------------------------------------------------------- +| +| Endpoint: /server/{server}/databases +| +*/ +Route::group(['prefix' => 'databases'], function () { + Route::get('/', 'DatabaseController@index')->name('server.databases.index'); + + Route::patch('/password', 'DatabaseController@update')->middleware('server..database')->name('server.databases.password'); +}); + /* |-------------------------------------------------------------------------- | Server File Manager Controller Routes @@ -56,13 +69,13 @@ Route::group(['prefix' => 'files'], function () { Route::group(['prefix' => 'users'], function () { Route::get('/', 'SubuserController@index')->name('server.subusers'); Route::get('/new', 'SubuserController@create')->name('server.subusers.new'); - Route::get('/view/{subuser}', 'SubuserController@view')->middleware('subuser')->name('server.subusers.view'); + Route::get('/view/{subuser}', 'SubuserController@view')->middleware('server..subuser')->name('server.subusers.view'); Route::post('/new', 'SubuserController@store'); - Route::patch('/view/{subuser}', 'SubuserController@update')->middleware('subuser'); + Route::patch('/view/{subuser}', 'SubuserController@update')->middleware('server..subuser'); - Route::delete('/view/{subuser}/delete', 'SubuserController@delete')->middleware('subuser')->name('server.subusers.delete'); + Route::delete('/view/{subuser}/delete', 'SubuserController@delete')->middleware('server..subuser')->name('server.subusers.delete'); }); /* @@ -76,24 +89,12 @@ Route::group(['prefix' => 'users'], function () { Route::group(['prefix' => 'schedules'], function () { Route::get('/', 'Tasks\TaskManagementController@index')->name('server.schedules'); Route::get('/new', 'Tasks\TaskManagementController@create')->name('server.schedules.new'); - Route::get('/view/{schedule}', 'Tasks\TaskManagementController@view')->middleware('schedule')->name('server.schedules.view'); + Route::get('/view/{schedule}', 'Tasks\TaskManagementController@view')->middleware('server..schedule')->name('server.schedules.view'); Route::post('/new', 'Tasks\TaskManagementController@store'); - Route::patch('/view/{schedule}', 'Tasks\TaskManagementController@update')->middleware('schedule'); - Route::patch('/view/{schedule}/toggle', 'Tasks\TaskToggleController@index')->middleware('schedule')->name('server.schedules.toggle'); + Route::patch('/view/{schedule}', 'Tasks\TaskManagementController@update')->middleware('server..schedule'); + Route::patch('/view/{schedule}/toggle', 'Tasks\TaskToggleController@index')->middleware('server..schedule')->name('server.schedules.toggle'); - Route::delete('/view/{schedule}/delete', 'Tasks\TaskManagementController@delete')->middleware('schedule')->name('server.schedules.delete'); -}); - -/* -|-------------------------------------------------------------------------- -| Server Ajax Controller Routes -|-------------------------------------------------------------------------- -| -| Endpoint: /server/{server}/ajax -| -*/ -Route::group(['prefix' => 'ajax'], function () { - Route::post('/settings/reset-database-password', 'AjaxController@postResetDatabasePassword')->name('server.ajax.reset-database-password'); + Route::delete('/view/{schedule}/delete', 'Tasks\TaskManagementController@delete')->middleware('server..schedule')->name('server.schedules.delete'); }); diff --git a/tests/Unit/Http/Controllers/Admin/DatabaseControllerTest.php b/tests/Unit/Http/Controllers/Admin/DatabaseControllerTest.php index 3e98fad4c..ac723f6e9 100644 --- a/tests/Unit/Http/Controllers/Admin/DatabaseControllerTest.php +++ b/tests/Unit/Http/Controllers/Admin/DatabaseControllerTest.php @@ -13,7 +13,6 @@ use Mockery as m; use Tests\TestCase; use Prologue\Alerts\AlertsMessageBag; use Tests\Assertions\ControllerAssertionsTrait; -use Pterodactyl\Services\Database\DatabaseHostService; use Pterodactyl\Http\Controllers\Admin\DatabaseController; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; @@ -43,7 +42,7 @@ class DatabaseControllerTest extends TestCase protected $repository; /** - * @var \Pterodactyl\Services\Database\DatabaseHostService + * @var \Pterodactyl\Services\Databases\HostsUpdateService */ protected $service; @@ -57,7 +56,7 @@ class DatabaseControllerTest extends TestCase $this->alert = m::mock(AlertsMessageBag::class); $this->locationRepository = m::mock(LocationRepositoryInterface::class); $this->repository = m::mock(DatabaseHostRepositoryInterface::class); - $this->service = m::mock(DatabaseHostService::class); + $this->service = m::mock(HostUpdateService::class); $this->controller = new DatabaseController( $this->alert, diff --git a/tests/Unit/Services/Database/DatabaseHostServiceTest.php b/tests/Unit/Services/Database/DatabaseHostServiceTest.php index bf8b5dee7..f5e8d09f6 100644 --- a/tests/Unit/Services/Database/DatabaseHostServiceTest.php +++ b/tests/Unit/Services/Database/DatabaseHostServiceTest.php @@ -15,7 +15,6 @@ use Illuminate\Database\DatabaseManager; use Pterodactyl\Exceptions\DisplayException; use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Extensions\DynamicDatabaseConnection; -use Pterodactyl\Services\Database\DatabaseHostService; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; @@ -47,7 +46,7 @@ class DatabaseHostServiceTest extends TestCase protected $repository; /** - * @var \Pterodactyl\Services\Database\DatabaseHostService + * @var \Pterodactyl\Services\Databases\HostsUpdateService */ protected $service; @@ -64,7 +63,7 @@ class DatabaseHostServiceTest extends TestCase $this->encrypter = m::mock(Encrypter::class); $this->repository = m::mock(DatabaseHostRepositoryInterface::class); - $this->service = new DatabaseHostService( + $this->service = new HostUpdateService( $this->database, $this->databaseRepository, $this->repository, diff --git a/tests/Unit/Services/Database/DatabaseManagementServiceTest.php b/tests/Unit/Services/Database/DatabaseManagementServiceTest.php index c679ffa12..a721b4760 100644 --- a/tests/Unit/Services/Database/DatabaseManagementServiceTest.php +++ b/tests/Unit/Services/Database/DatabaseManagementServiceTest.php @@ -16,7 +16,7 @@ use phpmock\phpunit\PHPMock; use Illuminate\Database\DatabaseManager; use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Extensions\DynamicDatabaseConnection; -use Pterodactyl\Services\Database\DatabaseManagementService; +use Pterodactyl\Services\Databases\DatabaseManagementService; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; class DatabaseManagementServiceTest extends TestCase @@ -53,7 +53,7 @@ class DatabaseManagementServiceTest extends TestCase protected $repository; /** - * @var \Pterodactyl\Services\Database\DatabaseManagementService + * @var \Pterodactyl\Services\Databases\DatabaseManagementService */ protected $service; diff --git a/tests/Unit/Services/Servers/ServerDeletionServiceTest.php b/tests/Unit/Services/Servers/ServerDeletionServiceTest.php index 93702fd29..93fa478f2 100644 --- a/tests/Unit/Services/Servers/ServerDeletionServiceTest.php +++ b/tests/Unit/Services/Servers/ServerDeletionServiceTest.php @@ -18,7 +18,7 @@ use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Services\Servers\ServerDeletionService; -use Pterodactyl\Services\Database\DatabaseManagementService; +use Pterodactyl\Services\Databases\DatabaseManagementService; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; @@ -36,7 +36,7 @@ class ServerDeletionServiceTest extends TestCase protected $daemonServerRepository; /** - * @var \Pterodactyl\Services\Database\DatabaseManagementService + * @var \Pterodactyl\Services\Databases\DatabaseManagementService */ protected $databaseManagementService; From d50ea185987907e78d91437fa55b5b9dac2e6db5 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 20 Oct 2017 21:32:57 -0500 Subject: [PATCH 02/11] Add support for changing the server default allocation as a normal user --- CHANGELOG.md | 1 + ...locationDoesNotBelongToServerException.php | 9 ++ .../Controllers/Server/DatabaseController.php | 8 +- .../Controllers/Server/ServerController.php | 22 ---- .../Server/Settings/AllocationController.php | 97 +++++++++++++++ app/Models/Allocation.php | 10 ++ app/Models/Permission.php | 3 +- .../SetDefaultAllocationService.php | 110 ++++++++++++++++++ .../Controllers/JavascriptInjection.php | 27 ++++- resources/lang/en/navigation.php | 2 +- resources/lang/en/server.php | 10 +- .../pterodactyl/layouts/master.blade.php | 2 +- .../server/settings/allocation.blade.php | 71 +++++------ routes/server.php | 4 +- 14 files changed, 308 insertions(+), 68 deletions(-) create mode 100644 app/Exceptions/Service/Allocation/AllocationDoesNotBelongToServerException.php create mode 100644 app/Http/Controllers/Server/Settings/AllocationController.php create mode 100644 app/Services/Allocations/SetDefaultAllocationService.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 990cc2937..67b19c3a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. * Ability to delete users and locations via the CLI. * You can now require 2FA for all users, admins only, or at will using a simple configuration in the Admin CP. * Added ability to export and import service options and their associated settings and environment variables via the Admin CP. +* Default allocation for a server can be changed on the front-end by users. This includes two new subuser permissions as well. ### Changed * Theme colors and login pages updated to give a more unique feel to the project. diff --git a/app/Exceptions/Service/Allocation/AllocationDoesNotBelongToServerException.php b/app/Exceptions/Service/Allocation/AllocationDoesNotBelongToServerException.php new file mode 100644 index 000000000..81f056b56 --- /dev/null +++ b/app/Exceptions/Service/Allocation/AllocationDoesNotBelongToServerException.php @@ -0,0 +1,9 @@ +attributes->get('server'); - $this->injectJavascript(); + $this->authorize('view-databases', $server); + $this->setRequest($request)->injectJavascript(); return view('server.databases.index', [ 'databases' => $this->repository->getDatabasesForServer($server->id), @@ -58,11 +61,14 @@ class DatabaseController extends Controller * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\JsonResponse * + * @throws \Illuminate\Auth\Access\AuthorizationException * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function update(Request $request): JsonResponse { + $this->authorize('reset-db-password', $request->attributes->get('server')); + $password = str_random(20); $this->passwordService->handle($request->attributes->get('database'), $password); diff --git a/app/Http/Controllers/Server/ServerController.php b/app/Http/Controllers/Server/ServerController.php index 9b4208319..65700b24f 100644 --- a/app/Http/Controllers/Server/ServerController.php +++ b/app/Http/Controllers/Server/ServerController.php @@ -86,28 +86,6 @@ class ServerController extends Controller ]); } - /** - * Returns the database overview for a server. - * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @return \Illuminate\View\View - */ - public function getDatabases(Request $request, $uuid) - { - $server = Models\Server::byUuid($uuid); - $this->authorize('view-databases', $server); - - $server->load('node', 'databases.host'); - $server->js(); - - return view('server.settings.databases', [ - 'server' => $server, - 'node' => $server->node, - 'databases' => $server->databases, - ]); - } - /** * Returns the SFTP overview for a server. * diff --git a/app/Http/Controllers/Server/Settings/AllocationController.php b/app/Http/Controllers/Server/Settings/AllocationController.php new file mode 100644 index 000000000..18a42f963 --- /dev/null +++ b/app/Http/Controllers/Server/Settings/AllocationController.php @@ -0,0 +1,97 @@ +defaultAllocationService = $defaultAllocationService; + $this->hashids = $hashids; + $this->repository = $repository; + } + + /** + * Render the allocation management overview page for a server. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\View\View + * + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + public function index(Request $request): View + { + $server = $request->attributes->get('server'); + $this->authorize('view-allocations', $server); + $this->setRequest($request)->injectJavascript(); + + return view('server.settings.allocation', [ + 'allocations' => $this->repository->findWhere([['server_id', '=', $server->id]]), + ]); + } + + /** + * Update the default allocation for a server. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + * + * @throws \Illuminate\Auth\Access\AuthorizationException + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function update(Request $request): JsonResponse + { + $server = $request->attributes->get('server'); + $this->authorize('edit-allocation', $server); + + $allocation = $this->hashids->decodeFirst($request->input('allocation'), 0); + + try { + $this->defaultAllocationService->handle($server->id, $allocation); + } catch (AllocationDoesNotBelongToServerException $exception) { + return response()->json(['error' => 'No matching allocation was located for this server.'], 404); + } + + return response()->json(); + } +} diff --git a/app/Models/Allocation.php b/app/Models/Allocation.php index 9593a7744..bb77647d9 100644 --- a/app/Models/Allocation.php +++ b/app/Models/Allocation.php @@ -64,6 +64,16 @@ class Allocation extends Model implements CleansAttributes, ValidableContract 'server_id' => 'nullable|exists:servers,id', ]; + /** + * Return a hashid encoded string to represent the ID of the allocation. + * + * @return string + */ + public function getHashidAttribute() + { + return app()->make('hashids')->encode($this->id); + } + /** * Accessor to automatically provide the IP alias if defined. * diff --git a/app/Models/Permission.php b/app/Models/Permission.php index 1fc57cc57..61b67e487 100644 --- a/app/Models/Permission.php +++ b/app/Models/Permission.php @@ -86,7 +86,8 @@ class Permission extends Model implements CleansAttributes, ValidableContract 'delete-subuser' => null, ], 'server' => [ - 'set-connection' => null, + 'view-allocations' => null, + 'edit-allocation' => null, 'view-startup' => null, 'edit-startup' => null, ], diff --git a/app/Services/Allocations/SetDefaultAllocationService.php b/app/Services/Allocations/SetDefaultAllocationService.php new file mode 100644 index 000000000..66a858be3 --- /dev/null +++ b/app/Services/Allocations/SetDefaultAllocationService.php @@ -0,0 +1,110 @@ +connection = $connection; + $this->daemonRepository = $daemonRepository; + $this->repository = $repository; + $this->serverRepository = $serverRepository; + } + + /** + * Update the default allocation for a server only if that allocation is currently + * assigned to the specified server. + * + * @param int|\Pterodactyl\Models\Server $server + * @param int $allocation + * @return \Pterodactyl\Models\Allocation + * + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Service\Allocation\AllocationDoesNotBelongToServerException + */ + public function handle($server, int $allocation): Allocation + { + if (! $server instanceof Server) { + $server = $this->serverRepository->find($server); + } + + $allocations = $this->repository->findWhere([['server_id', '=', $server->id]]); + $model = $allocations->filter(function ($model) use ($allocation) { + return $model->id === $allocation; + })->first(); + + if (! $model instanceof Allocation) { + throw new AllocationDoesNotBelongToServerException; + } + + $this->connection->beginTransaction(); + $this->serverRepository->withoutFresh()->update($server->id, ['allocation_id' => $model->id]); + + // Update on the daemon. + try { + $this->daemonRepository->setAccessServer($server->uuid)->setNode($server->node_id)->update([ + 'build' => [ + 'default' => [ + 'ip' => $model->ip, + 'port' => $model->port, + ], + 'ports|overwrite' => $allocations->groupBy('ip')->map(function ($item) { + return $item->pluck('port'); + })->toArray(), + ], + ]); + + $this->connection->commit(); + } catch (RequestException $exception) { + $this->connection->rollBack(); + throw new DaemonConnectionException($exception); + } + + return $model; + } +} diff --git a/app/Traits/Controllers/JavascriptInjection.php b/app/Traits/Controllers/JavascriptInjection.php index cce577819..7c7ee3c16 100644 --- a/app/Traits/Controllers/JavascriptInjection.php +++ b/app/Traits/Controllers/JavascriptInjection.php @@ -14,17 +14,34 @@ use Illuminate\Http\Request; trait JavascriptInjection { + /** + * @var \Illuminate\Http\Request + */ + private $request; + + /** + * Set the request object to use when injecting JS. + * + * @param \Illuminate\Http\Request $request + * @return $this + */ + public function setRequest(Request $request) + { + $this->request = $request; + + return $this; + } + /** * Injects server javascript into the page to be used by other services. * - * @param array $args - * @param bool $overwrite - * @param \Illuminate\Http\Request|null $request + * @param array $args + * @param bool $overwrite * @return array */ - public function injectJavascript($args = [], $overwrite = false, Request $request = null) + public function injectJavascript($args = [], $overwrite = false) { - $request = $request ?? app()->make(Request::class); + $request = $this->request ?? app()->make(Request::class); $server = $request->attributes->get('server'); $token = $request->attributes->get('server_token'); diff --git a/resources/lang/en/navigation.php b/resources/lang/en/navigation.php index 5693d825d..8435eba77 100644 --- a/resources/lang/en/navigation.php +++ b/resources/lang/en/navigation.php @@ -20,7 +20,7 @@ return [ 'subusers' => 'Subusers', 'schedules' => 'Schedules', 'configuration' => 'Configuration', - 'port_allocations' => 'Port Allocations', + 'port_allocations' => 'Allocation Settings', 'sftp_settings' => 'SFTP Settings', 'startup_parameters' => 'Startup Parameters', 'databases' => 'Databases', diff --git a/resources/lang/en/server.php b/resources/lang/en/server.php index ec570e4a8..489803b87 100644 --- a/resources/lang/en/server.php +++ b/resources/lang/en/server.php @@ -189,9 +189,13 @@ return [ 'title' => 'Delete Subuser', 'description' => 'Allows a user to delete other subusers on the server.', ], - 'set_connection' => [ - 'title' => 'Set Default Connection', - 'description' => 'Allows user to set the default connection used for a server as well as view avaliable ports.', + 'view_allocations' => [ + 'title' => 'View Allocations', + 'description' => 'Allows user to view all of the IPs and ports assigned to a server.', + ], + 'edit_allocation' => [ + 'title' => 'Edit Default Connection', + 'description' => 'Allows user to change the default connection allocation to use for a server.', ], 'view_startup' => [ 'title' => 'View Startup Command', diff --git a/resources/themes/pterodactyl/layouts/master.blade.php b/resources/themes/pterodactyl/layouts/master.blade.php index 1a53c4089..9d48dc9e1 100644 --- a/resources/themes/pterodactyl/layouts/master.blade.php +++ b/resources/themes/pterodactyl/layouts/master.blade.php @@ -166,7 +166,7 @@ @endcan - @if(Gate::allows('view-startup', $server) || Gate::allows('view-sftp', $server) || Gate::allows('view-databases', $server) || Gate::allows('view-allocation', $server)) + @if(Gate::allows('view-startup', $server) || Gate::allows('view-sftp', $server) || Gate::allows('view-allocation', $server))
  • @if($allocation->id === $server->allocation_id) - @lang('strings.primary') + @lang('strings.primary') @else - @lang('strings.make_primary') + @lang('strings.make_primary') @endif @@ -60,6 +60,9 @@
  • +
    @@ -79,37 +82,39 @@ @parent {!! Theme::js('js/frontend/server.socket.js') !!} @endsection diff --git a/routes/server.php b/routes/server.php index 6386b6618..fc658b673 100644 --- a/routes/server.php +++ b/routes/server.php @@ -18,9 +18,11 @@ Route::get('/console', 'ConsoleController@console')->name('server.console'); | */ Route::group(['prefix' => 'settings'], function () { + Route::get('/allocation', 'Settings\AllocationController@index')->name('server.settings.allocation'); + Route::patch('/allocation', 'Settings\AllocationController@update'); + Route::get('/sftp', 'ServerController@getSFTP')->name('server.settings.sftp'); Route::get('/startup', 'ServerController@getStartup')->name('server.settings.startup'); - Route::get('/allocation', 'ServerController@getAllocation')->name('server.settings.allocation'); Route::post('/sftp', 'ServerController@postSettingsSFTP'); Route::post('/startup', 'ServerController@postSettingsStartup'); From 532025a348ac57a0cee779352b94c08e2889dec6 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 23 Oct 2017 20:12:15 -0500 Subject: [PATCH 03/11] Fix tests --- .../Databases/DatabaseManagementService.php | 3 +- database/factories/ModelFactory.php | 19 +- .../Admin/DatabaseControllerTest.php | 60 ++- .../Eloquent/DatabaseRepositoryTest.php | 12 +- .../Database/DatabaseHostServiceTest.php | 201 ---------- .../DatabaseManagementServiceTest.php | 344 ------------------ .../Databases/DatabasePasswordServiceTest.php | 99 +++++ .../Hosts/HostCreationServiceTest.php | 101 +++++ .../Hosts/HostDeletionServiceTest.php | 85 +++++ .../Databases/Hosts/HostUpdateServiceTest.php | 112 ++++++ 10 files changed, 463 insertions(+), 573 deletions(-) delete mode 100644 tests/Unit/Services/Database/DatabaseHostServiceTest.php delete mode 100644 tests/Unit/Services/Database/DatabaseManagementServiceTest.php create mode 100644 tests/Unit/Services/Databases/DatabasePasswordServiceTest.php create mode 100644 tests/Unit/Services/Databases/Hosts/HostCreationServiceTest.php create mode 100644 tests/Unit/Services/Databases/Hosts/HostDeletionServiceTest.php create mode 100644 tests/Unit/Services/Databases/Hosts/HostUpdateServiceTest.php diff --git a/app/Services/Databases/DatabaseManagementService.php b/app/Services/Databases/DatabaseManagementService.php index 845ee6282..95182a288 100644 --- a/app/Services/Databases/DatabaseManagementService.php +++ b/app/Services/Databases/DatabaseManagementService.php @@ -9,6 +9,7 @@ namespace Pterodactyl\Services\Databases; +use Pterodactyl\Models\Database; use Illuminate\Database\DatabaseManager; use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Extensions\DynamicDatabaseConnection; @@ -95,7 +96,7 @@ class DatabaseManagementService $this->database->commit(); } catch (\Exception $ex) { try { - if (isset($database)) { + if (isset($database) && $database instanceof Database) { $this->repository->dropDatabase($database->database); $this->repository->dropUser($database->username, $database->remote); $this->repository->flush(); diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index f3e4f4093..82d7ad8b0 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -39,6 +39,8 @@ $factory->define(Pterodactyl\Models\Server::class, function (Faker\Generator $fa }); $factory->define(Pterodactyl\Models\User::class, function (Faker\Generator $faker) { + static $password; + return [ 'id' => $faker->unique()->randomNumber(), 'external_id' => null, @@ -47,7 +49,7 @@ $factory->define(Pterodactyl\Models\User::class, function (Faker\Generator $fake 'email' => $faker->safeEmail, 'name_first' => $faker->firstName, 'name_last' => $faker->lastName, - 'password' => bcrypt('password'), + 'password' => $password ?: $password = bcrypt('password'), 'language' => 'en', 'root_admin' => false, 'use_totp' => false, @@ -173,6 +175,21 @@ $factory->define(Pterodactyl\Models\DatabaseHost::class, function (Faker\Generat ]; }); +$factory->define(Pterodactyl\Models\Database::class, function (Faker\Generator $faker) { + static $password; + + return [ + 'id' => $faker->unique()->randomNumber(), + 'server_id' => $faker->randomNumber(), + 'database_host_id' => $faker->randomNumber(), + 'database' => str_random(10), + 'username' => str_random(10), + 'password' => $password ?: bcrypt('test123'), + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(), + ]; +}); + $factory->define(Pterodactyl\Models\Schedule::class, function (Faker\Generator $faker) { return [ 'id' => $faker->unique()->randomNumber(), diff --git a/tests/Unit/Http/Controllers/Admin/DatabaseControllerTest.php b/tests/Unit/Http/Controllers/Admin/DatabaseControllerTest.php index ac723f6e9..62b66d0bb 100644 --- a/tests/Unit/Http/Controllers/Admin/DatabaseControllerTest.php +++ b/tests/Unit/Http/Controllers/Admin/DatabaseControllerTest.php @@ -14,6 +14,9 @@ use Tests\TestCase; use Prologue\Alerts\AlertsMessageBag; use Tests\Assertions\ControllerAssertionsTrait; use Pterodactyl\Http\Controllers\Admin\DatabaseController; +use Pterodactyl\Services\Databases\Hosts\HostUpdateService; +use Pterodactyl\Services\Databases\Hosts\HostCreationService; +use Pterodactyl\Services\Databases\Hosts\HostDeletionService; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; @@ -22,29 +25,34 @@ class DatabaseControllerTest extends TestCase use ControllerAssertionsTrait; /** - * @var \Prologue\Alerts\AlertsMessageBag + * @var \Prologue\Alerts\AlertsMessageBag|\Mockery\Mock */ - protected $alert; + private $alert; /** - * @var \Pterodactyl\Http\Controllers\Admin\DatabaseController + * @var \Pterodactyl\Services\Databases\Hosts\HostCreationService|\Mockery\Mock */ - protected $controller; + private $creationService; /** - * @var \Pterodactyl\Contracts\Repository\LocationRepositoryInterface + * @var \Pterodactyl\Services\Databases\Hosts\HostDeletionService|\Mockery\Mock */ - protected $locationRepository; + private $deletionService; /** - * @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface + * @var \Pterodactyl\Contracts\Repository\LocationRepositoryInterface|\Mockery\Mock */ - protected $repository; + private $locationRepository; /** - * @var \Pterodactyl\Services\Databases\HostsUpdateService + * @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface|\Mockery\Mock */ - protected $service; + private $repository; + + /** + * @var \Pterodactyl\Services\Databases\Hosts\HostUpdateService|\Mockery\Mock + */ + private $updateService; /** * Setup tests. @@ -54,16 +62,11 @@ class DatabaseControllerTest extends TestCase parent::setUp(); $this->alert = m::mock(AlertsMessageBag::class); + $this->creationService = m::mock(HostCreationService::class); + $this->deletionService = m::mock(HostDeletionService::class); $this->locationRepository = m::mock(LocationRepositoryInterface::class); $this->repository = m::mock(DatabaseHostRepositoryInterface::class); - $this->service = m::mock(HostUpdateService::class); - - $this->controller = new DatabaseController( - $this->alert, - $this->repository, - $this->service, - $this->locationRepository - ); + $this->updateService = m::mock(HostUpdateService::class); } /** @@ -74,7 +77,7 @@ class DatabaseControllerTest extends TestCase $this->locationRepository->shouldReceive('getAllWithNodes')->withNoArgs()->once()->andReturn('getAllWithNodes'); $this->repository->shouldReceive('getWithViewDetails')->withNoArgs()->once()->andReturn('getWithViewDetails'); - $response = $this->controller->index(); + $response = $this->getController()->index(); $this->assertIsViewResponse($response); $this->assertViewNameEquals('admin.databases.index', $response); @@ -92,7 +95,7 @@ class DatabaseControllerTest extends TestCase $this->locationRepository->shouldReceive('getAllWithNodes')->withNoArgs()->once()->andReturn('getAllWithNodes'); $this->repository->shouldReceive('getWithServers')->with(1)->once()->andReturn('getWithServers'); - $response = $this->controller->view(1); + $response = $this->getController()->view(1); $this->assertIsViewResponse($response); $this->assertViewNameEquals('admin.databases.view', $response); @@ -101,4 +104,21 @@ class DatabaseControllerTest extends TestCase $this->assertViewKeyEquals('locations', 'getAllWithNodes', $response); $this->assertViewKeyEquals('host', 'getWithServers', $response); } + + /** + * Return an instance of the DatabaseController with mock dependencies. + * + * @return \Pterodactyl\Http\Controllers\Admin\DatabaseController + */ + private function getController(): DatabaseController + { + return new DatabaseController( + $this->alert, + $this->repository, + $this->creationService, + $this->deletionService, + $this->updateService, + $this->locationRepository + ); + } } diff --git a/tests/Unit/Repositories/Eloquent/DatabaseRepositoryTest.php b/tests/Unit/Repositories/Eloquent/DatabaseRepositoryTest.php index f33ec15e3..4a7f0ccc3 100644 --- a/tests/Unit/Repositories/Eloquent/DatabaseRepositoryTest.php +++ b/tests/Unit/Repositories/Eloquent/DatabaseRepositoryTest.php @@ -96,7 +96,7 @@ class DatabaseRepositoryTest extends TestCase public function testCreateDatabaseStatement() { $query = sprintf('CREATE DATABASE IF NOT EXISTS `%s`', 'test_database'); - $this->repository->shouldReceive('runStatement')->with($query, 'test')->once()->andReturn(true); + $this->repository->shouldReceive('runStatement')->with($query)->once()->andReturn(true); $this->assertTrue($this->repository->createDatabase('test_database', 'test')); } @@ -107,7 +107,7 @@ class DatabaseRepositoryTest extends TestCase public function testCreateUserStatement() { $query = sprintf('CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\'', 'test', '%', 'password'); - $this->repository->shouldReceive('runStatement')->with($query, 'test')->once()->andReturn(true); + $this->repository->shouldReceive('runStatement')->with($query)->once()->andReturn(true); $this->assertTrue($this->repository->createUser('test', '%', 'password', 'test')); } @@ -118,7 +118,7 @@ class DatabaseRepositoryTest extends TestCase public function testUserAssignmentToDatabaseStatement() { $query = sprintf('GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX, EXECUTE ON `%s`.* TO `%s`@`%s`', 'test_database', 'test', '%'); - $this->repository->shouldReceive('runStatement')->with($query, 'test')->once()->andReturn(true); + $this->repository->shouldReceive('runStatement')->with($query)->once()->andReturn(true); $this->assertTrue($this->repository->assignUserToDatabase('test_database', 'test', '%', 'test')); } @@ -128,7 +128,7 @@ class DatabaseRepositoryTest extends TestCase */ public function testFlushStatement() { - $this->repository->shouldReceive('runStatement')->with('FLUSH PRIVILEGES', 'test')->once()->andReturn(true); + $this->repository->shouldReceive('runStatement')->with('FLUSH PRIVILEGES')->once()->andReturn(true); $this->assertTrue($this->repository->flush('test')); } @@ -139,7 +139,7 @@ class DatabaseRepositoryTest extends TestCase public function testDropDatabaseStatement() { $query = sprintf('DROP DATABASE IF EXISTS `%s`', 'test_database'); - $this->repository->shouldReceive('runStatement')->with($query, 'test')->once()->andReturn(true); + $this->repository->shouldReceive('runStatement')->with($query)->once()->andReturn(true); $this->assertTrue($this->repository->dropDatabase('test_database', 'test')); } @@ -150,7 +150,7 @@ class DatabaseRepositoryTest extends TestCase public function testDropUserStatement() { $query = sprintf('DROP USER IF EXISTS `%s`@`%s`', 'test', '%'); - $this->repository->shouldReceive('runStatement')->with($query, 'test')->once()->andReturn(true); + $this->repository->shouldReceive('runStatement')->with($query)->once()->andReturn(true); $this->assertTrue($this->repository->dropUser('test', '%', 'test')); } diff --git a/tests/Unit/Services/Database/DatabaseHostServiceTest.php b/tests/Unit/Services/Database/DatabaseHostServiceTest.php deleted file mode 100644 index f5e8d09f6..000000000 --- a/tests/Unit/Services/Database/DatabaseHostServiceTest.php +++ /dev/null @@ -1,201 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Tests\Unit\Services\Administrative; - -use Mockery as m; -use Tests\TestCase; -use Illuminate\Database\DatabaseManager; -use Pterodactyl\Exceptions\DisplayException; -use Illuminate\Contracts\Encryption\Encrypter; -use Pterodactyl\Extensions\DynamicDatabaseConnection; -use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; -use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; - -class DatabaseHostServiceTest extends TestCase -{ - /** - * @var \Illuminate\Database\DatabaseManager - */ - protected $database; - - /** - * @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface - */ - protected $databaseRepository; - - /** - * @var \Pterodactyl\Extensions\DynamicDatabaseConnection - */ - protected $dynamic; - - /** - * @var \Illuminate\Contracts\Encryption\Encrypter - */ - protected $encrypter; - - /** - * @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface - */ - protected $repository; - - /** - * @var \Pterodactyl\Services\Databases\HostsUpdateService - */ - protected $service; - - /** - * Setup tests. - */ - public function setUp() - { - parent::setUp(); - - $this->database = m::mock(DatabaseManager::class); - $this->databaseRepository = m::mock(DatabaseRepositoryInterface::class); - $this->dynamic = m::mock(DynamicDatabaseConnection::class); - $this->encrypter = m::mock(Encrypter::class); - $this->repository = m::mock(DatabaseHostRepositoryInterface::class); - - $this->service = new HostUpdateService( - $this->database, - $this->databaseRepository, - $this->repository, - $this->dynamic, - $this->encrypter - ); - } - - /** - * Test that creating a host returns the correct data. - */ - public function testHostIsCreated() - { - $data = [ - 'password' => 'raw-password', - 'name' => 'HostName', - 'host' => '127.0.0.1', - 'port' => 3306, - 'username' => 'someusername', - 'node_id' => null, - ]; - - $finalData = (object) array_replace($data, ['password' => 'enc-password']); - - $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->encrypter->shouldReceive('encrypt')->with('raw-password')->once()->andReturn('enc-password'); - - $this->repository->shouldReceive('create')->with([ - 'password' => 'enc-password', - 'name' => 'HostName', - 'host' => '127.0.0.1', - 'port' => 3306, - 'username' => 'someusername', - 'max_databases' => null, - 'node_id' => null, - ])->once()->andReturn($finalData); - - $this->dynamic->shouldReceive('set')->with('dynamic', $finalData)->once()->andReturnNull(); - $this->database->shouldReceive('connection')->with('dynamic')->once()->andReturnSelf() - ->shouldReceive('select')->with('SELECT 1 FROM dual')->once()->andReturnNull(); - - $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - - $response = $this->service->create($data); - - $this->assertNotNull($response); - $this->assertTrue(is_object($response), 'Assert that response is an object.'); - - $this->assertEquals('enc-password', $response->password); - $this->assertEquals('HostName', $response->name); - $this->assertEquals('127.0.0.1', $response->host); - $this->assertEquals(3306, $response->port); - $this->assertEquals('someusername', $response->username); - $this->assertNull($response->node_id); - } - - /** - * Test that passing a password will store an encrypted version in the DB. - */ - public function testHostIsUpdatedWithPasswordProvided() - { - $finalData = (object) ['password' => 'enc-pass', 'host' => '123.456.78.9']; - - $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->encrypter->shouldReceive('encrypt')->with('raw-pass')->once()->andReturn('enc-pass'); - - $this->repository->shouldReceive('update')->with(1, [ - 'password' => 'enc-pass', - 'host' => '123.456.78.9', - ])->once()->andReturn($finalData); - - $this->dynamic->shouldReceive('set')->with('dynamic', $finalData)->once()->andReturnNull(); - $this->database->shouldReceive('connection')->with('dynamic')->once()->andReturnSelf() - ->shouldReceive('select')->with('SELECT 1 FROM dual')->once()->andReturnNull(); - - $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - - $response = $this->service->update(1, ['password' => 'raw-pass', 'host' => '123.456.78.9']); - - $this->assertNotNull($response); - $this->assertEquals('enc-pass', $response->password); - $this->assertEquals('123.456.78.9', $response->host); - } - - /** - * Test that passing no or empty password will skip storing it. - */ - public function testHostIsUpdatedWithoutPassword() - { - $finalData = (object) ['host' => '123.456.78.9']; - - $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->encrypter->shouldNotReceive('encrypt'); - - $this->repository->shouldReceive('update')->with(1, ['host' => '123.456.78.9'])->once()->andReturn($finalData); - - $this->dynamic->shouldReceive('set')->with('dynamic', $finalData)->once()->andReturnNull(); - $this->database->shouldReceive('connection')->with('dynamic')->once()->andReturnSelf() - ->shouldReceive('select')->with('SELECT 1 FROM dual')->once()->andReturnNull(); - - $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - - $response = $this->service->update(1, ['password' => '', 'host' => '123.456.78.9']); - - $this->assertNotNull($response); - $this->assertEquals('123.456.78.9', $response->host); - } - - /** - * Test that a database host can be deleted. - */ - public function testHostIsDeleted() - { - $this->databaseRepository->shouldReceive('findCountWhere')->with([['database_host_id', '=', 1]])->once()->andReturn(0); - $this->repository->shouldReceive('delete')->with(1)->once()->andReturn(true); - - $response = $this->service->delete(1); - - $this->assertTrue($response, 'Assert that response is true.'); - } - - /** - * Test exception is thrown when there are databases attached to a host. - */ - public function testExceptionIsThrownIfHostHasDatabases() - { - $this->databaseRepository->shouldReceive('findCountWhere')->with([['database_host_id', '=', 1]])->once()->andReturn(2); - - try { - $this->service->delete(1); - } catch (DisplayException $exception) { - $this->assertEquals(trans('exceptions.databases.delete_has_databases'), $exception->getMessage()); - } - } -} diff --git a/tests/Unit/Services/Database/DatabaseManagementServiceTest.php b/tests/Unit/Services/Database/DatabaseManagementServiceTest.php deleted file mode 100644 index a721b4760..000000000 --- a/tests/Unit/Services/Database/DatabaseManagementServiceTest.php +++ /dev/null @@ -1,344 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Tests\Unit\Services\Database; - -use Exception; -use Mockery as m; -use Tests\TestCase; -use phpmock\phpunit\PHPMock; -use Illuminate\Database\DatabaseManager; -use Illuminate\Contracts\Encryption\Encrypter; -use Pterodactyl\Extensions\DynamicDatabaseConnection; -use Pterodactyl\Services\Databases\DatabaseManagementService; -use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; - -class DatabaseManagementServiceTest extends TestCase -{ - use PHPMock; - - const TEST_DATA = [ - 'server_id' => 1, - 'database' => 'd1_dbname', - 'remote' => '%', - 'username' => 'u1_str_random', - 'password' => 'enc_password', - 'database_host_id' => 3, - ]; - - /** - * @var \Illuminate\Database\DatabaseManager - */ - protected $database; - - /** - * @var \Pterodactyl\Extensions\DynamicDatabaseConnection - */ - protected $dynamic; - - /** - * @var \Illuminate\Contracts\Encryption\Encrypter - */ - protected $encrypter; - - /** - * @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface - */ - protected $repository; - - /** - * @var \Pterodactyl\Services\Databases\DatabaseManagementService - */ - protected $service; - - /** - * Setup tests. - */ - public function setUp() - { - parent::setUp(); - - $this->database = m::mock(DatabaseManager::class); - $this->dynamic = m::mock(DynamicDatabaseConnection::class); - $this->encrypter = m::mock(Encrypter::class); - $this->repository = m::mock(DatabaseRepositoryInterface::class); - - $this->getFunctionMock('\\Pterodactyl\\Services\\Database', 'str_random') - ->expects($this->any())->willReturn('str_random'); - - $this->service = new DatabaseManagementService( - $this->database, - $this->dynamic, - $this->repository, - $this->encrypter - ); - } - - /** - * Test that a new database can be created that is linked to a specific host. - */ - public function testCreateANewDatabaseThatIsLinkedToAHost() - { - $this->encrypter->shouldReceive('encrypt')->with('str_random')->once()->andReturn('enc_password'); - $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - - $this->repository->shouldReceive('createIfNotExists') - ->with(self::TEST_DATA) - ->once() - ->andReturn((object) self::TEST_DATA); - $this->dynamic->shouldReceive('set') - ->with('dynamic', self::TEST_DATA['database_host_id']) - ->once() - ->andReturnNull(); - $this->repository->shouldReceive('createDatabase')->with( - self::TEST_DATA['database'], - 'dynamic' - )->once()->andReturnNull(); - - $this->encrypter->shouldReceive('decrypt')->with('enc_password')->once()->andReturn('str_random'); - $this->repository->shouldReceive('createUser')->with( - self::TEST_DATA['username'], - self::TEST_DATA['remote'], - 'str_random', - 'dynamic' - )->once()->andReturnNull(); - - $this->repository->shouldReceive('assignUserToDatabase')->with( - self::TEST_DATA['database'], - self::TEST_DATA['username'], - self::TEST_DATA['remote'], - 'dynamic' - )->once()->andReturnNull(); - - $this->repository->shouldReceive('flush')->with('dynamic')->once()->andReturnNull(); - $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - - $response = $this->service->create(1, [ - 'database' => 'dbname', - 'remote' => '%', - 'database_host_id' => 3, - ]); - - $this->assertNotEmpty($response); - $this->assertTrue(is_object($response), 'Assert that response is an object.'); - - $this->assertEquals(self::TEST_DATA['database'], $response->database); - $this->assertEquals(self::TEST_DATA['remote'], $response->remote); - $this->assertEquals(self::TEST_DATA['username'], $response->username); - $this->assertEquals(self::TEST_DATA['password'], $response->password); - $this->assertEquals(self::TEST_DATA['database_host_id'], $response->database_host_id); - } - - /** - * Test that an exception before the database is created and returned does not attempt any actions. - * - * @expectedException \Exception - */ - public function testExceptionBeforeDatabaseIsCreatedShouldNotAttemptAnyRollBackOperations() - { - $this->encrypter->shouldReceive('encrypt')->with('str_random')->once()->andReturn('enc_password'); - $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('createIfNotExists') - ->with(self::TEST_DATA) - ->once() - ->andThrow(new Exception('Test Message')); - $this->repository->shouldNotReceive('dropDatabase'); - $this->database->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); - - $this->service->create(1, [ - 'database' => 'dbname', - 'remote' => '%', - 'database_host_id' => 3, - ]); - } - - /** - * Test that an exception after database creation attempts to clean up previous operations. - * - * @expectedException \Exception - */ - public function testExceptionAfterDatabaseCreationShouldAttemptRollBackOperations() - { - $this->encrypter->shouldReceive('encrypt')->with('str_random')->once()->andReturn('enc_password'); - $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('createIfNotExists') - ->with(self::TEST_DATA) - ->once() - ->andReturn((object) self::TEST_DATA); - $this->dynamic->shouldReceive('set') - ->with('dynamic', self::TEST_DATA['database_host_id']) - ->once() - ->andReturnNull(); - $this->repository->shouldReceive('createDatabase')->with( - self::TEST_DATA['database'], - 'dynamic' - )->once()->andThrow(new Exception('Test Message')); - - $this->repository->shouldReceive('dropDatabase') - ->with(self::TEST_DATA['database'], 'dynamic') - ->once() - ->andReturnNull(); - $this->repository->shouldReceive('dropUser')->with( - self::TEST_DATA['username'], - self::TEST_DATA['remote'], - 'dynamic' - )->once()->andReturnNull(); - $this->repository->shouldReceive('flush')->with('dynamic')->once()->andReturnNull(); - - $this->database->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); - - $this->service->create(1, [ - 'database' => 'dbname', - 'remote' => '%', - 'database_host_id' => 3, - ]); - } - - /** - * Test that an exception thrown during a rollback operation is silently handled and not returned. - */ - public function testExceptionThrownDuringRollBackProcessShouldNotBeThrownToCallingFunction() - { - $this->encrypter->shouldReceive('encrypt')->with('str_random')->once()->andReturn('enc_password'); - $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('createIfNotExists') - ->with(self::TEST_DATA) - ->once() - ->andReturn((object) self::TEST_DATA); - $this->dynamic->shouldReceive('set') - ->with('dynamic', self::TEST_DATA['database_host_id']) - ->once() - ->andReturnNull(); - $this->repository->shouldReceive('createDatabase')->with( - self::TEST_DATA['database'], - 'dynamic' - )->once()->andThrow(new Exception('Test One')); - - $this->repository->shouldReceive('dropDatabase')->with(self::TEST_DATA['database'], 'dynamic') - ->once()->andThrow(new Exception('Test Two')); - - $this->database->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); - - try { - $this->service->create(1, [ - 'database' => 'dbname', - 'remote' => '%', - 'database_host_id' => 3, - ]); - } catch (Exception $ex) { - $this->assertInstanceOf(Exception::class, $ex); - $this->assertEquals('Test One', $ex->getMessage()); - } - } - - /** - * Test that a password can be changed for a given database. - */ - public function testDatabasePasswordShouldBeChanged() - { - $this->repository->shouldReceive('find')->with(1)->once()->andReturn((object) self::TEST_DATA); - $this->dynamic->shouldReceive('set') - ->with('dynamic', self::TEST_DATA['database_host_id']) - ->once() - ->andReturnNull(); - $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - - $this->encrypter->shouldReceive('encrypt')->with('new_password')->once()->andReturn('new_enc_password'); - $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with(1, [ - 'password' => 'new_enc_password', - ])->andReturn(true); - - $this->repository->shouldReceive('dropUser')->with( - self::TEST_DATA['username'], - self::TEST_DATA['remote'], - 'dynamic' - )->once()->andReturnNull(); - - $this->repository->shouldReceive('createUser')->with( - self::TEST_DATA['username'], - self::TEST_DATA['remote'], - 'new_password', - 'dynamic' - )->once()->andReturnNull(); - - $this->repository->shouldReceive('assignUserToDatabase')->with( - self::TEST_DATA['database'], - self::TEST_DATA['username'], - self::TEST_DATA['remote'], - 'dynamic' - )->once()->andReturnNull(); - - $this->repository->shouldReceive('flush')->with('dynamic')->once()->andReturnNull(); - $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - - $response = $this->service->changePassword(1, 'new_password'); - - $this->assertTrue($response); - } - - /** - * Test that an exception thrown while changing a password will attempt a rollback. - * - * @expectedException \Exception - */ - public function testExceptionThrownWhileChangingDatabasePasswordShouldRollBack() - { - $this->repository->shouldReceive('find')->with(1)->once()->andReturn((object) self::TEST_DATA); - $this->dynamic->shouldReceive('set') - ->with('dynamic', self::TEST_DATA['database_host_id']) - ->once() - ->andReturnNull(); - $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - - $this->encrypter->shouldReceive('encrypt')->with('new_password')->once()->andReturn('new_enc_password'); - $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with(1, [ - 'password' => 'new_enc_password', - ])->andReturn(true); - - $this->repository->shouldReceive('dropUser')->with( - self::TEST_DATA['username'], - self::TEST_DATA['remote'], - 'dynamic' - )->once()->andThrow(new Exception()); - - $this->database->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); - - $this->service->changePassword(1, 'new_password'); - } - - /** - * Test that a database can be deleted. - */ - public function testDatabaseShouldBeDeleted() - { - $this->repository->shouldReceive('find')->with(1)->once()->andReturn((object) self::TEST_DATA); - $this->dynamic->shouldReceive('set') - ->with('dynamic', self::TEST_DATA['database_host_id']) - ->once() - ->andReturnNull(); - - $this->repository->shouldReceive('dropDatabase') - ->with(self::TEST_DATA['database'], 'dynamic') - ->once() - ->andReturnNull(); - $this->repository->shouldReceive('dropUser')->with( - self::TEST_DATA['username'], - self::TEST_DATA['remote'], - 'dynamic' - )->once()->andReturnNull(); - $this->repository->shouldReceive('flush')->with('dynamic')->once()->andReturnNull(); - $this->repository->shouldReceive('delete')->with(1)->once()->andReturn(1); - - $response = $this->service->delete(1); - - $this->assertEquals(1, $response); - } -} diff --git a/tests/Unit/Services/Databases/DatabasePasswordServiceTest.php b/tests/Unit/Services/Databases/DatabasePasswordServiceTest.php new file mode 100644 index 000000000..099d44616 --- /dev/null +++ b/tests/Unit/Services/Databases/DatabasePasswordServiceTest.php @@ -0,0 +1,99 @@ +connection = m::mock(ConnectionInterface::class); + $this->dynamic = m::mock(DynamicDatabaseConnection::class); + $this->encrypter = m::mock(Encrypter::class); + $this->repository = m::mock(DatabaseRepositoryInterface::class); + } + + /** + * Test that a password can be updated. + * + * @dataProvider useModelDataProvider + */ + public function testPasswordIsChanged($useModel) + { + $model = factory(Database::class)->make(); + + if (! $useModel) { + $this->repository->shouldReceive('find')->with(1234)->once()->andReturn($model); + } + + $this->dynamic->shouldReceive('set')->with('dynamic', $model->database_host_id)->once()->andReturnNull(); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->encrypter->shouldReceive('encrypt')->with('test123')->once()->andReturn('enc123'); + + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf(); + $this->repository->shouldReceive('update')->with($model->id, ['password' => 'enc123'])->once()->andReturn(true); + + $this->repository->shouldReceive('dropUser')->with($model->username, $model->remote)->once()->andReturnNull(); + $this->repository->shouldReceive('createUser')->with($model->username, $model->remote, 'test123')->once()->andReturnNull(); + $this->repository->shouldReceive('assignUserToDatabase')->with($model->database, $model->username, $model->remote)->once()->andReturnNull(); + $this->repository->shouldReceive('flush')->withNoArgs()->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $response = $this->getService()->handle($useModel ? $model : 1234, 'test123'); + $this->assertNotEmpty($response); + $this->assertTrue($response); + } + + /** + * Data provider to determine if a model should be passed or an int. + * + * @return array + */ + public function useModelDataProvider(): array + { + return [[false], [true]]; + } + + /** + * Return an instance of the service with mocked dependencies. + * + * @return \Pterodactyl\Services\Databases\DatabasePasswordService + */ + private function getService(): DatabasePasswordService + { + return new DatabasePasswordService($this->connection, $this->repository, $this->dynamic, $this->encrypter); + } +} diff --git a/tests/Unit/Services/Databases/Hosts/HostCreationServiceTest.php b/tests/Unit/Services/Databases/Hosts/HostCreationServiceTest.php new file mode 100644 index 000000000..603b871a0 --- /dev/null +++ b/tests/Unit/Services/Databases/Hosts/HostCreationServiceTest.php @@ -0,0 +1,101 @@ +connection = m::mock(ConnectionInterface::class); + $this->databaseManager = m::mock(DatabaseManager::class); + $this->dynamic = m::mock(DynamicDatabaseConnection::class); + $this->encrypter = m::mock(Encrypter::class); + $this->repository = m::mock(DatabaseHostRepositoryInterface::class); + } + + /** + * Test that a database host can be created. + */ + public function testDatabaseHostIsCreated() + { + $model = factory(DatabaseHost::class)->make(); + + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->encrypter->shouldReceive('encrypt')->with('test123')->once()->andReturn('enc123'); + $this->repository->shouldReceive('create')->with(m::subset([ + 'password' => 'enc123', + 'username' => $model->username, + 'node_id' => $model->node_id, + ]))->once()->andReturn($model); + + $this->dynamic->shouldReceive('set')->with('dynamic', $model)->once()->andReturnNull(); + $this->databaseManager->shouldReceive('connection')->with('dynamic')->once()->andReturnSelf(); + $this->databaseManager->shouldReceive('select')->with('SELECT 1 FROM dual')->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $response = $this->getService()->handle([ + 'password' => 'test123', + 'username' => $model->username, + 'node_id' => $model->node_id, + ]); + + $this->assertNotEmpty($response); + $this->assertSame($model, $response); + } + + /** + * Return an instance of the service with mocked dependencies. + * + * @return \Pterodactyl\Services\Databases\Hosts\HostCreationService + */ + private function getService(): HostCreationService + { + return new HostCreationService( + $this->connection, + $this->databaseManager, + $this->repository, + $this->dynamic, + $this->encrypter + ); + } +} diff --git a/tests/Unit/Services/Databases/Hosts/HostDeletionServiceTest.php b/tests/Unit/Services/Databases/Hosts/HostDeletionServiceTest.php new file mode 100644 index 000000000..402bf507c --- /dev/null +++ b/tests/Unit/Services/Databases/Hosts/HostDeletionServiceTest.php @@ -0,0 +1,85 @@ +databaseRepository = m::mock(DatabaseRepositoryInterface::class); + $this->repository = m::mock(DatabaseHostRepositoryInterface::class); + } + + /** + * Test that a host can be deleted. + */ + public function testHostIsDeleted() + { + $this->databaseRepository->shouldReceive('findCountWhere')->with([['database_host_id', '=', 1234]])->once()->andReturn(0); + $this->repository->shouldReceive('delete')->with(1234)->once()->andReturn(1); + + $response = $this->getService()->handle(1234); + $this->assertNotEmpty($response); + $this->assertSame(1, $response); + } + + /** + * Test that an exception is thrown if a host with databases is deleted. + * + * @dataProvider databaseCountDataProvider + */ + public function testExceptionIsThrownIfDeletingHostWithDatabases($count) + { + $this->databaseRepository->shouldReceive('findCountWhere')->with([['database_host_id', '=', 1234]])->once()->andReturn($count); + + try { + $this->getService()->handle(1234); + } catch (PterodactylException $exception) { + $this->assertInstanceOf(HasActiveServersException::class, $exception); + $this->assertEquals(trans('exceptions.databases.delete_has_databases'), $exception->getMessage()); + } + } + + /** + * Data provider to ensure exceptions are thrown for any value > 0. + * + * @return array + */ + public function databaseCountDataProvider(): array + { + return [[1], [2], [10]]; + } + + /** + * Return an instance of the service with mocked dependencies. + * + * @return \Pterodactyl\Services\Databases\Hosts\HostDeletionService + */ + private function getService(): HostDeletionService + { + return new HostDeletionService($this->databaseRepository, $this->repository); + } +} diff --git a/tests/Unit/Services/Databases/Hosts/HostUpdateServiceTest.php b/tests/Unit/Services/Databases/Hosts/HostUpdateServiceTest.php new file mode 100644 index 000000000..7e115c000 --- /dev/null +++ b/tests/Unit/Services/Databases/Hosts/HostUpdateServiceTest.php @@ -0,0 +1,112 @@ +connection = m::mock(ConnectionInterface::class); + $this->databaseManager = m::mock(DatabaseManager::class); + $this->dynamic = m::mock(DynamicDatabaseConnection::class); + $this->encrypter = m::mock(Encrypter::class); + $this->repository = m::mock(DatabaseHostRepositoryInterface::class); + } + + /** + * Test that a password is encrypted before storage if provided. + */ + public function testPasswordIsEncryptedWhenProvided() + { + $model = factory(DatabaseHost::class)->make(); + + $this->encrypter->shouldReceive('encrypt')->with('test123')->once()->andReturn('enc123'); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('update')->with(1234, ['password' => 'enc123'])->once()->andReturn($model); + + $this->dynamic->shouldReceive('set')->with('dynamic', $model)->once()->andReturnNull(); + $this->databaseManager->shouldReceive('connection')->with('dynamic')->once()->andReturnSelf(); + $this->databaseManager->shouldReceive('select')->with('SELECT 1 FROM dual')->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $response = $this->getService()->handle(1234, ['password' => 'test123']); + $this->assertNotEmpty($response); + $this->assertSame($model, $response); + } + + /** + * Test that updates still occur when no password is provided. + */ + public function testUpdateOccursWhenNoPasswordIsProvided() + { + $model = factory(DatabaseHost::class)->make(); + + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('update')->with(1234, ['username' => 'test'])->once()->andReturn($model); + + $this->dynamic->shouldReceive('set')->with('dynamic', $model)->once()->andReturnNull(); + $this->databaseManager->shouldReceive('connection')->with('dynamic')->once()->andReturnSelf(); + $this->databaseManager->shouldReceive('select')->with('SELECT 1 FROM dual')->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $response = $this->getService()->handle(1234, ['password' => '', 'username' => 'test']); + $this->assertNotEmpty($response); + $this->assertSame($model, $response); + } + + /** + * Return an instance of the service with mocked dependencies. + * + * @return \Pterodactyl\Services\Databases\Hosts\HostUpdateService + */ + private function getService(): HostUpdateService + { + return new HostUpdateService( + $this->connection, + $this->databaseManager, + $this->repository, + $this->dynamic, + $this->encrypter + ); + } +} From 57db949a9c2f0a9daae531be4b2b3bea53cfadeb Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 23 Oct 2017 21:10:32 -0500 Subject: [PATCH 04/11] Tests a'hoy --- app/Exceptions/DisplayException.php | 6 +- tests/Traits/MocksRequestException.php | 49 ++++++ .../SetDefaultAllocationServiceTest.php | 156 ++++++++++++++++++ .../Databases/DatabasePasswordServiceTest.php | 2 +- .../Hosts/HostDeletionServiceTest.php | 2 +- 5 files changed, 210 insertions(+), 5 deletions(-) create mode 100644 tests/Traits/MocksRequestException.php create mode 100644 tests/Unit/Services/Allocations/SetDefaultAllocationServiceTest.php diff --git a/app/Exceptions/DisplayException.php b/app/Exceptions/DisplayException.php index 80c5771a5..aa18a1c1b 100644 --- a/app/Exceptions/DisplayException.php +++ b/app/Exceptions/DisplayException.php @@ -28,9 +28,9 @@ class DisplayException extends PterodactylException * @param string $message * @param Throwable|null $previous * @param string $level - * @internal param mixed $log + * @param int $code */ - public function __construct($message, Throwable $previous = null, $level = self::LEVEL_ERROR) + public function __construct($message, Throwable $previous = null, $level = self::LEVEL_ERROR, $code = 0) { $this->level = $level; @@ -38,7 +38,7 @@ class DisplayException extends PterodactylException Log::{$level}($previous); } - parent::__construct($message); + parent::__construct($message, $code, $previous); } /** diff --git a/tests/Traits/MocksRequestException.php b/tests/Traits/MocksRequestException.php new file mode 100644 index 000000000..81e0e5414 --- /dev/null +++ b/tests/Traits/MocksRequestException.php @@ -0,0 +1,49 @@ +getExceptionMock()->shouldReceive('getResponse')->andReturn($this->exceptionResponse); + } + + /** + * Return a mocked instance of the request exception. + * + * @return \Mockery\MockInterface + */ + private function getExceptionMock(): MockInterface + { + return $this->exception ?? $this->exception = Mockery::mock(RequestException::class); + } + + /** + * Set the exception response. + * + * @param mixed $response + */ + protected function setExceptionResponse($response) + { + $this->exceptionResponse = $response; + } +} diff --git a/tests/Unit/Services/Allocations/SetDefaultAllocationServiceTest.php b/tests/Unit/Services/Allocations/SetDefaultAllocationServiceTest.php new file mode 100644 index 000000000..72a837e78 --- /dev/null +++ b/tests/Unit/Services/Allocations/SetDefaultAllocationServiceTest.php @@ -0,0 +1,156 @@ +connection = m::mock(ConnectionInterface::class); + $this->daemonRepository = m::mock(DaemonRepositoryInterface::class); + $this->repository = m::mock(AllocationRepositoryInterface::class); + $this->serverRepository = m::mock(ServerRepositoryInterface::class); + } + + /** + * Test that an allocation can be updated. + * + * @dataProvider useModelDataProvider + */ + public function testAllocationIsUpdated(bool $useModel) + { + $allocations = factory(Allocation::class)->times(2)->make(); + $model = factory(Server::class)->make(); + if (! $useModel) { + $this->serverRepository->shouldReceive('find')->with(1234)->once()->andReturn($model); + } + + $this->repository->shouldReceive('findWhere')->with([['server_id', '=', $model->id]])->once()->andReturn($allocations); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->serverRepository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf(); + $this->serverRepository->shouldReceive('update')->with($model->id, [ + 'allocation_id' => $allocations->first()->id, + ])->once()->andReturnNull(); + + $this->daemonRepository->shouldReceive('setAccessServer')->with($model->uuid)->once()->andReturnSelf(); + $this->daemonRepository->shouldReceive('setNode')->with($model->node_id)->once()->andReturnSelf(); + $this->daemonRepository->shouldReceive('update')->with([ + 'build' => [ + 'default' => [ + 'ip' => $allocations->first()->ip, + 'port' => $allocations->first()->port, + ], + 'ports|overwrite' => $allocations->groupBy('ip')->map(function ($item) { + return $item->pluck('port'); + })->toArray(), + ], + ])->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $response = $this->getService()->handle($useModel ? $model : 1234, $allocations->first()->id); + $this->assertNotEmpty($response); + $this->assertSame($allocations->first(), $response); + } + + /** + * Test that an allocation that doesn't belong to a server throws an exception. + * + * @expectedException \Pterodactyl\Exceptions\Service\Allocation\AllocationDoesNotBelongToServerException + */ + public function testAllocationNotBelongingToServerThrowsException() + { + $model = factory(Server::class)->make(); + $this->repository->shouldReceive('findWhere')->with([['server_id', '=', $model->id]])->once()->andReturn(collect()); + + $this->getService()->handle($model, 1234); + } + + /** + * Test that an exception thrown by guzzle is handled properly. + */ + public function testExceptionThrownByGuzzleIsHandled() + { + $this->configureExceptionMock(); + + $allocation = factory(Allocation::class)->make(); + $model = factory(Server::class)->make(); + + $this->repository->shouldReceive('findWhere')->with([['server_id', '=', $model->id]])->once()->andReturn(collect([$allocation])); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->serverRepository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf(); + $this->serverRepository->shouldReceive('update')->with($model->id, [ + 'allocation_id' => $allocation->id, + ])->once()->andReturnNull(); + + $this->daemonRepository->shouldReceive('setAccessServer->setNode->update')->once()->andThrow($this->getExceptionMock()); + $this->connection->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); + + try { + $this->getService()->handle($model, $allocation->id); + } catch (PterodactylException $exception) { + $this->assertInstanceOf(DaemonConnectionException::class, $exception); + $this->assertInstanceOf(RequestException::class, $exception->getPrevious()); + } + } + + /** + * Data provider to determine if a model should be passed or an int. + * + * @return array + */ + public function useModelDataProvider(): array + { + return [[false], [true]]; + } + + /** + * Return an instance of the service with mocked dependencies. + * + * @return \Pterodactyl\Services\Allocations\SetDefaultAllocationService + */ + private function getService(): SetDefaultAllocationService + { + return new SetDefaultAllocationService($this->repository, $this->connection, $this->daemonRepository, $this->serverRepository); + } +} diff --git a/tests/Unit/Services/Databases/DatabasePasswordServiceTest.php b/tests/Unit/Services/Databases/DatabasePasswordServiceTest.php index 099d44616..54d46b950 100644 --- a/tests/Unit/Services/Databases/DatabasePasswordServiceTest.php +++ b/tests/Unit/Services/Databases/DatabasePasswordServiceTest.php @@ -51,7 +51,7 @@ class DatabasePasswordServiceTest extends TestCase * * @dataProvider useModelDataProvider */ - public function testPasswordIsChanged($useModel) + public function testPasswordIsChanged(bool $useModel) { $model = factory(Database::class)->make(); diff --git a/tests/Unit/Services/Databases/Hosts/HostDeletionServiceTest.php b/tests/Unit/Services/Databases/Hosts/HostDeletionServiceTest.php index 402bf507c..bd927b8e8 100644 --- a/tests/Unit/Services/Databases/Hosts/HostDeletionServiceTest.php +++ b/tests/Unit/Services/Databases/Hosts/HostDeletionServiceTest.php @@ -51,7 +51,7 @@ class HostDeletionServiceTest extends TestCase * * @dataProvider databaseCountDataProvider */ - public function testExceptionIsThrownIfDeletingHostWithDatabases($count) + public function testExceptionIsThrownIfDeletingHostWithDatabases(int $count) { $this->databaseRepository->shouldReceive('findCountWhere')->with([['database_host_id', '=', 1234]])->once()->andReturn($count); From 058e490ec42664406bc4ed3b5c00d2878a2c5f11 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Wed, 25 Oct 2017 00:35:25 -0400 Subject: [PATCH 05/11] Implement Panel changes to support internal SFTP subsystem on Daemon (#703) --- .../Controllers/API/Remote/SftpController.php | 85 ++++++++ .../Server/Settings/SftpController.php | 26 +++ .../Middleware/Daemon/DaemonAuthenticate.php | 2 +- .../Remote/SftpAuthenticationFormRequest.php | 44 +++++ app/Models/Node.php | 12 +- app/Models/Server.php | 17 +- .../Servers/ServerCreationService.php | 10 - .../Servers/UsernameGenerationService.php | 40 ---- .../Sftp/AuthenticateUsingPasswordService.php | 90 +++++++++ .../Controllers/JavascriptInjection.php | 1 - database/factories/ModelFactory.php | 2 - ..._24_222238_RemoveLegacySFTPInformation.php | 32 ++++ resources/lang/en/server.php | 2 +- .../pterodactyl/admin/servers/index.blade.php | 2 - .../admin/servers/view/index.blade.php | 8 - .../pterodactyl/server/console.blade.php | 2 +- .../themes/pterodactyl/server/index.blade.php | 2 +- .../server/settings/sftp.blade.php | 44 +---- routes/api-remote.php | 4 + routes/server.php | 5 +- .../Servers/ServerCreationServiceTest.php | 11 -- .../Servers/UsernameGenerationServiceTest.php | 109 ----------- .../AuthenticateUsingPasswordServiceTest.php | 181 ++++++++++++++++++ 23 files changed, 484 insertions(+), 247 deletions(-) create mode 100644 app/Http/Controllers/API/Remote/SftpController.php create mode 100644 app/Http/Controllers/Server/Settings/SftpController.php create mode 100644 app/Http/Requests/API/Remote/SftpAuthenticationFormRequest.php delete mode 100644 app/Services/Servers/UsernameGenerationService.php create mode 100644 app/Services/Sftp/AuthenticateUsingPasswordService.php create mode 100644 database/migrations/2017_10_24_222238_RemoveLegacySFTPInformation.php delete mode 100644 tests/Unit/Services/Servers/UsernameGenerationServiceTest.php create mode 100644 tests/Unit/Services/Sftp/AuthenticateUsingPasswordServiceTest.php diff --git a/app/Http/Controllers/API/Remote/SftpController.php b/app/Http/Controllers/API/Remote/SftpController.php new file mode 100644 index 000000000..07582153d --- /dev/null +++ b/app/Http/Controllers/API/Remote/SftpController.php @@ -0,0 +1,85 @@ +authenticationService = $authenticationService; + } + + /** + * Authenticate a set of credentials and return the associated server details + * for a SFTP connection on the daemon. + * + * @param \Pterodactyl\Http\Requests\API\Remote\SftpAuthenticationFormRequest $request + * @return \Illuminate\Http\JsonResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function index(SftpAuthenticationFormRequest $request): JsonResponse + { + $connection = explode('.', $request->input('username')); + $this->incrementLoginAttempts($request); + + if ($this->hasTooManyLoginAttempts($request)) { + return response()->json([ + 'error' => 'Logins throttled.', + ], 429); + } + + try { + $data = $this->authenticationService->handle( + array_get($connection, 0), + $request->input('password'), + object_get($request->attributes->get('node'), 'id', 0), + array_get($connection, 1) + ); + + $this->clearLoginAttempts($request); + } catch (AuthenticationException $exception) { + return response()->json([ + 'error' => 'Invalid credentials.', + ], 403); + } catch (RecordNotFoundException $exception) { + return response()->json([ + 'error' => 'Invalid server.', + ], 404); + } + + return response()->json($data); + } + + /** + * Get the throttle key for the given request. + * + * @param \Illuminate\Http\Request $request + * @return string + */ + protected function throttleKey(Request $request) + { + return strtolower(array_get(explode('.', $request->input('username')), 0) . '|' . $request->ip()); + } +} diff --git a/app/Http/Controllers/Server/Settings/SftpController.php b/app/Http/Controllers/Server/Settings/SftpController.php new file mode 100644 index 000000000..b128ba5c9 --- /dev/null +++ b/app/Http/Controllers/Server/Settings/SftpController.php @@ -0,0 +1,26 @@ +setRequest($request)->injectJavascript(); + + return view('server.settings.sftp'); + } +} diff --git a/app/Http/Middleware/Daemon/DaemonAuthenticate.php b/app/Http/Middleware/Daemon/DaemonAuthenticate.php index 2804fa923..2572ba854 100644 --- a/app/Http/Middleware/Daemon/DaemonAuthenticate.php +++ b/app/Http/Middleware/Daemon/DaemonAuthenticate.php @@ -75,7 +75,7 @@ class DaemonAuthenticate throw new HttpException(403); } - $request->attributes->set('node.model', $node); + $request->attributes->set('node', $node); return $next($request); } diff --git a/app/Http/Requests/API/Remote/SftpAuthenticationFormRequest.php b/app/Http/Requests/API/Remote/SftpAuthenticationFormRequest.php new file mode 100644 index 000000000..5d82f55c7 --- /dev/null +++ b/app/Http/Requests/API/Remote/SftpAuthenticationFormRequest.php @@ -0,0 +1,44 @@ + 'required|string', + 'password' => 'required|string', + ]; + } + + /** + * Return only the fields that we are interested in from the request. + * This will include empty fields as a null value. + * + * @return array + */ + public function normalize() + { + return $this->only( + array_keys($this->rules()) + ); + } +} diff --git a/app/Models/Node.php b/app/Models/Node.php index 368c6f3d8..71f5614b5 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -144,13 +144,23 @@ class Node extends Model implements CleansAttributes, ValidableContract ], ], 'docker' => [ + 'container' => [ + 'user' => null, + ], + 'network' => [ + 'name' => 'pterodactyl_nw', + ], 'socket' => '/var/run/docker.sock', 'autoupdate_images' => true, ], 'sftp' => [ 'path' => $this->daemonBase, + 'ip' => '0.0.0.0', 'port' => $this->daemonSFTP, - 'container' => 'ptdl-sftp', + 'keypair' => [ + 'bits' => 2048, + 'e' => 65537, + ], ], 'logger' => [ 'path' => 'logs/', diff --git a/app/Models/Server.php b/app/Models/Server.php index 09563baf1..04ac19e43 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -29,19 +29,12 @@ class Server extends Model implements CleansAttributes, ValidableContract */ protected $table = 'servers'; - /** - * The attributes excluded from the model's JSON form. - * - * @var array - */ - protected $hidden = ['sftp_password']; - /** * The attributes that should be mutated to dates. * * @var array */ - protected $dates = ['deleted_at']; + protected $dates = [self::CREATED_AT, self::UPDATED_AT, 'deleted_at']; /** * Always eager load these relationships on the model. @@ -55,7 +48,7 @@ class Server extends Model implements CleansAttributes, ValidableContract * * @var array */ - protected $guarded = ['id', 'installed', 'created_at', 'updated_at', 'deleted_at']; + protected $guarded = ['id', 'installed', self::CREATED_AT, self::UPDATED_AT, 'deleted_at']; /** * @var array @@ -73,8 +66,6 @@ class Server extends Model implements CleansAttributes, ValidableContract 'node_id' => 'required', 'allocation_id' => 'required', 'pack_id' => 'sometimes', - 'auto_deploy' => 'sometimes', - 'custom_id' => 'sometimes', 'skip_scripts' => 'sometimes', ]; @@ -95,10 +86,7 @@ class Server extends Model implements CleansAttributes, ValidableContract 'nest_id' => 'exists:nests,id', 'egg_id' => 'exists:eggs,id', 'pack_id' => 'nullable|numeric|min:0', - 'custom_container' => 'nullable|string', 'startup' => 'nullable|string', - 'auto_deploy' => 'accepted', - 'custom_id' => 'numeric|unique:servers,id', 'skip_scripts' => 'boolean', ]; @@ -132,7 +120,6 @@ class Server extends Model implements CleansAttributes, ValidableContract */ protected $searchableColumns = [ 'name' => 10, - 'username' => 10, 'uuidShort' => 9, 'uuid' => 8, 'pack.name' => 7, diff --git a/app/Services/Servers/ServerCreationService.php b/app/Services/Servers/ServerCreationService.php index 0994abe55..33d23b000 100644 --- a/app/Services/Servers/ServerCreationService.php +++ b/app/Services/Servers/ServerCreationService.php @@ -63,11 +63,6 @@ class ServerCreationService */ protected $userRepository; - /** - * @var \Pterodactyl\Services\Servers\UsernameGenerationService - */ - protected $usernameService; - /** * @var \Pterodactyl\Services\Servers\VariableValidatorService */ @@ -84,7 +79,6 @@ class ServerCreationService * @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( @@ -96,7 +90,6 @@ class ServerCreationService ServerRepositoryInterface $repository, ServerVariableRepositoryInterface $serverVariableRepository, UserRepositoryInterface $userRepository, - UsernameGenerationService $usernameService, VariableValidatorService $validatorService ) { $this->allocationRepository = $allocationRepository; @@ -107,7 +100,6 @@ class ServerCreationService $this->repository = $repository; $this->serverVariableRepository = $serverVariableRepository; $this->userRepository = $userRepository; - $this->usernameService = $usernameService; $this->validatorService = $validatorService; } @@ -151,8 +143,6 @@ class ServerCreationService 'startup' => $data['startup'], 'daemonSecret' => str_random(NodeCreationService::DAEMON_SECRET_LENGTH), 'image' => $data['docker_image'], - 'username' => $this->usernameService->generate($data['name'], $uniqueShort), - 'sftp_password' => null, ]); // Process allocations and assign them to the server in the database. diff --git a/app/Services/Servers/UsernameGenerationService.php b/app/Services/Servers/UsernameGenerationService.php deleted file mode 100644 index 3fb2c6d3b..000000000 --- a/app/Services/Servers/UsernameGenerationService.php +++ /dev/null @@ -1,40 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Services\Servers; - -class UsernameGenerationService -{ - /** - * Generate a unique username to be used for SFTP connections and identification - * of the server docker container on the host system. - * - * @param string $name - * @param null $identifier - * @return string - */ - public function generate($name, $identifier = null) - { - if (is_null($identifier) || ! ctype_alnum($identifier)) { - $unique = str_random(8); - } else { - if (strlen($identifier) < 8) { - $unique = $identifier . str_random((8 - strlen($identifier))); - } else { - $unique = substr($identifier, 0, 8); - } - } - - // Filter the Server 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/Sftp/AuthenticateUsingPasswordService.php b/app/Services/Sftp/AuthenticateUsingPasswordService.php new file mode 100644 index 000000000..487d251d4 --- /dev/null +++ b/app/Services/Sftp/AuthenticateUsingPasswordService.php @@ -0,0 +1,90 @@ +keyProviderService = $keyProviderService; + $this->repository = $repository; + $this->userRepository = $userRepository; + } + + /** + * Attempt to authenticate a provded username and password and determine if they + * have permission to access a given server. This function does not account for + * subusers currently. Only administrators and server owners can login to access + * their files at this time. + * + * Server must exist on the node that the API call is being made from in order for a + * valid response to be provided. + * + * @param string $username + * @param string $password + * @param string|null $server + * @param int $node + * @return array + * + * @throws \Illuminate\Auth\AuthenticationException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle(string $username, string $password, int $node, string $server = null): array + { + if (is_null($server)) { + throw new RecordNotFoundException; + } + + try { + $user = $this->userRepository->withColumns(['id', 'root_admin', 'password'])->findFirstWhere([['username', '=', $username]]); + + if (! password_verify($password, $user->password)) { + throw new AuthenticationException; + } + } catch (RecordNotFoundException $exception) { + throw new AuthenticationException; + } + + $server = $this->repository->withColumns(['id', 'node_id', 'owner_id', 'uuid'])->getByUuid($server); + if ($server->node_id !== $node || (! $user->root_admin && $server->owner_id !== $user->id)) { + throw new RecordNotFoundException; + } + + return [ + 'server' => $server->uuid, + 'token' => $this->keyProviderService->handle($server->id, $user->id), + ]; + } +} diff --git a/app/Traits/Controllers/JavascriptInjection.php b/app/Traits/Controllers/JavascriptInjection.php index 7c7ee3c16..c6efc86ac 100644 --- a/app/Traits/Controllers/JavascriptInjection.php +++ b/app/Traits/Controllers/JavascriptInjection.php @@ -50,7 +50,6 @@ trait JavascriptInjection 'uuid' => $server->uuid, 'uuidShort' => $server->uuidShort, 'daemonSecret' => $token, - 'username' => $server->username, ], 'node' => [ 'fqdn' => $server->node->fqdn, diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index 82d7ad8b0..233aeee01 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -30,8 +30,6 @@ $factory->define(Pterodactyl\Models\Server::class, function (Faker\Generator $fa 'cpu' => 0, 'oom_disabled' => 0, 'pack_id' => null, - 'username' => $faker->userName, - 'sftp_password' => null, 'installed' => 1, 'created_at' => \Carbon\Carbon::now(), 'updated_at' => \Carbon\Carbon::now(), diff --git a/database/migrations/2017_10_24_222238_RemoveLegacySFTPInformation.php b/database/migrations/2017_10_24_222238_RemoveLegacySFTPInformation.php new file mode 100644 index 000000000..e41acd275 --- /dev/null +++ b/database/migrations/2017_10_24_222238_RemoveLegacySFTPInformation.php @@ -0,0 +1,32 @@ +dropUnique(['username']); + + $table->dropColumn('username'); + $table->dropColumn('sftp_password'); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + { + Schema::table('servers', function (Blueprint $table) { + $table->string('username')->nullable()->after('image')->unique(); + $table->text('sftp_password')->after('image'); + }); + } +} diff --git a/resources/lang/en/server.php b/resources/lang/en/server.php index 489803b87..3ed89c511 100644 --- a/resources/lang/en/server.php +++ b/resources/lang/en/server.php @@ -301,7 +301,7 @@ return [ 'change_pass' => 'Change SFTP Password', 'details' => 'SFTP Details', 'conn_addr' => 'Connection Address', - 'warning' => 'Ensure that your client is set to use SFTP and not FTP or FTPS for connections, there is a difference between the protocols.', + 'warning' => 'The SFTP password is your account password. Ensure that your client is set to use SFTP and not FTP or FTPS for connections, there is a difference between the protocols.', ], 'database' => [ 'header' => 'Databases', diff --git a/resources/themes/pterodactyl/admin/servers/index.blade.php b/resources/themes/pterodactyl/admin/servers/index.blade.php index 1c066a4a0..5619ea2f8 100644 --- a/resources/themes/pterodactyl/admin/servers/index.blade.php +++ b/resources/themes/pterodactyl/admin/servers/index.blade.php @@ -42,7 +42,6 @@ ID Server Name Owner - Username Node Connection @@ -52,7 +51,6 @@ {{ $server->uuidShort }} {{ $server->name }} {{ $server->user->username }} - {{ $server->username }} {{ $server->node->name }} {{ $server->allocation->alias }}:{{ $server->allocation->port }} diff --git a/resources/themes/pterodactyl/admin/servers/view/index.blade.php b/resources/themes/pterodactyl/admin/servers/view/index.blade.php index 81d73d06e..d5020bcdf 100644 --- a/resources/themes/pterodactyl/admin/servers/view/index.blade.php +++ b/resources/themes/pterodactyl/admin/servers/view/index.blade.php @@ -55,14 +55,6 @@ Docker Container ID - - Docker User ID - - - - Docker Container Name - {{ $server->username }} - Service diff --git a/resources/themes/pterodactyl/server/console.blade.php b/resources/themes/pterodactyl/server/console.blade.php index 20356c272..f50db615f 100644 --- a/resources/themes/pterodactyl/server/console.blade.php +++ b/resources/themes/pterodactyl/server/console.blade.php @@ -16,7 +16,7 @@
    -
    {{ $server->username }}:~$
    +
    container:~/$
    diff --git a/resources/themes/pterodactyl/server/index.blade.php b/resources/themes/pterodactyl/server/index.blade.php index 6470a7703..9cb4ba4a9 100644 --- a/resources/themes/pterodactyl/server/index.blade.php +++ b/resources/themes/pterodactyl/server/index.blade.php @@ -30,7 +30,7 @@
    -
    {{ $server->username }}:~$
    +
    container:~/$
    diff --git a/resources/themes/pterodactyl/server/settings/sftp.blade.php b/resources/themes/pterodactyl/server/settings/sftp.blade.php index 3e00bdf57..5da21ef77 100644 --- a/resources/themes/pterodactyl/server/settings/sftp.blade.php +++ b/resources/themes/pterodactyl/server/settings/sftp.blade.php @@ -21,37 +21,7 @@ @section('content')
    -
    -
    -
    -

    @lang('server.config.sftp.change_pass')

    -
    - @can('reset-sftp', $server) -
    -
    -
    - -
    - -

    @lang('auth.password_requirements')

    -
    -
    -
    - -
    - @else -
    -
    -

    @lang('auth.not_authorized')

    -
    -
    - @endcan -
    -
    -
    +

    @lang('server.config.sftp.details')

    @@ -66,20 +36,12 @@
    - +
    - @can('view-sftp-password', $server) -
    - -
    - sftp_password))value="{{ Crypt::decrypt($server->sftp_password) }}"@endif /> -
    -
    - @endcan
    diff --git a/routes/api-remote.php b/routes/api-remote.php index 28f1edb38..0aa42b1a2 100644 --- a/routes/api-remote.php +++ b/routes/api-remote.php @@ -12,3 +12,7 @@ Route::group(['prefix' => '/eggs'], function () { Route::get('/', 'EggRetrievalController@index')->name('api.remote.eggs'); Route::get('/{uuid}', 'EggRetrievalController@download')->name('api.remote.eggs.download'); }); + +Route::group(['prefix' => '/sftp'], function () { + Route::post('/', 'SftpController@index')->name('api.remote.sftp'); +}); diff --git a/routes/server.php b/routes/server.php index fc658b673..1ceafbe87 100644 --- a/routes/server.php +++ b/routes/server.php @@ -21,10 +21,9 @@ Route::group(['prefix' => 'settings'], function () { Route::get('/allocation', 'Settings\AllocationController@index')->name('server.settings.allocation'); Route::patch('/allocation', 'Settings\AllocationController@update'); - Route::get('/sftp', 'ServerController@getSFTP')->name('server.settings.sftp'); - Route::get('/startup', 'ServerController@getStartup')->name('server.settings.startup'); + Route::get('/sftp', 'Settings\SftpController@index')->name('server.settings.sftp'); - Route::post('/sftp', 'ServerController@postSettingsSFTP'); + Route::get('/startup', 'ServerController@getStartup')->name('server.settings.startup'); Route::post('/startup', 'ServerController@postSettingsStartup'); }); diff --git a/tests/Unit/Services/Servers/ServerCreationServiceTest.php b/tests/Unit/Services/Servers/ServerCreationServiceTest.php index da2e33af2..89e67d916 100644 --- a/tests/Unit/Services/Servers/ServerCreationServiceTest.php +++ b/tests/Unit/Services/Servers/ServerCreationServiceTest.php @@ -18,7 +18,6 @@ use Illuminate\Database\ConnectionInterface; use Pterodactyl\Exceptions\PterodactylException; use Pterodactyl\Services\Servers\ServerCreationService; use Pterodactyl\Services\Servers\VariableValidatorService; -use Pterodactyl\Services\Servers\UsernameGenerationService; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; @@ -109,11 +108,6 @@ class ServerCreationServiceTest extends TestCase */ protected $userRepository; - /** - * @var \Pterodactyl\Services\Servers\UsernameGenerationService|\Mockery\Mock - */ - protected $usernameService; - /** * @var \Pterodactyl\Services\Servers\VariableValidatorService|\Mockery\Mock */ @@ -135,7 +129,6 @@ class ServerCreationServiceTest extends TestCase $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->getFunctionMock('\\Pterodactyl\\Services\\Servers', 'str_random') @@ -150,7 +143,6 @@ class ServerCreationServiceTest extends TestCase $this->repository, $this->serverVariableRepository, $this->userRepository, - $this->usernameService, $this->validatorService ); } @@ -165,8 +157,6 @@ class ServerCreationServiceTest extends TestCase ->shouldReceive('validate')->with($this->data['egg_id'])->once()->andReturnSelf(); $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->usernameService->shouldReceive('generate')->with($this->data['name'], 'random_string') - ->once()->andReturn('user_name'); $this->repository->shouldReceive('create')->with(m::subset([ 'uuid' => $this->getKnownUuid(), @@ -211,7 +201,6 @@ class ServerCreationServiceTest extends TestCase { $this->validatorService->shouldReceive('isAdmin->setFields->validate->getResults')->once()->andReturn([]); $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->usernameService->shouldReceive('generate')->once()->andReturn('user_name'); $this->repository->shouldReceive('create')->once()->andReturn((object) [ 'node_id' => 1, 'id' => 1, diff --git a/tests/Unit/Services/Servers/UsernameGenerationServiceTest.php b/tests/Unit/Services/Servers/UsernameGenerationServiceTest.php deleted file mode 100644 index 61c364338..000000000 --- a/tests/Unit/Services/Servers/UsernameGenerationServiceTest.php +++ /dev/null @@ -1,109 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -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', 'str_random') - ->expects($this->any())->willReturnCallback(function ($count) { - return str_pad('', $count, '0'); - }); - } - - /** - * Test that a valid username is returned and is the correct length. - */ - public function testShouldReturnAValidUsernameWithASelfGeneratedIdentifier() - { - $response = $this->service->generate('testname'); - - $this->assertEquals('testna_00000000', $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_xyz00000', $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('000000_00000000', $response); - } - - /** - * Test that a name consisting entirely of special characters is handled. - */ - public function testNameOfOnlySpecialCharactersIsHandledProperly() - { - $response = $this->service->generate('$%#*#(@#(#*$&#(#!#@'); - - $this->assertEquals('000000_00000000', $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/Sftp/AuthenticateUsingPasswordServiceTest.php b/tests/Unit/Services/Sftp/AuthenticateUsingPasswordServiceTest.php new file mode 100644 index 000000000..87ceccd07 --- /dev/null +++ b/tests/Unit/Services/Sftp/AuthenticateUsingPasswordServiceTest.php @@ -0,0 +1,181 @@ +keyProviderService = m::mock(DaemonKeyProviderService::class); + $this->repository = m::mock(ServerRepositoryInterface::class); + $this->userRepository = m::mock(UserRepositoryInterface::class); + } + + /** + * Test that an account can be authenticated. + */ + public function testNonAdminAccountIsAuthenticated() + { + $user = factory(User::class)->make(['root_admin' => 0]); + $server = factory(Server::class)->make(['node_id' => 1, 'owner_id' => $user->id]); + + $this->userRepository->shouldReceive('withColumns')->with(['id', 'root_admin', 'password'])->once()->andReturnSelf(); + $this->userRepository->shouldReceive('findFirstWhere')->with([['username', '=', $user->username]])->once()->andReturn($user); + + $this->repository->shouldReceive('withColumns')->with(['id', 'node_id', 'owner_id', 'uuid'])->once()->andReturnSelf(); + $this->repository->shouldReceive('getByUuid')->with($server->uuidShort)->once()->andReturn($server); + + $this->keyProviderService->shouldReceive('handle')->with($server->id, $user->id)->once()->andReturn('server_token'); + + $response = $this->getService()->handle($user->username, 'password', 1, $server->uuidShort); + $this->assertNotEmpty($response); + $this->assertArrayHasKey('server', $response); + $this->assertArrayHasKey('token', $response); + $this->assertSame($server->uuid, $response['server']); + $this->assertSame('server_token', $response['token']); + } + + /** + * Test that an administrative user can access servers that they are not + * set as the owner of. + */ + public function testAdminAccountIsAuthenticated() + { + $user = factory(User::class)->make(['root_admin' => 1]); + $server = factory(Server::class)->make(['node_id' => 1, 'owner_id' => $user->id + 1]); + + $this->userRepository->shouldReceive('withColumns')->with(['id', 'root_admin', 'password'])->once()->andReturnSelf(); + $this->userRepository->shouldReceive('findFirstWhere')->with([['username', '=', $user->username]])->once()->andReturn($user); + + $this->repository->shouldReceive('withColumns')->with(['id', 'node_id', 'owner_id', 'uuid'])->once()->andReturnSelf(); + $this->repository->shouldReceive('getByUuid')->with($server->uuidShort)->once()->andReturn($server); + + $this->keyProviderService->shouldReceive('handle')->with($server->id, $user->id)->once()->andReturn('server_token'); + + $response = $this->getService()->handle($user->username, 'password', 1, $server->uuidShort); + $this->assertNotEmpty($response); + $this->assertArrayHasKey('server', $response); + $this->assertArrayHasKey('token', $response); + $this->assertSame($server->uuid, $response['server']); + $this->assertSame('server_token', $response['token']); + } + + /** + * Test exception gets thrown if no server is passed into the function. + * + * @expectedException \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function testExceptionIsThrownIfNoServerIsProvided() + { + $this->getService()->handle('username', 'password', 1); + } + + /** + * Test that an exception is thrown if the user account exists but the wrong + * credentials are passed. + * + * @expectedException \Illuminate\Auth\AuthenticationException + */ + public function testExceptionIsThrownIfUserDetailsAreIncorrect() + { + $user = factory(User::class)->make(); + + $this->userRepository->shouldReceive('withColumns')->with(['id', 'root_admin', 'password'])->once()->andReturnSelf(); + $this->userRepository->shouldReceive('findFirstWhere')->with([['username', '=', $user->username]])->once()->andReturn($user); + + $this->getService()->handle($user->username, 'wrongpassword', 1, '1234'); + } + + /** + * Test that an exception is thrown if no user account is found. + * + * @expectedException \Illuminate\Auth\AuthenticationException + */ + public function testExceptionIsThrownIfNoUserAccountIsFound() + { + $this->userRepository->shouldReceive('withColumns')->with(['id', 'root_admin', 'password'])->once()->andReturnSelf(); + $this->userRepository->shouldReceive('findFirstWhere')->with([['username', '=', 'something']])->once()->andThrow(new RecordNotFoundException); + + $this->getService()->handle('something', 'password', 1, '1234'); + } + + /** + * Test that an exception is thrown if the user is not the owner of the server + * and is not an administrator. + * + * @expectedException \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function testExceptionIsThrownIfUserDoesNotOwnServer() + { + $user = factory(User::class)->make(['root_admin' => 0]); + $server = factory(Server::class)->make(['node_id' => 1, 'owner_id' => $user->id + 1]); + + $this->userRepository->shouldReceive('withColumns')->with(['id', 'root_admin', 'password'])->once()->andReturnSelf(); + $this->userRepository->shouldReceive('findFirstWhere')->with([['username', '=', $user->username]])->once()->andReturn($user); + + $this->repository->shouldReceive('withColumns')->with(['id', 'node_id', 'owner_id', 'uuid'])->once()->andReturnSelf(); + $this->repository->shouldReceive('getByUuid')->with($server->uuidShort)->once()->andReturn($server); + + $this->getService()->handle($user->username, 'password', 1, $server->uuidShort); + } + + /** + * Test that an exception is thrown if the requested server does not belong to + * the node that the request is made from. + * + * @expectedException \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function testExceptionIsThrownIfServerDoesNotExistOnCurrentNode() + { + $user = factory(User::class)->make(['root_admin' => 0]); + $server = factory(Server::class)->make(['node_id' => 2, 'owner_id' => $user->id]); + + $this->userRepository->shouldReceive('withColumns')->with(['id', 'root_admin', 'password'])->once()->andReturnSelf(); + $this->userRepository->shouldReceive('findFirstWhere')->with([['username', '=', $user->username]])->once()->andReturn($user); + + $this->repository->shouldReceive('withColumns')->with(['id', 'node_id', 'owner_id', 'uuid'])->once()->andReturnSelf(); + $this->repository->shouldReceive('getByUuid')->with($server->uuidShort)->once()->andReturn($server); + + $this->getService()->handle($user->username, 'password', 1, $server->uuidShort); + } + + /** + * Return an instance of the service with mocked dependencies. + * + * @return \Pterodactyl\Services\Sftp\AuthenticateUsingPasswordService + */ + private function getService(): AuthenticateUsingPasswordService + { + return new AuthenticateUsingPasswordService($this->keyProviderService, $this->repository, $this->userRepository); + } +} From 5fb4b2cdcf170addfc170166afd321446b641efc Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 24 Oct 2017 23:42:40 -0500 Subject: [PATCH 06/11] More distinct server config if admin --- resources/lang/en/navigation.php | 3 ++- resources/themes/pterodactyl/layouts/master.blade.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/resources/lang/en/navigation.php b/resources/lang/en/navigation.php index 8435eba77..b8f2fb83a 100644 --- a/resources/lang/en/navigation.php +++ b/resources/lang/en/navigation.php @@ -25,6 +25,7 @@ return [ 'startup_parameters' => 'Startup Parameters', 'databases' => 'Databases', 'edit_file' => 'Edit File', - 'admin' => 'Manage', + 'admin_header' => 'ADMINISTRATIVE', + 'admin' => 'Server Configuration', ], ]; diff --git a/resources/themes/pterodactyl/layouts/master.blade.php b/resources/themes/pterodactyl/layouts/master.blade.php index 9d48dc9e1..62bb8c90d 100644 --- a/resources/themes/pterodactyl/layouts/master.blade.php +++ b/resources/themes/pterodactyl/layouts/master.blade.php @@ -193,8 +193,9 @@ @endif @if(Auth::user()->root_admin) +
  • @lang('navigation.server.admin_header')
  • - + @lang('navigation.server.admin')
  • From 508ff8cfb3ae60276d905ac80978468de9906b34 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Wed, 25 Oct 2017 22:33:28 -0500 Subject: [PATCH 07/11] Finish front-end server modification changes. Everything is back to the point that it was before this massive code overhaul began. FInal steps before merging this into develop will be some unit tests. --- .../EggVariableRepositoryInterface.php | 10 ++ .../Repository/ServerRepositoryInterface.php | 14 ++ .../Controllers/Server/ServerController.php | 163 ------------------ .../Server/Settings/StartupController.php | 92 ++++++++++ .../UpdateStartupParametersFormRequest.php | 61 +++++++ .../Eloquent/EggVariableRepository.php | 17 ++ .../Eloquent/ServerRepository.php | 34 +++- .../Servers/ServerAccessHelperService.php | 0 .../Servers/StartupCommandViewService.php | 55 ++++++ .../Servers/StartupModificationService.php | 15 +- resources/lang/en/server.php | 2 +- .../server/settings/startup.blade.php | 46 ++--- routes/server.php | 4 +- 13 files changed, 315 insertions(+), 198 deletions(-) delete mode 100644 app/Http/Controllers/Server/ServerController.php create mode 100644 app/Http/Controllers/Server/Settings/StartupController.php create mode 100644 app/Http/Requests/Server/UpdateStartupParametersFormRequest.php delete mode 100644 app/Services/Servers/ServerAccessHelperService.php create mode 100644 app/Services/Servers/StartupCommandViewService.php diff --git a/app/Contracts/Repository/EggVariableRepositoryInterface.php b/app/Contracts/Repository/EggVariableRepositoryInterface.php index afaf7463b..77b46f96d 100644 --- a/app/Contracts/Repository/EggVariableRepositoryInterface.php +++ b/app/Contracts/Repository/EggVariableRepositoryInterface.php @@ -9,6 +9,16 @@ namespace Pterodactyl\Contracts\Repository; +use Illuminate\Support\Collection; + interface EggVariableRepositoryInterface extends RepositoryInterface { + /** + * Return editable variables for a given egg. Editable variables must be set to + * user viewable in order to be picked up by this function. + * + * @param int $egg + * @return \Illuminate\Support\Collection + */ + public function getEditableVariables(int $egg): Collection; } diff --git a/app/Contracts/Repository/ServerRepositoryInterface.php b/app/Contracts/Repository/ServerRepositoryInterface.php index 73cb5c71e..7031b27e6 100644 --- a/app/Contracts/Repository/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/ServerRepositoryInterface.php @@ -9,6 +9,7 @@ namespace Pterodactyl\Contracts\Repository; +use Pterodactyl\Models\Server; use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface; interface ServerRepositoryInterface extends RepositoryInterface, SearchableInterface @@ -52,6 +53,19 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter */ public function getVariablesWithValues($id, $returnAsObject = false); + /** + * Get the primary allocation for a given server. If a model is passed into + * the function, load the allocation relationship onto it. Otherwise, find and + * return the server from the database. + * + * @param int|\Pterodactyl\Models\Server $server + * @param bool $refresh + * @return \Pterodactyl\Models\Server + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function getPrimaryAllocation($server, bool $refresh = false): Server; + /** * Return enough data to be used for the creation of a server via the daemon. * diff --git a/app/Http/Controllers/Server/ServerController.php b/app/Http/Controllers/Server/ServerController.php deleted file mode 100644 index 65700b24f..000000000 --- a/app/Http/Controllers/Server/ServerController.php +++ /dev/null @@ -1,163 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Http\Controllers\Server; - -use Log; -use Alert; -use Pterodactyl\Models; -use Illuminate\Http\Request; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Exceptions\DisplayValidationException; - -class ServerController extends Controller -{ - /** - * Returns the allocation overview for a server. - * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @return \Illuminate\View\View - */ - public function getAllocation(Request $request, $uuid) - { - $server = Models\Server::byUuid($uuid); - $this->authorize('view-allocation', $server); - $server->js(); - - return view('server.settings.allocation', [ - 'server' => $server->load(['allocations' => function ($query) { - $query->orderBy('ip', 'asc'); - $query->orderBy('port', 'asc'); - }]), - 'node' => $server->node, - ]); - } - - /** - * Returns the startup overview for a server. - * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @return \Illuminate\View\View - */ - public function getStartup(Request $request, $uuid) - { - $server = Models\Server::byUuid($uuid); - $this->authorize('view-startup', $server); - - $server->load(['node', 'allocation', 'variables']); - $variables = Models\EggVariable::where('option_id', $server->option_id)->get(); - - $replacements = [ - '{{SERVER_MEMORY}}' => $server->memory, - '{{SERVER_IP}}' => $server->allocation->ip, - '{{SERVER_PORT}}' => $server->allocation->port, - ]; - - $processed = str_replace(array_keys($replacements), array_values($replacements), $server->startup); - - foreach ($variables as $var) { - if ($var->user_viewable) { - $serverVar = $server->variables->where('variable_id', $var->id)->first(); - $var->server_set_value = $serverVar->variable_value ?? $var->default_value; - } else { - $var->server_set_value = '[hidden]'; - } - - $processed = str_replace('{{' . $var->env_variable . '}}', $var->server_set_value, $processed); - } - - $server->js(); - - return view('server.settings.startup', [ - 'server' => $server, - 'node' => $server->node, - 'variables' => $variables->where('user_viewable', 1), - 'service' => $server->service, - 'processedStartup' => $processed, - ]); - } - - /** - * Returns the SFTP overview for a server. - * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @return \Illuminate\View\View - */ - public function getSFTP(Request $request, $uuid) - { - $server = Models\Server::byUuid($uuid); - $this->authorize('view-sftp', $server); - $server->js(); - - return view('server.settings.sftp', [ - 'server' => $server, - 'node' => $server->node, - ]); - } - - /** - * Handles changing the SFTP password for a server. - * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @return \Illuminate\Http\RedirectResponse - */ - public function postSettingsSFTP(Request $request, $uuid) - { - $server = Models\Server::byUuid($uuid); - $this->authorize('reset-sftp', $server); - - try { - $repo = new ServerRepository; - $repo->updateSFTPPassword($server->id, $request->input('sftp_pass')); - Alert::success('Successfully updated this servers SFTP password.')->flash(); - } catch (DisplayValidationException $ex) { - return redirect()->route('server.settings.sftp', $uuid)->withErrors(json_decode($ex->getMessage())); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unknown error occured while attempting to update this server\'s SFTP settings.')->flash(); - } - - return redirect()->route('server.settings.sftp', $uuid); - } - - /** - * Handles changing the startup settings for a server. - * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @return \Illuminate\Http\RedirectResponse - */ - public function postSettingsStartup(Request $request, $uuid) - { - $server = Models\Server::byUuid($uuid); - $this->authorize('edit-startup', $server); - - try { - $repo = new ServerRepository; - $repo->updateStartup($server->id, $request->except('_token')); - Alert::success('Server startup variables were successfully updated.')->flash(); - } catch (DisplayValidationException $ex) { - return redirect()->route('server.settings.startup', $uuid)->withErrors(json_decode($ex->getMessage())); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception occured while attemping to update startup variables for this server. Please try again.')->flash(); - } - - return redirect()->route('server.settings.startup', $uuid); - } -} diff --git a/app/Http/Controllers/Server/Settings/StartupController.php b/app/Http/Controllers/Server/Settings/StartupController.php new file mode 100644 index 000000000..f5ea20a47 --- /dev/null +++ b/app/Http/Controllers/Server/Settings/StartupController.php @@ -0,0 +1,92 @@ +alert = $alert; + $this->commandViewService = $commandViewService; + $this->modificationService = $modificationService; + } + + /** + * Render the server startup page. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\View\View + * + * @throws \Illuminate\Auth\Access\AuthorizationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function index(Request $request) + { + $server = $request->attributes->get('server'); + $this->authorize('view-startup', $server); + $this->injectJavascript(); + + $data = $this->commandViewService->handle($server->id); + + return view('server.settings.startup', [ + 'variables' => $data->get('variables'), + 'server_values' => $data->get('server_values'), + 'startup' => $data->get('startup'), + ]); + } + + /** + * Handle request to update the startup variables for a server. Authorization + * is handled in the form request. + * + * @param \Pterodactyl\Http\Requests\Server\UpdateStartupParametersFormRequest $request + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function update(UpdateStartupParametersFormRequest $request): RedirectResponse + { + $this->modificationService->handle($request->attributes->get('server'), $request->normalize()); + $this->alert->success(trans('server.config.startup.edited'))->flash(); + + return redirect()->route('server.settings.startup', ['server' => $request->attributes->get('server')->uuidShort]); + } +} diff --git a/app/Http/Requests/Server/UpdateStartupParametersFormRequest.php b/app/Http/Requests/Server/UpdateStartupParametersFormRequest.php new file mode 100644 index 000000000..41c15103f --- /dev/null +++ b/app/Http/Requests/Server/UpdateStartupParametersFormRequest.php @@ -0,0 +1,61 @@ +user()->can('edit-startup', $this->attributes->get('server')); + } + + /** + * Validate that all of the required fields were passed and that the environment + * variable values meet the defined criteria for those fields. + * + * @return array + */ + public function rules() + { + $repository = $this->container->make(EggVariableRepositoryInterface::class); + + $variables = $repository->getEditableVariables($this->attributes->get('server')->egg_id); + $rules = $variables->mapWithKeys(function ($variable) { + $this->validationAttributes['environment.' . $variable->env_variable] = $variable->name; + + return ['environment.' . $variable->env_variable => $variable->rules]; + })->toArray(); + + return array_merge($rules, [ + 'environment' => 'required|array', + ]); + } + + /** + * Return attributes to provide better naming conventions for error messages. + * + * @return array + */ + public function attributes() + { + return $this->validationAttributes; + } +} diff --git a/app/Repositories/Eloquent/EggVariableRepository.php b/app/Repositories/Eloquent/EggVariableRepository.php index 9fe1174b4..2c34c7527 100644 --- a/app/Repositories/Eloquent/EggVariableRepository.php +++ b/app/Repositories/Eloquent/EggVariableRepository.php @@ -9,6 +9,7 @@ namespace Pterodactyl\Repositories\Eloquent; +use Illuminate\Support\Collection; use Pterodactyl\Models\EggVariable; use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface; @@ -21,4 +22,20 @@ class EggVariableRepository extends EloquentRepository implements EggVariableRep { return EggVariable::class; } + + /** + * Return editable variables for a given egg. Editable variables must be set to + * user viewable in order to be picked up by this function. + * + * @param int $egg + * @return \Illuminate\Support\Collection + */ + public function getEditableVariables(int $egg): Collection + { + return $this->getBuilder()->where([ + ['egg_id', '=', $egg], + ['user_viewable', '=', 1], + ['user_editable', '=', 1], + ])->get($this->getColumns()); + } } diff --git a/app/Repositories/Eloquent/ServerRepository.php b/app/Repositories/Eloquent/ServerRepository.php index 10805fdea..da15c89b1 100644 --- a/app/Repositories/Eloquent/ServerRepository.php +++ b/app/Repositories/Eloquent/ServerRepository.php @@ -67,8 +67,8 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt Assert::integerish($id, 'First argument passed to findWithVariables must be integer, received %s.'); $instance = $this->getBuilder()->with('egg.variables', 'variables') - ->where($this->getModel()->getKeyName(), '=', $id) - ->first($this->getColumns()); + ->where($this->getModel()->getKeyName(), '=', $id) + ->first($this->getColumns()); if (is_null($instance)) { throw new RecordNotFoundException(); @@ -77,6 +77,36 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt return $instance; } + /** + * Get the primary allocation for a given server. If a model is passed into + * the function, load the allocation relationship onto it. Otherwise, find and + * return the server from the database. + * + * @param int|\Pterodactyl\Models\Server $server + * @param bool $refresh + * @return \Pterodactyl\Models\Server + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function getPrimaryAllocation($server, bool $refresh = false): Server + { + $instance = $server; + if (! $instance instanceof Server) { + Assert::integerish($server, 'First argument passed to getPrimaryAllocation must be instance of \Pterodactyl\Models\Server or integer, received %s.'); + $instance = $this->getBuilder()->find($server, $this->getColumns()); + } + + if (! $instance) { + throw new RecordNotFoundException; + } + + if (! $instance->relationLoaded('allocation') || $refresh) { + $instance->load('allocation'); + } + + return $instance; + } + /** * {@inheritdoc} */ diff --git a/app/Services/Servers/ServerAccessHelperService.php b/app/Services/Servers/ServerAccessHelperService.php deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/Services/Servers/StartupCommandViewService.php b/app/Services/Servers/StartupCommandViewService.php new file mode 100644 index 000000000..14a4cc3c3 --- /dev/null +++ b/app/Services/Servers/StartupCommandViewService.php @@ -0,0 +1,55 @@ +repository = $repository; + } + + /** + * Generate a startup command for a server and return all of the user-viewable variables + * as well as thier assigned values. + * + * @param int $server + * @return \Illuminate\Support\Collection + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle(int $server): Collection + { + $response = $this->repository->getVariablesWithValues($server, true); + $server = $this->repository->getPrimaryAllocation($response->server); + + $find = ['{{SERVER_MEMORY}}', '{{SERVER_IP}}', '{{SERVER_PORT}}']; + $replace = [$server->memory, $server->allocation->ip, $server->allocation->port]; + + $variables = $server->egg->variables->each(function ($variable) use (&$find, &$replace, $response) { + $find[] = '{{' . $variable->env_variable . '}}'; + $replace[] = $variable->user_viewable ? $response->data[$variable->env_variable] : '[hidden]'; + })->filter(function ($variable) { + return $variable->user_viewable === 1; + }); + + return collect([ + 'startup' => str_replace($find, $replace, $server->startup), + 'variables' => $variables, + 'server_values' => $response->data, + ]); + } +} diff --git a/app/Services/Servers/StartupModificationService.php b/app/Services/Servers/StartupModificationService.php index 25650b1c0..78460c197 100644 --- a/app/Services/Servers/StartupModificationService.php +++ b/app/Services/Servers/StartupModificationService.php @@ -12,8 +12,8 @@ namespace Pterodactyl\Services\Servers; use Pterodactyl\Models\Server; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; -use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; @@ -118,9 +118,9 @@ class StartupModificationService } $this->connection->beginTransaction(); - if (isset($data['environment'])) { + if (! is_null(array_get($data, 'environment'))) { $validator = $this->validatorService->isAdmin($this->admin) - ->setFields($data['environment']) + ->setFields(array_get($data, 'environment', [])) ->validate(array_get($data, 'egg_id', $server->egg_id)); foreach ($validator->getResults() as $result) { @@ -159,12 +159,11 @@ class StartupModificationService try { $this->daemonServerRepository->setNode($server->node_id)->setAccessServer($server->uuid)->update($daemonData); - $this->connection->commit(); } catch (RequestException $exception) { - $response = $exception->getResponse(); - throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [ - 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), - ]), $exception, 'warning'); + $this->connection->rollBack(); + throw new DaemonConnectionException($exception); } + + $this->connection->commit(); } } diff --git a/resources/lang/en/server.php b/resources/lang/en/server.php index 3ed89c511..3aa27e5b2 100644 --- a/resources/lang/en/server.php +++ b/resources/lang/en/server.php @@ -292,8 +292,8 @@ return [ 'command' => 'Startup Command', 'edit_params' => 'Edit Parameters', 'update' => 'Update Startup Parameters', - 'startup_var' => 'Startup Command Variable', 'startup_regex' => 'Input Rules', + 'edited' => 'Startup variables have been successfully edited. They will take effect the next time this server is started.', ], 'sftp' => [ 'header' => 'SFTP Configuration', diff --git a/resources/themes/pterodactyl/server/settings/startup.blade.php b/resources/themes/pterodactyl/server/settings/startup.blade.php index ff52fae9b..594dae4d2 100644 --- a/resources/themes/pterodactyl/server/settings/startup.blade.php +++ b/resources/themes/pterodactyl/server/settings/startup.blade.php @@ -21,26 +21,20 @@ @section('content')
    -
    -
    -
    -
    -

    @lang('server.config.startup.command')

    +
    +
    +
    +

    @lang('server.config.startup.command')

    +
    +
    +
    +
    -
    -
    - -
    -
    - @can('edit-startup', $server) - - @endcan
    - @can('edit-startup', $server) +
    + @can('edit-startup', $server) + @foreach($variables as $v)
    @@ -50,11 +44,11 @@
    user_editable) - name="env_{{ $v->id }}" + name="environment[{{ $v->env_variable }}]" @else readonly @endif - class="form-control" type="text" value="{{ old('env_' . $v->id, $v->server_set_value) }}" /> + class="form-control" type="text" value="{{ old('environment.' . $v->env_variable, $server_values[$v->env_variable]) }}" />

    {{ $v->description }}

    @if($v->required && $v->user_editable ) @@ -68,14 +62,22 @@

    @endforeach - @endcan - +
    +
    + +
    +
    + + @endcan
    @endsection diff --git a/routes/server.php b/routes/server.php index 1ceafbe87..f6333a20d 100644 --- a/routes/server.php +++ b/routes/server.php @@ -23,8 +23,8 @@ Route::group(['prefix' => 'settings'], function () { Route::get('/sftp', 'Settings\SftpController@index')->name('server.settings.sftp'); - Route::get('/startup', 'ServerController@getStartup')->name('server.settings.startup'); - Route::post('/startup', 'ServerController@postSettingsStartup'); + Route::get('/startup', 'Settings\StartupController@index')->name('server.settings.startup'); + Route::patch('/startup', 'Settings\StartupController@update'); }); /* From 7022ec788fb089d823e951567fe70d259c557fda Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 26 Oct 2017 20:23:43 -0500 Subject: [PATCH 08/11] Test for server config structure --- ...erverConfigurationStructureServiceTest.php | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 tests/Unit/Services/Servers/ServerConfigurationStructureServiceTest.php diff --git a/tests/Unit/Services/Servers/ServerConfigurationStructureServiceTest.php b/tests/Unit/Services/Servers/ServerConfigurationStructureServiceTest.php new file mode 100644 index 000000000..e3b51693a --- /dev/null +++ b/tests/Unit/Services/Servers/ServerConfigurationStructureServiceTest.php @@ -0,0 +1,100 @@ +environment = m::mock(EnvironmentService::class); + $this->repository = m::mock(ServerRepositoryInterface::class); + } + + /** + * Test that a configuration is returned in the proper format when passed a + * server model that is missing required relationships. + */ + public function testCorrectStructureIsReturned() + { + $model = factory(Server::class)->make(); + $model->allocation = factory(Allocation::class)->make(); + $model->allocations = collect(factory(Allocation::class)->times(2)->make()); + $model->egg = factory(Egg::class)->make(); + + $portListing = $model->allocations->groupBy('ip')->map(function ($item) { + return $item->pluck('port'); + })->toArray(); + + $this->repository->shouldReceive('getDataForCreation')->with($model)->once()->andReturn($model); + $this->environment->shouldReceive('process')->with($model)->once()->andReturn(['environment_array']); + + $response = $this->getService()->handle($model); + $this->assertNotEmpty($response); + $this->assertArrayNotHasKey('user', $response); + $this->assertArrayNotHasKey('keys', $response); + $this->assertArrayHasKey('uuid', $response); + $this->assertArrayHasKey('build', $response); + $this->assertArrayHasKey('service', $response); + $this->assertArrayHasKey('rebuild', $response); + $this->assertArrayHasKey('suspended', $response); + + $this->assertArraySubset([ + 'default' => [ + 'ip' => $model->allocation->ip, + 'port' => $model->allocation->port, + ], + ], $response['build'], true, 'Assert server default allocation is correct.'); + $this->assertArraySubset(['ports' => $portListing], $response['build'], true, 'Assert server ports are correct.'); + $this->assertArraySubset([ + 'env' => ['environment_array'], + 'swap' => (int) $model->swap, + 'io' => (int) $model->io, + 'cpu' => (int) $model->cpu, + 'disk' => (int) $model->disk, + 'image' => $model->image, + ], $response['build'], true, 'Assert server build data is correct.'); + + $this->assertArraySubset([ + 'egg' => $model->egg->uuid, + 'pack' => null, + 'skip_scripts' => $model->skip_scripts, + ], $response['service']); + + $this->assertFalse($response['rebuild']); + $this->assertSame((int) $model->suspended, $response['suspended']); + } + + /** + * Return an instance of the service with mocked dependencies. + * + * @return \Pterodactyl\Services\Servers\ServerConfigurationStructureService + */ + private function getService(): ServerConfigurationStructureService + { + return new ServerConfigurationStructureService($this->repository, $this->environment); + } +} From fa62a0982e36e4e47bcf4e9a29ec2e516b9ba6bd Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 26 Oct 2017 23:49:54 -0500 Subject: [PATCH 09/11] Refactor startup modification and environment variable services Better setup, more flexibility, more tests. --- .../Repository/ServerRepositoryInterface.php | 9 +- .../Controllers/Admin/ServersController.php | 7 +- .../Server/Settings/StartupController.php | 2 + app/Models/Node.php | 2 + app/Models/User.php | 3 + .../Eloquent/ServerRepository.php | 17 +- app/Services/Servers/EnvironmentService.php | 83 ++++--- .../ServerConfigurationStructureService.php | 19 +- .../Servers/ServerCreationService.php | 87 ++++--- .../Servers/StartupModificationService.php | 129 +++++------ .../Servers/VariableValidatorService.php | 88 ++----- app/Traits/Services/HasUserLevels.php | 44 ++++ config/pterodactyl.php | 15 ++ database/factories/ModelFactory.php | 3 + .../Servers/EnvironmentServiceTest.php | 156 ++++++++----- ...erverConfigurationStructureServiceTest.php | 2 +- .../Servers/ServerCreationServiceTest.php | 217 ++++++++--------- .../StartupModificationServiceTest.php | 121 ++++++++-- .../Servers/VariableValidatorServiceTest.php | 219 ++++++++---------- 19 files changed, 660 insertions(+), 563 deletions(-) create mode 100644 app/Traits/Services/HasUserLevels.php diff --git a/app/Contracts/Repository/ServerRepositoryInterface.php b/app/Contracts/Repository/ServerRepositoryInterface.php index 7031b27e6..9e597bb73 100644 --- a/app/Contracts/Repository/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/ServerRepositoryInterface.php @@ -69,12 +69,11 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter /** * Return enough data to be used for the creation of a server via the daemon. * - * @param int $id - * @return \Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model - * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @param \Pterodactyl\Models\Server $server + * @param bool $refresh + * @return \Pterodactyl\Models\Server */ - public function getDataForCreation($id); + public function getDataForCreation(Server $server, bool $refresh = false): Server; /** * Return a server as well as associated databases and their hosts. diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index a10c56904..f79d257a4 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -11,6 +11,7 @@ namespace Pterodactyl\Http\Controllers\Admin; use Javascript; use Illuminate\Http\Request; +use Pterodactyl\Models\User; use Pterodactyl\Models\Server; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Exceptions\DisplayException; @@ -570,10 +571,8 @@ class ServersController extends Controller */ public function saveStartup(Request $request, Server $server) { - $this->startupModificationService->isAdmin()->handle( - $server, - $request->except('_token') - ); + $this->startupModificationService->setUserLevel(User::USER_LEVEL_ADMIN); + $this->startupModificationService->handle($server, $request->except('_token')); $this->alert->success(trans('admin/server.alerts.startup_changed'))->flash(); return redirect()->route('admin.servers.view.startup', $server->id); diff --git a/app/Http/Controllers/Server/Settings/StartupController.php b/app/Http/Controllers/Server/Settings/StartupController.php index f5ea20a47..5d299c42e 100644 --- a/app/Http/Controllers/Server/Settings/StartupController.php +++ b/app/Http/Controllers/Server/Settings/StartupController.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Http\Controllers\Server\Settings; use Illuminate\Http\Request; +use Pterodactyl\Models\User; use Illuminate\Http\RedirectResponse; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; @@ -84,6 +85,7 @@ class StartupController extends Controller */ public function update(UpdateStartupParametersFormRequest $request): RedirectResponse { + $this->modificationService->setUserLevel(User::USER_LEVEL_USER); $this->modificationService->handle($request->attributes->get('server'), $request->normalize()); $this->alert->success(trans('server.config.startup.edited'))->flash(); diff --git a/app/Models/Node.php b/app/Models/Node.php index 71f5614b5..cc22a724e 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -20,6 +20,8 @@ class Node extends Model implements CleansAttributes, ValidableContract { use Eloquence, Notifiable, Validable; + const DAEMON_SECRET_LENGTH = 36; + /** * The table associated with the model. * diff --git a/app/Models/User.php b/app/Models/User.php index 9f063c8ed..7b09165aa 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -32,6 +32,9 @@ class User extends Model implements { use Authenticatable, Authorizable, CanResetPassword, Eloquence, Notifiable, Validable; + const USER_LEVEL_USER = 0; + const USER_LEVEL_ADMIN = 1; + /** * Level of servers to display when using access() on a user. * diff --git a/app/Repositories/Eloquent/ServerRepository.php b/app/Repositories/Eloquent/ServerRepository.php index da15c89b1..ac023ff17 100644 --- a/app/Repositories/Eloquent/ServerRepository.php +++ b/app/Repositories/Eloquent/ServerRepository.php @@ -137,16 +137,21 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt } /** - * {@inheritdoc} + * Return enough data to be used for the creation of a server via the daemon. + * + * @param \Pterodactyl\Models\Server $server + * @param bool $refresh + * @return \Pterodactyl\Models\Server */ - public function getDataForCreation($id) + public function getDataForCreation(Server $server, bool $refresh = false): Server { - $instance = $this->getBuilder()->with(['allocation', 'allocations', 'pack', 'egg'])->find($id, $this->getColumns()); - if (! $instance) { - throw new RecordNotFoundException(); + foreach (['allocation', 'allocations', 'pack', 'egg'] as $relation) { + if (! $server->relationLoaded($relation) || $refresh) { + $server->load($relation); + } } - return $instance; + return $server; } /** diff --git a/app/Services/Servers/EnvironmentService.php b/app/Services/Servers/EnvironmentService.php index 987d90b2d..177034e10 100644 --- a/app/Services/Servers/EnvironmentService.php +++ b/app/Services/Servers/EnvironmentService.php @@ -1,42 +1,37 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Services\Servers; use Pterodactyl\Models\Server; +use Illuminate\Contracts\Config\Repository as ConfigRepository; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; class EnvironmentService { - const ENVIRONMENT_CASTS = [ - 'STARTUP' => 'startup', - 'P_SERVER_LOCATION' => 'location.short', - 'P_SERVER_UUID' => 'uuid', - ]; - /** * @var array */ - protected $additional = []; + private $additional = []; + + /** + * @var \Illuminate\Contracts\Config\Repository + */ + private $config; /** * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface */ - protected $repository; + private $repository; /** * EnvironmentService constructor. * + * @param \Illuminate\Contracts\Config\Repository $config * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository */ - public function __construct(ServerRepositoryInterface $repository) + public function __construct(ConfigRepository $config, ServerRepositoryInterface $repository) { + $this->config = $config; $this->repository = $repository; } @@ -46,42 +41,70 @@ class EnvironmentService * * @param string $key * @param callable $closure - * @return $this */ - public function setEnvironmentKey($key, callable $closure) + public function setEnvironmentKey(string $key, callable $closure) { - $this->additional[] = [$key, $closure]; + $this->additional[$key] = $closure; + } - return $this; + /** + * Return the dynamically added additional keys. + * + * @return array + */ + public function getEnvironmentKeys(): array + { + return $this->additional; } /** * Take all of the environment variables configured for this server and return * them in an easy to process format. * - * @param int|\Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Server $server * @return array * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function process($server) + public function handle(Server $server): array { - if (! $server instanceof Server) { - $server = $this->repository->find($server); - } - $variables = $this->repository->getVariablesWithValues($server->id); - // Process static environment variables defined in this file. - foreach (self::ENVIRONMENT_CASTS as $key => $object) { + // Process environment variables defined in this file. This is done first + // in order to allow run-time and config defined variables to take + // priority over built-in values. + foreach ($this->getEnvironmentMappings() as $key => $object) { $variables[$key] = object_get($server, $object); } + // Process variables set in the configuration file. + foreach ($this->config->get('pterodactyl.environment_mappings', []) as $key => $object) { + if (is_callable($object)) { + $variables[$key] = call_user_func($object, $server); + } else { + $variables[$key] = object_get($server, $object); + } + } + // Process dynamically included environment variables. - foreach ($this->additional as $item) { - $variables[$item[0]] = call_user_func($item[1], $server); + foreach ($this->additional as $key => $closure) { + $variables[$key] = call_user_func($closure, $server); } return $variables; } + + /** + * Return a mapping of Panel default environment variables. + * + * @return array + */ + final private function getEnvironmentMappings(): array + { + return [ + 'STARTUP' => 'startup', + 'P_SERVER_LOCATION' => 'location.short', + 'P_SERVER_UUID' => 'uuid', + ]; + } } diff --git a/app/Services/Servers/ServerConfigurationStructureService.php b/app/Services/Servers/ServerConfigurationStructureService.php index b92191711..d91d9db95 100644 --- a/app/Services/Servers/ServerConfigurationStructureService.php +++ b/app/Services/Servers/ServerConfigurationStructureService.php @@ -19,12 +19,12 @@ class ServerConfigurationStructureService /** * @var \Pterodactyl\Services\Servers\EnvironmentService */ - protected $environment; + private $environment; /** * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface */ - protected $repository; + private $repository; /** * ServerConfigurationStructureService constructor. @@ -41,19 +41,21 @@ class ServerConfigurationStructureService } /** - * @param int|\Pterodactyl\Models\Server $server + * Return a configuration array for a specific server when passed a server model. + * + * @param \Pterodactyl\Models\Server $server * @return array + * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function handle($server): array + public function handle(Server $server): array { - if (! $server instanceof Server || array_diff(self::REQUIRED_RELATIONS, $server->getRelations())) { - $server = $this->repository->getDataForCreation(is_digit($server) ? $server : $server->id); + if (array_diff(self::REQUIRED_RELATIONS, $server->getRelations())) { + $server = $this->repository->getDataForCreation($server); } return [ 'uuid' => $server->uuid, - 'user' => $server->username, 'build' => [ 'default' => [ 'ip' => $server->allocation->ip, @@ -62,7 +64,7 @@ class ServerConfigurationStructureService 'ports' => $server->allocations->groupBy('ip')->map(function ($item) { return $item->pluck('port'); })->toArray(), - 'env' => $this->environment->process($server), + 'env' => $this->environment->handle($server), 'memory' => (int) $server->memory, 'swap' => (int) $server->swap, 'io' => (int) $server->io, @@ -70,7 +72,6 @@ class ServerConfigurationStructureService 'disk' => (int) $server->disk, 'image' => $server->image, ], - 'keys' => [], 'service' => [ 'egg' => $server->egg->uuid, 'pack' => object_get($server, 'pack.uuid'), diff --git a/app/Services/Servers/ServerCreationService.php b/app/Services/Servers/ServerCreationService.php index 33d23b000..86e580a22 100644 --- a/app/Services/Servers/ServerCreationService.php +++ b/app/Services/Servers/ServerCreationService.php @@ -1,18 +1,12 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Services\Servers; use Ramsey\Uuid\Uuid; +use Pterodactyl\Models\Node; +use Pterodactyl\Models\User; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; -use Pterodactyl\Services\Nodes\NodeCreationService; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; @@ -26,47 +20,47 @@ class ServerCreationService /** * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface */ - protected $allocationRepository; + private $allocationRepository; /** * @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService */ - protected $configurationStructureService; + private $configurationStructureService; /** * @var \Illuminate\Database\ConnectionInterface */ - protected $connection; + private $connection; /** * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface */ - protected $daemonServerRepository; + private $daemonServerRepository; /** * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface */ - protected $nodeRepository; + private $nodeRepository; /** * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface */ - protected $repository; + private $repository; /** * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface */ - protected $serverVariableRepository; + private $serverVariableRepository; /** * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface */ - protected $userRepository; + private $userRepository; /** * @var \Pterodactyl\Services\Servers\VariableValidatorService */ - protected $validatorService; + private $validatorService; /** * CreationService constructor. @@ -116,33 +110,30 @@ class ServerCreationService public function create(array $data) { // @todo auto-deployment - $validator = $this->validatorService->isAdmin()->setFields($data['environment'])->validate($data['egg_id']); - $uniqueShort = str_random(8); $this->connection->beginTransaction(); - $server = $this->repository->create([ 'uuid' => Uuid::uuid4()->toString(), - 'uuidShort' => $uniqueShort, - 'node_id' => $data['node_id'], - 'name' => $data['name'], - 'description' => $data['description'], + 'uuidShort' => str_random(8), + 'node_id' => array_get($data, 'node_id'), + 'name' => array_get($data, 'name'), + 'description' => array_get($data, 'description'), 'skip_scripts' => isset($data['skip_scripts']), 'suspended' => false, - 'owner_id' => $data['owner_id'], - 'memory' => $data['memory'], - 'swap' => $data['swap'], - 'disk' => $data['disk'], - 'io' => $data['io'], - 'cpu' => $data['cpu'], + 'owner_id' => array_get($data, 'owner_id'), + 'memory' => array_get($data, 'memory'), + 'swap' => array_get($data, 'swap'), + 'disk' => array_get($data, 'disk'), + 'io' => array_get($data, 'io'), + 'cpu' => array_get($data, 'cpu'), 'oom_disabled' => isset($data['oom_disabled']), - 'allocation_id' => $data['allocation_id'], - 'nest_id' => $data['nest_id'], - 'egg_id' => $data['egg_id'], + 'allocation_id' => array_get($data, 'allocation_id'), + 'nest_id' => array_get($data, 'nest_id'), + 'egg_id' => array_get($data, 'egg_id'), 'pack_id' => (! isset($data['pack_id']) || $data['pack_id'] == 0) ? null : $data['pack_id'], - 'startup' => $data['startup'], - 'daemonSecret' => str_random(NodeCreationService::DAEMON_SECRET_LENGTH), - 'image' => $data['docker_image'], + 'startup' => array_get($data, 'startup'), + 'daemonSecret' => str_random(Node::DAEMON_SECRET_LENGTH), + 'image' => array_get($data, 'docker_image'), ]); // Process allocations and assign them to the server in the database. @@ -154,17 +145,21 @@ class ServerCreationService $this->allocationRepository->assignAllocationsToServer($server->id, $records); // Process the passed variables and store them in the database. - $records = []; - foreach ($validator->getResults() as $result) { - $records[] = [ - 'server_id' => $server->id, - 'variable_id' => $result['id'], - 'variable_value' => $result['value'], - ]; - } + $this->validatorService->setUserLevel(User::USER_LEVEL_ADMIN); + $results = $this->validatorService->handle(array_get($data, 'egg_id'), array_get($data, 'environment', [])); - $this->serverVariableRepository->insert($records); - $structure = $this->configurationStructureService->handle($server->id); + $records = $results->map(function ($result) use ($server) { + return [ + 'server_id' => $server->id, + 'variable_id' => $result->id, + 'variable_value' => $result->value, + ]; + })->toArray(); + + if (! empty($records)) { + $this->serverVariableRepository->insert($records); + } + $structure = $this->configurationStructureService->handle($server); // Create the server on the daemon & commit it to the database. try { diff --git a/app/Services/Servers/StartupModificationService.php b/app/Services/Servers/StartupModificationService.php index 78460c197..ae91fb3ba 100644 --- a/app/Services/Servers/StartupModificationService.php +++ b/app/Services/Servers/StartupModificationService.php @@ -1,17 +1,12 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Services\Servers; +use Pterodactyl\Models\User; use Pterodactyl\Models\Server; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Traits\Services\HasUserLevels; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; @@ -19,40 +14,37 @@ use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonS class StartupModificationService { - /** - * @var bool - */ - protected $admin = false; + use HasUserLevels; /** * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface */ - protected $daemonServerRepository; + private $daemonServerRepository; /** * @var \Illuminate\Database\ConnectionInterface */ - protected $connection; + private $connection; /** * @var \Pterodactyl\Services\Servers\EnvironmentService */ - protected $environmentService; + private $environmentService; /** * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface */ - protected $repository; + private $repository; /** * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface */ - protected $serverVariableRepository; + private $serverVariableRepository; /** * @var \Pterodactyl\Services\Servers\VariableValidatorService */ - protected $validatorService; + private $validatorService; /** * StartupModificationService constructor. @@ -80,81 +72,39 @@ class StartupModificationService $this->validatorService = $validatorService; } - /** - * Determine if this function should run at an administrative level. - * - * @param bool $bool - * @return $this - */ - public function isAdmin($bool = true) - { - $this->admin = $bool; - - return $this; - } - /** * Process startup modification for a server. * - * @param int|\Pterodactyl\Models\Server $server - * @param array $data + * @param \Pterodactyl\Models\Server $server + * @param array $data * * @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); - } - - if ( - $server->nest_id != array_get($data, 'nest_id', $server->nest_id) || - $server->egg_id != array_get($data, 'egg_id', $server->egg_id) || - $server->pack_id != array_get($data, 'pack_id', $server->pack_id) - ) { - $hasServiceChanges = true; - } - $this->connection->beginTransaction(); if (! is_null(array_get($data, 'environment'))) { - $validator = $this->validatorService->isAdmin($this->admin) - ->setFields(array_get($data, 'environment', [])) - ->validate(array_get($data, 'egg_id', $server->egg_id)); + $this->validatorService->setUserLevel($this->getUserLevel()); + $results = $this->validatorService->handle(array_get($data, 'egg_id', $server->egg_id), array_get($data, 'environment', [])); - foreach ($validator->getResults() as $result) { + $results->each(function ($result) use ($server) { $this->serverVariableRepository->withoutFresh()->updateOrCreate([ 'server_id' => $server->id, - 'variable_id' => $result['id'], + 'variable_id' => $result->id, ], [ - 'variable_value' => $result['value'], + 'variable_value' => $result->value, ]); - } + }); } - $daemonData = [ - 'build' => [ - 'env|overwrite' => $this->environmentService->process($server), - ], - ]; + $daemonData = ['build' => [ + 'env|overwrite' => $this->environmentService->handle($server), + ]]; - if ($this->admin) { - $server = $this->repository->update($server->id, [ - 'installed' => 0, - 'startup' => array_get($data, 'startup', $server->startup), - 'nest_id' => array_get($data, 'nest_id', $server->nest_id), - 'egg_id' => array_get($data, 'egg_id', $server->egg_id), - 'pack_id' => array_get($data, 'pack_id', $server->pack_id) > 0 ? array_get($data, 'pack_id', $server->pack_id) : null, - 'skip_scripts' => isset($data['skip_scripts']), - ]); - - if (isset($hasServiceChanges)) { - $daemonData['service'] = array_merge( - $this->repository->withColumns(['id', 'egg_id', 'pack_id'])->getDaemonServiceData($server->id), - ['skip_scripts' => isset($data['skip_scripts'])] - ); - } + if ($this->isUserLevel(User::USER_LEVEL_ADMIN)) { + $this->updateAdministrativeSettings($data, $server, $daemonData); } try { @@ -166,4 +116,37 @@ class StartupModificationService $this->connection->commit(); } + + /** + * Update certain administrative settings for a server in the DB. + * + * @param array $data + * @param \Pterodactyl\Models\Server $server + * @param array $daemonData + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + private function updateAdministrativeSettings(array $data, Server &$server, array &$daemonData) + { + $server = $this->repository->update($server->id, [ + 'installed' => 0, + 'startup' => array_get($data, 'startup', $server->startup), + 'nest_id' => array_get($data, 'nest_id', $server->nest_id), + 'egg_id' => array_get($data, 'egg_id', $server->egg_id), + 'pack_id' => array_get($data, 'pack_id', $server->pack_id) > 0 ? array_get($data, 'pack_id', $server->pack_id) : null, + 'skip_scripts' => isset($data['skip_scripts']), + ]); + + if ( + $server->nest_id != array_get($data, 'nest_id', $server->nest_id) || + $server->egg_id != array_get($data, 'egg_id', $server->egg_id) || + $server->pack_id != array_get($data, 'pack_id', $server->pack_id) + ) { + $daemonData['service'] = array_merge( + $this->repository->withColumns(['id', 'egg_id', 'pack_id'])->getDaemonServiceData($server->id), + ['skip_scripts' => isset($data['skip_scripts'])] + ); + } + } } diff --git a/app/Services/Servers/VariableValidatorService.php b/app/Services/Servers/VariableValidatorService.php index 7340d2f7b..b4f722c79 100644 --- a/app/Services/Servers/VariableValidatorService.php +++ b/app/Services/Servers/VariableValidatorService.php @@ -9,6 +9,9 @@ namespace Pterodactyl\Services\Servers; +use Pterodactyl\Models\User; +use Illuminate\Support\Collection; +use Pterodactyl\Traits\Services\HasUserLevels; use Pterodactyl\Exceptions\DisplayValidationException; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Illuminate\Contracts\Validation\Factory as ValidationFactory; @@ -17,20 +20,7 @@ use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; class VariableValidatorService { - /** - * @var bool - */ - protected $isAdmin = false; - - /** - * @var array - */ - protected $fields = []; - - /** - * @var array - */ - protected $results = []; + use HasUserLevels; /** * @var \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface @@ -72,56 +62,26 @@ class VariableValidatorService $this->validator = $validator; } - /** - * Set the fields with populated data to validate. - * - * @param array $fields - * @return $this - */ - public function setFields(array $fields) - { - $this->fields = $fields; - - return $this; - } - - /** - * Set this function to be running at the administrative level. - * - * @param bool $bool - * @return $this - */ - public function isAdmin($bool = true) - { - $this->isAdmin = $bool; - - return $this; - } - /** * Validate all of the passed data aganist the given service option variables. * - * @param int $option - * @return $this + * @param int $egg + * @param array $fields + * @return \Illuminate\Support\Collection */ - public function validate($option) + public function handle(int $egg, array $fields = []): Collection { - $variables = $this->optionVariableRepository->findWhere([['egg_id', '=', $option]]); - if (count($variables) === 0) { - $this->results = []; + $variables = $this->optionVariableRepository->findWhere([['egg_id', '=', $egg]]); - return $this; - } - - $variables->each(function ($item) { - // 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)) { - return; + return $variables->map(function ($item) use ($fields) { + // Skip doing anything if user is not an admin and + // variable is not user viewable or editable. + if (! $this->isUserLevel(User::USER_LEVEL_ADMIN) && (! $item->user_editable || ! $item->user_viewable)) { + return false; } $validator = $this->validator->make([ - 'variable_value' => array_key_exists($item->env_variable, $this->fields) ? $this->fields[$item->env_variable] : null, + 'variable_value' => array_get($fields, $item->env_variable), ], [ 'variable_value' => $item->rules, ]); @@ -136,23 +96,13 @@ class VariableValidatorService )); } - $this->results[] = [ + return (object) [ 'id' => $item->id, 'key' => $item->env_variable, - 'value' => $this->fields[$item->env_variable], + 'value' => array_get($fields, $item->env_variable), ]; + })->filter(function ($item) { + return is_object($item); }); - - return $this; - } - - /** - * Return the final results after everything has been validated. - * - * @return array - */ - public function getResults() - { - return $this->results; } } diff --git a/app/Traits/Services/HasUserLevels.php b/app/Traits/Services/HasUserLevels.php new file mode 100644 index 000000000..d2d95e233 --- /dev/null +++ b/app/Traits/Services/HasUserLevels.php @@ -0,0 +1,44 @@ +userLevel = $level; + } + + /** + * Determine which level this function is running at. + * + * @return int + */ + public function getUserLevel(): int + { + return $this->userLevel; + } + + /** + * Determine if the current user level is set to a specific level. + * + * @param int $level + * @return bool + */ + public function isUserLevel(int $level): bool + { + return $this->getUserLevel() === $level; + } +} diff --git a/config/pterodactyl.php b/config/pterodactyl.php index 2bf1b2c8a..f232d8a1e 100644 --- a/config/pterodactyl.php +++ b/config/pterodactyl.php @@ -184,4 +184,19 @@ return [ 'daemon/*', 'remote/*', ], + + /* + |-------------------------------------------------------------------------- + | Dynamic Environment Variables + |-------------------------------------------------------------------------- + | + | Place dynamic environment variables here that should be auto-appended + | to server environment fields when the server is created or updated. + | + | Items should be in 'key' => 'value' format, where key is the environment + | variable name, and value is the server-object key. For example: + | + | 'P_SERVER_CREATED_AT' => 'created_at' + */ + 'environment_variables' => [], ]; diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index 233aeee01..54ba984be 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -29,6 +29,9 @@ $factory->define(Pterodactyl\Models\Server::class, function (Faker\Generator $fa 'io' => 500, 'cpu' => 0, 'oom_disabled' => 0, + 'allocation_id' => $faker->randomNumber(), + 'nest_id' => $faker->randomNumber(), + 'egg_id' => $faker->randomNumber(), 'pack_id' => null, 'installed' => 1, 'created_at' => \Carbon\Carbon::now(), diff --git a/tests/Unit/Services/Servers/EnvironmentServiceTest.php b/tests/Unit/Services/Servers/EnvironmentServiceTest.php index f0ff4fef7..435eb9bfb 100644 --- a/tests/Unit/Services/Servers/EnvironmentServiceTest.php +++ b/tests/Unit/Services/Servers/EnvironmentServiceTest.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Tests\Unit\Services\Servers; @@ -13,25 +6,23 @@ use Mockery as m; use Tests\TestCase; use Pterodactyl\Models\Server; use Pterodactyl\Models\Location; +use Illuminate\Contracts\Config\Repository; use Pterodactyl\Services\Servers\EnvironmentService; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; class EnvironmentServiceTest extends TestCase { - /** - * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface - */ - protected $repository; + const CONFIG_MAPPING = 'pterodactyl.environment_mappings'; /** - * @var \Pterodactyl\Services\Servers\EnvironmentService + * @var \Illuminate\Contracts\Config\Repository|\Mockery\Mock */ - protected $service; + private $config; /** - * @var \Pterodactyl\Models\Server + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock */ - protected $server; + private $repository; /** * Setup tests. @@ -40,24 +31,23 @@ class EnvironmentServiceTest extends TestCase { parent::setUp(); + $this->config = m::mock(Repository::class); $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. + * Test that set environment key stores the key into a retreviable array. */ - public function testSettingEnvironmentKeyShouldReturnInstanceOfSelf() + public function testSettingEnvironmentKeyPersistsItInArray() { - $instance = $this->service->setEnvironmentKey('TEST_KEY', function () { + $service = $this->getService(); + + $service->setEnvironmentKey('TEST_KEY', function () { return true; }); - $this->assertInstanceOf(EnvironmentService::class, $instance); + $this->assertNotEmpty($service->getEnvironmentKeys()); + $this->assertArrayHasKey('TEST_KEY', $service->getEnvironmentKeys()); } /** @@ -65,22 +55,17 @@ class EnvironmentServiceTest extends TestCase */ public function testProcessShouldReturnDefaultEnvironmentVariablesForAServer() { - $this->repository->shouldReceive('getVariablesWithValues')->with($this->server->id)->once()->andReturn([ + $model = $this->getServerModel(); + $this->config->shouldReceive('get')->with(self::CONFIG_MAPPING, [])->once()->andReturn([]); + $this->repository->shouldReceive('getVariablesWithValues')->with($model->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.'); - + $response = $this->getService()->handle($model); + $this->assertNotEmpty($response); + $this->assertEquals(4, count($response)); $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]); - } + $this->assertSame('Test Variable', $response['TEST_VARIABLE']); } /** @@ -88,43 +73,106 @@ class EnvironmentServiceTest extends TestCase */ public function testProcessShouldReturnKeySetAtRuntime() { - $this->repository->shouldReceive('getVariablesWithValues')->with($this->server->id)->once()->andReturn([]); + $model = $this->getServerModel(); + $this->config->shouldReceive('get')->with(self::CONFIG_MAPPING, [])->once()->andReturn([]); + $this->repository->shouldReceive('getVariablesWithValues')->with($model->id)->once()->andReturn([]); - $response = $this->service->setEnvironmentKey('TEST_VARIABLE', function ($server) { + $service = $this->getService(); + $service->setEnvironmentKey('TEST_VARIABLE', function ($server) { return $server->uuidShort; - })->process($this->server); + }); - $this->assertTrue(is_array($response), 'Assert response is an array.'); + $response = $service->handle($model); + + $this->assertNotEmpty($response); $this->assertArrayHasKey('TEST_VARIABLE', $response); - $this->assertEquals($this->server->uuidShort, $response['TEST_VARIABLE']); + $this->assertSame($model->uuidShort, $response['TEST_VARIABLE']); } /** - * Test that duplicate variables provided at run-time override the defaults. + * Test that duplicate variables provided in config override the defaults. + */ + public function testProcessShouldAllowOverwritingVaraiblesWithConfigurationFile() + { + $model = $this->getServerModel(); + $this->repository->shouldReceive('getVariablesWithValues')->with($model->id)->once()->andReturn([]); + $this->config->shouldReceive('get')->with(self::CONFIG_MAPPING, [])->once()->andReturn([ + 'P_SERVER_UUID' => 'name', + ]); + + $response = $this->getService()->handle($model); + + $this->assertNotEmpty($response); + $this->assertSame(3, count($response)); + $this->assertArrayHasKey('P_SERVER_UUID', $response); + $this->assertSame($model->name, $response['P_SERVER_UUID']); + } + + /** + * Test that config based environment variables can be done using closures. + */ + public function testVariablesSetInConfigurationAllowForClosures() + { + $model = $this->getServerModel(); + $this->config->shouldReceive('get')->with(self::CONFIG_MAPPING, [])->once()->andReturn([ + 'P_SERVER_UUID' => function ($server) { + return $server->id * 2; + }, + ]); + $this->repository->shouldReceive('getVariablesWithValues')->with($model->id)->once()->andReturn([]); + + $response = $this->getService()->handle($model); + + $this->assertNotEmpty($response); + $this->assertSame(3, count($response)); + $this->assertArrayHasKey('P_SERVER_UUID', $response); + $this->assertSame($model->id * 2, $response['P_SERVER_UUID']); + } + + /** + * Test that duplicate variables provided at run-time override the defaults and those + * that are defined in the configuration file. */ public function testProcessShouldAllowOverwritingDefaultVariablesWithRuntimeProvided() { - $this->repository->shouldReceive('getVariablesWithValues')->with($this->server->id)->once()->andReturn([]); + $model = $this->getServerModel(); + $this->config->shouldReceive('get')->with(self::CONFIG_MAPPING, [])->once()->andReturn([ + 'P_SERVER_UUID' => 'overwritten-config', + ]); + $this->repository->shouldReceive('getVariablesWithValues')->with($model->id)->once()->andReturn([]); - $response = $this->service->setEnvironmentKey('P_SERVER_UUID', function ($server) { + $service = $this->getService(); + $service->setEnvironmentKey('P_SERVER_UUID', function ($model) { return 'overwritten'; - })->process($this->server); + }); - $this->assertTrue(is_array($response), 'Assert response is an array.'); + $response = $service->handle($model); + + $this->assertNotEmpty($response); + $this->assertSame(3, count($response)); $this->assertArrayHasKey('P_SERVER_UUID', $response); - $this->assertEquals('overwritten', $response['P_SERVER_UUID']); + $this->assertSame('overwritten', $response['P_SERVER_UUID']); } /** - * Test that function can run when an ID is provided rather than a server model. + * Return an instance of the service with mocked dependencies. + * + * @return \Pterodactyl\Services\Servers\EnvironmentService */ - public function testProcessShouldAcceptAnIntegerInPlaceOfAServerModel() + private function getService(): EnvironmentService { - $this->repository->shouldReceive('find')->with($this->server->id)->once()->andReturn($this->server); - $this->repository->shouldReceive('getVariablesWithValues')->with($this->server->id)->once()->andReturn([]); + return new EnvironmentService($this->config, $this->repository); + } - $response = $this->service->process($this->server->id); - - $this->assertTrue(is_array($response), 'Assert that response is an array.'); + /** + * Return a server model with a location relationship to be used in the tests. + * + * @return \Pterodactyl\Models\Server + */ + private function getServerModel(): Server + { + return factory(Server::class)->make([ + 'location' => factory(Location::class)->make(), + ]); } } diff --git a/tests/Unit/Services/Servers/ServerConfigurationStructureServiceTest.php b/tests/Unit/Services/Servers/ServerConfigurationStructureServiceTest.php index e3b51693a..ceca81758 100644 --- a/tests/Unit/Services/Servers/ServerConfigurationStructureServiceTest.php +++ b/tests/Unit/Services/Servers/ServerConfigurationStructureServiceTest.php @@ -50,7 +50,7 @@ class ServerConfigurationStructureServiceTest extends TestCase })->toArray(); $this->repository->shouldReceive('getDataForCreation')->with($model)->once()->andReturn($model); - $this->environment->shouldReceive('process')->with($model)->once()->andReturn(['environment_array']); + $this->environment->shouldReceive('handle')->with($model)->once()->andReturn(['environment_array']); $response = $this->getService()->handle($model); $this->assertNotEmpty($response); diff --git a/tests/Unit/Services/Servers/ServerCreationServiceTest.php b/tests/Unit/Services/Servers/ServerCreationServiceTest.php index 89e67d916..c430cc22b 100644 --- a/tests/Unit/Services/Servers/ServerCreationServiceTest.php +++ b/tests/Unit/Services/Servers/ServerCreationServiceTest.php @@ -1,18 +1,13 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Tests\Unit\Services\Servers; use Mockery as m; use Tests\TestCase; -use phpmock\phpunit\PHPMock; +use Pterodactyl\Models\User; use Tests\Traits\MocksUuids; +use Pterodactyl\Models\Server; +use Tests\Traits\MocksRequestException; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Exceptions\PterodactylException; @@ -32,86 +27,57 @@ use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonS */ class ServerCreationServiceTest extends TestCase { - use MocksUuids, PHPMock; + use MocksRequestException, MocksUuids; /** * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface|\Mockery\Mock */ - protected $allocationRepository; + private $allocationRepository; /** * @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService|\Mockery\Mock */ - protected $configurationStructureService; + private $configurationStructureService; /** * @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock */ - protected $connection; + private $connection; /** * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface|\Mockery\Mock */ - protected $daemonServerRepository; - - /** - * @var array - */ - protected $data = [ - 'node_id' => 1, - 'name' => 'SomeName', - 'description' => null, - 'owner_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', - ], - 'nest_id' => 1, - 'egg_id' => 1, - 'startup' => 'startup-param', - 'docker_image' => 'some/image', - ]; + private $daemonServerRepository; /** * @var \GuzzleHttp\Exception\RequestException|\Mockery\Mock */ - protected $exception; + private $exception; /** * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface|\Mockery\Mock */ - protected $nodeRepository; + private $nodeRepository; /** * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock */ - protected $repository; + private $repository; /** * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface|\Mockery\Mock */ - protected $serverVariableRepository; - - /** - * @var \Pterodactyl\Services\Servers\ServerCreationService - */ - protected $service; + private $serverVariableRepository; /** * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface|\Mockery\Mock */ - protected $userRepository; + private $userRepository; /** * @var \Pterodactyl\Services\Servers\VariableValidatorService|\Mockery\Mock */ - protected $validatorService; + private $validatorService; /** * Setup tests. @@ -130,11 +96,87 @@ class ServerCreationServiceTest extends TestCase $this->serverVariableRepository = m::mock(ServerVariableRepositoryInterface::class); $this->userRepository = m::mock(UserRepositoryInterface::class); $this->validatorService = m::mock(VariableValidatorService::class); + } - $this->getFunctionMock('\\Pterodactyl\\Services\\Servers', 'str_random') - ->expects($this->any())->willReturn('random_string'); + /** + * Test core functionality of the creation process. + */ + public function testCreateShouldHitAllOfTheNecessaryServicesAndStoreTheServer() + { + $model = factory(Server::class)->make([ + 'uuid' => $this->getKnownUuid(), + ]); - $this->service = new ServerCreationService( + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('create')->with(m::subset([ + 'uuid' => $this->getKnownUuid(), + 'node_id' => $model->node_id, + 'owner_id' => $model->owner_id, + 'nest_id' => $model->nest_id, + 'egg_id' => $model->egg_id, + ]))->once()->andReturn($model); + + $this->allocationRepository->shouldReceive('assignAllocationsToServer')->with($model->id, [$model->allocation_id])->once()->andReturnNull(); + + $this->validatorService->shouldReceive('setUserLevel')->with(User::USER_LEVEL_ADMIN)->once()->andReturnNull(); + $this->validatorService->shouldReceive('handle')->with($model->egg_id, [])->once()->andReturn( + collect([(object) ['id' => 123, 'value' => 'var1-value']]) + ); + + $this->serverVariableRepository->shouldReceive('insert')->with([ + [ + 'server_id' => $model->id, + 'variable_id' => 123, + 'variable_value' => 'var1-value', + ], + ])->once()->andReturnNull(); + $this->configurationStructureService->shouldReceive('handle')->with($model)->once()->andReturn(['test' => 'struct']); + + $this->daemonServerRepository->shouldReceive('setNode')->with($model->node_id)->once()->andReturnSelf(); + $this->daemonServerRepository->shouldReceive('create')->with(['test' => 'struct'], ['start_on_completion' => false])->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $response = $this->getService()->create($model->toArray()); + + $this->assertSame($model, $response); + } + + /** + * Test handling of node timeout or other daemon error. + */ + public function testExceptionShouldBeThrownIfTheRequestFails() + { + $this->configureExceptionMock(); + + $model = factory(Server::class)->make([ + 'uuid' => $this->getKnownUuid(), + ]); + + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('create')->once()->andReturn($model); + $this->allocationRepository->shouldReceive('assignAllocationsToServer')->once()->andReturnNull(); + $this->validatorService->shouldReceive('setUserLevel')->once()->andReturnNull(); + $this->validatorService->shouldReceive('handle')->once()->andReturn(collect([])); + $this->configurationStructureService->shouldReceive('handle')->once()->andReturn([]); + $this->daemonServerRepository->shouldReceive('setNode')->with($model->node_id)->once()->andThrow($this->exception); + $this->connection->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); + + try { + $this->getService()->create($model->toArray()); + } catch (PterodactylException $exception) { + $this->assertInstanceOf(DaemonConnectionException::class, $exception); + $this->assertInstanceOf(RequestException::class, $exception->getPrevious()); + } + } + + /** + * Return an instance of the service with mocked dependencies. + * + * @return \Pterodactyl\Services\Servers\ServerCreationService + */ + private function getService(): ServerCreationService + { + return new ServerCreationService( $this->allocationRepository, $this->connection, $this->daemonServerRepository, @@ -146,77 +188,4 @@ class ServerCreationServiceTest extends TestCase $this->validatorService ); } - - /** - * Test core functionality of the creation process. - */ - public function testCreateShouldHitAllOfTheNecessaryServicesAndStoreTheServer() - { - $this->validatorService->shouldReceive('isAdmin')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('setFields')->with($this->data['environment'])->once()->andReturnSelf() - ->shouldReceive('validate')->with($this->data['egg_id'])->once()->andReturnSelf(); - - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - - $this->repository->shouldReceive('create')->with(m::subset([ - 'uuid' => $this->getKnownUuid(), - 'node_id' => $this->data['node_id'], - 'owner_id' => 1, - 'nest_id' => 1, - 'egg_id' => 1, - ]))->once()->andReturn((object) [ - 'node_id' => 1, - 'id' => 1, - ]); - - $this->allocationRepository->shouldReceive('assignAllocationsToServer')->with(1, [1, 2, 3])->once()->andReturnNull(); - $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->configurationStructureService->shouldReceive('handle')->with(1)->once()->andReturn(['test' => 'struct']); - - $this->daemonServerRepository->shouldReceive('setNode')->with(1)->once()->andReturnSelf() - ->shouldReceive('create')->with(['test' => 'struct'], ['start_on_completion' => false])->once()->andReturnNull(); - $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - - $response = $this->service->create($this->data); - - $this->assertEquals(1, $response->id); - $this->assertEquals(1, $response->node_id); - } - - /** - * Test handling of node timeout or other daemon error. - */ - public function testExceptionShouldBeThrownIfTheRequestFails() - { - $this->validatorService->shouldReceive('isAdmin->setFields->validate->getResults')->once()->andReturn([]); - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('create')->once()->andReturn((object) [ - 'node_id' => 1, - 'id' => 1, - ]); - - $this->allocationRepository->shouldReceive('assignAllocationsToServer')->once()->andReturnNull(); - $this->serverVariableRepository->shouldReceive('insert')->with([])->once()->andReturnNull(); - $this->configurationStructureService->shouldReceive('handle')->once()->andReturnNull(); - $this->daemonServerRepository->shouldReceive('setNode->create')->once()->andThrow($this->exception); - $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnNull(); - $this->connection->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); - - try { - $this->service->create($this->data); - } catch (PterodactylException $exception) { - $this->assertInstanceOf(DaemonConnectionException::class, $exception); - } - } } diff --git a/tests/Unit/Services/Servers/StartupModificationServiceTest.php b/tests/Unit/Services/Servers/StartupModificationServiceTest.php index d35ad551b..5d8076ae8 100644 --- a/tests/Unit/Services/Servers/StartupModificationServiceTest.php +++ b/tests/Unit/Services/Servers/StartupModificationServiceTest.php @@ -11,6 +11,7 @@ namespace Tests\Unit\Services\Servers; use Mockery as m; use Tests\TestCase; +use Pterodactyl\Models\User; use Pterodactyl\Models\Server; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Services\Servers\EnvironmentService; @@ -25,37 +26,32 @@ class StartupModificationServiceTest extends TestCase /** * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface|\Mockery\Mock */ - protected $daemonServerRepository; + private $daemonServerRepository; /** * @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock */ - protected $connection; + private $connection; /** * @var \Pterodactyl\Services\Servers\EnvironmentService|\Mockery\Mock */ - protected $environmentService; + private $environmentService; /** * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock */ - protected $repository; + private $repository; /** * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface|\Mockery\Mock */ - protected $serverVariableRepository; - - /** - * @var \Pterodactyl\Services\Servers\StartupModificationService - */ - protected $service; + private $serverVariableRepository; /** * @var \Pterodactyl\Services\Servers\VariableValidatorService|\Mockery\Mock */ - protected $validatorService; + private $validatorService; /** * Setup tests. @@ -70,8 +66,97 @@ class StartupModificationServiceTest extends TestCase $this->repository = m::mock(ServerRepositoryInterface::class); $this->serverVariableRepository = m::mock(ServerVariableRepositoryInterface::class); $this->validatorService = m::mock(VariableValidatorService::class); + } - $this->service = new StartupModificationService( + /** + * Test startup modification as a non-admin user. + */ + public function testStartupModifiedAsNormalUser() + { + $model = factory(Server::class)->make(); + + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->validatorService->shouldReceive('setUserLevel')->with(User::USER_LEVEL_USER)->once()->andReturnNull(); + $this->validatorService->shouldReceive('handle')->with(123, ['test' => 'abcd1234'])->once()->andReturn( + collect([(object) ['id' => 1, 'value' => 'stored-value']]) + ); + + $this->serverVariableRepository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf(); + $this->serverVariableRepository->shouldReceive('updateOrCreate')->with([ + 'server_id' => $model->id, + 'variable_id' => 1, + ], ['variable_value' => 'stored-value'])->once()->andReturnNull(); + + $this->environmentService->shouldReceive('handle')->with($model)->once()->andReturn(['env']); + $this->daemonServerRepository->shouldReceive('setNode')->with($model->node_id)->once()->andReturnSelf(); + $this->daemonServerRepository->shouldReceive('setAccessServer')->with($model->uuid)->once()->andReturnSelf(); + $this->daemonServerRepository->shouldReceive('update')->with([ + 'build' => ['env|overwrite' => ['env']], + ])->once()->andReturnSelf(); + + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $this->getService()->handle($model, ['egg_id' => 123, 'environment' => ['test' => 'abcd1234']]); + $this->assertTrue(true); + } + + /** + * Test startup modification as an admin user. + */ + public function testStartupModificationAsAdminUser() + { + $model = factory(Server::class)->make([ + 'egg_id' => 123, + ]); + + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->validatorService->shouldReceive('setUserLevel')->with(User::USER_LEVEL_ADMIN)->once()->andReturnNull(); + $this->validatorService->shouldReceive('handle')->with(456, ['test' => 'abcd1234'])->once()->andReturn( + collect([(object) ['id' => 1, 'value' => 'stored-value']]) + ); + + $this->serverVariableRepository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf(); + $this->serverVariableRepository->shouldReceive('updateOrCreate')->with([ + 'server_id' => $model->id, + 'variable_id' => 1, + ], ['variable_value' => 'stored-value'])->once()->andReturnNull(); + + $this->environmentService->shouldReceive('handle')->with($model)->once()->andReturn(['env']); + + $this->repository->shouldReceive('update')->with($model->id, m::subset([ + 'installed' => 0, + 'egg_id' => 456, + 'pack_id' => 789, + ]))->once()->andReturn($model); + $this->repository->shouldReceive('withColumns->getDaemonServiceData')->with($model->id)->once()->andReturn([]); + + $this->daemonServerRepository->shouldReceive('setNode')->with($model->node_id)->once()->andReturnSelf(); + $this->daemonServerRepository->shouldReceive('setAccessServer')->with($model->uuid)->once()->andReturnSelf(); + $this->daemonServerRepository->shouldReceive('update')->with([ + 'build' => [ + 'env|overwrite' => ['env'], + ], + 'service' => [ + 'skip_scripts' => false, + ], + ])->once()->andReturnSelf(); + + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $service = $this->getService(); + $service->setUserLevel(User::USER_LEVEL_ADMIN); + $service->handle($model, ['egg_id' => 456, 'pack_id' => 789, 'environment' => ['test' => 'abcd1234']]); + $this->assertTrue(true); + } + + /** + * Return an instance of the service with mocked dependencies. + * + * @return \Pterodactyl\Services\Servers\StartupModificationService + */ + private function getService(): StartupModificationService + { + return new StartupModificationService( $this->connection, $this->daemonServerRepository, $this->environmentService, @@ -80,16 +165,4 @@ class StartupModificationServiceTest extends TestCase $this->validatorService ); } - - /** - * Test startup is modified when user is not an administrator. - * - * @todo this test works, but not for the right reasons... - */ - public function testStartupIsModifiedAsNonAdmin() - { - $model = factory(Server::class)->make(); - - $this->assertTrue(true); - } } diff --git a/tests/Unit/Services/Servers/VariableValidatorServiceTest.php b/tests/Unit/Services/Servers/VariableValidatorServiceTest.php index ce2eaf7d8..b949b3ae8 100644 --- a/tests/Unit/Services/Servers/VariableValidatorServiceTest.php +++ b/tests/Unit/Services/Servers/VariableValidatorServiceTest.php @@ -11,8 +11,11 @@ namespace Tests\Unit\Services\Servers; use Mockery as m; use Tests\TestCase; +use Pterodactyl\Models\User; +use Illuminate\Support\Collection; use Pterodactyl\Models\EggVariable; use Illuminate\Contracts\Validation\Factory; +use Pterodactyl\Exceptions\PterodactylException; use Pterodactyl\Exceptions\DisplayValidationException; use Pterodactyl\Services\Servers\VariableValidatorService; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; @@ -22,35 +25,25 @@ use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; class VariableValidatorServiceTest extends TestCase { /** - * @var \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface + * @var \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface|\Mockery\Mock */ protected $optionVariableRepository; /** - * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock */ protected $serverRepository; /** - * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface + * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface|\Mockery\Mock */ protected $serverVariableRepository; /** - * @var \Pterodactyl\Services\Servers\VariableValidatorService - */ - protected $service; - - /** - * @var \Illuminate\Validation\Factory + * @var \Illuminate\Contracts\Validation\Factory|\Mockery\Mock */ protected $validator; - /** - * @var \Illuminate\Support\Collection - */ - protected $variables; - /** * Setup tests. */ @@ -58,56 +51,10 @@ class VariableValidatorServiceTest extends TestCase { parent::setUp(); - $this->variables = collect( - [ - factory(EggVariable::class)->states('editable', 'viewable')->make(), - factory(EggVariable::class)->states('viewable')->make(), - factory(EggVariable::class)->states('editable')->make(), - factory(EggVariable::class)->make(), - ] - ); - $this->optionVariableRepository = m::mock(EggVariableRepositoryInterface::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->isAdmin(); - - $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)); } /** @@ -115,13 +62,11 @@ class VariableValidatorServiceTest extends TestCase */ public function testEmptyResultSetShouldBeReturnedIfNoVariablesAreFound() { - $this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn([]); + $this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn(collect([])); - $response = $this->service->validate(1); - - $this->assertInstanceOf(VariableValidatorService::class, $response); - $this->assertTrue(is_array($response->getResults())); - $this->assertEmpty($response->getResults()); + $response = $this->getService()->handle(1, []); + $this->assertEmpty($response); + $this->assertInstanceOf(Collection::class, $response); } /** @@ -129,31 +74,34 @@ class VariableValidatorServiceTest extends TestCase */ public function testValidatorShouldNotProcessVariablesSetAsNotUserEditableWhenAdminFlagIsNotPassed() { - $this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn($this->variables); + $variables = $this->getVariableCollection(); + $this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn($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); + 'variable_value' => $variables[0]->rules, + ])->once()->andReturnSelf(); + $this->validator->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(); + $response = $this->getService()->handle(1, [ + $variables[0]->env_variable => 'Test_SomeValue_0', + $variables[1]->env_variable => 'Test_SomeValue_1', + $variables[2]->env_variable => 'Test_SomeValue_2', + $variables[3]->env_variable => 'Test_SomeValue_3', + ]); - $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->assertNotEmpty($response); + $this->assertInstanceOf(Collection::class, $response); + $this->assertEquals(1, $response->count(), 'Assert response has a single item in collection.'); - $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']); + $variable = $response->first(); + $this->assertObjectHasAttribute('id', $variable); + $this->assertObjectHasAttribute('key', $variable); + $this->assertObjectHasAttribute('value', $variable); + $this->assertSame($variables[0]->id, $variable->id); + $this->assertSame($variables[0]->env_variable, $variable->key); + $this->assertSame('Test_SomeValue_0', $variable->value); } /** @@ -161,36 +109,39 @@ class VariableValidatorServiceTest extends TestCase */ public function testValidatorShouldProcessAllVariablesWhenAdminFlagIsSet() { - $this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn($this->variables); + $variables = $this->getVariableCollection(); + $this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn($variables); - foreach ($this->variables as $key => $variable) { + foreach ($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); + 'variable_value' => $variables[$key]->rules, + ])->once()->andReturnSelf(); + $this->validator->shouldReceive('fails')->withNoArgs()->once()->andReturn(false); } - $response = $this->service->isAdmin()->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(); + $service = $this->getService(); + $service->setUserLevel(User::USER_LEVEL_ADMIN); + $response = $service->handle(1, [ + $variables[0]->env_variable => 'Test_SomeValue_0', + $variables[1]->env_variable => 'Test_SomeValue_1', + $variables[2]->env_variable => 'Test_SomeValue_2', + $variables[3]->env_variable => 'Test_SomeValue_3', + ]); - $this->assertEquals(4, count($response), 'Assert response has all four items in array.'); + $this->assertNotEmpty($response); + $this->assertInstanceOf(Collection::class, $response); + $this->assertEquals(4, $response->count(), 'Assert response has all four items in collection.'); - 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']); - } + $response->each(function ($variable, $key) use ($variables) { + $this->assertObjectHasAttribute('id', $variable); + $this->assertObjectHasAttribute('key', $variable); + $this->assertObjectHasAttribute('value', $variable); + $this->assertSame($variables[$key]->id, $variable->id); + $this->assertSame($variables[$key]->env_variable, $variable->key); + $this->assertSame('Test_SomeValue_' . $key, $variable->value); + }); } /** @@ -198,31 +149,63 @@ class VariableValidatorServiceTest extends TestCase */ public function testValidatorShouldThrowExceptionWhenAValidationErrorIsEncountered() { - $this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn($this->variables); + $variables = $this->getVariableCollection(); + $this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn($variables); $this->validator->shouldReceive('make')->with([ 'variable_value' => null, ], [ - 'variable_value' => $this->variables[0]->rules, - ])->once()->andReturnSelf() - ->shouldReceive('fails')->withNoArgs()->once()->andReturn(true); + 'variable_value' => $variables[0]->rules, + ])->once()->andReturnSelf(); + $this->validator->shouldReceive('fails')->withNoArgs()->once()->andReturn(true); - $this->validator->shouldReceive('errors')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('toArray')->withNoArgs()->once()->andReturn([]); + $this->validator->shouldReceive('errors')->withNoArgs()->once()->andReturnSelf(); + $this->validator->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->getService()->handle(1, [$variables[0]->env_variable => null]); + } catch (PterodactylException $exception) { + $this->assertInstanceOf(DisplayValidationException::class, $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]), + trans('admin/server.exceptions.bad_variable', ['name' => $variables[0]->name]), $decoded->notice[0] ); } } + + /** + * Return a collection of fake variables to use for testing. + * + * @return \Illuminate\Support\Collection + */ + private function getVariableCollection(): Collection + { + return collect( + [ + factory(EggVariable::class)->states('editable', 'viewable')->make(), + factory(EggVariable::class)->states('viewable')->make(), + factory(EggVariable::class)->states('editable')->make(), + factory(EggVariable::class)->make(), + ] + ); + } + + /** + * Return an instance of the service with mocked dependencies. + * + * @return \Pterodactyl\Services\Servers\VariableValidatorService + */ + private function getService(): VariableValidatorService + { + return new VariableValidatorService( + $this->optionVariableRepository, + $this->serverRepository, + $this->serverVariableRepository, + $this->validator + ); + } } From 7d5e75c56ad7d812bb01281cf2b25ca0a346acea Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 27 Oct 2017 00:05:26 -0500 Subject: [PATCH 10/11] Changelog updates [skip ci] [ci skip] --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67b19c3a2..9d26b3b73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. * You can now require 2FA for all users, admins only, or at will using a simple configuration in the Admin CP. * Added ability to export and import service options and their associated settings and environment variables via the Admin CP. * Default allocation for a server can be changed on the front-end by users. This includes two new subuser permissions as well. +* Significant improvements to environment variable control for servers. Now ships with built-in abilities to define extra variables in the Panel's configuration file, or in-code for those heavily modifying the Panel. +* Quick link to server edit view in ACP on frontend when viewing servers. ### Changed * Theme colors and login pages updated to give a more unique feel to the project. @@ -22,6 +24,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. * Logout icon is now more universal and not just a power icon. * Administrative logout notice now uses SWAL rather than a generic javascript popup. * Server creation page now only asks for a node to deploy to, rather than requiring a location and then a node. +* Database passwords are now hidden by default and will only show if clicked on. In addition, database view in ACP now indicates that passwords must be viewed on the front-end. ### Fixed * Unable to change the daemon secret for a server via the Admin CP. @@ -29,6 +32,10 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. * Fixes a design-flaw in the allocation management part of nodes that would run a MySQL query for each port being allocated. This behavior is now changed to only execute one query to add multiple ports at once. * Attempting to create a server when no nodes are configured now redirects to the node creation page. * Fixes missing library issue for teamspeak when used with mariadb. +* Fixes inability to change the default port on front-end when viewing a server. + +### Removed +* SFTP settings page now only displays connection address and username. Password setting was removed as it is no longer necessary with Daemon changes. ## v0.6.4 (Courageous Carniadactylus) ### Fixed From 25b2093c38a538dbb76525e43df14afab2bb9a0b Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 27 Oct 2017 00:16:00 -0500 Subject: [PATCH 11/11] =?UTF-8?q?More=20changelog.=20=F0=9F=A5=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [ci skip] [skip ci] --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d26b3b73..e44b65b9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,12 +12,14 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. * New CLI command to disabled 2-Factor Authentication on an account if necessary. * Ability to delete users and locations via the CLI. * You can now require 2FA for all users, admins only, or at will using a simple configuration in the Admin CP. -* Added ability to export and import service options and their associated settings and environment variables via the Admin CP. +* **Added ability to export and import service options and their associated settings and environment variables via the Admin CP.** * Default allocation for a server can be changed on the front-end by users. This includes two new subuser permissions as well. * Significant improvements to environment variable control for servers. Now ships with built-in abilities to define extra variables in the Panel's configuration file, or in-code for those heavily modifying the Panel. * Quick link to server edit view in ACP on frontend when viewing servers. +* Databases created in the Panel now include `EXECUTE` privilege. ### Changed +* **Services renamed to Nests. Service Options renamed to Eggs.** 🥚 * Theme colors and login pages updated to give a more unique feel to the project. * Massive overhaul to the backend code that allows for much easier updating of core functionality as well as support for better testing. This overhaul also reduces complex code logic, and allows for faster response times in the application. * CLI commands updated to be easier to type, now stored in the `p:` namespace. @@ -25,6 +27,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. * Administrative logout notice now uses SWAL rather than a generic javascript popup. * Server creation page now only asks for a node to deploy to, rather than requiring a location and then a node. * Database passwords are now hidden by default and will only show if clicked on. In addition, database view in ACP now indicates that passwords must be viewed on the front-end. +* Localhost cannot be used as a connection address in the environment configuration script. `127.0.0.1` is allowed. ### Fixed * Unable to change the daemon secret for a server via the Admin CP. @@ -33,6 +36,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. * Attempting to create a server when no nodes are configured now redirects to the node creation page. * Fixes missing library issue for teamspeak when used with mariadb. * Fixes inability to change the default port on front-end when viewing a server. +* Fixes bug preventing deletion of nests that have other nests referencing them as children. ### Removed * SFTP settings page now only displays connection address and username. Password setting was removed as it is no longer necessary with Daemon changes.