diff --git a/CHANGELOG.md b/CHANGELOG.md index 990cc2937..e44b65b9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,15 +12,22 @@ 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. * 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. +* 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. @@ -28,6 +35,11 @@ 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. +* 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. ## v0.6.4 (Courageous Carniadactylus) ### Fixed 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/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..9e597bb73 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 @@ -53,14 +54,26 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter public function getVariablesWithValues($id, $returnAsObject = false); /** - * Return enough data to be used for the creation of a server via the daemon. + * 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 $id - * @return \Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model + * @param int|\Pterodactyl\Models\Server $server + * @param bool $refresh + * @return \Pterodactyl\Models\Server * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function getDataForCreation($id); + public function getPrimaryAllocation($server, bool $refresh = false): Server; + + /** + * 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(Server $server, bool $refresh = false): Server; /** * Return a server as well as associated databases and their hosts. 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/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 @@ +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/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..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; @@ -22,12 +23,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 +69,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 +142,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 +164,7 @@ class ServersController extends Controller ContainerRebuildService $containerRebuildService, ServerCreationService $service, DatabaseManagementService $databaseManagementService, + DatabasePasswordService $databasePasswordService, DatabaseRepositoryInterface $databaseRepository, DatabaseHostRepository $databaseHostRepository, ServerDeletionService $deletionService, @@ -173,9 +182,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; @@ -561,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); @@ -609,7 +617,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..06636c4c0 --- /dev/null +++ b/app/Http/Controllers/Server/DatabaseController.php @@ -0,0 +1,77 @@ +passwordService = $passwordService; + $this->repository = $repository; + } + + /** + * Render the database listing 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-databases', $server); + $this->setRequest($request)->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 \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); + + return response()->json(['password' => $password]); + } +} diff --git a/app/Http/Controllers/Server/ServerController.php b/app/Http/Controllers/Server/ServerController.php deleted file mode 100644 index 9b4208319..000000000 --- a/app/Http/Controllers/Server/ServerController.php +++ /dev/null @@ -1,185 +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 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. - * - * @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/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/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/Controllers/Server/Settings/StartupController.php b/app/Http/Controllers/Server/Settings/StartupController.php new file mode 100644 index 000000000..5d299c42e --- /dev/null +++ b/app/Http/Controllers/Server/Settings/StartupController.php @@ -0,0 +1,94 @@ +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->setUserLevel(User::USER_LEVEL_USER); + $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/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/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/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/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/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/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/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/Node.php b/app/Models/Node.php index 368c6f3d8..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. * @@ -144,13 +146,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/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/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/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/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/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..ac023ff17 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} */ @@ -107,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/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/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..95182a288 100644 --- a/app/Services/Database/DatabaseManagementService.php +++ b/app/Services/Databases/DatabaseManagementService.php @@ -7,8 +7,9 @@ * https://opensource.org/licenses/MIT */ -namespace Pterodactyl\Services\Database; +namespace Pterodactyl\Services\Databases; +use Pterodactyl\Models\Database; use Illuminate\Database\DatabaseManager; use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Extensions\DynamicDatabaseConnection; @@ -79,28 +80,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'); + if (isset($database) && $database instanceof Database) { + $this->repository->dropDatabase($database->database); + $this->repository->dropUser($database->username, $database->remote); + $this->repository->flush(); } } catch (\Exception $exTwo) { // ignore an exception @@ -113,62 +112,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/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/ServerAccessHelperService.php b/app/Services/Servers/ServerAccessHelperService.php deleted file mode 100644 index e69de29bb..000000000 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 0994abe55..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,52 +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; - - /** - * @var \Pterodactyl\Services\Servers\UsernameGenerationService - */ - protected $usernameService; + private $userRepository; /** * @var \Pterodactyl\Services\Servers\VariableValidatorService */ - protected $validatorService; + private $validatorService; /** * CreationService constructor. @@ -84,7 +73,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 +84,6 @@ class ServerCreationService ServerRepositoryInterface $repository, ServerVariableRepositoryInterface $serverVariableRepository, UserRepositoryInterface $userRepository, - UsernameGenerationService $usernameService, VariableValidatorService $validatorService ) { $this->allocationRepository = $allocationRepository; @@ -107,7 +94,6 @@ class ServerCreationService $this->repository = $repository; $this->serverVariableRepository = $serverVariableRepository; $this->userRepository = $userRepository; - $this->usernameService = $usernameService; $this->validatorService = $validatorService; } @@ -124,35 +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'], - 'username' => $this->usernameService->generate($data['name'], $uniqueShort), - 'sftp_password' => null, + '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. @@ -164,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/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/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..ae91fb3ba 100644 --- a/app/Services/Servers/StartupModificationService.php +++ b/app/Services/Servers/StartupModificationService.php @@ -1,58 +1,50 @@ . - * - * 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\Exceptions\DisplayException; +use Pterodactyl\Traits\Services\HasUserLevels; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; 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,91 +72,81 @@ 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); + $this->connection->beginTransaction(); + if (! is_null(array_get($data, 'environment'))) { + $this->validatorService->setUserLevel($this->getUserLevel()); + $results = $this->validatorService->handle(array_get($data, 'egg_id', $server->egg_id), array_get($data, 'environment', [])); + + $results->each(function ($result) use ($server) { + $this->serverVariableRepository->withoutFresh()->updateOrCreate([ + 'server_id' => $server->id, + 'variable_id' => $result->id, + ], [ + 'variable_value' => $result->value, + ]); + }); } + $daemonData = ['build' => [ + 'env|overwrite' => $this->environmentService->handle($server), + ]]; + + if ($this->isUserLevel(User::USER_LEVEL_ADMIN)) { + $this->updateAdministrativeSettings($data, $server, $daemonData); + } + + try { + $this->daemonServerRepository->setNode($server->node_id)->setAccessServer($server->uuid)->update($daemonData); + } catch (RequestException $exception) { + $this->connection->rollBack(); + throw new DaemonConnectionException($exception); + } + + $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) ) { - $hasServiceChanges = true; - } - - $this->connection->beginTransaction(); - if (isset($data['environment'])) { - $validator = $this->validatorService->isAdmin($this->admin) - ->setFields($data['environment']) - ->validate(array_get($data, 'egg_id', $server->egg_id)); - - foreach ($validator->getResults() as $result) { - $this->serverVariableRepository->withoutFresh()->updateOrCreate([ - 'server_id' => $server->id, - 'variable_id' => $result['id'], - ], [ - 'variable_value' => $result['value'], - ]); - } - } - - $daemonData = [ - 'build' => [ - 'env|overwrite' => $this->environmentService->process($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'])] - ); - } - } - - 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'); + $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/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/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/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 5063a50f2..c6efc86ac 100644 --- a/app/Traits/Controllers/JavascriptInjection.php +++ b/app/Traits/Controllers/JavascriptInjection.php @@ -10,32 +10,46 @@ namespace Pterodactyl\Traits\Controllers; use Javascript; +use Illuminate\Http\Request; trait JavascriptInjection { /** - * @var \Illuminate\Contracts\Session\Session + * @var \Illuminate\Http\Request */ - protected $session; + 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 - * @return mixed + * @return array */ public function injectJavascript($args = [], $overwrite = false) { - $server = $this->session->get('server_data.model'); - $token = $this->session->get('server_data.token'); + $request = $this->request ?? app()->make(Request::class); + $server = $request->attributes->get('server'); + $token = $request->attributes->get('server_token'); $response = array_merge([ 'server' => [ 'uuid' => $server->uuid, 'uuidShort' => $server->uuidShort, 'daemonSecret' => $token, - 'username' => $server->username, ], 'node' => [ 'fqdn' => $server->node->fqdn, 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 f3e4f4093..54ba984be 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -29,9 +29,10 @@ $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, - 'username' => $faker->userName, - 'sftp_password' => null, 'installed' => 1, 'created_at' => \Carbon\Carbon::now(), 'updated_at' => \Carbon\Carbon::now(), @@ -39,6 +40,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 +50,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 +176,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/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/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/navigation.php b/resources/lang/en/navigation.php index 5693d825d..b8f2fb83a 100644 --- a/resources/lang/en/navigation.php +++ b/resources/lang/en/navigation.php @@ -20,11 +20,12 @@ 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', 'edit_file' => 'Edit File', - 'admin' => 'Manage', + 'admin_header' => 'ADMINISTRATIVE', + 'admin' => 'Server Configuration', ], ]; diff --git a/resources/lang/en/server.php b/resources/lang/en/server.php index 0aa414fd2..3aa27e5b2 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', @@ -288,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', @@ -297,12 +301,12 @@ 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', '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/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/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/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/layouts/master.blade.php b/resources/themes/pterodactyl/layouts/master.blade.php index 367d29959..62bb8c90d 100644 --- a/resources/themes/pterodactyl/layouts/master.blade.php +++ b/resources/themes/pterodactyl/layouts/master.blade.php @@ -155,7 +155,18 @@ @endcan - @if(Gate::allows('view-startup', $server) || Gate::allows('view-sftp', $server) || Gate::allows('view-databases', $server) || Gate::allows('view-allocation', $server)) + @can('view-databases', $server) +
  • + + @lang('navigation.server.databases') + +
  • + @endcan + @if(Gate::allows('view-startup', $server) || Gate::allows('view-sftp', $server) || Gate::allows('view-allocation', $server))
  • @lang('navigation.server.startup_parameters')
  • @endcan - @can('view-databases', $server) -
  • @lang('navigation.server.databases')
  • - @endcan @endif @if(Auth::user()->root_admin) +
  • @lang('navigation.server.admin_header')
  • - + @lang('navigation.server.admin')
  • 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/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
    @@ -79,37 +82,39 @@ @parent {!! Theme::js('js/frontend/server.socket.js') !!} @endsection 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/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/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/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 c0d04dd39..f6333a20d 100644 --- a/routes/server.php +++ b/routes/server.php @@ -18,13 +18,27 @@ 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'); + Route::get('/allocation', 'Settings\AllocationController@index')->name('server.settings.allocation'); + Route::patch('/allocation', 'Settings\AllocationController@update'); - Route::post('/sftp', 'ServerController@postSettingsSFTP'); - Route::post('/startup', 'ServerController@postSettingsStartup'); + Route::get('/sftp', 'Settings\SftpController@index')->name('server.settings.sftp'); + + Route::get('/startup', 'Settings\StartupController@index')->name('server.settings.startup'); + Route::patch('/startup', 'Settings\StartupController@update'); +}); + +/* +|-------------------------------------------------------------------------- +| 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'); }); /* @@ -56,13 +70,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 +90,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/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/Http/Controllers/Admin/DatabaseControllerTest.php b/tests/Unit/Http/Controllers/Admin/DatabaseControllerTest.php index 3e98fad4c..62b66d0bb 100644 --- a/tests/Unit/Http/Controllers/Admin/DatabaseControllerTest.php +++ b/tests/Unit/Http/Controllers/Admin/DatabaseControllerTest.php @@ -13,8 +13,10 @@ 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\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; @@ -23,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\Database\DatabaseHostService + * @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface|\Mockery\Mock */ - protected $service; + private $repository; + + /** + * @var \Pterodactyl\Services\Databases\Hosts\HostUpdateService|\Mockery\Mock + */ + private $updateService; /** * Setup tests. @@ -55,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(DatabaseHostService::class); - - $this->controller = new DatabaseController( - $this->alert, - $this->repository, - $this->service, - $this->locationRepository - ); + $this->updateService = m::mock(HostUpdateService::class); } /** @@ -75,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); @@ -93,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); @@ -102,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/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/Database/DatabaseHostServiceTest.php b/tests/Unit/Services/Database/DatabaseHostServiceTest.php deleted file mode 100644 index bf8b5dee7..000000000 --- a/tests/Unit/Services/Database/DatabaseHostServiceTest.php +++ /dev/null @@ -1,202 +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\Services\Database\DatabaseHostService; -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\Database\DatabaseHostService - */ - 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 DatabaseHostService( - $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 c679ffa12..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\Database\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\Database\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..54d46b950 --- /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(bool $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..bd927b8e8 --- /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(int $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 + ); + } +} 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 new file mode 100644 index 000000000..ceca81758 --- /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('handle')->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); + } +} diff --git a/tests/Unit/Services/Servers/ServerCreationServiceTest.php b/tests/Unit/Services/Servers/ServerCreationServiceTest.php index da2e33af2..c430cc22b 100644 --- a/tests/Unit/Services/Servers/ServerCreationServiceTest.php +++ b/tests/Unit/Services/Servers/ServerCreationServiceTest.php @@ -1,24 +1,18 @@ . - * - * 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; 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; @@ -33,91 +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; - - /** - * @var \Pterodactyl\Services\Servers\UsernameGenerationService|\Mockery\Mock - */ - protected $usernameService; + private $userRepository; /** * @var \Pterodactyl\Services\Servers\VariableValidatorService|\Mockery\Mock */ - protected $validatorService; + private $validatorService; /** * Setup tests. @@ -135,13 +95,88 @@ 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') - ->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, @@ -150,84 +185,7 @@ class ServerCreationServiceTest extends TestCase $this->repository, $this->serverVariableRepository, $this->userRepository, - $this->usernameService, $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->usernameService->shouldReceive('generate')->with($this->data['name'], 'random_string') - ->once()->andReturn('user_name'); - - $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->usernameService->shouldReceive('generate')->once()->andReturn('user_name'); - $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/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; 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/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/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 + ); + } } 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); + } +}