diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c69c877..947098b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,24 @@ This file is a running track of new features and fixes to each version of the pa This project follows [Semantic Versioning](http://semver.org) guidelines. +## v0.7.4 (Derelict Dermodactylus) +### Fixed +* Fixes a bug when reinstalling a server that would not mark the server as installing, resulting in some UI issues. +* Handle 404 errors from missing models in the application API bindings correctly. +* Fix validation error returned when no environment variables are passed, even if there are no variables required. +* Fix improper permissions on `PATCH /api/servers//startup` endpoint which was preventing enditing any start variables. +* Should fix migration issues from 0.6 when there are more than API key in the database. + +### Changed +* Changes order that validation of resource existence occurs in API requests to not try and use a non-existent model when validating data. + +### Added +* Adds back client API for sending commands or power toggles to a server though the Panel API: `/api/client/servers/` +* Added proper transformer for Packs and re-enabled missing includes on server. +* Added support for using Filesystem as a caching driver, although not recommended. +* Added support for user management of server databases. +* **Added bulk power management CLI interface to send start, stop, kill, restart actions to servers across configurable nodes.** + ## v0.7.3 (Derelict Dermodactylus) ### Fixed * Fixes server creation API endpoint not passing the provided `external_id` to the creation service. diff --git a/app/Console/Commands/Environment/AppSettingsCommand.php b/app/Console/Commands/Environment/AppSettingsCommand.php index ecf171ad..17c2b5bb 100644 --- a/app/Console/Commands/Environment/AppSettingsCommand.php +++ b/app/Console/Commands/Environment/AppSettingsCommand.php @@ -22,6 +22,7 @@ class AppSettingsCommand extends Command const ALLOWED_CACHE_DRIVERS = [ 'redis' => 'Redis (recommended)', 'memcached' => 'Memcached', + 'file' => 'Filesystem', ]; const ALLOWED_SESSION_DRIVERS = [ diff --git a/app/Console/Commands/Server/BulkPowerActionCommand.php b/app/Console/Commands/Server/BulkPowerActionCommand.php new file mode 100644 index 00000000..ced90db8 --- /dev/null +++ b/app/Console/Commands/Server/BulkPowerActionCommand.php @@ -0,0 +1,121 @@ +powerRepository = $powerRepository; + $this->repository = $repository; + $this->validator = $validator; + } + + /** + * Handle the bulk power request. + * + * @throws \Illuminate\Validation\ValidationException + * @throws \Pterodactyl\Exceptions\Repository\Daemon\InvalidPowerSignalException + */ + public function handle() + { + $action = $this->argument('action'); + $nodes = empty($this->option('nodes')) ? [] : explode(',', $this->option('nodes')); + $servers = empty($this->option('servers')) ? [] : explode(',', $this->option('servers')); + + $validator = $this->validator->make([ + 'action' => $action, + 'nodes' => $nodes, + 'servers' => $servers, + ], [ + 'action' => 'string|in:start,stop,kill,restart', + 'nodes' => 'array', + 'nodes.*' => 'integer|min:1', + 'servers' => 'array', + 'servers.*' => 'integer|min:1', + ]); + + if ($validator->fails()) { + foreach ($validator->getMessageBag()->all() as $message) { + $this->output->error($message); + } + + throw new ValidationException($validator); + } + + $count = $this->repository->getServersForPowerActionCount($servers, $nodes); + if (! $this->confirm(trans('command/messages.server.power.confirm', ['action' => $action, 'count' => $count]))) { + return; + } + + $bar = $this->output->createProgressBar($count); + $servers = $this->repository->getServersForPowerAction($servers, $nodes); + + foreach ($servers as $server) { + $bar->clear(); + + try { + $this->powerRepository->setServer($server)->sendSignal($action); + } catch (RequestException $exception) { + $this->output->error(trans('command/messages.server.power.action_failed', [ + 'name' => $server->name, + 'id' => $server->id, + 'node' => $server->node->name, + 'message' => $exception->getMessage(), + ])); + } + + $bar->advance(); + $bar->display(); + } + + $this->line(''); + } +} diff --git a/app/Contracts/Repository/ServerRepositoryInterface.php b/app/Contracts/Repository/ServerRepositoryInterface.php index 0ca74bf4..983cf7e6 100644 --- a/app/Contracts/Repository/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/ServerRepositoryInterface.php @@ -117,4 +117,23 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function getByUuid(string $uuid): Server; + + /** + * Return all of the servers that should have a power action performed aganist them. + * + * @param int[] $servers + * @param int[] $nodes + * @param bool $returnCount + * @return int|\Generator + */ + public function getServersForPowerAction(array $servers = [], array $nodes = [], bool $returnCount = false); + + /** + * Return the total number of servers that will be affected by the query. + * + * @param int[] $servers + * @param int[] $nodes + * @return int + */ + public function getServersForPowerActionCount(array $servers = [], array $nodes = []): int; } diff --git a/app/Exceptions/Service/Database/DatabaseClientFeatureNotEnabledException.php b/app/Exceptions/Service/Database/DatabaseClientFeatureNotEnabledException.php new file mode 100644 index 00000000..809cb4fb --- /dev/null +++ b/app/Exceptions/Service/Database/DatabaseClientFeatureNotEnabledException.php @@ -0,0 +1,13 @@ +cache->tags(['Node:Configuration'])->put($token, $node->id, 5); + $this->cache->put('Node:Configuration:' . $token, $node->id, 5); return response()->json(['token' => $token]); } diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index 6b9d44cb..8adff82d 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -498,15 +498,17 @@ class ServersController extends Controller * @param \Illuminate\Http\Request $request * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\RedirectResponse + * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @internal param int $id + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function updateBuild(Request $request, Server $server) { $this->buildModificationService->handle($server, $request->only([ 'allocation_id', 'add_allocations', 'remove_allocations', 'memory', 'swap', 'io', 'cpu', 'disk', + 'database_limit', 'allocation_limit', ])); $this->alert->success(trans('admin/server.alerts.build_updated'))->flash(); diff --git a/app/Http/Controllers/Api/Application/ApplicationApiController.php b/app/Http/Controllers/Api/Application/ApplicationApiController.php index c932c364..bdd5f9e7 100644 --- a/app/Http/Controllers/Api/Application/ApplicationApiController.php +++ b/app/Http/Controllers/Api/Application/ApplicationApiController.php @@ -3,17 +3,19 @@ namespace Pterodactyl\Http\Controllers\Api\Application; use Illuminate\Http\Request; +use Webmozart\Assert\Assert; use Illuminate\Http\Response; use Illuminate\Container\Container; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Extensions\Spatie\Fractalistic\Fractal; +use Pterodactyl\Transformers\Api\Application\BaseTransformer; abstract class ApplicationApiController extends Controller { /** * @var \Illuminate\Http\Request */ - private $request; + protected $request; /** * @var \Pterodactyl\Extensions\Spatie\Fractalistic\Fractal @@ -61,6 +63,8 @@ abstract class ApplicationApiController extends Controller $transformer = Container::getInstance()->make($abstract); $transformer->setKey($this->request->attributes->get('api_key')); + Assert::isInstanceOf($transformer, BaseTransformer::class); + return $transformer; } diff --git a/app/Http/Controllers/Api/Application/Servers/StartupController.php b/app/Http/Controllers/Api/Application/Servers/StartupController.php index e6b8015d..0265af46 100644 --- a/app/Http/Controllers/Api/Application/Servers/StartupController.php +++ b/app/Http/Controllers/Api/Application/Servers/StartupController.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Http\Controllers\Api\Application\Servers; +use Pterodactyl\Models\User; use Pterodactyl\Models\Server; use Pterodactyl\Services\Servers\StartupModificationService; use Pterodactyl\Transformers\Api\Application\ServerTransformer; @@ -40,7 +41,9 @@ class StartupController extends ApplicationApiController */ public function index(UpdateServerStartupRequest $request): array { - $server = $this->modificationService->handle($request->getModel(Server::class), $request->validated()); + $server = $this->modificationService + ->setUserLevel(User::USER_LEVEL_ADMIN) + ->handle($request->getModel(Server::class), $request->validated()); return $this->fractal->item($server) ->transformWith($this->getTransformer(ServerTransformer::class)) diff --git a/app/Http/Controllers/Api/Client/ClientApiController.php b/app/Http/Controllers/Api/Client/ClientApiController.php new file mode 100644 index 00000000..e2d4b3f8 --- /dev/null +++ b/app/Http/Controllers/Api/Client/ClientApiController.php @@ -0,0 +1,29 @@ +make($abstract); + Assert::isInstanceOf($transformer, BaseClientTransformer::class); + + $transformer->setKey($this->request->attributes->get('api_key')); + $transformer->setUser($this->request->user()); + + return $transformer; + } +} diff --git a/app/Http/Controllers/Api/Client/ClientController.php b/app/Http/Controllers/Api/Client/ClientController.php new file mode 100644 index 00000000..d2e1f33a --- /dev/null +++ b/app/Http/Controllers/Api/Client/ClientController.php @@ -0,0 +1,44 @@ +repository = $repository; + } + + /** + * Return all of the servers available to the client making the API + * request, including servers the user has access to as a subuser. + * + * @param \Pterodactyl\Http\Requests\Api\Client\GetServersRequest $request + * @return array + */ + public function index(GetServersRequest $request): array + { + $servers = $this->repository->filterUserAccessServers($request->user(), User::FILTER_LEVEL_SUBUSER); + + return $this->fractal->collection($servers) + ->transformWith($this->getTransformer(ServerTransformer::class)) + ->toArray(); + } +} diff --git a/app/Http/Controllers/Api/Client/Servers/CommandController.php b/app/Http/Controllers/Api/Client/Servers/CommandController.php new file mode 100644 index 00000000..8a5b951f --- /dev/null +++ b/app/Http/Controllers/Api/Client/Servers/CommandController.php @@ -0,0 +1,74 @@ +keyProviderService = $keyProviderService; + $this->repository = $repository; + } + + /** + * Send a command to a running server. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\SendCommandRequest $request + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + */ + public function index(SendCommandRequest $request): Response + { + $server = $request->getModel(Server::class); + $token = $this->keyProviderService->handle($server, $request->user()); + + try { + $this->repository->setServer($server) + ->setToken($token) + ->send($request->input('command')); + } catch (RequestException $exception) { + if ($exception instanceof ClientException) { + if ($exception->getResponse() instanceof ResponseInterface && $exception->getResponse()->getStatusCode() === 412) { + throw new PreconditionFailedHttpException('Server is not online.'); + } + } + + throw new DaemonConnectionException($exception); + } + + return $this->returnNoContent(); + } +} diff --git a/app/Http/Controllers/Api/Client/Servers/PowerController.php b/app/Http/Controllers/Api/Client/Servers/PowerController.php new file mode 100644 index 00000000..113b8339 --- /dev/null +++ b/app/Http/Controllers/Api/Client/Servers/PowerController.php @@ -0,0 +1,57 @@ +keyProviderService = $keyProviderService; + $this->repository = $repository; + } + + /** + * Send a power action to a server. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\SendPowerRequest $request + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Repository\Daemon\InvalidPowerSignalException + */ + public function index(SendPowerRequest $request): Response + { + $server = $request->getModel(Server::class); + $token = $this->keyProviderService->handle($server, $request->user()); + + $this->repository->setServer($server)->setToken($token)->sendSignal($request->input('signal')); + + return $this->returnNoContent(); + } +} diff --git a/app/Http/Controllers/Api/Client/Servers/ServerController.php b/app/Http/Controllers/Api/Client/Servers/ServerController.php new file mode 100644 index 00000000..ce4502e3 --- /dev/null +++ b/app/Http/Controllers/Api/Client/Servers/ServerController.php @@ -0,0 +1,25 @@ +fractal->item($request->getModel(Server::class)) + ->transformWith($this->getTransformer(ServerTransformer::class)) + ->toArray(); + } +} diff --git a/app/Http/Controllers/Api/Remote/FileDownloadController.php b/app/Http/Controllers/Api/Remote/FileDownloadController.php new file mode 100644 index 00000000..fa4818fc --- /dev/null +++ b/app/Http/Controllers/Api/Remote/FileDownloadController.php @@ -0,0 +1,50 @@ +cache = $cache; + } + + /** + * Handle a request to authenticate a download using a token and return + * the path of the file to the daemon. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + * + * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException + */ + public function index(Request $request): JsonResponse + { + $download = $this->cache->pull('Server:Downloads:' . $request->input('token', '')); + + if (is_null($download)) { + throw new NotFoundHttpException('No file was found using the token provided.'); + } + + return response()->json([ + 'path' => array_get($download, 'path'), + 'server' => array_get($download, 'server'), + ]); + } +} diff --git a/app/Http/Controllers/Base/ClientApiController.php b/app/Http/Controllers/Base/ClientApiController.php new file mode 100644 index 00000000..a74c28db --- /dev/null +++ b/app/Http/Controllers/Base/ClientApiController.php @@ -0,0 +1,109 @@ +alert = $alert; + $this->creationService = $creationService; + $this->repository = $repository; + } + + /** + * Return all of the API keys available to this user. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\View\View + */ + public function index(Request $request): View + { + return view('base.api.index', [ + 'keys' => $this->repository->getAccountKeys($request->user()), + ]); + } + + /** + * Render UI to allow creation of an API key. + * + * @return \Illuminate\View\View + */ + public function create(): View + { + return view('base.api.new'); + } + + /** + * Create the API key and return the user to the key listing page. + * + * @param \Pterodactyl\Http\Requests\Base\CreateClientApiKeyRequest $request + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function store(CreateClientApiKeyRequest $request): RedirectResponse + { + $allowedIps = null; + if (! is_null($request->input('allowed_ips'))) { + $allowedIps = json_encode(explode(PHP_EOL, $request->input('allowed_ips'))); + } + + $this->creationService->setKeyType(ApiKey::TYPE_ACCOUNT)->handle([ + 'memo' => $request->input('memo'), + 'allowed_ips' => $allowedIps, + 'user_id' => $request->user()->id, + ]); + + $this->alert->success('A new client API key has been generated for your account.')->flash(); + + return redirect()->route('account.api'); + } + + /** + * Delete a client's API key from the panel. + * + * @param \Illuminate\Http\Request $request + * @param $identifier + * @return \Illuminate\Http\Response + */ + public function delete(Request $request, $identifier): Response + { + $this->repository->deleteAccountKey($request->user(), $identifier); + + return response('', 204); + } +} diff --git a/app/Http/Controllers/Daemon/ActionController.php b/app/Http/Controllers/Daemon/ActionController.php index 1c67ec6c..fef0b35b 100644 --- a/app/Http/Controllers/Daemon/ActionController.php +++ b/app/Http/Controllers/Daemon/ActionController.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Http\Controllers\Daemon; @@ -17,28 +10,6 @@ use Pterodactyl\Http\Controllers\Controller; class ActionController extends Controller { - /** - * Handles download request from daemon. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\JsonResponse - */ - public function authenticateDownload(Request $request) - { - $download = Cache::tags(['Server:Downloads'])->pull($request->input('token')); - - if (is_null($download)) { - return response()->json([ - 'error' => 'An invalid request token was recieved with this request.', - ], 403); - } - - return response()->json([ - 'path' => $download['path'], - 'server' => $download['server'], - ]); - } - /** * Handles install toggle request from daemon. * @@ -78,7 +49,7 @@ class ActionController extends Controller */ public function configuration(Request $request, $token) { - $nodeId = Cache::tags(['Node:Configuration'])->pull($token); + $nodeId = Cache::pull('Node:Configuration:' . $token); if (is_null($nodeId)) { return response()->json(['error' => 'token_invalid'], 403); } diff --git a/app/Http/Controllers/Server/DatabaseController.php b/app/Http/Controllers/Server/DatabaseController.php index 06636c4c..be7d501b 100644 --- a/app/Http/Controllers/Server/DatabaseController.php +++ b/app/Http/Controllers/Server/DatabaseController.php @@ -4,34 +4,76 @@ namespace Pterodactyl\Http\Controllers\Server; use Illuminate\View\View; use Illuminate\Http\Request; +use Illuminate\Http\Response; use Illuminate\Http\JsonResponse; +use Illuminate\Http\RedirectResponse; +use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Traits\Controllers\JavascriptInjection; use Pterodactyl\Services\Databases\DatabasePasswordService; +use Pterodactyl\Services\Databases\DatabaseManagementService; +use Pterodactyl\Services\Databases\DeployServerDatabaseService; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; +use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; +use Pterodactyl\Http\Requests\Server\Database\StoreServerDatabaseRequest; +use Pterodactyl\Http\Requests\Server\Database\DeleteServerDatabaseRequest; class DatabaseController extends Controller { use JavascriptInjection; + /** + * @var \Prologue\Alerts\AlertsMessageBag + */ + private $alert; + + /** + * @var \Pterodactyl\Services\Databases\DeployServerDatabaseService + */ + private $deployServerDatabaseService; + + /** + * @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface + */ + private $databaseHostRepository; + + /** + * @var \Pterodactyl\Services\Databases\DatabaseManagementService + */ + private $managementService; + /** * @var \Pterodactyl\Services\Databases\DatabasePasswordService */ - protected $passwordService; + private $passwordService; /** * @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface */ - protected $repository; + private $repository; /** * DatabaseController constructor. * - * @param \Pterodactyl\Services\Databases\DatabasePasswordService $passwordService - * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Pterodactyl\Services\Databases\DeployServerDatabaseService $deployServerDatabaseService + * @param \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface $databaseHostRepository + * @param \Pterodactyl\Services\Databases\DatabaseManagementService $managementService + * @param \Pterodactyl\Services\Databases\DatabasePasswordService $passwordService + * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository */ - public function __construct(DatabasePasswordService $passwordService, DatabaseRepositoryInterface $repository) - { + public function __construct( + AlertsMessageBag $alert, + DeployServerDatabaseService $deployServerDatabaseService, + DatabaseHostRepositoryInterface $databaseHostRepository, + DatabaseManagementService $managementService, + DatabasePasswordService $passwordService, + DatabaseRepositoryInterface $repository + ) { + $this->alert = $alert; + $this->databaseHostRepository = $databaseHostRepository; + $this->deployServerDatabaseService = $deployServerDatabaseService; + $this->managementService = $managementService; $this->passwordService = $passwordService; $this->repository = $repository; } @@ -50,11 +92,42 @@ class DatabaseController extends Controller $this->authorize('view-databases', $server); $this->setRequest($request)->injectJavascript(); + $canCreateDatabase = config('pterodactyl.client_features.databases.enabled'); + $allowRandom = config('pterodactyl.client_features.databases.allow_random'); + + if ($this->databaseHostRepository->findCountWhere([['node_id', '=', $server->node_id]]) === 0) { + if ($canCreateDatabase && ! $allowRandom) { + $canCreateDatabase = false; + } + } + + $databases = $this->repository->getDatabasesForServer($server->id); + return view('server.databases.index', [ - 'databases' => $this->repository->getDatabasesForServer($server->id), + 'allowCreation' => $canCreateDatabase, + 'overLimit' => ! is_null($server->database_limit) && count($databases) >= $server->database_limit, + 'databases' => $databases, ]); } + /** + * Handle a request from a user to create a new database for the server. + * + * @param \Pterodactyl\Http\Requests\Server\Database\StoreServerDatabaseRequest $request + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Exception + * @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException + */ + public function store(StoreServerDatabaseRequest $request): RedirectResponse + { + $this->deployServerDatabaseService->handle($request->getServer(), $request->validated()); + + $this->alert->success('Successfully created a new database.')->flash(); + + return redirect()->route('server.databases.index', $request->getServer()->uuidShort); + } + /** * Handle a request to update the password for a specific database. * @@ -74,4 +147,19 @@ class DatabaseController extends Controller return response()->json(['password' => $password]); } + + /** + * Delete a database for this server from the SQL server and Panel database. + * + * @param \Pterodactyl\Http\Requests\Server\Database\DeleteServerDatabaseRequest $request + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function delete(DeleteServerDatabaseRequest $request): Response + { + $this->managementService->delete($request->attributes->get('database')->id); + + return response('', Response::HTTP_NO_CONTENT); + } } diff --git a/app/Http/Controllers/Server/Files/DownloadController.php b/app/Http/Controllers/Server/Files/DownloadController.php index 79155a63..04b16d08 100644 --- a/app/Http/Controllers/Server/Files/DownloadController.php +++ b/app/Http/Controllers/Server/Files/DownloadController.php @@ -9,6 +9,7 @@ namespace Pterodactyl\Http\Controllers\Server\Files; +use Ramsey\Uuid\Uuid; use Illuminate\Http\Request; use Illuminate\Cache\Repository; use Illuminate\Http\RedirectResponse; @@ -46,9 +47,10 @@ class DownloadController extends Controller $server = $request->attributes->get('server'); $this->authorize('download-files', $server); - $token = str_random(40); + $token = Uuid::uuid4()->toString(); $node = $server->getRelation('node'); - $this->cache->tags(['Server:Downloads'])->put($token, ['server' => $server->uuid, 'path' => $file], 5); + + $this->cache->put('Server:Downloads:' . $token, ['server' => $server->uuid, 'path' => $file], 5); return redirect(sprintf('%s://%s:%s/v1/server/file/download/%s', $node->scheme, $node->fqdn, $node->daemonListen, $token)); } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 1d33e210..b6d44530 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Http; +use Pterodactyl\Models\ApiKey; use Illuminate\Auth\Middleware\Authorize; use Illuminate\Auth\Middleware\Authenticate; use Pterodactyl\Http\Middleware\TrimStrings; @@ -14,11 +15,14 @@ use Pterodactyl\Http\Middleware\AdminAuthenticate; use Illuminate\Routing\Middleware\ThrottleRequests; use Pterodactyl\Http\Middleware\LanguageMiddleware; use Illuminate\Foundation\Http\Kernel as HttpKernel; +use Pterodactyl\Http\Middleware\Api\AuthenticateKey; use Illuminate\Routing\Middleware\SubstituteBindings; use Pterodactyl\Http\Middleware\AccessingValidServer; +use Pterodactyl\Http\Middleware\Api\SetSessionDriver; use Illuminate\View\Middleware\ShareErrorsFromSession; use Pterodactyl\Http\Middleware\RedirectIfAuthenticated; use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth; +use Pterodactyl\Http\Middleware\Api\AuthenticateIPAccess; use Pterodactyl\Http\Middleware\Api\ApiSubstituteBindings; use Illuminate\Foundation\Http\Middleware\ValidatePostSize; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; @@ -28,12 +32,10 @@ use Pterodactyl\Http\Middleware\Server\SubuserBelongsToServer; use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication; use Pterodactyl\Http\Middleware\Server\DatabaseBelongsToServer; use Pterodactyl\Http\Middleware\Server\ScheduleBelongsToServer; -use Pterodactyl\Http\Middleware\Api\Application\AuthenticateKey; -use Pterodactyl\Http\Middleware\Api\Application\AuthenticateUser; -use Pterodactyl\Http\Middleware\Api\Application\SetSessionDriver; use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode; use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull; -use Pterodactyl\Http\Middleware\Api\Application\AuthenticateIPAccess; +use Pterodactyl\Http\Middleware\Api\Client\SubstituteClientApiBindings; +use Pterodactyl\Http\Middleware\Api\Application\AuthenticateApplicationUser; use Pterodactyl\Http\Middleware\DaemonAuthenticate as OldDaemonAuthenticate; class Kernel extends HttpKernel @@ -71,8 +73,15 @@ class Kernel extends HttpKernel 'throttle:120,1', ApiSubstituteBindings::class, SetSessionDriver::class, - AuthenticateKey::class, - AuthenticateUser::class, + 'api..key:' . ApiKey::TYPE_APPLICATION, + AuthenticateApplicationUser::class, + AuthenticateIPAccess::class, + ], + 'client-api' => [ + 'throttle:60,1', + SubstituteClientApiBindings::class, + SetSessionDriver::class, + 'api..key:' . ApiKey::TYPE_ACCOUNT, AuthenticateIPAccess::class, ], 'daemon' => [ @@ -107,5 +116,8 @@ class Kernel extends HttpKernel 'server..database' => DatabaseBelongsToServer::class, 'server..subuser' => SubuserBelongsToServer::class, 'server..schedule' => ScheduleBelongsToServer::class, + + // API Specific Middleware + 'api..key' => AuthenticateKey::class, ]; } diff --git a/app/Http/Middleware/Api/ApiSubstituteBindings.php b/app/Http/Middleware/Api/ApiSubstituteBindings.php index b270be4c..94af9b1d 100644 --- a/app/Http/Middleware/Api/ApiSubstituteBindings.php +++ b/app/Http/Middleware/Api/ApiSubstituteBindings.php @@ -32,6 +32,11 @@ class ApiSubstituteBindings extends SubstituteBindings 'user' => User::class, ]; + /** + * @var \Illuminate\Routing\Router + */ + protected $router; + /** * Perform substitution of route parameters without triggering * a 404 error if a model is not found. @@ -45,7 +50,13 @@ class ApiSubstituteBindings extends SubstituteBindings $route = $request->route(); foreach (self::$mappings as $key => $model) { - $this->router->model($key, $model); + if (! is_null($this->router->getBindingCallback($key))) { + continue; + } + + $this->router->model($key, $model, function () use ($request) { + $request->attributes->set('is_missing_model', true); + }); } $this->router->substituteBindings($route); diff --git a/app/Http/Middleware/Api/Application/AuthenticateUser.php b/app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php similarity index 95% rename from app/Http/Middleware/Api/Application/AuthenticateUser.php rename to app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php index 7208dbaf..48da8a74 100644 --- a/app/Http/Middleware/Api/Application/AuthenticateUser.php +++ b/app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php @@ -6,7 +6,7 @@ use Closure; use Illuminate\Http\Request; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; -class AuthenticateUser +class AuthenticateApplicationUser { /** * Authenticate that the currently authenticated user is an administrator diff --git a/app/Http/Middleware/Api/Application/AuthenticateIPAccess.php b/app/Http/Middleware/Api/AuthenticateIPAccess.php similarity index 77% rename from app/Http/Middleware/Api/Application/AuthenticateIPAccess.php rename to app/Http/Middleware/Api/AuthenticateIPAccess.php index 6988c637..aed8f53a 100644 --- a/app/Http/Middleware/Api/Application/AuthenticateIPAccess.php +++ b/app/Http/Middleware/Api/AuthenticateIPAccess.php @@ -1,6 +1,6 @@ ip()); - foreach ($model->allowed_ips as $ip) { + foreach (json_decode($model->allowed_ips) as $ip) { if (Range::parse($ip)->contains($find)) { return $next($request); } } - throw new AccessDeniedHttpException('This IP address does not have permission to access the API using these credentials.'); + throw new AccessDeniedHttpException('This IP address (' . $request->ip() . ') does not have permission to access the API using these credentials.'); } } diff --git a/app/Http/Middleware/Api/Application/AuthenticateKey.php b/app/Http/Middleware/Api/AuthenticateKey.php similarity index 92% rename from app/Http/Middleware/Api/Application/AuthenticateKey.php rename to app/Http/Middleware/Api/AuthenticateKey.php index 30e6236e..8f400bb4 100644 --- a/app/Http/Middleware/Api/Application/AuthenticateKey.php +++ b/app/Http/Middleware/Api/AuthenticateKey.php @@ -1,6 +1,6 @@ bearerToken())) { throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']); @@ -68,7 +69,7 @@ class AuthenticateKey try { $model = $this->repository->findFirstWhere([ ['identifier', '=', $identifier], - ['key_type', '=', ApiKey::TYPE_APPLICATION], + ['key_type', '=', $keyType], ]); } catch (RecordNotFoundException $exception) { throw new AccessDeniedHttpException; diff --git a/app/Http/Middleware/Api/Client/AuthenticateClientAccess.php b/app/Http/Middleware/Api/Client/AuthenticateClientAccess.php new file mode 100644 index 00000000..0a006aef --- /dev/null +++ b/app/Http/Middleware/Api/Client/AuthenticateClientAccess.php @@ -0,0 +1,27 @@ +user())) { + throw new AccessDeniedHttpException('This account does not have permission to access this resource.'); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php b/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php new file mode 100644 index 00000000..f8a35fdd --- /dev/null +++ b/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php @@ -0,0 +1,39 @@ +router->bind('server', function ($value) use ($request) { + try { + return Container::getInstance()->make(ServerRepositoryInterface::class)->findFirstWhere([ + ['uuidShort', '=', $value], + ]); + } catch (RecordNotFoundException $ex) { + $request->attributes->set('is_missing_model', true); + + return null; + } + }); + + return parent::handle($request, $next); + } +} diff --git a/app/Http/Middleware/Api/Application/SetSessionDriver.php b/app/Http/Middleware/Api/SetSessionDriver.php similarity index 95% rename from app/Http/Middleware/Api/Application/SetSessionDriver.php rename to app/Http/Middleware/Api/SetSessionDriver.php index c4660ec9..c69311a6 100644 --- a/app/Http/Middleware/Api/Application/SetSessionDriver.php +++ b/app/Http/Middleware/Api/SetSessionDriver.php @@ -1,6 +1,6 @@ attributes->get('server'); + $database = $request->input('database') ?? $request->route()->parameter('database'); - $database = $this->repository->find($request->input('database')); + if (! is_digit($database)) { + throw new NotFoundHttpException; + } + + $database = $this->repository->find($database); if (is_null($database) || $database->server_id !== $server->id) { throw new NotFoundHttpException; } diff --git a/app/Http/Requests/Api/Application/ApplicationApiRequest.php b/app/Http/Requests/Api/Application/ApplicationApiRequest.php index ada9b4b0..084a89bd 100644 --- a/app/Http/Requests/Api/Application/ApplicationApiRequest.php +++ b/app/Http/Requests/Api/Application/ApplicationApiRequest.php @@ -3,7 +3,6 @@ namespace Pterodactyl\Http\Requests\Api\Application; use Pterodactyl\Models\ApiKey; -use Illuminate\Database\Eloquent\Model; use Pterodactyl\Services\Acl\Api\AdminAcl; use Illuminate\Foundation\Http\FormRequest; use Pterodactyl\Exceptions\PterodactylException; @@ -13,6 +12,14 @@ use Symfony\Component\Routing\Exception\InvalidParameterException; abstract class ApplicationApiRequest extends FormRequest { + /** + * Tracks if the request has been validated internally or not to avoid + * making duplicate validation calls. + * + * @var bool + */ + private $hasValidated = false; + /** * The resource that should be checked when performing the authorization * function for this request. @@ -96,6 +103,21 @@ abstract class ApplicationApiRequest extends FormRequest return $this->route()->parameter($parameterKey); } + /** + * Validate that the resource exists and can be accessed prior to booting + * the validator and attempting to use the data. + * + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + protected function prepareForValidation() + { + if (! $this->passesAuthorization()) { + $this->failedAuthorization(); + } + + $this->hasValidated = true; + } + /* * Determine if the request passes the authorization check as well * as the exists check. @@ -110,6 +132,14 @@ abstract class ApplicationApiRequest extends FormRequest */ protected function passesAuthorization() { + // If we have already validated we do not need to call this function + // again. This is needed to work around Laravel's normal auth validation + // that occurs after validating the request params since we are doing auth + // validation in the prepareForValidation() function. + if ($this->hasValidated) { + return true; + } + if (! parent::passesAuthorization()) { return false; } diff --git a/app/Http/Requests/Api/Application/Servers/ServerWriteRequest.php b/app/Http/Requests/Api/Application/Servers/ServerWriteRequest.php index 728b1ce5..07c20133 100644 --- a/app/Http/Requests/Api/Application/Servers/ServerWriteRequest.php +++ b/app/Http/Requests/Api/Application/Servers/ServerWriteRequest.php @@ -2,7 +2,6 @@ namespace Pterodactyl\Http\Requests\Api\Application\Servers; -use Pterodactyl\Models\Server; use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; @@ -17,16 +16,4 @@ class ServerWriteRequest extends ApplicationApiRequest * @var int */ protected $permission = AdminAcl::WRITE; - - /** - * Determine if the requested server exists on the Panel. - * - * @return bool - */ - public function resourceExists(): bool - { - $server = $this->route()->parameter('server'); - - return $server instanceof Server && $server->exists; - } } diff --git a/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php b/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php index 6d0d2ecf..d12b738e 100644 --- a/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php +++ b/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php @@ -39,7 +39,7 @@ class StoreServerRequest extends ApplicationApiRequest 'pack' => $rules['pack_id'], 'docker_image' => $rules['image'], 'startup' => $rules['startup'], - 'environment' => 'required|array', + 'environment' => 'present|array', 'skip_scripts' => 'sometimes|boolean', // Resource limitations diff --git a/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php b/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php index 893ff5ff..076abdf4 100644 --- a/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php +++ b/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php @@ -13,7 +13,7 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest */ public function rules(): array { - $rules = Server::getUpdateRulesForId($this->route()->parameter('server')->id); + $rules = Server::getUpdateRulesForId($this->getModel(Server::class)->id); return [ 'allocation' => $rules['allocation_id'], @@ -26,6 +26,9 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest 'add_allocations.*' => 'integer', 'remove_allocations' => 'bail|array', 'remove_allocations.*' => 'integer', + 'feature_limits' => 'required|array', + 'feature_limits.databases' => $rules['database_limit'], + 'feature_limits.allocations' => $rules['allocation_limit'], ]; } @@ -39,7 +42,9 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest $data = parent::validated(); $data['allocation_id'] = $data['allocation']; - unset($data['allocation']); + $data['database_limit'] = $data['feature_limits']['databases']; + $data['allocation_limit'] = $data['feature_limits']['allocations']; + unset($data['allocation'], $data['feature_limits']); return $data; } @@ -56,6 +61,8 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest 'remove_allocations' => 'allocations to remove', 'add_allocations.*' => 'allocation to add', 'remove_allocations.*' => 'allocation to remove', + 'feature_limits.databases' => 'Database Limit', + 'feature_limits.allocations' => 'Allocation Limit', ]; } } diff --git a/app/Http/Requests/Api/Client/ClientApiRequest.php b/app/Http/Requests/Api/Client/ClientApiRequest.php new file mode 100644 index 00000000..ed63ccbf --- /dev/null +++ b/app/Http/Requests/Api/Client/ClientApiRequest.php @@ -0,0 +1,19 @@ +user()->can('view-server', $this->getModel(Server::class)); + } +} diff --git a/app/Http/Requests/Api/Client/Servers/SendCommandRequest.php b/app/Http/Requests/Api/Client/Servers/SendCommandRequest.php new file mode 100644 index 00000000..788f9773 --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/SendCommandRequest.php @@ -0,0 +1,30 @@ +user()->can('send-command', $this->getModel(Server::class)); + } + + /** + * Rules to validate this request aganist. + * + * @return array + */ + public function rules(): array + { + return [ + 'command' => 'required|string|min:1', + ]; + } +} diff --git a/app/Http/Requests/Api/Client/Servers/SendPowerRequest.php b/app/Http/Requests/Api/Client/Servers/SendPowerRequest.php new file mode 100644 index 00000000..19614d18 --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/SendPowerRequest.php @@ -0,0 +1,31 @@ +user()->can('power-' . $this->input('signal', '_undefined'), $this->getModel(Server::class)); + } + + /** + * Rules to validate this request aganist. + * + * @return array + */ + public function rules(): array + { + return [ + 'signal' => 'required|string|in:start,stop,restart,kill', + ]; + } +} diff --git a/app/Http/Requests/Base/CreateClientApiKeyRequest.php b/app/Http/Requests/Base/CreateClientApiKeyRequest.php new file mode 100644 index 00000000..b8e7bbfe --- /dev/null +++ b/app/Http/Requests/Base/CreateClientApiKeyRequest.php @@ -0,0 +1,21 @@ + 'required|string|max:255', + 'allowed_ips' => 'nullable|string', + ]; + } +} diff --git a/app/Http/Requests/Server/Database/DeleteServerDatabaseRequest.php b/app/Http/Requests/Server/Database/DeleteServerDatabaseRequest.php new file mode 100644 index 00000000..f2c81d9c --- /dev/null +++ b/app/Http/Requests/Server/Database/DeleteServerDatabaseRequest.php @@ -0,0 +1,40 @@ + 'required|string|min:1', + 'remote' => 'required|string|regex:/^[0-9%.]{1,15}$/', + ]; + } +} diff --git a/app/Http/Requests/Server/ServerFormRequest.php b/app/Http/Requests/Server/ServerFormRequest.php index b796a21e..f59ea3ae 100644 --- a/app/Http/Requests/Server/ServerFormRequest.php +++ b/app/Http/Requests/Server/ServerFormRequest.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Http\Requests\Server; +use Pterodactyl\Models\Server; use Pterodactyl\Http\Requests\FrontendUserFormRequest; abstract class ServerFormRequest extends FrontendUserFormRequest @@ -24,6 +25,11 @@ abstract class ServerFormRequest extends FrontendUserFormRequest return false; } - return $this->user()->can($this->permission(), $this->attributes->get('server')); + return $this->user()->can($this->permission(), $this->getServer()); + } + + public function getServer(): Server + { + return $this->attributes->get('server'); } } diff --git a/app/Models/Permission.php b/app/Models/Permission.php index c9fd886f..5d9ea6c7 100644 --- a/app/Models/Permission.php +++ b/app/Models/Permission.php @@ -93,6 +93,8 @@ class Permission extends Model implements CleansAttributes, ValidableContract 'database' => [ 'view-databases' => null, 'reset-db-password' => null, + 'delete-database' => null, + 'create-database' => null, ], 'file' => [ 'access-sftp' => null, diff --git a/app/Models/Server.php b/app/Models/Server.php index 458bff1d..d5c6b3a8 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -69,6 +69,8 @@ class Server extends Model implements CleansAttributes, ValidableContract 'skip_scripts' => 'sometimes', 'image' => 'required', 'startup' => 'required', + 'database_limit' => 'present', + 'allocation_limit' => 'present', ]; /** @@ -93,6 +95,8 @@ class Server extends Model implements CleansAttributes, ValidableContract 'skip_scripts' => 'boolean', 'image' => 'string|max:255', 'installed' => 'boolean', + 'database_limit' => 'nullable|integer|min:0', + 'allocation_limit' => 'nullable|integer|min:0', ]; /** @@ -116,6 +120,8 @@ class Server extends Model implements CleansAttributes, ValidableContract 'egg_id' => 'integer', 'pack_id' => 'integer', 'installed' => 'integer', + 'database_limit' => 'integer', + 'allocation_limit' => 'integer', ]; /** diff --git a/app/Providers/MacroServiceProvider.php b/app/Providers/MacroServiceProvider.php index ddfbf7aa..9eae42b8 100644 --- a/app/Providers/MacroServiceProvider.php +++ b/app/Providers/MacroServiceProvider.php @@ -1,21 +1,9 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Providers; -use File; -use Cache; -use Carbon; -use Request; -use Pterodactyl\Models\ApiKey; +use Illuminate\Support\Facades\File; use Illuminate\Support\ServiceProvider; -use Pterodactyl\Services\ApiKeyService; class MacroServiceProvider extends ServiceProvider { @@ -36,35 +24,5 @@ class MacroServiceProvider extends ServiceProvider return round($size, ($i < 2) ? 0 : $precision) . ' ' . $units[$i]; }); - - Request::macro('apiKey', function () { - if (! Request::bearerToken()) { - return false; - } - - $parts = explode('.', Request::bearerToken()); - - if (count($parts) === 2 && strlen($parts[0]) === ApiKeyService::PUB_CRYPTO_BYTES * 2) { - // Because the key itself isn't changing frequently, we simply cache this for - // 15 minutes to speed up the API and keep requests flowing. - return Cache::tags([ - 'ApiKeyMacro', - 'ApiKeyMacro:Key:' . $parts[0], - ])->remember('ApiKeyMacro.' . $parts[0], Carbon::now()->addMinutes(15), function () use ($parts) { - return ApiKey::where('public', $parts[0])->first(); - }); - } - - return false; - }); - - Request::macro('apiKeyHasPermission', function ($permission) { - $key = Request::apiKey(); - if (! $key) { - return false; - } - - return Request::user()->can($permission, $key); - }); } } diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index f9f6ac31..3de307d9 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -41,6 +41,10 @@ class RouteServiceProvider extends ServiceProvider ->namespace($this->namespace . '\Api\Application') ->group(base_path('routes/api-application.php')); + Route::middleware(['client-api'])->prefix('/api/client') + ->namespace($this->namespace . '\Api\Client') + ->group(base_path('routes/api-client.php')); + Route::middleware(['daemon'])->prefix('/api/remote') ->namespace($this->namespace . '\Api\Remote') ->group(base_path('routes/api-remote.php')); diff --git a/app/Repositories/Daemon/CommandRepository.php b/app/Repositories/Daemon/CommandRepository.php index 31cb6b9b..7b7577b3 100644 --- a/app/Repositories/Daemon/CommandRepository.php +++ b/app/Repositories/Daemon/CommandRepository.php @@ -12,7 +12,6 @@ class CommandRepository extends BaseRepository implements CommandRepositoryInter * * @param string $command * @return \Psr\Http\Message\ResponseInterface - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function send(string $command): ResponseInterface { diff --git a/app/Repositories/Eloquent/ServerRepository.php b/app/Repositories/Eloquent/ServerRepository.php index 7bca1269..5a53d33f 100644 --- a/app/Repositories/Eloquent/ServerRepository.php +++ b/app/Repositories/Eloquent/ServerRepository.php @@ -264,6 +264,45 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt } } + /** + * Return all of the servers that should have a power action performed aganist them. + * + * @param int[] $servers + * @param int[] $nodes + * @param bool $returnCount + * @return int|\Generator + */ + public function getServersForPowerAction(array $servers = [], array $nodes = [], bool $returnCount = false) + { + $instance = $this->getBuilder(); + + if (! empty($nodes) && ! empty($servers)) { + $instance->whereIn('id', $servers)->orWhereIn('node_id', $nodes); + } elseif (empty($nodes) && ! empty($servers)) { + $instance->whereIn('id', $servers); + } elseif (! empty($nodes) && empty($servers)) { + $instance->whereIn('node_id', $nodes); + } + + if ($returnCount) { + return $instance->count(); + } + + return $instance->with('node')->cursor(); + } + + /** + * Return the total number of servers that will be affected by the query. + * + * @param int[] $servers + * @param int[] $nodes + * @return int + */ + public function getServersForPowerActionCount(array $servers = [], array $nodes = []): int + { + return $this->getServersForPowerAction($servers, $nodes, true); + } + /** * Return an array of server IDs that a given user can access based * on owner and subuser permissions. diff --git a/app/Services/Databases/DatabaseManagementService.php b/app/Services/Databases/DatabaseManagementService.php index b05ddc3f..dc91e11f 100644 --- a/app/Services/Databases/DatabaseManagementService.php +++ b/app/Services/Databases/DatabaseManagementService.php @@ -13,22 +13,27 @@ class DatabaseManagementService /** * @var \Illuminate\Database\DatabaseManager */ - protected $database; + private $database; /** * @var \Pterodactyl\Extensions\DynamicDatabaseConnection */ - protected $dynamic; + private $dynamic; /** * @var \Illuminate\Contracts\Encryption\Encrypter */ - protected $encrypter; + private $encrypter; /** * @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface */ - protected $repository; + private $repository; + + /** + * @var bool + */ + protected $useRandomHost = false; /** * CreationService constructor. @@ -55,7 +60,7 @@ class DatabaseManagementService * * @param int $server * @param array $data - * @return \Illuminate\Database\Eloquent\Model + * @return \Pterodactyl\Models\Database * * @throws \Exception */ diff --git a/app/Services/Databases/DeployServerDatabaseService.php b/app/Services/Databases/DeployServerDatabaseService.php new file mode 100644 index 00000000..c8b5ed17 --- /dev/null +++ b/app/Services/Databases/DeployServerDatabaseService.php @@ -0,0 +1,90 @@ +databaseHostRepository = $databaseHostRepository; + $this->managementService = $managementService; + $this->repository = $repository; + } + + /** + * @param \Pterodactyl\Models\Server $server + * @param array $data + * @return \Pterodactyl\Models\Database + * + * @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException + * @throws \Exception + */ + public function handle(Server $server, array $data): Database + { + if (! config('pterodactyl.client_features.databases.enabled')) { + throw new DatabaseClientFeatureNotEnabledException; + } + + $databases = $this->repository->findCountWhere([['server_id', '=', $server->id]]); + if (! is_null($server->database_limit) && $databases >= $server->database_limit) { + throw new TooManyDatabasesException; + } + + $allowRandom = config('pterodactyl.client_features.databases.allow_random'); + $hosts = $this->databaseHostRepository->setColumns(['id'])->findWhere([ + ['node_id', '=', $server->node_id], + ]); + + if ($hosts->isEmpty() && ! $allowRandom) { + throw new NoSuitableDatabaseHostException; + } + + if ($hosts->isEmpty()) { + $hosts = $this->databaseHostRepository->setColumns(['id'])->all(); + if ($hosts->isEmpty()) { + throw new NoSuitableDatabaseHostException; + } + } + + $host = $hosts->random(); + + return $this->managementService->create($server->id, [ + 'database_host_id' => $host->id, + 'database' => array_get($data, 'database'), + 'remote' => array_get($data, 'remote'), + ]); + } +} diff --git a/app/Services/Servers/BuildModificationService.php b/app/Services/Servers/BuildModificationService.php index 8924b2a0..5d36b4c5 100644 --- a/app/Services/Servers/BuildModificationService.php +++ b/app/Services/Servers/BuildModificationService.php @@ -91,6 +91,8 @@ class BuildModificationService 'cpu' => array_get($data, 'cpu'), 'disk' => array_get($data, 'disk'), 'allocation_id' => array_get($data, 'allocation_id'), + 'database_limit' => array_get($data, 'database_limit'), + 'allocation_limit' => array_get($data, 'allocation_limit'), ]); $allocations = $this->allocationRepository->findWhere([['server_id', '=', $server->id]]); diff --git a/app/Services/Servers/ReinstallServerService.php b/app/Services/Servers/ReinstallServerService.php index 682813e3..85800473 100644 --- a/app/Services/Servers/ReinstallServerService.php +++ b/app/Services/Servers/ReinstallServerService.php @@ -66,7 +66,7 @@ class ReinstallServerService $this->database->beginTransaction(); $this->repository->withoutFreshModel()->update($server->id, [ 'installed' => 0, - ]); + ], true, true); try { $this->daemonServerRepository->setServer($server)->reinstall(); diff --git a/app/Transformers/Api/Application/BaseTransformer.php b/app/Transformers/Api/Application/BaseTransformer.php index c2fdf613..5766088c 100644 --- a/app/Transformers/Api/Application/BaseTransformer.php +++ b/app/Transformers/Api/Application/BaseTransformer.php @@ -7,6 +7,7 @@ use Pterodactyl\Models\ApiKey; use Illuminate\Container\Container; use League\Fractal\TransformerAbstract; use Pterodactyl\Services\Acl\Api\AdminAcl; +use Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException; abstract class BaseTransformer extends TransformerAbstract { @@ -78,13 +79,19 @@ abstract class BaseTransformer extends TransformerAbstract * @param string $abstract * @param array $parameters * @return \Pterodactyl\Transformers\Api\Application\BaseTransformer + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ - protected function makeTransformer(string $abstract, array $parameters = []): self + protected function makeTransformer(string $abstract, array $parameters = []) { /** @var \Pterodactyl\Transformers\Api\Application\BaseTransformer $transformer */ $transformer = Container::getInstance()->makeWith($abstract, $parameters); $transformer->setKey($this->getKey()); + if (! $transformer instanceof self) { + throw new InvalidTransformerLevelException('Calls to ' . __METHOD__ . ' must return a transformer that is an instance of ' . __CLASS__); + } + return $transformer; } diff --git a/app/Transformers/Api/Application/PackTransformer.php b/app/Transformers/Api/Application/PackTransformer.php index 973002ae..e77bdd45 100644 --- a/app/Transformers/Api/Application/PackTransformer.php +++ b/app/Transformers/Api/Application/PackTransformer.php @@ -1,90 +1,40 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ -namespace Pterodactyl\Transformers\Admin; +namespace Pterodactyl\Transformers\Api\Application; -use Illuminate\Http\Request; use Pterodactyl\Models\Pack; -use League\Fractal\TransformerAbstract; -class PackTransformer extends TransformerAbstract +class PackTransformer extends BaseTransformer { /** - * List of resources that can be included. + * Return the resource name for the JSONAPI output. * - * @var array + * @return string */ - protected $availableIncludes = [ - 'option', - 'servers', - ]; - - /** - * The Illuminate Request object if provided. - * - * @var \Illuminate\Http\Request|bool - */ - protected $request; - - /** - * Setup request object for transformer. - * - * @param \Illuminate\Http\Request|bool $request - */ - public function __construct($request = false) + public function getResourceName(): string { - if (! $request instanceof Request && $request !== false) { - throw new DisplayException('Request passed to constructor must be of type Request or false.'); - } - - $this->request = $request; + return Pack::RESOURCE_NAME; } /** - * Return a generic transformed pack array. + * Return a transformed User model that can be consumed by external services. * + * @param \Pterodactyl\Models\Pack $pack * @return array */ - public function transform($pack) + public function transform(Pack $pack): array { - if (! $pack instanceof Pack) { - return ['id' => null]; - } - - return $pack->toArray(); - } - - /** - * Return the packs associated with this service. - * - * @return \Leauge\Fractal\Resource\Item - */ - public function includeOption(Pack $pack) - { - if ($this->request && ! $this->request->apiKeyHasPermission('option-view')) { - return; - } - - return $this->item($pack->option, new OptionTransformer($this->request), 'option'); - } - - /** - * Return the packs associated with this service. - * - * @return \Leauge\Fractal\Resource\Collection - */ - public function includeServers(Pack $pack) - { - if ($this->request && ! $this->request->apiKeyHasPermission('server-list')) { - return; - } - - return $this->collection($pack->servers, new ServerTransformer($this->request), 'server'); + return [ + 'id' => $pack->id, + 'uuid' => $pack->uuid, + 'egg' => $pack->egg_id, + 'name' => $pack->name, + 'description' => $pack->description, + 'is_selectable' => (bool) $pack->selectable, + 'is_visible' => (bool) $pack->visible, + 'is_locked' => (bool) $pack->locked, + 'created_at' => $this->formatTimestamp($pack->created_at), + 'updated_at' => $this->formatTimestamp($pack->updated_at), + ]; } } diff --git a/app/Transformers/Api/Application/ServerTransformer.php b/app/Transformers/Api/Application/ServerTransformer.php index 69115d1e..2a542dbc 100644 --- a/app/Transformers/Api/Application/ServerTransformer.php +++ b/app/Transformers/Api/Application/ServerTransformer.php @@ -75,6 +75,10 @@ class ServerTransformer extends BaseTransformer 'io' => $server->io, 'cpu' => $server->cpu, ], + 'feature_limits' => [ + 'databases' => $server->database_limit, + 'allocations' => $server->allocation_limit, + ], 'user' => $server->owner_id, 'node' => $server->node_id, 'allocation' => $server->allocation_id, @@ -97,6 +101,8 @@ class ServerTransformer extends BaseTransformer * * @param \Pterodactyl\Models\Server $server * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeAllocations(Server $server) { @@ -114,6 +120,8 @@ class ServerTransformer extends BaseTransformer * * @param \Pterodactyl\Models\Server $server * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeSubusers(Server $server) { @@ -131,6 +139,8 @@ class ServerTransformer extends BaseTransformer * * @param \Pterodactyl\Models\Server $server * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeUser(Server $server) { @@ -148,40 +158,49 @@ class ServerTransformer extends BaseTransformer * * @param \Pterodactyl\Models\Server $server * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ -// public function includePack(Server $server) -// { -// if (! $this->authorize(AdminAcl::RESOURCE_PACKS)) { -// return $this->null(); -// } -// -// $server->loadMissing('pack'); -// -// return $this->item($server->getRelation('pack'), $this->makeTransformer(PackTransformer::class), 'pack'); -// } + public function includePack(Server $server) + { + if (! $this->authorize(AdminAcl::RESOURCE_PACKS)) { + return $this->null(); + } + + $server->loadMissing('pack'); + if (is_null($server->getRelation('pack'))) { + return $this->null(); + } + + return $this->item($server->getRelation('pack'), $this->makeTransformer(PackTransformer::class), 'pack'); + } /** * Return a generic array with nest information for this server. * * @param \Pterodactyl\Models\Server $server * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ -// public function includeNest(Server $server) -// { -// if (! $this->authorize(AdminAcl::RESOURCE_NESTS)) { -// return $this->null(); -// } -// -// $server->loadMissing('nest'); -// -// return $this->item($server->getRelation('nest'), $this->makeTransformer(NestTransformer::class), 'nest'); -// } + public function includeNest(Server $server) + { + if (! $this->authorize(AdminAcl::RESOURCE_NESTS)) { + return $this->null(); + } + + $server->loadMissing('nest'); + + return $this->item($server->getRelation('nest'), $this->makeTransformer(NestTransformer::class), 'nest'); + } /** * Return a generic array with service option information for this server. * * @param \Pterodactyl\Models\Server $server * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeOption(Server $server) { @@ -199,6 +218,8 @@ class ServerTransformer extends BaseTransformer * * @param \Pterodactyl\Models\Server $server * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeVariables(Server $server) { @@ -216,6 +237,8 @@ class ServerTransformer extends BaseTransformer * * @param \Pterodactyl\Models\Server $server * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeLocation(Server $server) { @@ -233,6 +256,8 @@ class ServerTransformer extends BaseTransformer * * @param \Pterodactyl\Models\Server $server * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeNode(Server $server) { diff --git a/app/Transformers/Api/Application/SubuserTransformer.php b/app/Transformers/Api/Application/SubuserTransformer.php deleted file mode 100644 index 93ed25d5..00000000 --- a/app/Transformers/Api/Application/SubuserTransformer.php +++ /dev/null @@ -1,60 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Transformers\Admin; - -use Illuminate\Http\Request; -use Pterodactyl\Models\Subuser; -use League\Fractal\TransformerAbstract; - -class SubuserTransformer extends TransformerAbstract -{ - /** - * The Illuminate Request object if provided. - * - * @var \Illuminate\Http\Request|bool - */ - protected $request; - - /** - * Setup request object for transformer. - * - * @param \Illuminate\Http\Request|bool $request - */ - public function __construct($request = false) - { - if (! $request instanceof Request && $request !== false) { - throw new DisplayException('Request passed to constructor must be of type Request or false.'); - } - - $this->request = $request; - } - - /** - * Return a generic transformed subuser array. - * - * @return array - */ - public function transform(Subuser $subuser) - { - if ($this->request && ! $this->request->apiKeyHasPermission('server-view')) { - return; - } - - return [ - 'id' => $subuser->id, - 'username' => $subuser->user->username, - 'email' => $subuser->user->email, - '2fa' => (bool) $subuser->user->use_totp, - 'permissions' => $subuser->permissions->pluck('permission'), - 'created_at' => $subuser->created_at, - 'updated_at' => $subuser->updated_at, - ]; - } -} diff --git a/app/Transformers/Api/Client/BaseClientTransformer.php b/app/Transformers/Api/Client/BaseClientTransformer.php new file mode 100644 index 00000000..faa1abbe --- /dev/null +++ b/app/Transformers/Api/Client/BaseClientTransformer.php @@ -0,0 +1,74 @@ +user; + } + + /** + * Set the user model of the user requesting this transformation. + * + * @param \Pterodactyl\Models\User $user + */ + public function setUser(User $user) + { + $this->user = $user; + } + + /** + * Determine if the API key loaded onto the transformer has permission + * to access a different resource. This is used when including other + * models on a transformation request. + * + * @param string $ability + * @param \Pterodactyl\Models\Server $server + * @return bool + */ + protected function authorize(string $ability, Server $server = null): bool + { + Assert::isInstanceOf($server, Server::class); + + return $this->getUser()->can($ability, [$server]); + } + + /** + * Create a new instance of the transformer and pass along the currently + * set API key. + * + * @param string $abstract + * @param array $parameters + * @return self + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException + */ + protected function makeTransformer(string $abstract, array $parameters = []) + { + $transformer = parent::makeTransformer($abstract, $parameters); + + if (! $transformer instanceof self) { + throw new InvalidTransformerLevelException('Calls to ' . __METHOD__ . ' must return a transformer that is an instance of ' . __CLASS__); + } + + return $transformer; + } +} diff --git a/app/Transformers/Api/Client/ServerTransformer.php b/app/Transformers/Api/Client/ServerTransformer.php new file mode 100644 index 00000000..6816d6d7 --- /dev/null +++ b/app/Transformers/Api/Client/ServerTransformer.php @@ -0,0 +1,45 @@ + $this->getKey()->user_id === $server->owner_id, + 'identifier' => $server->uuidShort, + 'uuid' => $server->uuid, + 'name' => $server->name, + 'description' => $server->description, + 'limits' => [ + 'memory' => $server->memory, + 'swap' => $server->swap, + 'disk' => $server->disk, + 'io' => $server->io, + 'cpu' => $server->cpu, + ], + 'feature_limits' => [ + 'databases' => $server->database_limit, + 'allocations' => $server->allocation_limit, + ], + ]; + } +} diff --git a/app/Transformers/User/AllocationTransformer.php b/app/Transformers/User/AllocationTransformer.php deleted file mode 100644 index 8ea0c8f9..00000000 --- a/app/Transformers/User/AllocationTransformer.php +++ /dev/null @@ -1,47 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Transformers\User; - -use Pterodactyl\Models\Server; -use Pterodactyl\Models\Allocation; -use League\Fractal\TransformerAbstract; - -class AllocationTransformer extends TransformerAbstract -{ - /** - * Server eloquent model. - * - * @return \Pterodactyl\Models\Server - */ - protected $server; - - /** - * Setup allocation transformer with access to server data. - */ - public function __construct(Server $server) - { - $this->server = $server; - } - - /** - * Return a generic transformed allocation array. - * - * @return array - */ - public function transform(Allocation $allocation) - { - return [ - 'id' => $allocation->id, - 'ip' => $allocation->alias, - 'port' => $allocation->port, - 'default' => ($allocation->id === $this->server->allocation_id), - ]; - } -} diff --git a/app/Transformers/User/OverviewTransformer.php b/app/Transformers/User/OverviewTransformer.php deleted file mode 100644 index b5999076..00000000 --- a/app/Transformers/User/OverviewTransformer.php +++ /dev/null @@ -1,35 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Transformers\User; - -use Pterodactyl\Models\Server; -use League\Fractal\TransformerAbstract; - -class OverviewTransformer extends TransformerAbstract -{ - /** - * Return a generic transformed server array. - * - * @return array - */ - public function transform(Server $server) - { - return [ - 'id' => $server->uuidShort, - 'uuid' => $server->uuid, - 'name' => $server->name, - 'node' => $server->node->name, - 'ip' => $server->allocation->alias, - 'port' => $server->allocation->port, - 'service' => $server->service->name, - 'option' => $server->option->name, - ]; - } -} diff --git a/app/Transformers/User/ServerTransformer.php b/app/Transformers/User/ServerTransformer.php deleted file mode 100644 index 031ae82f..00000000 --- a/app/Transformers/User/ServerTransformer.php +++ /dev/null @@ -1,85 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Transformers\User; - -use Pterodactyl\Models\Server; -use League\Fractal\TransformerAbstract; - -class ServerTransformer extends TransformerAbstract -{ - /** - * List of resources that can be included. - * - * @var array - */ - protected $availableIncludes = [ - 'allocations', - 'subusers', - 'stats', - ]; - - /** - * Return a generic transformed server array. - * - * @return array - */ - public function transform(Server $server) - { - return [ - 'id' => $server->uuidShort, - 'uuid' => $server->uuid, - 'name' => $server->name, - 'description' => $server->description, - 'node' => $server->node->name, - 'limits' => [ - 'memory' => $server->memory, - 'swap' => $server->swap, - 'disk' => $server->disk, - 'io' => $server->io, - 'cpu' => $server->cpu, - 'oom_disabled' => (bool) $server->oom_disabled, - ], - ]; - } - - /** - * Return a generic array of allocations for this server. - * - * @return \Leauge\Fractal\Resource\Collection - */ - public function includeAllocations(Server $server) - { - $allocations = $server->allocations; - - return $this->collection($allocations, new AllocationTransformer($server), 'allocation'); - } - - /** - * Return a generic array of subusers for this server. - * - * @return \Leauge\Fractal\Resource\Collection - */ - public function includeSubusers(Server $server) - { - $server->load('subusers.permissions', 'subusers.user'); - - return $this->collection($server->subusers, new SubuserTransformer, 'subuser'); - } - - /** - * Return a generic array of allocations for this server. - * - * @return \Leauge\Fractal\Resource\Item - */ - public function includeStats(Server $server) - { - return $this->item($server->guzzleClient(), new StatsTransformer, 'stat'); - } -} diff --git a/app/Transformers/User/StatsTransformer.php b/app/Transformers/User/StatsTransformer.php deleted file mode 100644 index 2a2e1d5e..00000000 --- a/app/Transformers/User/StatsTransformer.php +++ /dev/null @@ -1,48 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Transformers\User; - -use GuzzleHttp\Client; -use League\Fractal\TransformerAbstract; -use GuzzleHttp\Exception\ConnectException; - -class StatsTransformer extends TransformerAbstract -{ - /** - * Return a generic transformed subuser array. - * - * @return array - */ - public function transform(Client $client) - { - try { - $res = $client->request('GET', '/server', ['http_errors' => false]); - - if ($res->getStatusCode() !== 200) { - return [ - 'error' => 'Error: HttpResponseException. Recieved non-200 HTTP status code from daemon: ' . $res->statusCode(), - ]; - } - - $json = json_decode($res->getBody()); - - return [ - 'id' => 1, - 'status' => $json->status, - 'resources' => $json->proc, - ]; - } catch (ConnectException $ex) { - return [ - 'error' => 'Error: ConnectException. Unable to contact the daemon to request server status.', - 'exception' => (config('app.debug')) ? $ex->getMessage() : null, - ]; - } - } -} diff --git a/app/Transformers/User/SubuserTransformer.php b/app/Transformers/User/SubuserTransformer.php deleted file mode 100644 index faac5965..00000000 --- a/app/Transformers/User/SubuserTransformer.php +++ /dev/null @@ -1,32 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Transformers\User; - -use Pterodactyl\Models\Subuser; -use League\Fractal\TransformerAbstract; - -class SubuserTransformer extends TransformerAbstract -{ - /** - * Return a generic transformed subuser array. - * - * @return array - */ - public function transform(Subuser $subuser) - { - return [ - 'id' => $subuser->id, - 'username' => $subuser->user->username, - 'email' => $subuser->user->email, - '2fa' => (bool) $subuser->user->use_totp, - 'permissions' => $subuser->permissions->pluck('permission'), - ]; - } -} diff --git a/config/app.php b/config/app.php index 50ab9f19..f95d7162 100644 --- a/config/app.php +++ b/config/app.php @@ -9,7 +9,7 @@ return [ | change this value if you are not maintaining your own internal versions. */ - 'version' => '0.7.3', + 'version' => '0.7.4', /* |-------------------------------------------------------------------------- diff --git a/config/pterodactyl.php b/config/pterodactyl.php index 523080ae..e06c709e 100644 --- a/config/pterodactyl.php +++ b/config/pterodactyl.php @@ -163,6 +163,21 @@ return [ 'in_context' => env('PHRASE_IN_CONTEXT', false), ], + /* + |-------------------------------------------------------------------------- + | Language Editor + |-------------------------------------------------------------------------- + | + | Set `PHRASE_IN_CONTEXT` to true to enable the PhaseApp in-context editor + | on this site which allows you to translate the panel, from the panel. + */ + 'client_features' => [ + 'databases' => [ + 'enabled' => env('PTERODACTYL_CLIENT_DATABASES_ENABLED', true), + 'allow_random' => env('PTERODACTYL_CLIENT_DATABASES_ALLOW_RANDOM', true), + ], + ], + /* |-------------------------------------------------------------------------- | File Editor diff --git a/database/migrations/2018_01_13_142012_SetupTableForKeyEncryption.php b/database/migrations/2018_01_13_142012_SetupTableForKeyEncryption.php index e7fd0c58..5dba9c11 100644 --- a/database/migrations/2018_01_13_142012_SetupTableForKeyEncryption.php +++ b/database/migrations/2018_01_13_142012_SetupTableForKeyEncryption.php @@ -16,7 +16,7 @@ class SetupTableForKeyEncryption extends Migration public function up() { Schema::table('api_keys', function (Blueprint $table) { - $table->char('identifier', 16)->unique()->after('user_id'); + $table->char('identifier', 16)->nullable()->unique()->after('user_id'); $table->dropUnique(['token']); }); diff --git a/database/migrations/2018_03_01_192831_add_database_and_port_limit_columns_to_servers_table.php b/database/migrations/2018_03_01_192831_add_database_and_port_limit_columns_to_servers_table.php new file mode 100644 index 00000000..4e85e8ae --- /dev/null +++ b/database/migrations/2018_03_01_192831_add_database_and_port_limit_columns_to_servers_table.php @@ -0,0 +1,33 @@ +unsignedInteger('database_limit')->after('installed')->nullable()->default(0); + $table->unsignedInteger('allocation_limit')->after('installed')->nullable()->default(0); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('servers', function (Blueprint $table) { + $table->dropColumn(['database_limit', 'allocation_limit']); + }); + } +} diff --git a/public/js/laroute.js b/public/js/laroute.js index 05740f25..1ffbc7b1 100644 --- a/public/js/laroute.js +++ b/public/js/laroute.js @@ -6,7 +6,7 @@ absolute: false, rootUrl: 'http://pterodactyl.local', - routes : [{"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"},{"host":null,"methods":["DELETE"],"uri":"_debugbar\/cache\/{key}\/{tags?}","name":"debugbar.cache.delete","action":"Barryvdh\Debugbar\Controllers\CacheController@delete"},{"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\/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@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/api","name":"admin.api.index","action":"Pterodactyl\Http\Controllers\Admin\ApiController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/api\/new","name":"admin.api.new","action":"Pterodactyl\Http\Controllers\Admin\ApiController@create"},{"host":null,"methods":["POST"],"uri":"admin\/api\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ApiController@store"},{"host":null,"methods":["DELETE"],"uri":"admin\/api\/revoke\/{identifier}","name":"admin.api.delete","action":"Pterodactyl\Http\Controllers\Admin\ApiController@delete"},{"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\Settings\IndexController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/settings\/mail","name":"admin.settings.mail","action":"Pterodactyl\Http\Controllers\Admin\Settings\MailController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/settings\/advanced","name":"admin.settings.advanced","action":"Pterodactyl\Http\Controllers\Admin\Settings\AdvancedController@index"},{"host":null,"methods":["PATCH"],"uri":"admin\/settings","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Settings\IndexController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/settings\/mail","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Settings\MailController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/settings\/advanced","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Settings\AdvancedController@update"},{"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\/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}\/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\/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@loginUsingTotp"},{"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":"auth\/logout","name":"auth.logout","action":"Pterodactyl\Http\Controllers\Auth\LoginController@logout"},{"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\/allocation","name":"server.settings.allocation","action":"Pterodactyl\Http\Controllers\Server\Settings\AllocationController@index"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/settings\/allocation","name":null,"action":"Pterodactyl\Http\Controllers\Server\Settings\AllocationController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/settings\/sftp","name":"server.settings.sftp","action":"Pterodactyl\Http\Controllers\Server\Settings\SftpController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/settings\/startup","name":"server.settings.startup","action":"Pterodactyl\Http\Controllers\Server\Settings\StartupController@index"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/settings\/startup","name":null,"action":"Pterodactyl\Http\Controllers\Server\Settings\StartupController@update"},{"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@view"},{"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":["POST"],"uri":"server\/{server}\/users\/new","name":null,"action":"Pterodactyl\Http\Controllers\Server\SubuserController@store"},{"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":["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}","name":null,"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":["POST"],"uri":"server\/{server}\/schedules\/new","name":null,"action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@store"},{"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":["PATCH"],"uri":"server\/{server}\/schedules\/view\/{schedule}","name":null,"action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@update"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/schedules\/view\/{schedule}\/toggle","name":"server.schedules.toggle","action":"Pterodactyl\Http\Controllers\Server\Tasks\ActionController@toggle"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/schedules\/view\/{schedule}\/trigger","name":"server.schedules.trigger","action":"Pterodactyl\Http\Controllers\Server\Tasks\ActionController@trigger"},{"host":null,"methods":["DELETE"],"uri":"server\/{server}\/schedules\/view\/{schedule}","name":null,"action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/users","name":"api.application.users","action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/users\/{user}","name":"api.application.users.view","action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/users\/external\/{external_id}","name":"api.application.users.external","action":"Pterodactyl\Http\Controllers\Api\Application\Users\ExternalUserController@index"},{"host":null,"methods":["POST"],"uri":"api\/application\/users","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/users\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/users\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nodes","name":"api.application.nodes","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nodes\/{node}","name":"api.application.nodes.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@view"},{"host":null,"methods":["POST"],"uri":"api\/application\/nodes","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/nodes\/{node}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/nodes\/{node}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nodes\/{node}\/allocations","name":"api.application.allocations","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\AllocationController@index"},{"host":null,"methods":["POST"],"uri":"api\/application\/nodes\/{node}\/allocations","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\AllocationController@store"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/nodes\/{node}\/allocations\/{allocation}","name":"api.application.allocations.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\AllocationController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/locations","name":"api.applications.locations","action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/locations\/{location}","name":"api.application.locations.view","action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@view"},{"host":null,"methods":["POST"],"uri":"api\/application\/locations","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/locations\/{location}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/locations\/{location}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers","name":"api.application.servers","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/{server}","name":"api.application.servers.view","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@view"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/servers\/{server}\/details","name":"api.application.servers.details","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerDetailsController@details"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/servers\/{server}\/build","name":"api.application.servers.build","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerDetailsController@build"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/servers\/{server}\/startup","name":"api.application.servers.startup","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\StartupController@index"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@store"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/suspend","name":"api.application.servers.suspend","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@suspend"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/unsuspend","name":"api.application.servers.unsuspend","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@unsuspend"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/reinstall","name":"api.application.servers.reinstall","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@reinstall"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/rebuild","name":"api.application.servers.rebuild","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@rebuild"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/servers\/{server}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@delete"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/servers\/{server}\/{force?}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/{server}\/databases","name":"api.application.servers.databases","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/{server}\/databases\/{database}","name":"api.application.servers.databases.view","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@view"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/databases","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@store"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/databases\/{database}\/reset-password","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@resetPassword"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/servers\/{server}\/databases\/{database}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests","name":"api.application.nests","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\NestController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests\/{nest}","name":"api.application.nests.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\NestController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests\/{nest}\/eggs","name":"api.application.nests.eggs","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\EggController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests\/{nest}\/eggs\/{egg}","name":"api.application.nests.eggs.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\EggController@view"},{"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":"api\/remote\/scripts\/{uuid}","name":"api.remote.scripts","action":"Pterodactyl\Http\Controllers\Api\Remote\EggInstallController@index"},{"host":null,"methods":["POST"],"uri":"api\/remote\/sftp","name":"api.remote.sftp","action":"Pterodactyl\Http\Controllers\Api\Remote\SftpController@index"},{"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\/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"}], + routes : [{"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"},{"host":null,"methods":["DELETE"],"uri":"_debugbar\/cache\/{key}\/{tags?}","name":"debugbar.cache.delete","action":"Barryvdh\Debugbar\Controllers\CacheController@delete"},{"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\ClientApiController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"account\/api\/new","name":"account.api.new","action":"Pterodactyl\Http\Controllers\Base\ClientApiController@create"},{"host":null,"methods":["POST"],"uri":"account\/api\/new","name":null,"action":"Pterodactyl\Http\Controllers\Base\ClientApiController@store"},{"host":null,"methods":["DELETE"],"uri":"account\/api\/revoke\/{identifier}","name":"account.api.revoke","action":"Pterodactyl\Http\Controllers\Base\ClientApiController@delete"},{"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@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/api","name":"admin.api.index","action":"Pterodactyl\Http\Controllers\Admin\ApiController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/api\/new","name":"admin.api.new","action":"Pterodactyl\Http\Controllers\Admin\ApiController@create"},{"host":null,"methods":["POST"],"uri":"admin\/api\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ApiController@store"},{"host":null,"methods":["DELETE"],"uri":"admin\/api\/revoke\/{identifier}","name":"admin.api.delete","action":"Pterodactyl\Http\Controllers\Admin\ApiController@delete"},{"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\Settings\IndexController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/settings\/mail","name":"admin.settings.mail","action":"Pterodactyl\Http\Controllers\Admin\Settings\MailController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/settings\/advanced","name":"admin.settings.advanced","action":"Pterodactyl\Http\Controllers\Admin\Settings\AdvancedController@index"},{"host":null,"methods":["PATCH"],"uri":"admin\/settings","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Settings\IndexController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/settings\/mail","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Settings\MailController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/settings\/advanced","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Settings\AdvancedController@update"},{"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\/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}\/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\/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@loginUsingTotp"},{"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":"auth\/logout","name":"auth.logout","action":"Pterodactyl\Http\Controllers\Auth\LoginController@logout"},{"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\/allocation","name":"server.settings.allocation","action":"Pterodactyl\Http\Controllers\Server\Settings\AllocationController@index"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/settings\/allocation","name":null,"action":"Pterodactyl\Http\Controllers\Server\Settings\AllocationController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/settings\/sftp","name":"server.settings.sftp","action":"Pterodactyl\Http\Controllers\Server\Settings\SftpController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/settings\/startup","name":"server.settings.startup","action":"Pterodactyl\Http\Controllers\Server\Settings\StartupController@index"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/settings\/startup","name":null,"action":"Pterodactyl\Http\Controllers\Server\Settings\StartupController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/databases","name":"server.databases.index","action":"Pterodactyl\Http\Controllers\Server\DatabaseController@index"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/databases\/new","name":"server.databases.new","action":"Pterodactyl\Http\Controllers\Server\DatabaseController@store"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/databases\/password","name":"server.databases.password","action":"Pterodactyl\Http\Controllers\Server\DatabaseController@update"},{"host":null,"methods":["DELETE"],"uri":"server\/{server}\/databases\/delete\/{database}","name":"server.databases.delete","action":"Pterodactyl\Http\Controllers\Server\DatabaseController@delete"},{"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@view"},{"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":["POST"],"uri":"server\/{server}\/users\/new","name":null,"action":"Pterodactyl\Http\Controllers\Server\SubuserController@store"},{"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":["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}","name":null,"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":["POST"],"uri":"server\/{server}\/schedules\/new","name":null,"action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@store"},{"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":["PATCH"],"uri":"server\/{server}\/schedules\/view\/{schedule}","name":null,"action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@update"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/schedules\/view\/{schedule}\/toggle","name":"server.schedules.toggle","action":"Pterodactyl\Http\Controllers\Server\Tasks\ActionController@toggle"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/schedules\/view\/{schedule}\/trigger","name":"server.schedules.trigger","action":"Pterodactyl\Http\Controllers\Server\Tasks\ActionController@trigger"},{"host":null,"methods":["DELETE"],"uri":"server\/{server}\/schedules\/view\/{schedule}","name":null,"action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/users","name":"api.application.users","action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/users\/{user}","name":"api.application.users.view","action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/users\/external\/{external_id}","name":"api.application.users.external","action":"Pterodactyl\Http\Controllers\Api\Application\Users\ExternalUserController@index"},{"host":null,"methods":["POST"],"uri":"api\/application\/users","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/users\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/users\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nodes","name":"api.application.nodes","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nodes\/{node}","name":"api.application.nodes.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@view"},{"host":null,"methods":["POST"],"uri":"api\/application\/nodes","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/nodes\/{node}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/nodes\/{node}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nodes\/{node}\/allocations","name":"api.application.allocations","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\AllocationController@index"},{"host":null,"methods":["POST"],"uri":"api\/application\/nodes\/{node}\/allocations","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\AllocationController@store"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/nodes\/{node}\/allocations\/{allocation}","name":"api.application.allocations.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\AllocationController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/locations","name":"api.applications.locations","action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/locations\/{location}","name":"api.application.locations.view","action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@view"},{"host":null,"methods":["POST"],"uri":"api\/application\/locations","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/locations\/{location}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/locations\/{location}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers","name":"api.application.servers","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/{server}","name":"api.application.servers.view","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/external\/{external_id}","name":"api.application.servers.external","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ExternalServerController@index"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/servers\/{server}\/details","name":"api.application.servers.details","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerDetailsController@details"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/servers\/{server}\/build","name":"api.application.servers.build","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerDetailsController@build"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/servers\/{server}\/startup","name":"api.application.servers.startup","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\StartupController@index"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@store"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/suspend","name":"api.application.servers.suspend","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@suspend"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/unsuspend","name":"api.application.servers.unsuspend","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@unsuspend"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/reinstall","name":"api.application.servers.reinstall","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@reinstall"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/rebuild","name":"api.application.servers.rebuild","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@rebuild"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/servers\/{server}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@delete"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/servers\/{server}\/{force?}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/{server}\/databases","name":"api.application.servers.databases","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/{server}\/databases\/{database}","name":"api.application.servers.databases.view","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@view"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/databases","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@store"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/databases\/{database}\/reset-password","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@resetPassword"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/servers\/{server}\/databases\/{database}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests","name":"api.application.nests","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\NestController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests\/{nest}","name":"api.application.nests.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\NestController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests\/{nest}\/eggs","name":"api.application.nests.eggs","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\EggController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests\/{nest}\/eggs\/{egg}","name":"api.application.nests.eggs.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\EggController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/client","name":"api.client.index","action":"Pterodactyl\Http\Controllers\Api\Client\ClientController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/client\/servers\/{server}","name":"api.client.servers.view","action":"Pterodactyl\Http\Controllers\Api\Client\Servers\ServerController@index"},{"host":null,"methods":["POST"],"uri":"api\/client\/servers\/{server}\/command","name":"api.client.servers.command","action":"Pterodactyl\Http\Controllers\Api\Client\Servers\CommandController@index"},{"host":null,"methods":["POST"],"uri":"api\/client\/servers\/{server}\/power","name":"api.client.servers.power","action":"Pterodactyl\Http\Controllers\Api\Client\Servers\PowerController@index"},{"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":["POST"],"uri":"api\/remote\/download-file","name":"api.remote.download_file","action":"Pterodactyl\Http\Controllers\Api\Remote\FileDownloadController@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":"api\/remote\/scripts\/{uuid}","name":"api.remote.scripts","action":"Pterodactyl\Http\Controllers\Api\Remote\EggInstallController@index"},{"host":null,"methods":["POST"],"uri":"api\/remote\/sftp","name":"api.remote.sftp","action":"Pterodactyl\Http\Controllers\Api\Remote\SftpController@index"},{"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\/configure\/{token}","name":"daemon.configuration","action":"Pterodactyl\Http\Controllers\Daemon\ActionController@configuration"},{"host":null,"methods":["POST"],"uri":"daemon\/install","name":"daemon.install","action":"Pterodactyl\Http\Controllers\Daemon\ActionController@markInstall"}], prefix: '', route : function (name, parameters, route) { diff --git a/resources/lang/en/command/messages.php b/resources/lang/en/command/messages.php index 77f67c66..4a525032 100644 --- a/resources/lang/en/command/messages.php +++ b/resources/lang/en/command/messages.php @@ -37,6 +37,10 @@ return [ ], 'server' => [ 'rebuild_failed' => 'Rebuild request for ":name" (#:id) on node ":node" failed with error: :message', + 'power' => [ + 'confirm' => 'You are about to perform a :action aganist :count servers. Do you wish to continue?', + 'action_failed' => 'Power action request for ":name" (#:id) on node ":node" failed with error: :message', + ], ], 'environment' => [ 'mail' => [ diff --git a/resources/lang/en/server.php b/resources/lang/en/server.php index cd89e1f8..8941c479 100644 --- a/resources/lang/en/server.php +++ b/resources/lang/en/server.php @@ -248,6 +248,14 @@ return [ 'title' => 'Reset Database Password', 'description' => 'Allows a user to reset passwords for databases.', ], + 'delete_database' => [ + 'title' => 'Delete Databases', + 'description' => 'Allows a user to delete databases for this server from the Panel.', + ], + 'create_database' => [ + 'title' => 'Create Database', + 'description' => 'Allows a user to create additional databases for this server.', + ], ], ], 'files' => [ diff --git a/resources/themes/pterodactyl/admin/api/new.blade.php b/resources/themes/pterodactyl/admin/api/new.blade.php index b5876ee8..b5db6a15 100644 --- a/resources/themes/pterodactyl/admin/api/new.blade.php +++ b/resources/themes/pterodactyl/admin/api/new.blade.php @@ -15,7 +15,7 @@ @section('content')
-
+
diff --git a/resources/themes/pterodactyl/admin/servers/new.blade.php b/resources/themes/pterodactyl/admin/servers/new.blade.php index bfb6760b..bad45231 100644 --- a/resources/themes/pterodactyl/admin/servers/new.blade.php +++ b/resources/themes/pterodactyl/admin/servers/new.blade.php @@ -111,7 +111,7 @@
- + MB
diff --git a/resources/themes/pterodactyl/admin/servers/view/build.blade.php b/resources/themes/pterodactyl/admin/servers/view/build.blade.php index f6e9e607..8900bf90 100644 --- a/resources/themes/pterodactyl/admin/servers/view/build.blade.php +++ b/resources/themes/pterodactyl/admin/servers/view/build.blade.php @@ -89,50 +89,79 @@
-
-
-

Allocation Management

-
-
-
- - -

The default connection address that will be used for this game server.

-
-
- -
- +
+
+
+
+

Application Feature Limits

-

Please note that due to software limitations you cannot assign identical ports on different IPs to the same server.

-
-
- -
- +
+
+
+ +
+ +
+

The total number of databases a user is allowed to create for this server. Leave blank to allow unlimmited.

+
+
+ +
+ +
+

This feature is not currently implemented. The total number of allocations a user is allowed to create for this server. Leave blank to allow unlimited.

+
+
-

Simply select which ports you would like to remove from the list above. If you want to assign a port on a different IP that is already in use you can select it from the left and delete it here.

-
diff --git a/resources/themes/pterodactyl/base/api/index.blade.php b/resources/themes/pterodactyl/base/api/index.blade.php index e21e5aec..24a71734 100644 --- a/resources/themes/pterodactyl/base/api/index.blade.php +++ b/resources/themes/pterodactyl/base/api/index.blade.php @@ -18,57 +18,70 @@ @endsection @section('content') -
-
-
-
-

@lang('base.api.index.list')

-
- +
+
+
+
+

Credentials List

+
-
-
- - +
+
- - - - + + + + - @foreach ($keys as $key) + @foreach($keys as $key) + - - - - + @endforeach - -
@lang('strings.memo')@lang('strings.public_key')KeyMemoLast UsedCreated
+ + •••••••• + + + {{ $key->memo }}{{ $key->identifier . decrypt($key->token) }} @if(!is_null($key->last_used_at)) @datetimeHuman($key->last_used_at) - @else + @else — @endif - + @datetimeHuman($key->created_at) + + +
+ +
-
@endsection @section('footer-scripts') @parent -@endsection - @section('content') -
-
-
-
-
@lang('base.api.new.form_title')
-
-
-
-
- - -

@lang('base.api.new.descriptive_memo.description')

-
-
- - -

@lang('base.api.new.allowed_ips.description')

-
-
-
-
- {!! csrf_field() !!} - + +
+
+
+
+ +
+

Set an easy to understand description for this API key to help you identify it later on.

-
+
+
+
+
+ + +
+

If you would like to limit this API key to specific IP addresses enter them above, one per line. CIDR notation is allowed for each IP address. Leave blank to allow any IP address.

+
+ +
+
+
- @endsection diff --git a/resources/themes/pterodactyl/layouts/master.blade.php b/resources/themes/pterodactyl/layouts/master.blade.php index 39e5a9dd..060c76ce 100644 --- a/resources/themes/pterodactyl/layouts/master.blade.php +++ b/resources/themes/pterodactyl/layouts/master.blade.php @@ -101,11 +101,11 @@ @lang('navigation.account.security_controls') - {{--
  • --}} - {{----}} - {{-- @lang('navigation.account.api_access')--}} - {{----}} - {{--
  • --}} +
  • + + @lang('navigation.account.api_access') + +
  • @lang('navigation.account.my_servers') diff --git a/resources/themes/pterodactyl/server/databases/index.blade.php b/resources/themes/pterodactyl/server/databases/index.blade.php index 6ba24c1a..fb618b64 100644 --- a/resources/themes/pterodactyl/server/databases/index.blade.php +++ b/resources/themes/pterodactyl/server/databases/index.blade.php @@ -21,15 +21,10 @@ @section('content')
    -
    +
    @if(count($databases) > 0)
    @@ -55,11 +50,20 @@ {{ $database->host->host }}:{{ $database->host->port }} - @can('reset-db-password', $server) + @if(Gate::allows('reset-db-password', $server) || Gate::allows('delete-database', $server)) - + @can('delete-database', $server) + + @endcan + @can('reset-db-password', $server) + + @endcan - @endcan + @endif @endforeach @@ -69,17 +73,49 @@
    @lang('server.config.database.no_dbs') - @if(Auth::user()->root_admin === 1) - @lang('server.config.database.add_db') - @endif
    @endif
    + @if($allowCreation && Gate::allows('create-database', $server)) +
    +
    +
    +

    Create New Database

    +
    + @if($overLimit) +
    +
    + You are currently using {{ count($databases) }} of your {{ $server->database_limit ?? '∞' }} allowed databases. +
    +
    + @else +
    +
    +
    + +
    + s{{ $server->id }}_ + +
    +
    +
    + + +

    This should reflect the IP address that connections are allowed from. Uses standard MySQL notation. If unsure leave as %.

    +
    +
    + +
    + @endif +
    +
    + @endif
    @endsection @@ -126,5 +162,37 @@ }); }); @endcan + @can('delete-database', $server) + $('[data-action="delete-database"]').click(function (event) { + event.preventDefault(); + var self = $(this); + swal({ + title: '', + type: 'warning', + text: 'Are you sure that you want to delete this database? There is no going back, all data will immediately be removed.', + showCancelButton: true, + confirmButtonText: 'Delete', + confirmButtonColor: '#d9534f', + closeOnConfirm: false, + showLoaderOnConfirm: true, + }, function () { + $.ajax({ + method: 'DELETE', + url: Router.route('server.databases.delete', { server: '{{ $server->uuidShort }}', database: self.data('id') }), + headers: { 'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content') }, + }).done(function () { + self.parent().parent().slideUp(); + swal.close(); + }).fail(function (jqXHR) { + console.error(jqXHR); + swal({ + type: 'error', + title: 'Whoops!', + text: (typeof jqXHR.responseJSON.error !== 'undefined') ? jqXHR.responseJSON.error : 'An error occured while processing this request.' + }); + }); + }); + }); + @endcan @endsection diff --git a/routes/api-client.php b/routes/api-client.php new file mode 100644 index 00000000..b1f4c1b1 --- /dev/null +++ b/routes/api-client.php @@ -0,0 +1,28 @@ +name('api.client.index'); + +/* +|-------------------------------------------------------------------------- +| Client Control API +|-------------------------------------------------------------------------- +| +| Endpoint: /api/client/servers/{server} +| +*/ +Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateClientAccess::class]], function () { + Route::get('/', 'Servers\ServerController@index')->name('api.client.servers.view'); + + Route::post('/command', 'Servers\CommandController@index')->name('api.client.servers.command'); + Route::post('/power', 'Servers\PowerController@index')->name('api.client.servers.power'); +}); diff --git a/routes/api-remote.php b/routes/api-remote.php index a06a72fe..5566651d 100644 --- a/routes/api-remote.php +++ b/routes/api-remote.php @@ -1,12 +1,7 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ + Route::get('/authenticate/{token}', 'ValidateKeyController@index')->name('api.remote.authenticate'); +Route::post('/download-file', 'FileDownloadController@index')->name('api.remote.download_file'); Route::group(['prefix' => '/eggs'], function () { Route::get('/', 'EggRetrievalController@index')->name('api.remote.eggs'); diff --git a/routes/api.php b/routes/api.php deleted file mode 100644 index 96dfe5dd..00000000 --- a/routes/api.php +++ /dev/null @@ -1,27 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ -//Route::get('/', 'CoreController@index')->name('api.user'); -// -///* -//|-------------------------------------------------------------------------- -//| Server Controller Routes -//|-------------------------------------------------------------------------- -//| -//| Endpoint: /api/user/server/{server} -//| -//*/ -//Route::group([ -// 'prefix' => '/server/{server}', -// 'middleware' => 'server', -//], function () { -// Route::get('/', 'ServerController@index')->name('api.user.server'); -// -// Route::post('/power', 'ServerController@power')->name('api.user.server.power'); -// Route::post('/command', 'ServerController@command')->name('api.user.server.command'); -//}); diff --git a/routes/base.php b/routes/base.php index 3947b755..4955afd4 100644 --- a/routes/base.php +++ b/routes/base.php @@ -30,16 +30,15 @@ Route::group(['prefix' => 'account'], function () { | | Endpoint: /account/api | -| Temporarily Disabled */ -//Route::group(['prefix' => 'account/api'], function () { -// Route::get('/', 'AccountKeyController@index')->name('account.api'); -// Route::get('/new', 'AccountKeyController@create')->name('account.api.new'); -// -// Route::post('/new', 'AccountKeyController@store'); -// -// Route::delete('/revoke/{identifier}', 'AccountKeyController@revoke')->name('account.api.revoke'); -//}); +Route::group(['prefix' => 'account/api'], function () { + Route::get('/', 'ClientApiController@index')->name('account.api'); + Route::get('/new', 'ClientApiController@create')->name('account.api.new'); + + Route::post('/new', 'ClientApiController@store'); + + Route::delete('/revoke/{identifier}', 'ClientApiController@delete')->name('account.api.revoke'); +}); /* |-------------------------------------------------------------------------- diff --git a/routes/daemon.php b/routes/daemon.php index b74a005a..2c8058e3 100644 --- a/routes/daemon.php +++ b/routes/daemon.php @@ -10,5 +10,4 @@ Route::get('/packs/pull/{uuid}', 'PackController@pull')->name('daemon.pack.pull' Route::get('/packs/pull/{uuid}/hash', 'PackController@hash')->name('daemon.pack.hash'); Route::get('/configure/{token}', 'ActionController@configuration')->name('daemon.configuration'); -Route::post('/download', 'ActionController@authenticateDownload')->name('daemon.download'); Route::post('/install', 'ActionController@markInstall')->name('daemon.install'); diff --git a/routes/server.php b/routes/server.php index 85283df9..a05f4b00 100644 --- a/routes/server.php +++ b/routes/server.php @@ -38,7 +38,11 @@ Route::group(['prefix' => 'settings'], function () { Route::group(['prefix' => 'databases'], function () { Route::get('/', 'DatabaseController@index')->name('server.databases.index'); + Route::post('/new', 'DatabaseController@store')->name('server.databases.new'); + Route::patch('/password', 'DatabaseController@update')->middleware('server..database')->name('server.databases.password'); + + Route::delete('/delete/{database}', 'DatabaseController@delete')->middleware('server..database')->name('server.databases.delete'); }); /* diff --git a/tests/TestCase.php b/tests/TestCase.php index 427744f7..4d4ce896 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,6 +3,7 @@ namespace Tests; use Cake\Chronos\Chronos; +use Illuminate\Support\Facades\Hash; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase @@ -16,6 +17,7 @@ abstract class TestCase extends BaseTestCase { parent::setUp(); + Hash::setRounds(4); $this->setKnownUuidFactory(); } diff --git a/tests/Unit/Commands/Server/BulkPowerActionCommandTest.php b/tests/Unit/Commands/Server/BulkPowerActionCommandTest.php new file mode 100644 index 00000000..684baf93 --- /dev/null +++ b/tests/Unit/Commands/Server/BulkPowerActionCommandTest.php @@ -0,0 +1,164 @@ +powerRepository = m::mock(PowerRepositoryInterface::class); + $this->repository = m::mock(ServerRepositoryInterface::class); + } + + /** + * Test that an action can be sent to all servers. + */ + public function testSendAction() + { + $servers = factory(Server::class)->times(2)->make(); + + $this->repository->shouldReceive('getServersForPowerActionCount') + ->once() + ->with([], []) + ->andReturn(2); + + $this->repository->shouldReceive('getServersForPowerAction') + ->once() + ->with([], []) + ->andReturn($servers); + + for ($i = 0; $i < count($servers); $i++) { + $this->powerRepository->shouldReceive('setServer->sendSignal') + ->once() + ->with('kill') + ->andReturnNull(); + } + + $display = $this->runCommand($this->getCommand(), ['action' => 'kill'], ['yes']); + + $this->assertNotEmpty($display); + $this->assertContains('2/2', $display); + $this->assertContains(trans('command/messages.server.power.confirm', ['action' => 'kill', 'count' => 2]), $display); + } + + /** + * Test filtering servers and nodes. + */ + public function testSendWithFilters() + { + $server = factory(Server::class)->make(); + + $this->repository->shouldReceive('getServersForPowerActionCount') + ->once() + ->with([1, 2], [3, 4]) + ->andReturn(1); + + $this->repository->shouldReceive('getServersForPowerAction') + ->once() + ->with([1, 2], [3, 4]) + ->andReturn([$server]); + + $this->powerRepository->shouldReceive('setServer->sendSignal') + ->once() + ->with('kill') + ->andReturnNull(); + + $display = $this->runCommand($this->getCommand(), [ + 'action' => 'kill', + '--servers' => '1,2', + '--nodes' => '3,4', + ], ['yes']); + + $this->assertNotEmpty($display); + $this->assertContains('1/1', $display); + $this->assertContains(trans('command/messages.server.power.confirm', ['action' => 'kill', 'count' => 1]), $display); + } + + /** + * Test that sending empty options returns the expected results. + */ + public function testSendWithEmptyOptions() + { + $server = factory(Server::class)->make(); + + $this->repository->shouldReceive('getServersForPowerActionCount') + ->once() + ->with([], []) + ->andReturn(1); + + $this->repository->shouldReceive('getServersForPowerAction')->once()->with([], [])->andReturn([$server]); + $this->powerRepository->shouldReceive('setServer->sendSignal')->once()->with('kill')->andReturnNull(); + + $display = $this->runCommand($this->getCommand(), [ + 'action' => 'kill', + '--servers' => '', + '--nodes' => '', + ], ['yes']); + + $this->assertNotEmpty($display); + $this->assertContains('1/1', $display); + $this->assertContains(trans('command/messages.server.power.confirm', ['action' => 'kill', 'count' => 1]), $display); + } + + /** + * Test that validation occurrs correctly. + * + * @param array $data + * + * @dataProvider validationFailureDataProvider + * @expectedException \Illuminate\Validation\ValidationException + */ + public function testValidationErrors(array $data) + { + $this->runCommand($this->getCommand(), $data); + } + + /** + * Provide invalid data for the command. + * + * @return array + */ + public function validationFailureDataProvider(): array + { + return [ + [['action' => 'hodor']], + [['action' => 'hodor', '--servers' => 'hodor']], + [['action' => 'kill', '--servers' => 'hodor']], + [['action' => 'kill', '--servers' => '1,2,3', '--nodes' => 'hodor']], + [['action' => 'kill', '--servers' => '1,2,3', '--nodes' => '1,2,test']], + ]; + } + + /** + * Return an instance of the command with mocked dependencies. + * + * @return \Pterodactyl\Console\Commands\Server\BulkPowerActionCommand + */ + private function getCommand(): BulkPowerActionCommand + { + return new BulkPowerActionCommand($this->powerRepository, $this->repository, $this->app->make(Factory::class)); + } +} diff --git a/tests/Unit/Http/Controllers/Server/Files/DownloadControllerTest.php b/tests/Unit/Http/Controllers/Server/Files/DownloadControllerTest.php index d44481c1..b76cbfb8 100644 --- a/tests/Unit/Http/Controllers/Server/Files/DownloadControllerTest.php +++ b/tests/Unit/Http/Controllers/Server/Files/DownloadControllerTest.php @@ -1,17 +1,10 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Tests\Unit\Http\Controllers\Server\Files; use Mockery as m; -use phpmock\phpunit\PHPMock; use Pterodactyl\Models\Node; +use Tests\Traits\MocksUuids; use Pterodactyl\Models\Server; use Illuminate\Cache\Repository; use Tests\Unit\Http\Controllers\ControllerTestCase; @@ -19,7 +12,7 @@ use Pterodactyl\Http\Controllers\Server\Files\DownloadController; class DownloadControllerTest extends ControllerTestCase { - use PHPMock; + use MocksUuids; /** * @var \Illuminate\Cache\Repository|\Mockery\Mock @@ -48,16 +41,20 @@ class DownloadControllerTest extends ControllerTestCase $this->setRequestAttribute('server', $server); $controller->shouldReceive('authorize')->with('download-files', $server)->once()->andReturnNull(); - $this->getFunctionMock('\\Pterodactyl\\Http\\Controllers\\Server\\Files', 'str_random') - ->expects($this->once())->willReturn('randomString'); - $this->cache->shouldReceive('tags')->with(['Server:Downloads'])->once()->andReturnSelf(); - $this->cache->shouldReceive('put')->with('randomString', ['server' => $server->uuid, 'path' => '/my/file.txt'], 5)->once()->andReturnNull(); + $this->cache->shouldReceive('put') + ->once() + ->with('Server:Downloads:' . $this->getKnownUuid(), ['server' => $server->uuid, 'path' => '/my/file.txt'], 5) + ->andReturnNull(); $response = $controller->index($this->request, $server->uuidShort, '/my/file.txt'); $this->assertIsRedirectResponse($response); $this->assertRedirectUrlEquals(sprintf( - '%s://%s:%s/v1/server/file/download/%s', $server->node->scheme, $server->node->fqdn, $server->node->daemonListen, 'randomString' + '%s://%s:%s/v1/server/file/download/%s', + $server->node->scheme, + $server->node->fqdn, + $server->node->daemonListen, + $this->getKnownUuid() ), $response); } diff --git a/tests/Unit/Http/Middleware/Api/Application/AuthenticateIPAccessTest.php b/tests/Unit/Http/Middleware/API/AuthenticateIPAccessTest.php similarity index 85% rename from tests/Unit/Http/Middleware/Api/Application/AuthenticateIPAccessTest.php rename to tests/Unit/Http/Middleware/API/AuthenticateIPAccessTest.php index cf23d029..babd9535 100644 --- a/tests/Unit/Http/Middleware/Api/Application/AuthenticateIPAccessTest.php +++ b/tests/Unit/Http/Middleware/API/AuthenticateIPAccessTest.php @@ -1,10 +1,10 @@ make(['allowed_ips' => ['127.0.0.1']]); + $model = factory(ApiKey::class)->make(['allowed_ips' => '["127.0.0.1"]']); $this->setRequestAttribute('api_key', $model); $this->request->shouldReceive('ip')->withNoArgs()->once()->andReturn('127.0.0.1'); @@ -38,7 +38,7 @@ class AuthenticateIPAccessTest extends MiddlewareTestCase */ public function testValidIPAganistCIDRRange() { - $model = factory(ApiKey::class)->make(['allowed_ips' => ['192.168.1.1/28']]); + $model = factory(ApiKey::class)->make(['allowed_ips' => '["192.168.1.1/28"]']); $this->setRequestAttribute('api_key', $model); $this->request->shouldReceive('ip')->withNoArgs()->once()->andReturn('192.168.1.15'); @@ -54,10 +54,10 @@ class AuthenticateIPAccessTest extends MiddlewareTestCase */ public function testWithInvalidIP() { - $model = factory(ApiKey::class)->make(['allowed_ips' => ['127.0.0.1']]); + $model = factory(ApiKey::class)->make(['allowed_ips' => '["127.0.0.1"]']); $this->setRequestAttribute('api_key', $model); - $this->request->shouldReceive('ip')->withNoArgs()->once()->andReturn('127.0.0.2'); + $this->request->shouldReceive('ip')->withNoArgs()->twice()->andReturn('127.0.0.2'); $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); } @@ -65,7 +65,7 @@ class AuthenticateIPAccessTest extends MiddlewareTestCase /** * Return an instance of the middleware to be used when testing. * - * @return \Pterodactyl\Http\Middleware\Api\Application\AuthenticateIPAccess + * @return \Pterodactyl\Http\Middleware\Api\AuthenticateIPAccess */ private function getMiddleware(): AuthenticateIPAccess { diff --git a/tests/Unit/Http/Middleware/Api/Application/AuthenticateKeyTest.php b/tests/Unit/Http/Middleware/API/AuthenticateKeyTest.php similarity index 74% rename from tests/Unit/Http/Middleware/Api/Application/AuthenticateKeyTest.php rename to tests/Unit/Http/Middleware/API/AuthenticateKeyTest.php index 48622226..354838b3 100644 --- a/tests/Unit/Http/Middleware/Api/Application/AuthenticateKeyTest.php +++ b/tests/Unit/Http/Middleware/API/AuthenticateKeyTest.php @@ -1,6 +1,6 @@ request->shouldReceive('bearerToken')->withNoArgs()->once()->andReturnNull(); try { - $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions(), ApiKey::TYPE_APPLICATION); } catch (HttpException $exception) { $this->assertEquals(401, $exception->getStatusCode()); $this->assertEquals(['WWW-Authenticate' => 'Bearer'], $exception->getHeaders()); @@ -68,7 +68,7 @@ class AuthenticateKeyTest extends MiddlewareTestCase $this->request->shouldReceive('bearerToken')->withNoArgs()->twice()->andReturn('abcd1234'); $this->repository->shouldReceive('findFirstWhere')->andThrow(new RecordNotFoundException); - $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions(), ApiKey::TYPE_APPLICATION); } /** @@ -90,7 +90,30 @@ class AuthenticateKeyTest extends MiddlewareTestCase 'last_used_at' => Chronos::now(), ])->once()->andReturnNull(); - $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions(), ApiKey::TYPE_APPLICATION); + $this->assertEquals($model, $this->request->attributes->get('api_key')); + } + + /** + * Test that a valid token can continue past the middleware when set as a user token. + */ + public function testValidTokenWithUserKey() + { + $model = factory(ApiKey::class)->make(); + + $this->request->shouldReceive('bearerToken')->withNoArgs()->twice()->andReturn($model->identifier . 'decrypted'); + $this->repository->shouldReceive('findFirstWhere')->with([ + ['identifier', '=', $model->identifier], + ['key_type', '=', ApiKey::TYPE_ACCOUNT], + ])->once()->andReturn($model); + $this->encrypter->shouldReceive('decrypt')->with($model->token)->once()->andReturn('decrypted'); + $this->auth->shouldReceive('guard->loginUsingId')->with($model->user_id)->once()->andReturnNull(); + + $this->repository->shouldReceive('withoutFreshModel->update')->with($model->id, [ + 'last_used_at' => Chronos::now(), + ])->once()->andReturnNull(); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions(), ApiKey::TYPE_ACCOUNT); $this->assertEquals($model, $this->request->attributes->get('api_key')); } @@ -111,13 +134,13 @@ class AuthenticateKeyTest extends MiddlewareTestCase ])->once()->andReturn($model); $this->encrypter->shouldReceive('decrypt')->with($model->token)->once()->andReturn('decrypted'); - $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions(), ApiKey::TYPE_APPLICATION); } /** * Return an instance of the middleware with mocked dependencies for testing. * - * @return \Pterodactyl\Http\Middleware\Api\Application\AuthenticateKey + * @return \Pterodactyl\Http\Middleware\Api\AuthenticateKey */ private function getMiddleware(): AuthenticateKey { diff --git a/tests/Unit/Http/Middleware/Api/Application/SetSessionDriverTest.php b/tests/Unit/Http/Middleware/API/SetSessionDriverTest.php similarity index 90% rename from tests/Unit/Http/Middleware/Api/Application/SetSessionDriverTest.php rename to tests/Unit/Http/Middleware/API/SetSessionDriverTest.php index 7804f820..0f33f673 100644 --- a/tests/Unit/Http/Middleware/Api/Application/SetSessionDriverTest.php +++ b/tests/Unit/Http/Middleware/API/SetSessionDriverTest.php @@ -1,13 +1,13 @@ databaseHostRepository = m::mock(DatabaseHostRepositoryInterface::class); + $this->managementService = m::mock(DatabaseManagementService::class); + $this->repository = m::mock(DatabaseRepositoryInterface::class); + + // Set configs for testing instances. + config()->set('pterodactyl.client_features.databases.enabled', true); + config()->set('pterodactyl.client_features.databases.allow_random', true); + } + + /** + * Test handling of non-random hosts when a host is found. + * + * @dataProvider databaseLimitDataProvider + */ + public function testNonRandomFoundHost($limit, $count) + { + config()->set('pterodactyl.client_features.databases.allow_random', false); + + $server = factory(Server::class)->make(['database_limit' => $limit]); + $model = factory(Database::class)->make(); + + $this->repository->shouldReceive('findCountWhere') + ->once() + ->with([['server_id', '=', $server->id]]) + ->andReturn($count); + + $this->databaseHostRepository->shouldReceive('setColumns->findWhere') + ->once() + ->with([['node_id', '=', $server->node_id]]) + ->andReturn(collect([$model])); + + $this->managementService->shouldReceive('create') + ->once() + ->with($server->id, [ + 'database_host_id' => $model->id, + 'database' => 'testdb', + 'remote' => null, + ]) + ->andReturn($model); + + $response = $this->getService()->handle($server, ['database' => 'testdb']); + + $this->assertInstanceOf(Database::class, $response); + $this->assertSame($model, $response); + } + + /** + * Test that an exception is thrown if in non-random mode and no host is found. + * + * @expectedException \Pterodactyl\Exceptions\Service\Database\NoSuitableDatabaseHostException + */ + public function testNonRandomNoHost() + { + config()->set('pterodactyl.client_features.databases.allow_random', false); + + $server = factory(Server::class)->make(['database_limit' => 1]); + + $this->repository->shouldReceive('findCountWhere') + ->once() + ->with([['server_id', '=', $server->id]]) + ->andReturn(0); + + $this->databaseHostRepository->shouldReceive('setColumns->findWhere') + ->once() + ->with([['node_id', '=', $server->node_id]]) + ->andReturn(collect()); + + $this->getService()->handle($server, []); + } + + /** + * Test handling of random host selection. + */ + public function testRandomFoundHost() + { + $server = factory(Server::class)->make(['database_limit' => 1]); + $model = factory(Database::class)->make(); + + $this->repository->shouldReceive('findCountWhere') + ->once() + ->with([['server_id', '=', $server->id]]) + ->andReturn(0); + + $this->databaseHostRepository->shouldReceive('setColumns->findWhere') + ->once() + ->with([['node_id', '=', $server->node_id]]) + ->andReturn(collect()); + + $this->databaseHostRepository->shouldReceive('setColumns->all') + ->once() + ->andReturn(collect([$model])); + + $this->managementService->shouldReceive('create') + ->once() + ->with($server->id, [ + 'database_host_id' => $model->id, + 'database' => 'testdb', + 'remote' => null, + ]) + ->andReturn($model); + + $response = $this->getService()->handle($server, ['database' => 'testdb']); + + $this->assertInstanceOf(Database::class, $response); + $this->assertSame($model, $response); + } + + /** + * Test that an exception is thrown when no host is found and random is allowed. + * + * @expectedException \Pterodactyl\Exceptions\Service\Database\NoSuitableDatabaseHostException + */ + public function testRandomNoHost() + { + $server = factory(Server::class)->make(['database_limit' => 1]); + + $this->repository->shouldReceive('findCountWhere') + ->once() + ->with([['server_id', '=', $server->id]]) + ->andReturn(0); + + $this->databaseHostRepository->shouldReceive('setColumns->findWhere') + ->once() + ->with([['node_id', '=', $server->node_id]]) + ->andReturn(collect()); + + $this->databaseHostRepository->shouldReceive('setColumns->all') + ->once() + ->andReturn(collect()); + + $this->getService()->handle($server, []); + } + + /** + * Test that a server over the database limit throws an exception. + * + * @dataProvider databaseExceedingLimitDataProvider + * @expectedException \Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException + */ + public function testServerOverDatabaseLimit($limit, $count) + { + $server = factory(Server::class)->make(['database_limit' => $limit]); + + $this->repository->shouldReceive('findCountWhere') + ->once() + ->with([['server_id', '=', $server->id]]) + ->andReturn($count); + + $this->getService()->handle($server, []); + } + + /** + * Test that an exception is thrown if the feature is not enabled. + * + * @expectedException \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException + */ + public function testFeatureNotEnabled() + { + config()->set('pterodactyl.client_features.databases.enabled', false); + + $this->getService()->handle(factory(Server::class)->make(), []); + } + + /** + * Provide limits and current database counts for testing. + * + * @return array + */ + public function databaseLimitDataProvider(): array + { + return [ + [null, 10], + [1, 0], + ]; + } + + /** + * Provide data for servers over their database limit. + * + * @return array + */ + public function databaseExceedingLimitDataProvider(): array + { + return [ + [2, 2], + [2, 3], + ]; + } + + /** + * Return an instance of the service with mocked dependencies for testing. + * + * @return \Pterodactyl\Services\Databases\DeployServerDatabaseService + */ + private function getService(): DeployServerDatabaseService + { + return new DeployServerDatabaseService($this->repository, $this->databaseHostRepository, $this->managementService); + } +} diff --git a/tests/Unit/Services/Servers/ReinstallServerServiceTest.php b/tests/Unit/Services/Servers/ReinstallServerServiceTest.php index 349aa571..f00614f7 100644 --- a/tests/Unit/Services/Servers/ReinstallServerServiceTest.php +++ b/tests/Unit/Services/Servers/ReinstallServerServiceTest.php @@ -81,10 +81,9 @@ class ReinstallServerServiceTest extends TestCase $this->repository->shouldNotReceive('find'); $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($this->server->id, [ - 'installed' => 0, - ])->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFreshModel->update')->with($this->server->id, [ + 'installed' => 0, + ], true, true)->once()->andReturnNull(); $this->daemonServerRepository->shouldReceive('setServer')->with($this->server)->once()->andReturnSelf() ->shouldReceive('reinstall')->withNoArgs()->once()->andReturn(new Response); @@ -101,10 +100,9 @@ class ReinstallServerServiceTest extends TestCase $this->repository->shouldReceive('find')->with($this->server->id)->once()->andReturn($this->server); $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($this->server->id, [ - 'installed' => 0, - ])->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFreshModel->update')->with($this->server->id, [ + 'installed' => 0, + ], true, true)->once()->andReturnNull(); $this->daemonServerRepository->shouldReceive('setServer')->with($this->server)->once()->andReturnSelf() ->shouldReceive('reinstall')->withNoArgs()->once()->andReturn(new Response); @@ -121,10 +119,9 @@ class ReinstallServerServiceTest extends TestCase public function testExceptionThrownByGuzzleShouldBeReRenderedAsDisplayable() { $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($this->server->id, [ - 'installed' => 0, - ])->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFreshModel->update')->with($this->server->id, [ + 'installed' => 0, + ], true, true)->once()->andReturnNull(); $this->daemonServerRepository->shouldReceive('setServer')->with($this->server)->once()->andThrow($this->exception); @@ -139,10 +136,9 @@ class ReinstallServerServiceTest extends TestCase public function testExceptionNotThrownByGuzzleShouldNotBeTransformedToDisplayable() { $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($this->server->id, [ - 'installed' => 0, - ])->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFreshModel->update')->with($this->server->id, [ + 'installed' => 0, + ], true, true)->once()->andReturnNull(); $this->daemonServerRepository->shouldReceive('setServer')->with($this->server)->once()->andThrow(new Exception());