diff --git a/app/Exceptions/DisplayException.php b/app/Exceptions/DisplayException.php index e6488f89c..e3a839c5e 100644 --- a/app/Exceptions/DisplayException.php +++ b/app/Exceptions/DisplayException.php @@ -46,6 +46,14 @@ class DisplayException extends PterodactylException return $this->level; } + /** + * @return int + */ + public function getStatusCode() + { + return Response::HTTP_BAD_REQUEST; + } + /** * Render the exception to the user by adding a flashed message to the session * and then redirecting them back to the page that they came from. If the diff --git a/app/Extensions/Spatie/Fractalistic/Fractal.php b/app/Extensions/Spatie/Fractalistic/Fractal.php index 753cd4758..35a8b8f3b 100644 --- a/app/Extensions/Spatie/Fractalistic/Fractal.php +++ b/app/Extensions/Spatie/Fractalistic/Fractal.php @@ -3,8 +3,8 @@ namespace Pterodactyl\Extensions\Spatie\Fractalistic; use League\Fractal\TransformerAbstract; +use Spatie\Fractal\Fractal as SpatieFractal; use League\Fractal\Serializer\JsonApiSerializer; -use Spatie\Fractalistic\Fractal as SpatieFractal; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use League\Fractal\Pagination\IlluminatePaginatorAdapter; diff --git a/app/Http/Controllers/Api/Application/Servers/DatabaseController.php b/app/Http/Controllers/Api/Application/Servers/DatabaseController.php index e1fec4943..41d637489 100644 --- a/app/Http/Controllers/Api/Application/Servers/DatabaseController.php +++ b/app/Http/Controllers/Api/Application/Servers/DatabaseController.php @@ -2,13 +2,32 @@ namespace Pterodactyl\Http\Controllers\Api\Application\Servers; +use Illuminate\Http\Response; use Pterodactyl\Models\Server; +use Pterodactyl\Models\Database; +use Illuminate\Http\JsonResponse; +use Pterodactyl\Services\Databases\DatabasePasswordService; +use Pterodactyl\Services\Databases\DatabaseManagementService; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Transformers\Api\Application\ServerDatabaseTransformer; use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; +use Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabaseRequest; +use Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabasesRequest; +use Pterodactyl\Http\Requests\Api\Application\Servers\Databases\ServerDatabaseWriteRequest; +use Pterodactyl\Http\Requests\Api\Application\Servers\Databases\StoreServerDatabaseRequest; class DatabaseController extends ApplicationApiController { + /** + * @var \Pterodactyl\Services\Databases\DatabaseManagementService + */ + private $databaseManagementService; + + /** + * @var \Pterodactyl\Services\Databases\DatabasePasswordService + */ + private $databasePasswordService; + /** * @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface */ @@ -17,12 +36,19 @@ class DatabaseController extends ApplicationApiController /** * DatabaseController constructor. * + * @param \Pterodactyl\Services\Databases\DatabaseManagementService $databaseManagementService + * @param \Pterodactyl\Services\Databases\DatabasePasswordService $databasePasswordService * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository */ - public function __construct(DatabaseRepositoryInterface $repository) - { + public function __construct( + DatabaseManagementService $databaseManagementService, + DatabasePasswordService $databasePasswordService, + DatabaseRepositoryInterface $repository + ) { parent::__construct(); + $this->databaseManagementService = $databaseManagementService; + $this->databasePasswordService = $databasePasswordService; $this->repository = $repository; } @@ -30,10 +56,11 @@ class DatabaseController extends ApplicationApiController * Return a listing of all databases currently available to a single * server. * - * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabasesRequest $request + * @param \Pterodactyl\Models\Server $server * @return array */ - public function index(Server $server): array + public function index(GetServerDatabasesRequest $request, Server $server): array { $databases = $this->repository->getDatabasesForServer($server->id); @@ -41,4 +68,73 @@ class DatabaseController extends ApplicationApiController ->transformWith($this->getTransformer(ServerDatabaseTransformer::class)) ->toArray(); } + + /** + * Return a single server database. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabaseRequest $request + * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Database $database + * @return array + */ + public function view(GetServerDatabaseRequest $request, Server $server, Database $database): array + { + return $this->fractal->item($database) + ->transformWith($this->getTransformer(ServerDatabaseTransformer::class)) + ->toArray(); + } + + /** + * Reset the password for a specific server database. + * + * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Database $database + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function resetPassword(ServerDatabaseWriteRequest $request, Server $server, Database $database): Response + { + $this->databasePasswordService->handle($database, str_random(24)); + + return response('', 204); + } + + /** + * Create a new database on the Panel for a given server. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\StoreServerDatabaseRequest $request + * @param \Pterodactyl\Models\Server $server + * @return \Illuminate\Http\JsonResponse + * + * @throws \Exception + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function store(StoreServerDatabaseRequest $request, Server $server): JsonResponse + { + $database = $this->databaseManagementService->create($server->id, $request->validated()); + + return $this->fractal->item($database) + ->transformWith($this->getTransformer(ServerDatabaseTransformer::class)) + ->respond(201); + } + + /** + * Delete a specific database from the Panel. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\ServerDatabaseWriteRequest $request + * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Database $database + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function delete(ServerDatabaseWriteRequest $request, Server $server, Database $database): Response + { + $this->databaseManagementService->delete($database->id); + + return response('', 204); + } } diff --git a/app/Http/Middleware/Api/ApiSubstituteBindings.php b/app/Http/Middleware/Api/ApiSubstituteBindings.php index 430d4597b..f12cc91a1 100644 --- a/app/Http/Middleware/Api/ApiSubstituteBindings.php +++ b/app/Http/Middleware/Api/ApiSubstituteBindings.php @@ -3,11 +3,8 @@ namespace Pterodactyl\Http\Middleware\Api; use Closure; -use ReflectionMethod; -use Illuminate\Container\Container; -use Illuminate\Routing\ImplicitRouteBinding; -use Illuminate\Contracts\Routing\UrlRoutable; use Illuminate\Routing\Middleware\SubstituteBindings; +use Illuminate\Database\Eloquent\ModelNotFoundException; class ApiSubstituteBindings extends SubstituteBindings { @@ -24,47 +21,18 @@ class ApiSubstituteBindings extends SubstituteBindings $route = $request->route(); $this->router->substituteBindings($route); - $this->resolveForRoute($route); + + // Attempt to resolve bindings for this route. If one of the models + // cannot be resolved do not immediately return a 404 error. Set a request + // attribute that can be checked in the base API request class to only + // trigger a 404 after validating that the API key making the request is valid + // and even has permission to access the requested resource. + try { + $this->router->substituteImplicitBindings($route); + } catch (ModelNotFoundException $exception) { + $request->attributes->set('is_missing_model', true); + } return $next($request); } - - /** - * Resolve the implicit route bindings for the given route. This function - * overrides Laravel's default inn \Illuminate\Routing\ImplictRouteBinding - * to not throw a 404 error when a model is not found. - * - * If a model is not found using the provided information, the binding is - * replaced with null which is then checked in the form requests on API - * routes. This avoids a potential imformation leak on API routes for - * unauthenticated users. - * - * @param \Illuminate\Routing\Route $route - */ - protected function resolveForRoute($route) - { - $parameters = $route->parameters(); - - // Avoid having to copy and paste the entirety of that class into this middleware - // by using reflection to access a protected method. - $reflection = new ReflectionMethod(ImplicitRouteBinding::class, 'getParameterName'); - $reflection->setAccessible(true); - - foreach ($route->signatureParameters(UrlRoutable::class) as $parameter) { - if (! $parameterName = $reflection->invokeArgs(null, [$parameter->name, $parameters])) { - continue; - } - - $parameterValue = $parameters[$parameterName]; - - if ($parameterValue instanceof UrlRoutable) { - continue; - } - - // Try to find an existing model, if one is not found simply bind the - // parameter as null. - $instance = Container::getInstance()->make($parameter->getClass()->name); - $route->setParameter($parameterName, $instance->resolveRouteBinding($parameterValue)); - } - } } diff --git a/app/Http/Requests/Api/Application/ApplicationApiRequest.php b/app/Http/Requests/Api/Application/ApplicationApiRequest.php index 2cf7c8478..3d110d52c 100644 --- a/app/Http/Requests/Api/Application/ApplicationApiRequest.php +++ b/app/Http/Requests/Api/Application/ApplicationApiRequest.php @@ -73,7 +73,7 @@ abstract class ApplicationApiRequest extends FormRequest return $this->attributes->get('api_key'); } - /** + /* * Determine if the request passes the authorization check as well * as the exists check. * @@ -81,18 +81,24 @@ abstract class ApplicationApiRequest extends FormRequest * * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException */ + + /** + * @return bool + */ protected function passesAuthorization() { - $passes = parent::passesAuthorization(); + if (! parent::passesAuthorization()) { + return false; + } // Only let the user know that a resource does not exist if they are // authenticated to access the endpoint. This avoids exposing that // an item exists (or does not exist) to the user until they can prove // that they have permission to know about it. - if ($passes && ! $this->resourceExists()) { + if ($this->attributes->get('is_missing_model', false) || ! $this->resourceExists()) { throw new NotFoundHttpException('The requested resource does not exist on this server.'); } - return $passes; + return true; } } diff --git a/app/Http/Requests/Api/Application/Servers/Databases/GetServerDatabaseRequest.php b/app/Http/Requests/Api/Application/Servers/Databases/GetServerDatabaseRequest.php new file mode 100644 index 000000000..e398a5bbf --- /dev/null +++ b/app/Http/Requests/Api/Application/Servers/Databases/GetServerDatabaseRequest.php @@ -0,0 +1,32 @@ +route()->parameter('server'); + $database = $this->route()->parameter('database'); + + return $database->server_id === $server->id; + } +} diff --git a/app/Http/Requests/Api/Application/Servers/Databases/GetServerDatabasesRequest.php b/app/Http/Requests/Api/Application/Servers/Databases/GetServerDatabasesRequest.php new file mode 100644 index 000000000..3e6cfc6fe --- /dev/null +++ b/app/Http/Requests/Api/Application/Servers/Databases/GetServerDatabasesRequest.php @@ -0,0 +1,19 @@ + 'required|string|min:1|max:24', + 'remote' => 'required|string|min:1', + 'host' => 'required|integer|exists:database_hosts,id', + ]; + } + + /** + * Return data formatted in the correct format for the service to consume. + * + * @return array + */ + public function validated() + { + return [ + 'database' => $this->input('database'), + 'remote' => $this->input('remote'), + 'database_host_id' => $this->input('host'), + ]; + } + + /** + * Format error messages in a more understandable format for API output. + * + * @return array + */ + public function attributes() + { + return [ + 'host' => 'Database Host Server ID', + 'remote' => 'Remote Connection String', + 'database' => 'Database Name', + ]; + } +} diff --git a/routes/api-application.php b/routes/api-application.php index 1b72ef2da..85566a68e 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -89,7 +89,7 @@ Route::group(['prefix' => '/servers'], function () { Route::get('/{database}', 'Servers\DatabaseController@view')->name('api.application.servers.databases.view'); Route::post('/', 'Servers\DatabaseController@store'); - Route::patch('/{database}', 'Servers\DatabaseController@update'); + Route::post('/{database}/reset-password', 'Servers\DatabaseController@resetPassword'); Route::delete('/{database}', 'Servers\DatabaseController@delete'); });