Merge branch 'develop' into feature/bulk-reinstall-command

This commit is contained in:
Dane Everitt 2019-07-26 11:04:48 -04:00 committed by GitHub
commit 215351eeb3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
308 changed files with 18740 additions and 3400 deletions

View File

@ -1,8 +0,0 @@
{
"presets": ["es2015"],
"compact": true,
"minified": true,
"only": "public/themes/pterodactyl/js/frontend/files/src/*.js",
"sourceMaps": "inline",
"comments": false
}

View File

@ -21,6 +21,12 @@ debconf-set-selections <<< 'mariadb-server-5.5 mysql-server/root_password_again
# actually install
apt-get install -y php7.2 php7.2-cli php7.2-gd php7.2-mysql php7.2-pdo php7.2-mbstring php7.2-tokenizer php7.2-bcmath php7.2-xml php7.2-fpm php7.2-memcached php7.2-curl php7.2-zip php-xdebug mariadb-server nginx curl tar unzip git memcached > /dev/null
echo "Install nodejs and yarn"
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
apt-get -y install nodejs yarn > /dev/null
echo "Install composer"
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

26
.env.dusk Normal file
View File

@ -0,0 +1,26 @@
APP_ENV=local
APP_DEBUG=false
APP_KEY=NDWgIKKi9ovNK1PXZpzfNVSBdfCXGb5i
APP_JWT_KEY=test1234
APP_TIMEZONE=America/Los_Angeles
APP_URL=http://pterodactyl.local
CACHE_DRIVER=file
SESSION_DRIVER=file
HASHIDS_SALT=IqRr0g82tCTeuyxGs8RV
HASHIDS_LENGTH=8
MAIL_DRIVER=log
MAIL_FROM=support@pterodactyl.io
QUEUE_DRIVER=array
APP_SERVICE_AUTHOR=testing@pterodactyl.io
MAIL_FROM_NAME="Pterodactyl Panel"
RECAPTCHA_ENABLED=false
DB_CONNECTION=testing
TESTING_DB_HOST=192.168.1.202
TESTING_DB_DATABASE=panel_test
TESTING_DB_USERNAME=panel_test
TESTING_DB_PASSWORD=Test1234

View File

@ -28,4 +28,4 @@ MAIL_FROM=no-reply@example.com
QUEUE_HIGH=high
QUEUE_STANDARD=standard
QUEUE_LOW=low
QUEUE_LOW=low

View File

@ -1,9 +1,17 @@
---
name: "\U0001F41B Bug Report"
about: Create a report to help us resolve a bug or error
about: For reporting code or design bugs with the software. DO NOT REPORT APACHE/NGINX/PHP CONFIGURATION ISSUES.
---
DO NOT REPORT ISSUES CONFIGURING: SSL, PHP, APACHE, NGINX, YOUR MACHINE, SSH, SFTP, ETC. ON THIS GITHUB TRACKER.
For assistance installating this software, as well as debugging issues with dependencies, please use our discord server: https://discord.gg/pterodactyl
You MUST complete all of the below information when reporting a bug, failure to do so will result in closure of your issue. PLEASE stop spamming our tracker with "bugs" that are not related to this project.
**STOP: READ FIRST, AND THEN DELETE THE ABOVE LINES**
**Background (please complete the following information):**
* Panel or Daemon:
* Version of Panel/Daemon:

View File

@ -6,3 +6,9 @@ about: 'Visit our Discord for installation help: https://pterodactyl.io/discord'
We use GitHub issues only to discuss about Pterodactyl bugs and new features. For
this kind of questions about using Pterodactyl, please visit our Discord for assistance: https://pterodactyl.io/discord
DO NOT REPORT ISSUES CONFIGURING: SSL, PHP, APACHE, NGINX, YOUR MACHINE, SSH, SFTP, ETC. ON THIS GITHUB TRACKER.
For assistance installating this software, as well as debugging issues with dependencies, please use our discord server: https://discord.gg/pterodactyl
PLEASE stop spamming our tracker with "bugs" that are not related to this project.

14
.gitignore vendored
View File

@ -7,15 +7,12 @@ storage/framework/*
/.idea
/nbproject
package-lock.json
composer.lock
node_modules
_ide_helper_models.php
*.log
_ide_helper.php
sami.phar
/.sami
.phpstorm.meta.php
.php_cs.cache
public/assets/manifest.json
# For local development with docker
# Remove if we ever put the Dockerfile in the repo
@ -32,3 +29,6 @@ coverage.xml
# Vagrant
*.log
resources/lang/locales.js
resources/assets/pterodactyl/scripts/helpers/ziggy.js
resources/assets/scripts/helpers/ziggy.js

57
BUILDING.md Normal file
View File

@ -0,0 +1,57 @@
# Local Development
Pterodactyl is now powered by Vuejs and Tailwindcss and uses webpack at its core to generate compiled assets. Release
versions of Pterodactyl will include pre-compiled, minified, and hashed assets ready-to-go.
However, if you are interested in running custom themes or making modifications to the Vue files you'll need a build
system in place to generate these compiled assets. To get your environment setup, you'll first need to install at least Nodejs
`8`, and it is _highly_ recommended that you also install [Yarn](https://yarnpkg.com) to manage your `node_modules`.
### Install Dependencies
```bash
yarn install
```
The command above will download all of the dependencies necessary to get Pterodactyl assets building. After that, its as
simple as running the command below to generate assets while you're developing.
```bash
# build the compiled assets for development
yarn run build
# build the assets automatically when files are modified
yarn run watch
```
### Hot Module Reloading
For more advanced users, we also support 'Hot Module Reloading', allowing you to quickly see changes you're making
to the Vue template files without having to reload the page you're on. To Get started with this, you just need
to run the command below.
```bash
PUBLIC_PATH=http://192.168.1.1:8080 yarn run serve --host 192.168.1.1
```
There are two _very important_ parts of this command to take note of and change for your specific environment. The first
is the `--host` flag, which is required and should point to the machine where the `webpack-serve` server will be running.
The second is the `PUBLIC_PATH` environment variable which is the URL pointing to the HMR server and is appended to all of
the asset URLs used in Pterodactyl.
#### Vagrant
If you want to use HMR with our Vagrant image, you can use `yarn run v:serve` as a shortcut for the correct parameters.
In order to have proper file change detection you can use the [`vagrant-notify-forwarder`](https://github.com/mhallin/vagrant-notify-forwarder) to notify file events from the host to the VM.
```sh
vagrant plugin install vagrant-notify-forwarder
vagrant reload
```
### Building for Production
Once you have your files squared away and ready for the live server, you'll be needing to generate compiled, minified, and
hashed assets to push live. To do so, run the command below:
```bash
yarn run build:production
```
This will generate a production ready `bundle.js` and `bundle.css` as well as a `manifest.json` and store them in
the `/public/assets` directory where they can then be access by clients, and read by the Panel.

View File

@ -3,17 +3,60 @@ 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.14 (Derelict Dermodactylus)
### Fixed
* **[SECURITY]** Fixes an XSS vulnerability when performing certain actions in the file manager.
* **[SECURITY]** Attempting to login as a user who has 2FA enabled will no longer request the 2FA token before validating
that their password is correct. This closes a user existence leak that would expose that an account exists if
it had 2FA enabled.
### Changed
* Support for setting a node to listen on ports lower than 1024.
* QR code URLs are now generated without the use of an external library to reduce the dependency tree.
* Regenerated database passwords now respect the same settings that were used when initially created.
* Cleaned up 2FA QR code generation to use a more up-to-date library and API.
* Console charts now properly start at 0 and scale based on server configuration. No more crazy spikes that
are due to a change of one unit.
## v0.7.13 (Derelict Dermodactylus)
### Fixed
* Fixes a bug with the location update API endpoint throwing an error due to an unexected response value.
* Fixes bug where node creation API endpoint was not correctly requiring the `disk_overallocate` key.
* Prevents an exception from being thrown when a database with the same name is created on two different hosts.
* Fixes the redis password not saving correctly when setting up the environment from the command line.
* Fixes a bug with transaction handling in many areas of the application that would cause validation error messages
and other session data to not be persisted properly when using the database as the session driver.
* Fix a bug introduced at some point in the past that causes internal data integrity exceptions to not bubble up to
the user correctly, leading to extraneous and confusing exception messages.
* Fixes a bug causing servers to not be marked as having failed installation in some cases.
### Changed
* `allocation_limit` for servers now defaults to a null value, and is not required in PATCH/POST requests when adding
a server through the API.
* The `PATCH` endpoint for `/api/applications/servers/{server}/build` now accepts an array called `limits` to match
the response from the server `GET` endpoint.
### Added
* The server listing for a node is now paginated to 25 servers per page to improve performance on large nodes.
## v0.7.12 (Derelict Dermodactylus)
### Fixed
* Fixes an issue with the locations API endpoint referencing an invalid namespace.
* Fixes the `store()` function on the locations API not working due to an incorrect return typehint.
* Fixes daemon secrets not being able to be reset on a Node.
* Fixes an issue where files were not editable due to missing URL encoding in the file manager.
* Fixed checking of language changes
* Fixed Spigot egg not building versions other than `latest`.
* Fixed the Forge egg install script.
* Fixes a bug that would ignore the `skip_scripts` setting when creating or editing a server.
### Updated
* Upgraded core to use Laravel `5.7.14`.
* Updated Simplified Chinese translation pack.
### Added
* Added support for opening and editing Python files through the web editor.
* Adds Russian translation.
## v0.7.11 (Derelict Dermodactylus)
### Fixed
@ -211,7 +254,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines.
* Nest and Egg listings now show the associated ID in order to make API requests easier.
* Added star indicators to user listing in Admin CP to indicate users who are set as a root admin.
* Creating a new node will now requires a SSL connection if the Panel is configured to use SSL as well.
* Socketio error messages due to permissions are now rendered correctly in the UI rather than causing a silent failure.
* Connector error messages due to permissions are now rendered correctly in the UI rather than causing a silent failure.
* File manager now supports mass deletion option for files and folders.
* Support for CS:GO as a default service option selection.
* Support for GMOD as a default service option selection.
@ -341,7 +384,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines.
* Changed 2FA login process to be more secure. Previously authentication checking happened on the 2FA post page, now it happens prior and is passed along to the 2FA page to avoid storing any credentials.
### Added
* Socketio error messages due to permissions are now rendered correctly in the UI rather than causing a silent failure.
* Connector error messages due to permissions are now rendered correctly in the UI rather than causing a silent failure.
## v0.7.0-beta.1 (Derelict Dermodactylus)
### Added

View File

@ -1,4 +1,4 @@
[![Logo Image](https://cdn.pterodactyl.io/logos/Banner%20Logo%20Black@2x.png)](https://pterodactyl.io)
[![Logo Image](https://cdn.pterodactyl.io/logos/new/pterodactyl_logo.png)](https://pterodactyl.io)
[![Build status](https://img.shields.io/travis/pterodactyl/panel/develop.svg?style=flat-square)](https://travis-ci.org/pterodactyl/panel)
[![StyleCI](https://styleci.io/repos/47508644/shield?branch=develop)](https://styleci.io/repos/47508644)

View File

@ -178,9 +178,7 @@ class AppSettingsCommand extends Command
if ($askForRedisPassword) {
$this->output->comment(trans('command/messages.environment.app.redis_pass_help'));
$this->variables['REDIS_PASSWORD'] = $this->option('redis-pass') ?? $this->output->askHidden(
trans('command/messages.environment.app.redis_password'), function () {
return '';
}
trans('command/messages.environment.app.redis_password')
);
}

View File

@ -0,0 +1,15 @@
<?php
namespace Pterodactyl\Contracts\Http;
interface ClientPermissionsRequest
{
/**
* Returns the permissions string indicating which permission should be used to
* validate that the authenticated user has permission to perform this action aganist
* the given resource (server).
*
* @return string
*/
public function permission(): string;
}

View File

@ -13,19 +13,18 @@ interface FileRepositoryInterface extends BaseRepositoryInterface
* @param string $path
* @return \stdClass
*
* @throws \GuzzleHttp\Exception\RequestException
* @throws \GuzzleHttp\Exception\TransferException
*/
public function getFileStat(string $path): stdClass;
/**
* Return the contents of a given file if it can be edited in the Panel.
*
* @param string $path
* @param string $path
* @param int|null $notLargerThan
* @return string
*
* @throws \GuzzleHttp\Exception\RequestException
*/
public function getContent(string $path): string;
public function getContent(string $path, int $notLargerThan = null): string;
/**
* Save new contents to a given file.
@ -34,7 +33,7 @@ interface FileRepositoryInterface extends BaseRepositoryInterface
* @param string $content
* @return \Psr\Http\Message\ResponseInterface
*
* @throws \GuzzleHttp\Exception\RequestException
* @throws \GuzzleHttp\Exception\TransferException
*/
public function putContent(string $path, string $content): ResponseInterface;
@ -44,7 +43,41 @@ interface FileRepositoryInterface extends BaseRepositoryInterface
* @param string $path
* @return array
*
* @throws \GuzzleHttp\Exception\RequestException
* @throws \GuzzleHttp\Exception\TransferException
*/
public function getDirectory(string $path): array;
/**
* Creates a new directory for the server in the given $path.
*
* @param string $name
* @param string $path
* @return \Psr\Http\Message\ResponseInterface
*/
public function createDirectory(string $name, string $path): ResponseInterface;
/**
* Renames or moves a file on the remote machine.
*
* @param string $from
* @param string $to
* @return \Psr\Http\Message\ResponseInterface
*/
public function renameFile(string $from, string $to): ResponseInterface;
/**
* Copy a given file and give it a unique name.
*
* @param string $location
* @return \Psr\Http\Message\ResponseInterface
*/
public function copyFile(string $location): ResponseInterface;
/**
* Delete a file or folder for the server.
*
* @param string $location
* @return \Psr\Http\Message\ResponseInterface
*/
public function deleteFile(string $location): ResponseInterface;
}

View File

@ -55,16 +55,6 @@ interface NodeRepositoryInterface extends RepositoryInterface, SearchableInterfa
*/
public function loadNodeAllocations(Node $node, bool $refresh = false): Node;
/**
* Return a node with all of the servers attached to that node.
*
* @param int $id
* @return \Pterodactyl\Models\Node
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getNodeServers(int $id): Node;
/**
* Return a collection of nodes for all locations to use in server creation UI.
*

View File

@ -161,4 +161,14 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter
* @return int
*/
public function getSuspendedServersCount(): int;
/**
* Returns all of the servers that exist for a given node in a paginated response.
*
* @param int $node
* @param int $limit
*
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
public function loadAllServersForNode(int $node, int $limit): LengthAwarePaginator;
}

View File

@ -6,6 +6,7 @@ use Exception;
use PDOException;
use Psr\Log\LoggerInterface;
use Illuminate\Container\Container;
use Illuminate\Database\Connection;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Session\TokenMismatchException;
use Illuminate\Validation\ValidationException;
@ -14,6 +15,7 @@ use Illuminate\Database\Eloquent\ModelNotFoundException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
class Handler extends ExceptionHandler
{
@ -137,6 +139,41 @@ class Handler extends ExceptionHandler
*/
public function render($request, Exception $exception)
{
$connections = Container::getInstance()->make(Connection::class);
// If we are currently wrapped up inside a transaction, we will roll all the way
// back to the beginning. This needs to happen, otherwise session data does not
// get properly persisted.
//
// This is kind of a hack, and ideally things like this should be handled as
// much as possible at the code level, but there are a lot of spots that do a
// ton of actions and were written before this bug discovery was made.
//
// @see https://github.com/pterodactyl/panel/pull/1468
if ($connections->transactionLevel()) {
$connections->rollBack(0);
}
// Because of some breaking change snuck into a Laravel update that didn't get caught
// by any of the tests, exceptions implementing the HttpExceptionInterface get marked
// as being HttpExceptions, but aren't actually implementing the HttpException abstract.
//
// This is incredibly annoying because we can't just temporarily override the handler to
// allow these (at least without taking on a high maintenance cost). Laravel 5.8 fixes this,
// so when we update (or have updated) this code can be removed.
//
// @see https://github.com/laravel/framework/pull/25975
// @todo remove this code when upgrading to Laravel 5.8
if ($exception instanceof HttpExceptionInterface && ! $exception instanceof HttpException) {
$exception = new HttpException(
$exception->getStatusCode(),
$exception->getMessage(),
$exception,
$exception->getHeaders(),
$exception->getCode()
);
}
return parent::render($request, $exception);
}
@ -231,7 +268,7 @@ class Handler extends ExceptionHandler
protected function unauthenticated($request, AuthenticationException $exception)
{
if ($request->expectsJson()) {
return response()->json(['error' => 'Unauthenticated.'], 401);
return response()->json(self::convertToArray($exception), 401);
}
return redirect()->guest(route('auth.login'));

View File

@ -2,19 +2,28 @@
namespace Pterodactyl\Exceptions\Repository;
class RecordNotFoundException extends RepositoryException
use Illuminate\Http\Response;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
class RecordNotFoundException extends RepositoryException implements HttpExceptionInterface
{
/**
* Handle request to render this exception to a user. Returns the default
* 404 page view.
* Returns the status code.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
* @return int
*/
public function render($request)
public function getStatusCode()
{
if (! config('app.debug')) {
return response()->view('errors.404', [], 404);
}
return Response::HTTP_NOT_FOUND;
}
/**
* Returns response headers.
*
* @return array
*/
public function getHeaders()
{
return [];
}
}

View File

@ -24,6 +24,7 @@ use Pterodactyl\Services\Allocations\AssignmentService;
use Pterodactyl\Services\Helpers\SoftwareVersionService;
use Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Http\Requests\Admin\Node\AllocationFormRequest;
use Pterodactyl\Services\Allocations\AllocationDeletionService;
use Pterodactyl\Contracts\Repository\LocationRepositoryInterface;
@ -32,6 +33,11 @@ use Pterodactyl\Http\Requests\Admin\Node\AllocationAliasFormRequest;
class NodesController extends Controller
{
/**
* @var \Pterodactyl\Services\Allocations\AllocationDeletionService
*/
protected $allocationDeletionService;
/**
* @var \Prologue\Alerts\AlertsMessageBag
*/
@ -72,6 +78,11 @@ class NodesController extends Controller
*/
protected $repository;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
protected $serverRepository;
/**
* @var \Pterodactyl\Services\Nodes\NodeUpdateService
*/
@ -81,10 +92,6 @@ class NodesController extends Controller
* @var \Pterodactyl\Services\Helpers\SoftwareVersionService
*/
protected $versionService;
/**
* @var \Pterodactyl\Services\Allocations\AllocationDeletionService
*/
private $allocationDeletionService;
/**
* NodesController constructor.
@ -98,6 +105,7 @@ class NodesController extends Controller
* @param \Pterodactyl\Services\Nodes\NodeDeletionService $deletionService
* @param \Pterodactyl\Contracts\Repository\LocationRepositoryInterface $locationRepository
* @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $serverRepository
* @param \Pterodactyl\Services\Nodes\NodeUpdateService $updateService
* @param \Pterodactyl\Services\Helpers\SoftwareVersionService $versionService
*/
@ -111,6 +119,7 @@ class NodesController extends Controller
NodeDeletionService $deletionService,
LocationRepositoryInterface $locationRepository,
NodeRepositoryInterface $repository,
ServerRepositoryInterface $serverRepository,
NodeUpdateService $updateService,
SoftwareVersionService $versionService
) {
@ -123,6 +132,7 @@ class NodesController extends Controller
$this->deletionService = $deletionService;
$this->locationRepository = $locationRepository;
$this->repository = $repository;
$this->serverRepository = $serverRepository;
$this->updateService = $updateService;
$this->versionService = $versionService;
}
@ -178,8 +188,6 @@ class NodesController extends Controller
*
* @param \Pterodactyl\Models\Node $node
* @return \Illuminate\View\View
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function viewIndex(Node $node)
{
@ -235,19 +243,17 @@ class NodesController extends Controller
/**
* Shows the server listing page for a specific node.
*
* @param int $node
* @param \Pterodactyl\Models\Node $node
* @return \Illuminate\View\View
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function viewServers($node)
public function viewServers(Node $node)
{
$node = $this->repository->getNodeServers($node);
$servers = $this->serverRepository->loadAllServersForNode($node->id, 25);
Javascript::put([
'node' => collect($node->makeVisible('daemonSecret'))->only(['scheme', 'fqdn', 'daemonListen', 'daemonSecret']),
]);
return view('admin.nodes.view.servers', ['node' => $node]);
return view('admin.nodes.view.servers', ['node' => $node, 'servers' => $servers]);
}
/**

View File

@ -599,7 +599,7 @@ class ServersController extends Controller
['id', '=', $request->input('database')],
]);
$this->databasePasswordService->handle($database, str_random(20));
$this->databasePasswordService->handle($database, str_random(24));
return response('', 204);
}

View File

@ -13,8 +13,8 @@ use Pterodactyl\Transformers\Api\Application\LocationTransformer;
use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
use Pterodactyl\Http\Requests\Api\Application\Locations\GetLocationRequest;
use Pterodactyl\Http\Requests\Api\Application\Locations\GetLocationsRequest;
use Pterodactyl\Http\Requests\Api\Application\Locations\DeleteLocationRequest;
use Pterodactyl\Http\Requests\Api\Application\Locations\StoreLocationRequest;
use Pterodactyl\Http\Requests\Api\Application\Locations\DeleteLocationRequest;
use Pterodactyl\Http\Requests\Api\Application\Locations\UpdateLocationRequest;
class LocationController extends ApplicationApiController

View File

@ -0,0 +1,73 @@
<?php
namespace Pterodactyl\Http\Controllers\Api\Client;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Pterodactyl\Services\Users\UserUpdateService;
use Pterodactyl\Transformers\Api\Client\AccountTransformer;
use Pterodactyl\Http\Requests\Api\Client\Account\UpdateEmailRequest;
use Pterodactyl\Http\Requests\Api\Client\Account\UpdatePasswordRequest;
class AccountController extends ClientApiController
{
/**
* @var \Pterodactyl\Services\Users\UserUpdateService
*/
private $updateService;
/**
* AccountController constructor.
*
* @param \Pterodactyl\Services\Users\UserUpdateService $updateService
*/
public function __construct(UserUpdateService $updateService)
{
parent::__construct();
$this->updateService = $updateService;
}
/**
* @param Request $request
* @return array
*/
public function index(Request $request): array
{
return $this->fractal->item($request->user())
->transformWith($this->getTransformer(AccountTransformer::class))
->toArray();
}
/**
* Update the authenticated user's email address.
*
* @param \Pterodactyl\Http\Requests\Api\Client\Account\UpdateEmailRequest $request
* @return \Illuminate\Http\Response
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function updateEmail(UpdateEmailRequest $request): Response
{
$this->updateService->handle($request->user(), $request->validated());
return response('', Response::HTTP_CREATED);
}
/**
* Update the authenticated user's password.
*
* @param \Pterodactyl\Http\Requests\Api\Client\Account\UpdatePasswordRequest $request
* @return \Illuminate\Http\Response
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function updatePassword(UpdatePasswordRequest $request): Response
{
$this->updateService->handle($request->user(), $request->validated());
return response('', Response::HTTP_CREATED);
}
}

View File

@ -35,7 +35,9 @@ class ClientController extends ClientApiController
*/
public function index(GetServersRequest $request): array
{
$servers = $this->repository->filterUserAccessServers($request->user(), User::FILTER_LEVEL_SUBUSER, config('pterodactyl.paginate.frontend.servers'));
$servers = $this->repository
->setSearchTerm($request->input('query'))
->filterUserAccessServers($request->user(), User::FILTER_LEVEL_ALL);
return $this->fractal->collection($servers)
->transformWith($this->getTransformer(ServerTransformer::class))

View File

@ -0,0 +1,96 @@
<?php
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
use Illuminate\Http\Response;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Database;
use Pterodactyl\Transformers\Api\Client\DatabaseTransformer;
use Pterodactyl\Services\Databases\DatabaseManagementService;
use Pterodactyl\Services\Databases\DeployServerDatabaseService;
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Http\Requests\Api\Client\Servers\Databases\GetDatabasesRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Databases\StoreDatabaseRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Databases\DeleteDatabaseRequest;
class DatabaseController extends ClientApiController
{
/**
* @var \Pterodactyl\Services\Databases\DeployServerDatabaseService
*/
private $deployDatabaseService;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface
*/
private $repository;
/**
* @var \Pterodactyl\Services\Databases\DatabaseManagementService
*/
private $managementService;
/**
* DatabaseController constructor.
*
* @param \Pterodactyl\Services\Databases\DatabaseManagementService $managementService
* @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository
* @param \Pterodactyl\Services\Databases\DeployServerDatabaseService $deployDatabaseService
*/
public function __construct(
DatabaseManagementService $managementService,
DatabaseRepositoryInterface $repository,
DeployServerDatabaseService $deployDatabaseService
) {
parent::__construct();
$this->deployDatabaseService = $deployDatabaseService;
$this->repository = $repository;
$this->managementService = $managementService;
}
/**
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Databases\GetDatabasesRequest $request
* @return array
*/
public function index(GetDatabasesRequest $request): array
{
$databases = $this->repository->getDatabasesForServer($request->getModel(Server::class)->id);
return $this->fractal->collection($databases)
->transformWith($this->getTransformer(DatabaseTransformer::class))
->toArray();
}
/**
* Create a new database for the given server and return it.
*
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Databases\StoreDatabaseRequest $request
* @return array
*
* @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException
*/
public function store(StoreDatabaseRequest $request): array
{
$database = $this->deployDatabaseService->handle($request->getModel(Server::class), $request->validated());
return $this->fractal->item($database)
->parseIncludes(['password'])
->transformWith($this->getTransformer(DatabaseTransformer::class))
->toArray();
}
/**
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Databases\DeleteDatabaseRequest $request
* @return \Illuminate\Http\Response
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function delete(DeleteDatabaseRequest $request): Response
{
$this->managementService->delete($request->getModel(Database::class)->id);
return Response::create('', Response::HTTP_NO_CONTENT);
}
}

View File

@ -0,0 +1,186 @@
<?php
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
use Carbon\Carbon;
use Ramsey\Uuid\Uuid;
use Illuminate\Http\Response;
use Pterodactyl\Models\Server;
use Illuminate\Http\JsonResponse;
use Illuminate\Contracts\Cache\Repository as CacheRepository;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\CopyFileRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\ListFilesRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\DeleteFileRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\RenameFileRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\CreateFolderRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\DownloadFileRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\GetFileContentsRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\WriteFileContentRequest;
class FileController extends ClientApiController
{
/**
* @var \Illuminate\Contracts\Cache\Factory
*/
private $cache;
/**
* @var \Illuminate\Contracts\Config\Repository
*/
private $config;
/**
* @var \Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface
*/
private $fileRepository;
/**
* FileController constructor.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @param \Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface $fileRepository
* @param \Illuminate\Contracts\Cache\Repository $cache
*/
public function __construct(ConfigRepository $config, FileRepositoryInterface $fileRepository, CacheRepository $cache)
{
parent::__construct();
$this->cache = $cache;
$this->config = $config;
$this->fileRepository = $fileRepository;
}
/**
* Returns a listing of files in a given directory.
*
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\ListFilesRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function listDirectory(ListFilesRequest $request): JsonResponse
{
return JsonResponse::create([
'contents' => $this->fileRepository->setServer($request->getModel(Server::class))->getDirectory(
$request->get('directory') ?? '/'
),
'editable' => $this->config->get('pterodactyl.files.editable', []),
]);
}
/**
* Return the contents of a specified file for the user.
*
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\GetFileContentsRequest $request
* @return \Illuminate\Http\Response
*/
public function getFileContents(GetFileContentsRequest $request): Response
{
return Response::create(
$this->fileRepository->setServer($request->getModel(Server::class))->getContent(
$request->get('file'), $this->config->get('pterodactyl.files.max_edit_size')
)
);
}
/**
* Writes the contents of the specified file to the server.
*
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\WriteFileContentRequest $request
* @return \Illuminate\Http\Response
*/
public function writeFileContents(WriteFileContentRequest $request): Response
{
$this->fileRepository->setServer($request->getModel(Server::class))->putContent(
$request->get('file'),
$request->getContent()
);
return Response::create('', Response::HTTP_NO_CONTENT);
}
/**
* Creates a new folder on the server.
*
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\CreateFolderRequest $request
* @return \Illuminate\Http\Response
*/
public function createFolder(CreateFolderRequest $request): Response
{
$this->fileRepository
->setServer($request->getModel(Server::class))
->createDirectory($request->input('name'), $request->input('directory', '/'));
return Response::create('', Response::HTTP_NO_CONTENT);
}
/**
* Renames a file on the remote machine.
*
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\RenameFileRequest $request
* @return \Illuminate\Http\Response
*/
public function renameFile(RenameFileRequest $request): Response
{
$this->fileRepository
->setServer($request->getModel(Server::class))
->renameFile($request->input('rename_from'), $request->input('rename_to'));
return Response::create('', Response::HTTP_NO_CONTENT);
}
/**
* Copies a file on the server.
*
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\CopyFileRequest $request
* @return \Illuminate\Http\Response
*/
public function copyFile(CopyFileRequest $request): Response
{
$this->fileRepository
->setServer($request->getModel(Server::class))
->copyFile($request->input('location'));
return Response::create('', Response::HTTP_NO_CONTENT);
}
/**
* Deletes a file or folder from the server.
*
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\DeleteFileRequest $request
* @return \Illuminate\Http\Response
*/
public function delete(DeleteFileRequest $request): Response
{
$this->fileRepository
->setServer($request->getModel(Server::class))
->deleteFile($request->input('location'));
return Response::create('', Response::HTTP_NO_CONTENT);
}
/**
* Configure a reference to a file to download in the cache so that when the
* user hits the Daemon and it verifies with the Panel they'll actually be able
* to download that file.
*
* Returns the token that needs to be used when downloading the file.
*
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\DownloadFileRequest $request
* @return \Illuminate\Http\JsonResponse
* @throws \Exception
*/
public function download(DownloadFileRequest $request): JsonResponse
{
/** @var \Pterodactyl\Models\Server $server */
$server = $request->getModel(Server::class);
$token = Uuid::uuid4()->toString();
$this->cache->put(
'Server:Downloads:' . $token, ['server' => $server->uuid, 'path' => $request->route()->parameter('file')], Carbon::now()->addMinutes(5)
);
return JsonResponse::create(['token' => $token]);
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
use Pterodactyl\Models\Server;
use Pterodactyl\Transformers\Api\Client\AllocationTransformer;
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
use Pterodactyl\Http\Requests\Api\Client\Servers\Network\GetNetworkRequest;
class NetworkController extends ClientApiController
{
/**
* @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface
*/
private $repository;
/**
* NetworkController constructor.
*
* @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $repository
*/
public function __construct(AllocationRepositoryInterface $repository)
{
parent::__construct();
$this->repository = $repository;
}
/**
* Lists all of the allocations available to a server and wether or
* not they are currently assigned as the primary for this server.
*
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Network\GetNetworkRequest $request
* @return array
*/
public function index(GetNetworkRequest $request): array
{
$server = $request->getModel(Server::class);
$allocations = $this->repository->findWhere([
['server_id', '=', $server->id],
]);
return $this->fractal->collection($allocations)
->transformWith($this->getTransformer(AllocationTransformer::class))
->toArray();
}
}

View File

@ -0,0 +1,132 @@
<?php
namespace Pterodactyl\Http\Controllers\Auth;
use Illuminate\Http\Request;
use Pterodactyl\Models\User;
use Illuminate\Auth\AuthManager;
use Illuminate\Http\JsonResponse;
use Illuminate\Auth\Events\Failed;
use Illuminate\Contracts\Config\Repository;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
abstract class AbstractLoginController extends Controller
{
use AuthenticatesUsers;
/**
* Lockout time for failed login requests.
*
* @var int
*/
protected $lockoutTime;
/**
* After how many attempts should logins be throttled and locked.
*
* @var int
*/
protected $maxLoginAttempts;
/**
* Where to redirect users after login / registration.
*
* @var string
*/
protected $redirectTo = '/';
/**
* @var \Illuminate\Auth\AuthManager
*/
protected $auth;
/**
* @var \Illuminate\Contracts\Config\Repository
*/
protected $config;
/**
* LoginController constructor.
*
* @param \Illuminate\Auth\AuthManager $auth
* @param \Illuminate\Contracts\Config\Repository $config
*/
public function __construct(AuthManager $auth, Repository $config)
{
$this->lockoutTime = $config->get('auth.lockout.time');
$this->maxLoginAttempts = $config->get('auth.lockout.attempts');
$this->auth = $auth;
$this->config = $config;
}
/**
* Get the failed login response instance.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Contracts\Auth\Authenticatable|null $user
*
* @throws \Pterodactyl\Exceptions\DisplayException
*/
protected function sendFailedLoginResponse(Request $request, Authenticatable $user = null)
{
$this->incrementLoginAttempts($request);
$this->fireFailedLoginEvent($user, [
$this->getField($request->input('user')) => $request->input('user'),
]);
if ($request->route()->named('auth.login-checkpoint')) {
throw new DisplayException(trans('auth.two_factor.checkpoint_failed'));
}
throw new DisplayException(trans('auth.failed'));
}
/**
* Send the response after the user was authenticated.
*
* @param \Pterodactyl\Models\User $user
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
protected function sendLoginResponse(User $user, Request $request): JsonResponse
{
$request->session()->regenerate();
$this->clearLoginAttempts($request);
$this->auth->guard()->login($user, true);
return JsonResponse::create([
'data' => [
'complete' => true,
'intended' => $this->redirectPath(),
'user' => $user->toVueObject(),
],
]);
}
/**
* Determine if the user is logging in using an email or username,.
*
* @param string $input
* @return string
*/
protected function getField(string $input = null): string
{
return str_contains($input, '@') ? 'email' : 'username';
}
/**
* Fire a failed login event.
*
* @param \Illuminate\Contracts\Auth\Authenticatable|null $user
* @param array $credentials
*/
protected function fireFailedLoginEvent(Authenticatable $user = null, array $credentials = [])
{
event(new Failed('auth', $user, $credentials));
}
}

View File

@ -3,7 +3,7 @@
namespace Pterodactyl\Http\Controllers\Auth;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Password;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Events\Auth\FailedPasswordReset;
@ -18,9 +18,9 @@ class ForgotPasswordController extends Controller
*
* @param \Illuminate\Http\Request
* @param string $response
* @return \Illuminate\Http\RedirectResponse
* @return \Illuminate\Http\JsonResponse
*/
protected function sendResetLinkFailedResponse(Request $request, $response): RedirectResponse
protected function sendResetLinkFailedResponse(Request $request, $response): JsonResponse
{
// As noted in #358 we will return success even if it failed
// to avoid pointing out that an account does or does not
@ -29,4 +29,18 @@ class ForgotPasswordController extends Controller
return $this->sendResetLinkResponse($request, Password::RESET_LINK_SENT);
}
/**
* Get the response for a successful password reset link.
*
* @param \Illuminate\Http\Request $request
* @param string $response
* @return \Illuminate\Http\JsonResponse
*/
protected function sendResetLinkResponse(Request $request, $response): JsonResponse
{
return response()->json([
'status' => trans($response),
]);
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace Pterodactyl\Http\Controllers\Auth;
use Illuminate\Auth\AuthManager;
use Illuminate\Http\JsonResponse;
use PragmaRX\Google2FA\Google2FA;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Http\Requests\Auth\LoginCheckpointRequest;
use Illuminate\Contracts\Cache\Repository as CacheRepository;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
class LoginCheckpointController extends AbstractLoginController
{
/**
* @var \Illuminate\Contracts\Cache\Repository
*/
private $cache;
/**
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface
*/
private $repository;
/**
* @var \PragmaRX\Google2FA\Google2FA
*/
private $google2FA;
/**
* @var \Illuminate\Contracts\Encryption\Encrypter
*/
private $encrypter;
/**
* LoginCheckpointController constructor.
*
* @param \Illuminate\Auth\AuthManager $auth
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
* @param \PragmaRX\Google2FA\Google2FA $google2FA
* @param \Illuminate\Contracts\Config\Repository $config
* @param \Illuminate\Contracts\Cache\Repository $cache
* @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository
*/
public function __construct(
AuthManager $auth,
Encrypter $encrypter,
Google2FA $google2FA,
Repository $config,
CacheRepository $cache,
UserRepositoryInterface $repository
) {
parent::__construct($auth, $config);
$this->google2FA = $google2FA;
$this->cache = $cache;
$this->repository = $repository;
$this->encrypter = $encrypter;
}
/**
* Handle a login where the user is required to provide a TOTP authentication
* token. Once a user has reached this stage it is assumed that they have already
* provided a valid username and password.
*
* @param \Pterodactyl\Http\Requests\Auth\LoginCheckpointRequest $request
* @return \Illuminate\Http\JsonResponse
*
* @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
* @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
* @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
* @throws \Pterodactyl\Exceptions\DisplayException
*/
public function __invoke(LoginCheckpointRequest $request): JsonResponse
{
try {
$user = $this->repository->find(
$this->cache->pull($request->input('confirmation_token'), 0)
);
} catch (RecordNotFoundException $exception) {
return $this->sendFailedLoginResponse($request);
}
$decrypted = $this->encrypter->decrypt($user->totp_secret);
$window = $this->config->get('pterodactyl.auth.2fa.window');
if ($this->google2FA->verifyKey($decrypted, $request->input('authentication_code'), $window)) {
return $this->sendLoginResponse($user, $request);
}
return $this->sendFailedLoginResponse($request, $user);
}
}

View File

@ -2,117 +2,81 @@
namespace Pterodactyl\Http\Controllers\Auth;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Illuminate\Auth\AuthManager;
use PragmaRX\Google2FA\Google2FA;
use Illuminate\Auth\Events\Failed;
use Illuminate\Http\RedirectResponse;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Encryption\Encrypter;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\JsonResponse;
use Illuminate\Contracts\View\View;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Contracts\View\Factory as ViewFactory;
use Illuminate\Contracts\Cache\Repository as CacheRepository;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
class LoginController extends Controller
class LoginController extends AbstractLoginController
{
use AuthenticatesUsers;
const USER_INPUT_FIELD = 'user';
/**
* @var \Illuminate\Auth\AuthManager
* @var \Illuminate\Contracts\View\Factory
*/
private $auth;
private $view;
/**
* @var \Illuminate\Contracts\Cache\Repository
*/
private $cache;
/**
* @var \Illuminate\Contracts\Config\Repository
*/
private $config;
/**
* @var \Illuminate\Contracts\Encryption\Encrypter
*/
private $encrypter;
/**
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface
*/
private $repository;
/**
* @var \PragmaRX\Google2FA\Google2FA
*/
private $google2FA;
/**
* Where to redirect users after login / registration.
*
* @var string
*/
protected $redirectTo = '/';
/**
* Lockout time for failed login requests.
*
* @var int
*/
protected $lockoutTime;
/**
* After how many attempts should logins be throttled and locked.
*
* @var int
*/
protected $maxLoginAttempts;
/**
* LoginController constructor.
*
* @param \Illuminate\Auth\AuthManager $auth
* @param \Illuminate\Contracts\Cache\Repository $cache
* @param \Illuminate\Contracts\Config\Repository $config
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
* @param \PragmaRX\Google2FA\Google2FA $google2FA
* @param \Illuminate\Contracts\Cache\Repository $cache
* @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository
* @param \Illuminate\Contracts\View\Factory $view
*/
public function __construct(
AuthManager $auth,
Repository $config,
CacheRepository $cache,
ConfigRepository $config,
Encrypter $encrypter,
Google2FA $google2FA,
UserRepositoryInterface $repository
UserRepositoryInterface $repository,
ViewFactory $view
) {
$this->auth = $auth;
$this->cache = $cache;
$this->config = $config;
$this->encrypter = $encrypter;
$this->google2FA = $google2FA;
$this->repository = $repository;
parent::__construct($auth, $config);
$this->lockoutTime = $this->config->get('auth.lockout.time');
$this->maxLoginAttempts = $this->config->get('auth.lockout.attempts');
$this->view = $view;
$this->cache = $cache;
$this->repository = $repository;
}
/**
* Handle all incoming requests for the authentication routes and render the
* base authentication view component. Vuejs will take over at this point and
* turn the login area into a SPA.
*
* @return \Illuminate\Contracts\View\View
*/
public function index(): View
{
return $this->view->make('templates/auth.core');
}
/**
* Handle a login request to the application.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Illuminate\Validation\ValidationException
*/
public function login(Request $request)
public function login(Request $request): JsonResponse
{
$username = $request->input(self::USER_INPUT_FIELD);
$username = $request->input('user');
$useColumn = $this->getField($username);
if ($this->hasTooManyLoginAttempts($request)) {
@ -126,122 +90,28 @@ class LoginController extends Controller
return $this->sendFailedLoginResponse($request);
}
$validCredentials = password_verify($request->input('password'), $user->password);
// Ensure that the account is using a valid username and password before trying to
// continue. Previously this was handled in the 2FA checkpoint, however that has
// a flaw in which you can discover if an account exists simply by seeing if you
// can proceede to the next step in the login process.
if (! password_verify($request->input('password'), $user->password)) {
return $this->sendFailedLoginResponse($request, $user);
}
if ($user->use_totp) {
$token = str_random(64);
$this->cache->put($token, ['user_id' => $user->id, 'valid_credentials' => $validCredentials], 5);
$token = Str::random(64);
$this->cache->put($token, $user->id, 5);
return redirect()->route('auth.totp')->with('authentication_token', $token);
}
if ($validCredentials) {
$this->auth->guard()->login($user, true);
return $this->sendLoginResponse($request);
}
return $this->sendFailedLoginResponse($request, $user);
}
/**
* Handle a TOTP implementation page.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
*/
public function totp(Request $request)
{
$token = $request->session()->get('authentication_token');
if (is_null($token) || $this->auth->guard()->user()) {
return redirect()->route('auth.login');
}
return view('auth.totp', ['verify_key' => $token]);
}
/**
* Handle a login where the user is required to provide a TOTP authentication
* token. In order to add additional layers of security, users are not
* informed of an incorrect password until this stage, forcing them to
* provide a token on each login attempt.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response
*/
public function loginUsingTotp(Request $request)
{
if (is_null($request->input('verify_token'))) {
return $this->sendFailedLoginResponse($request);
}
try {
$cache = $this->cache->pull($request->input('verify_token'), []);
$user = $this->repository->find(array_get($cache, 'user_id', 0));
} catch (RecordNotFoundException $exception) {
return $this->sendFailedLoginResponse($request);
}
if (is_null($request->input('2fa_token')) || ! array_get($cache, 'valid_credentials')) {
return $this->sendFailedLoginResponse($request, $user);
}
if (! $this->google2FA->verifyKey(
$this->encrypter->decrypt($user->totp_secret),
$request->input('2fa_token'),
$this->config->get('pterodactyl.auth.2fa.window')
)) {
return $this->sendFailedLoginResponse($request, $user);
return JsonResponse::create([
'data' => [
'complete' => false,
'confirmation_token' => $token,
],
]);
}
$this->auth->guard()->login($user, true);
return $this->sendLoginResponse($request);
}
/**
* Get the failed login response instance.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Contracts\Auth\Authenticatable|null $user
* @return \Illuminate\Http\RedirectResponse
*/
protected function sendFailedLoginResponse(Request $request, Authenticatable $user = null): RedirectResponse
{
$this->incrementLoginAttempts($request);
$this->fireFailedLoginEvent($user, [
$this->getField($request->input(self::USER_INPUT_FIELD)) => $request->input(self::USER_INPUT_FIELD),
]);
$errors = [self::USER_INPUT_FIELD => trans('auth.failed')];
if ($request->expectsJson()) {
return response()->json($errors, 422);
}
return redirect()->route('auth.login')
->withInput($request->only(self::USER_INPUT_FIELD))
->withErrors($errors);
}
/**
* Determine if the user is logging in using an email or username,.
*
* @param string $input
* @return string
*/
private function getField(string $input = null): string
{
return str_contains($input, '@') ? 'email' : 'username';
}
/**
* Fire a failed login event.
*
* @param \Illuminate\Contracts\Auth\Authenticatable|null $user
* @param array $credentials
*/
private function fireFailedLoginEvent(Authenticatable $user = null, array $credentials = [])
{
event(new Failed(config('auth.defaults.guard'), $user, $credentials));
return $this->sendLoginResponse($user, $request);
}
}

View File

@ -3,13 +3,15 @@
namespace Pterodactyl\Http\Controllers\Auth;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\Http\JsonResponse;
use Illuminate\Contracts\Hashing\Hasher;
use Illuminate\Support\Facades\Password;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Contracts\Events\Dispatcher;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Pterodactyl\Http\Requests\Auth\ResetPasswordRequest;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
class ResetPasswordController extends Controller
@ -28,11 +30,6 @@ class ResetPasswordController extends Controller
*/
protected $hasTwoFactor = false;
/**
* @var \Prologue\Alerts\AlertsMessageBag
*/
private $alerts;
/**
* @var \Illuminate\Contracts\Events\Dispatcher
*/
@ -51,31 +48,44 @@ class ResetPasswordController extends Controller
/**
* ResetPasswordController constructor.
*
* @param \Prologue\Alerts\AlertsMessageBag $alerts
* @param \Illuminate\Contracts\Events\Dispatcher $dispatcher
* @param \Illuminate\Contracts\Hashing\Hasher $hasher
* @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $userRepository
*/
public function __construct(AlertsMessageBag $alerts, Dispatcher $dispatcher, Hasher $hasher, UserRepositoryInterface $userRepository)
public function __construct(Dispatcher $dispatcher, Hasher $hasher, UserRepositoryInterface $userRepository)
{
$this->alerts = $alerts;
$this->dispatcher = $dispatcher;
$this->hasher = $hasher;
$this->userRepository = $userRepository;
}
/**
* Return the rules used when validating password reset.
* Reset the given user's password.
*
* @return array
* @param \Pterodactyl\Http\Requests\Auth\ResetPasswordRequest $request
* @return \Illuminate\Http\JsonResponse
*
* @throws \Pterodactyl\Exceptions\DisplayException
*/
protected function rules(): array
public function __invoke(ResetPasswordRequest $request): JsonResponse
{
return [
'token' => 'required',
'email' => 'required|email',
'password' => 'required|confirmed|min:8',
];
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$response = $this->broker()->reset(
$this->credentials($request), function ($user, $password) {
$this->resetPassword($user, $password);
}
);
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
if ($response === Password::PASSWORD_RESET) {
return $this->sendResetResponse();
}
throw new DisplayException(trans($response));
}
/**
@ -108,19 +118,16 @@ class ResetPasswordController extends Controller
}
/**
* Get the response for a successful password reset.
* Send a successful password reset response back to the callee.
*
* @param \Illuminate\Http\Request $request
* @param string $response
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
* @return \Illuminate\Http\JsonResponse
*/
protected function sendResetResponse(Request $request, $response)
protected function sendResetResponse(): JsonResponse
{
if ($this->hasTwoFactor) {
$this->alerts->success('Your password was successfully updated. Please log in to continue.')->flash();
}
return redirect($this->hasTwoFactor ? route('auth.login') : $this->redirectPath())
->with('status', trans($response));
return response()->json([
'success' => true,
'redirect_to' => $this->redirectTo,
'send_to_login' => $this->hasTwoFactor,
]);
}
}

View File

@ -1,91 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Base;
use Pterodactyl\Models\User;
use Illuminate\Auth\AuthManager;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\Contracts\Session\Session;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Users\UserUpdateService;
use Pterodactyl\Traits\Helpers\AvailableLanguages;
use Pterodactyl\Http\Requests\Base\AccountDataFormRequest;
class AccountController extends Controller
{
use AvailableLanguages;
/**
* @var \Prologue\Alerts\AlertsMessageBag
*/
protected $alert;
/**
* @var \Illuminate\Auth\SessionGuard
*/
protected $sessionGuard;
/**
* @var \Pterodactyl\Services\Users\UserUpdateService
*/
protected $updateService;
/**
* AccountController constructor.
*
* @param \Prologue\Alerts\AlertsMessageBag $alert
* @param \Illuminate\Auth\AuthManager $authManager
* @param \Pterodactyl\Services\Users\UserUpdateService $updateService
*/
public function __construct(AlertsMessageBag $alert, AuthManager $authManager, UserUpdateService $updateService)
{
$this->alert = $alert;
$this->updateService = $updateService;
$this->sessionGuard = $authManager->guard();
}
/**
* Display base account information page.
*
* @return \Illuminate\View\View
*/
public function index()
{
return view('base.account', [
'languages' => $this->getAvailableLanguages(true),
]);
}
/**
* Update details for a user's account.
*
* @param \Pterodactyl\Http\Requests\Base\AccountDataFormRequest $request
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function update(AccountDataFormRequest $request)
{
// Prevent logging this specific session out when the password is changed. This will
// automatically update the user's password anyways, so no need to do anything else here.
if ($request->input('do_action') === 'password') {
$this->sessionGuard->logoutOtherDevices($request->input('new_password'));
} else {
if ($request->input('do_action') === 'email') {
$data = ['email' => $request->input('new_email')];
} elseif ($request->input('do_action') === 'identity') {
$data = $request->only(['name_first', 'name_last', 'username', 'language']);
} else {
$data = [];
}
$this->updateService->setUserLevel(User::USER_LEVEL_USER);
$this->updateService->handle($request->user(), $data);
}
$this->alert->success(trans('base.account.details_updated'))->flash();
return redirect()->route('account');
}
}

View File

@ -53,13 +53,13 @@ class IndexController extends Controller
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*/
public function getIndex(Request $request)
public function index(Request $request)
{
$servers = $this->repository->setSearchTerm($request->input('query'))->filterUserAccessServers(
$request->user(), User::FILTER_LEVEL_ALL, config('pterodactyl.paginate.frontend.servers')
);
return view('base.index', ['servers' => $servers]);
return view('templates/base.core', ['servers' => $servers]);
}
/**

View File

@ -3,6 +3,7 @@
namespace Pterodactyl\Http\Controllers\Base;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Users\TwoFactorSetupService;
@ -62,36 +63,28 @@ class SecurityController extends Controller
}
/**
* Returns Security Management Page.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*/
public function index(Request $request)
{
if ($this->config->get('session.driver') === 'database') {
$activeSessions = $this->repository->getUserSessions($request->user()->id);
}
return view('base.security', [
'sessions' => $activeSessions ?? null,
]);
}
/**
* Generates TOTP Secret and returns popup data for user to verify
* that they can generate a valid response.
* Return information about the user's two-factor authentication status. If not enabled setup their
* secret and return information to allow the user to proceede with setup.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function generateTotp(Request $request)
public function index(Request $request): JsonResponse
{
return response()->json([
'qrImage' => $this->twoFactorSetupService->handle($request->user()),
if ($request->user()->use_totp) {
return JsonResponse::create([
'enabled' => true,
]);
}
$response = $this->twoFactorSetupService->handle($request->user());
return JsonResponse::create([
'enabled' => false,
'qr_image' => $response,
'secret' => '',
]);
}
@ -99,53 +92,43 @@ class SecurityController extends Controller
* Verifies that 2FA token received is valid and will work on the account.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
* @return \Illuminate\Http\JsonResponse
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function setTotp(Request $request)
public function store(Request $request): JsonResponse
{
try {
$this->toggleTwoFactorService->handle($request->user(), $request->input('token') ?? '');
return response('true');
} catch (TwoFactorAuthenticationTokenInvalid $exception) {
return response('false');
$error = true;
}
return JsonResponse::create([
'success' => ! isset($error),
]);
}
/**
* Disables TOTP on an account.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse
* @return \Illuminate\Http\JsonResponse
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function disableTotp(Request $request)
public function delete(Request $request): JsonResponse
{
try {
$this->toggleTwoFactorService->handle($request->user(), $request->input('token') ?? '', false);
} catch (TwoFactorAuthenticationTokenInvalid $exception) {
$this->alert->danger(trans('base.security.2fa_disable_error'))->flash();
$error = true;
}
return redirect()->route('account.security');
}
/**
* Revokes a user session.
*
* @param \Illuminate\Http\Request $request
* @param string $id
* @return \Illuminate\Http\RedirectResponse
*/
public function revoke(Request $request, string $id)
{
$this->repository->deleteUserSession($request->user()->id, $id);
return redirect()->route('account.security');
return JsonResponse::create([
'success' => ! isset($error),
]);
}
}

View File

@ -5,10 +5,14 @@ namespace Pterodactyl\Http\Controllers\Daemon;
use Cache;
use Illuminate\Http\Request;
use Pterodactyl\Models\Node;
use Illuminate\Http\Response;
use Pterodactyl\Models\Server;
use Illuminate\Http\JsonResponse;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Repositories\Eloquent\ServerRepository;
use Pterodactyl\Events\Server\Installed as ServerInstalled;
use Illuminate\Contracts\Events\Dispatcher as EventDispatcher;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
class ActionController extends Controller
{
@ -16,15 +20,21 @@ class ActionController extends Controller
* @var \Illuminate\Contracts\Events\Dispatcher
*/
private $eventDispatcher;
/**
* @var \Pterodactyl\Repositories\Eloquent\ServerRepository
*/
private $repository;
/**
* ActionController constructor.
*
* @param \Illuminate\Contracts\Events\Dispatcher $eventDispatcher
* @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository
* @param \Illuminate\Contracts\Events\Dispatcher $eventDispatcher
*/
public function __construct(EventDispatcher $eventDispatcher)
public function __construct(ServerRepository $repository, EventDispatcher $eventDispatcher)
{
$this->eventDispatcher = $eventDispatcher;
$this->repository = $repository;
}
/**
@ -32,34 +42,47 @@ class ActionController extends Controller
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function markInstall(Request $request)
public function markInstall(Request $request): JsonResponse
{
$server = Server::where('uuid', $request->input('server'))->with('node')->first();
if (! $server) {
return response()->json([
try {
/** @var \Pterodactyl\Models\Server $server */
$server = $this->repository->findFirstWhere([
'uuid' => $request->input('server'),
]);
} catch (RecordNotFoundException $exception) {
return JsonResponse::create([
'error' => 'No server by that ID was found on the system.',
], 422);
], Response::HTTP_UNPROCESSABLE_ENTITY);
}
if (! $server->relationLoaded('node')) {
$server->load('node');
}
$hmac = $request->input('signed');
$status = $request->input('installed');
if (! hash_equals(base64_decode($hmac), hash_hmac('sha256', $server->uuid, $server->node->daemonSecret, true))) {
return response()->json([
if (! hash_equals(base64_decode($hmac), hash_hmac('sha256', $server->uuid, $server->getRelation('node')->daemonSecret, true))) {
return JsonResponse::create([
'error' => 'Signed HMAC was invalid.',
], 403);
], Response::HTTP_FORBIDDEN);
}
$server->installed = ($status === 'installed') ? 1 : 2;
$server->save();
$this->repository->update($server->id, [
'installed' => ($status === 'installed') ? 1 : 2,
], true, true);
// Only fire event if server installed successfully.
if ($server->installed === 1) {
if ($status === 'installed') {
$this->eventDispatcher->dispatch(new ServerInstalled($server));
}
return response()->json([]);
// Don't use a 204 here, the daemon is hard-checking for a 200 code.
return JsonResponse::create([]);
}
/**

View File

@ -1,72 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Server;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Traits\Controllers\JavascriptInjection;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
class ConsoleController extends Controller
{
use JavascriptInjection;
/**
* @var \Illuminate\Contracts\Config\Repository
*/
protected $config;
/**
* ConsoleController constructor.
*
* @param \Illuminate\Contracts\Config\Repository $config
*/
public function __construct(ConfigRepository $config)
{
$this->config = $config;
}
/**
* Render server index page with the console and power options.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*/
public function index(Request $request): View
{
$server = $request->attributes->get('server');
$this->setRequest($request)->injectJavascript([
'server' => [
'cpu' => $server->cpu,
],
'meta' => [
'saveFile' => route('server.files.save', $server->uuidShort),
'csrfToken' => csrf_token(),
],
'config' => [
'console_count' => $this->config->get('pterodactyl.console.count'),
'console_freq' => $this->config->get('pterodactyl.console.frequency'),
],
]);
return view('server.index');
}
/**
* Render a stand-alone console in the browser.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*/
public function console(Request $request): View
{
$this->setRequest($request)->injectJavascript(['config' => [
'console_count' => $this->config->get('pterodactyl.console.count'),
'console_freq' => $this->config->get('pterodactyl.console.frequency'),
]]);
return view('server.console');
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace Pterodactyl\Http\Controllers\Server;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService;
class CredentialsController extends Controller
{
/**
* @var \Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService
*/
private $keyProviderService;
/**
* CredentialsController constructor.
*
* @param \Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService $keyProviderService
*/
public function __construct(DaemonKeyProviderService $keyProviderService)
{
$this->keyProviderService = $keyProviderService;
}
/**
* Return a set of credentials that the currently authenticated user can use to access
* a given server with.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function index(Request $request): JsonResponse
{
/** @var \Pterodactyl\Models\Server $server */
$server = $request->attributes->get('server');
$server->loadMissing('node');
return JsonResponse::create([
'node' => $server->getRelation('node')->getConnectionAddress(),
'key' => $this->keyProviderService->handle($server, $request->user()),
]);
}
}

View File

@ -1,165 +0,0 @@
<?php
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
*/
private $passwordService;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface
*/
private $repository;
/**
* DatabaseController constructor.
*
* @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(
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;
}
/**
* Render the database listing for a server.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function index(Request $request): View
{
$server = $request->attributes->get('server');
$this->authorize('view-databases', $server);
$this->setRequest($request)->injectJavascript();
$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', [
'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.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*
* @throws \Illuminate\Auth\Access\AuthorizationException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function update(Request $request): JsonResponse
{
$this->authorize('reset-db-password', $request->attributes->get('server'));
$password = str_random(20);
$this->passwordService->handle($request->attributes->get('database'), $password);
return response()->json(['password' => $password]);
}
/**
* 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);
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace Pterodactyl\Http\Controllers\Server;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use GuzzleHttp\Exception\TransferException;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
class FileController extends Controller
{
/**
* @var \Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface
*/
private $fileRepository;
/**
* FileController constructor.
*
* @param \Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface $fileRepository
*/
public function __construct(FileRepositoryInterface $fileRepository)
{
$this->fileRepository = $fileRepository;
}
/**
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*
* @throws \Illuminate\Auth\Access\AuthorizationException
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function index(Request $request): JsonResponse
{
$server = $request->attributes->get('server');
$this->authorize('list-files', $server);
$requestDirectory = '/' . trim(urldecode($request->route()->parameter('directory', '/')), '/');
try {
$contents = $this->fileRepository->setServer($server)->setToken(
$request->attributes->get('server_token')
)->getDirectory($requestDirectory);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception, true);
}
return JsonResponse::create([
'contents' => $contents,
'editable' => config('pterodactyl.files.editable'),
'current_directory' => $requestDirectory,
]);
}
}

View File

@ -1,57 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Http\Controllers\Server\Files;
use Ramsey\Uuid\Uuid;
use Illuminate\Http\Request;
use Illuminate\Cache\Repository;
use Illuminate\Http\RedirectResponse;
use Pterodactyl\Http\Controllers\Controller;
class DownloadController extends Controller
{
/**
* @var \Illuminate\Cache\Repository
*/
protected $cache;
/**
* DownloadController constructor.
*
* @param \Illuminate\Cache\Repository $cache
*/
public function __construct(Repository $cache)
{
$this->cache = $cache;
}
/**
* Setup a unique download link for a user to download a file from.
*
* @param \Illuminate\Http\Request $request
* @param string $uuid
* @param string $file
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function index(Request $request, string $uuid, string $file): RedirectResponse
{
$server = $request->attributes->get('server');
$this->authorize('download-files', $server);
$token = Uuid::uuid4()->toString();
$node = $server->getRelation('node');
$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));
}
}

View File

@ -1,120 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Http\Controllers\Server\Files;
use Illuminate\View\View;
use Illuminate\Http\Request;
use GuzzleHttp\Exception\RequestException;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Traits\Controllers\JavascriptInjection;
use Pterodactyl\Http\Requests\Server\UpdateFileContentsFormRequest;
use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
class FileActionsController extends Controller
{
use JavascriptInjection;
/**
* @var \Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface
*/
protected $repository;
/**
* FileActionsController constructor.
*
* @param \Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface $repository
*/
public function __construct(FileRepositoryInterface $repository)
{
$this->repository = $repository;
}
/**
* Display server file index list.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function index(Request $request): View
{
$server = $request->attributes->get('server');
$this->authorize('list-files', $server);
$this->setRequest($request)->injectJavascript([
'meta' => [
'directoryList' => route('server.files.directory-list', $server->uuidShort),
'csrftoken' => csrf_token(),
],
'permissions' => [
'moveFiles' => $request->user()->can('move-files', $server),
'copyFiles' => $request->user()->can('copy-files', $server),
'compressFiles' => $request->user()->can('compress-files', $server),
'decompressFiles' => $request->user()->can('decompress-files', $server),
'createFiles' => $request->user()->can('create-files', $server),
'downloadFiles' => $request->user()->can('download-files', $server),
'deleteFiles' => $request->user()->can('delete-files', $server),
],
]);
return view('server.files.index');
}
/**
* Render page to manually create a file in the panel.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function create(Request $request): View
{
$this->authorize('create-files', $request->attributes->get('server'));
$this->setRequest($request)->injectJavascript();
return view('server.files.add', [
'directory' => (in_array($request->get('dir'), [null, '/', ''])) ? '' : trim($request->get('dir'), '/') . '/',
]);
}
/**
* Display a form to allow for editing of a file.
*
* @param \Pterodactyl\Http\Requests\Server\UpdateFileContentsFormRequest $request
* @param string $uuid
* @param string $file
* @return \Illuminate\View\View
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function view(UpdateFileContentsFormRequest $request, string $uuid, string $file): View
{
$server = $request->attributes->get('server');
$dirname = str_replace('\\', '/', pathinfo($file, PATHINFO_DIRNAME));
try {
$content = $this->repository->setServer($server)->setToken($request->attributes->get('server_token'))->getContent($file);
} catch (RequestException $exception) {
throw new DaemonConnectionException($exception);
}
$this->setRequest($request)->injectJavascript(['stat' => $request->attributes->get('file_stats')]);
return view('server.files.edit', [
'file' => $file,
'stat' => $request->attributes->get('file_stats'),
'contents' => $content,
'directory' => (in_array($dirname, ['.', './', '/'])) ? '/' : trim($dirname, '/') . '/',
]);
}
}

View File

@ -1,105 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Server\Files;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use GuzzleHttp\Exception\RequestException;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
class RemoteRequestController extends Controller
{
/**
* @var \Illuminate\Contracts\Config\Repository
*/
protected $config;
/**
* @var \Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface
*/
protected $repository;
/**
* RemoteRequestController constructor.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @param \Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface $repository
*/
public function __construct(ConfigRepository $config, FileRepositoryInterface $repository)
{
$this->config = $config;
$this->repository = $repository;
}
/**
* Return a listing of a servers file directory.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*
* @throws \Illuminate\Auth\Access\AuthorizationException
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function directory(Request $request): View
{
$server = $request->attributes->get('server');
$this->authorize('list-files', $server);
$requestDirectory = '/' . trim(urldecode($request->input('directory', '/')), '/');
$directory = [
'header' => $requestDirectory !== '/' ? $requestDirectory : '',
'first' => $requestDirectory !== '/',
];
$goBack = explode('/', trim($requestDirectory, '/'));
if (! empty(array_filter($goBack)) && count($goBack) >= 2) {
array_pop($goBack);
$directory['show'] = true;
$directory['link'] = '/' . implode('/', $goBack);
$directory['link_show'] = implode('/', $goBack) . '/';
}
try {
$listing = $this->repository->setServer($server)->setToken($request->attributes->get('server_token'))->getDirectory($requestDirectory);
} catch (RequestException $exception) {
throw new DaemonConnectionException($exception, true);
}
return view('server.files.list', [
'files' => $listing['files'],
'folders' => $listing['folders'],
'editableMime' => $this->config->get('pterodactyl.files.editable'),
'directory' => $directory,
]);
}
/**
* Put the contents of a file onto the daemon.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*
* @throws \Illuminate\Auth\Access\AuthorizationException
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function store(Request $request): Response
{
$server = $request->attributes->get('server');
$this->authorize('save-files', $server);
try {
$this->repository->setServer($server)->setToken($request->attributes->get('server_token'))
->putContent($request->input('file'), $request->input('contents') ?? '');
return response('', 204);
} catch (RequestException $exception) {
throw new DaemonConnectionException($exception);
}
}
}

View File

@ -1,96 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Server\Settings;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Contracts\Extensions\HashidsInterface;
use Pterodactyl\Traits\Controllers\JavascriptInjection;
use Pterodactyl\Services\Allocations\SetDefaultAllocationService;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
use Pterodactyl\Exceptions\Service\Allocation\AllocationDoesNotBelongToServerException;
class AllocationController extends Controller
{
use JavascriptInjection;
/**
* @var \Pterodactyl\Services\Allocations\SetDefaultAllocationService
*/
private $defaultAllocationService;
/**
* @var \Pterodactyl\Contracts\Extensions\HashidsInterface
*/
private $hashids;
/**
* @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface
*/
private $repository;
/**
* AllocationController constructor.
*
* @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $repository
* @param \Pterodactyl\Contracts\Extensions\HashidsInterface $hashids
* @param \Pterodactyl\Services\Allocations\SetDefaultAllocationService $defaultAllocationService
*/
public function __construct(
AllocationRepositoryInterface $repository,
HashidsInterface $hashids,
SetDefaultAllocationService $defaultAllocationService
) {
$this->defaultAllocationService = $defaultAllocationService;
$this->hashids = $hashids;
$this->repository = $repository;
}
/**
* Render the allocation management overview page for a server.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function index(Request $request): View
{
$server = $request->attributes->get('server');
$this->authorize('view-allocations', $server);
$this->setRequest($request)->injectJavascript();
return view('server.settings.allocation', [
'allocations' => $this->repository->findWhere([['server_id', '=', $server->id]]),
]);
}
/**
* Update the default allocation for a server.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*
* @throws \Illuminate\Auth\Access\AuthorizationException
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function update(Request $request): JsonResponse
{
$server = $request->attributes->get('server');
$this->authorize('edit-allocation', $server);
$allocation = $this->hashids->decodeFirst($request->input('allocation'), 0);
try {
$this->defaultAllocationService->handle($server->id, $allocation);
} catch (AllocationDoesNotBelongToServerException $exception) {
return response()->json(['error' => 'No matching allocation was located for this server.'], 404);
}
return response()->json();
}
}

View File

@ -1,59 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Server\Settings;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Traits\Controllers\JavascriptInjection;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Http\Requests\Server\Settings\ChangeServerNameRequest;
class NameController extends Controller
{
use JavascriptInjection;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
private $repository;
/**
* NameController constructor.
*
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
*/
public function __construct(ServerRepositoryInterface $repository)
{
$this->repository = $repository;
}
/**
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function index(Request $request)
{
$this->authorize('view-name', $request->attributes->get('server'));
$this->setRequest($request)->injectJavascript();
return view('server.settings.name');
}
/**
* Update the stored name for a specific server.
*
* @param \Pterodactyl\Http\Requests\Server\Settings\ChangeServerNameRequest $request
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function update(ChangeServerNameRequest $request): RedirectResponse
{
$this->repository->update($request->getServer()->id, $request->validated());
return redirect()->route('server.settings.name', $request->getServer()->uuidShort);
}
}

View File

@ -1,29 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Server\Settings;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Traits\Controllers\JavascriptInjection;
class SftpController extends Controller
{
use JavascriptInjection;
/**
* Render the server SFTP settings page.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function index(Request $request): View
{
$this->authorize('access-sftp', $request->attributes->get('server'));
$this->setRequest($request)->injectJavascript();
return view('server.settings.sftp');
}
}

View File

@ -1,96 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Server\Settings;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Pterodactyl\Models\User;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Traits\Controllers\JavascriptInjection;
use Pterodactyl\Services\Servers\StartupCommandViewService;
use Pterodactyl\Services\Servers\StartupModificationService;
use Pterodactyl\Http\Requests\Server\UpdateStartupParametersFormRequest;
class StartupController extends Controller
{
use JavascriptInjection;
/**
* @var \Prologue\Alerts\AlertsMessageBag
*/
private $alert;
/**
* @var \Pterodactyl\Services\Servers\StartupCommandViewService
*/
private $commandViewService;
/**
* @var \Pterodactyl\Services\Servers\StartupModificationService
*/
private $modificationService;
/**
* StartupController constructor.
*
* @param \Prologue\Alerts\AlertsMessageBag $alert
* @param \Pterodactyl\Services\Servers\StartupCommandViewService $commandViewService
* @param \Pterodactyl\Services\Servers\StartupModificationService $modificationService
*/
public function __construct(
AlertsMessageBag $alert,
StartupCommandViewService $commandViewService,
StartupModificationService $modificationService
) {
$this->alert = $alert;
$this->commandViewService = $commandViewService;
$this->modificationService = $modificationService;
}
/**
* Render the server startup page.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*
* @throws \Illuminate\Auth\Access\AuthorizationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function index(Request $request): View
{
$server = $request->attributes->get('server');
$this->authorize('view-startup', $server);
$this->setRequest($request)->injectJavascript();
$data = $this->commandViewService->handle($server->id);
return view('server.settings.startup', [
'variables' => $data->get('variables'),
'server_values' => $data->get('server_values'),
'startup' => $data->get('startup'),
]);
}
/**
* Handle request to update the startup variables for a server. Authorization
* is handled in the form request.
*
* @param \Pterodactyl\Http\Requests\Server\UpdateStartupParametersFormRequest $request
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Illuminate\Validation\ValidationException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function update(UpdateStartupParametersFormRequest $request): RedirectResponse
{
$this->modificationService->setUserLevel(User::USER_LEVEL_USER);
$this->modificationService->handle($request->attributes->get('server'), $request->normalize());
$this->alert->success(trans('server.config.startup.edited'))->flash();
return redirect()->route('server.settings.startup', ['server' => $request->attributes->get('server')->uuidShort]);
}
}

View File

@ -1,197 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Server;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Pterodactyl\Models\Permission;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Subusers\SubuserUpdateService;
use Pterodactyl\Traits\Controllers\JavascriptInjection;
use Pterodactyl\Services\Subusers\SubuserCreationService;
use Pterodactyl\Services\Subusers\SubuserDeletionService;
use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface;
use Pterodactyl\Http\Requests\Server\Subuser\SubuserStoreFormRequest;
use Pterodactyl\Http\Requests\Server\Subuser\SubuserUpdateFormRequest;
class SubuserController extends Controller
{
use JavascriptInjection;
/**
* @var \Prologue\Alerts\AlertsMessageBag
*/
protected $alert;
/**
* @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface
*/
protected $repository;
/**
* @var \Pterodactyl\Services\Subusers\SubuserCreationService
*/
protected $subuserCreationService;
/**
* @var \Pterodactyl\Services\Subusers\SubuserDeletionService
*/
protected $subuserDeletionService;
/**
* @var \Pterodactyl\Services\Subusers\SubuserUpdateService
*/
protected $subuserUpdateService;
/**
* SubuserController constructor.
*
* @param \Prologue\Alerts\AlertsMessageBag $alert
* @param \Pterodactyl\Services\Subusers\SubuserCreationService $subuserCreationService
* @param \Pterodactyl\Services\Subusers\SubuserDeletionService $subuserDeletionService
* @param \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface $repository
* @param \Pterodactyl\Services\Subusers\SubuserUpdateService $subuserUpdateService
*/
public function __construct(
AlertsMessageBag $alert,
SubuserCreationService $subuserCreationService,
SubuserDeletionService $subuserDeletionService,
SubuserRepositoryInterface $repository,
SubuserUpdateService $subuserUpdateService
) {
$this->alert = $alert;
$this->repository = $repository;
$this->subuserCreationService = $subuserCreationService;
$this->subuserDeletionService = $subuserDeletionService;
$this->subuserUpdateService = $subuserUpdateService;
}
/**
* Displays the subuser overview index.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function index(Request $request): View
{
$server = $request->attributes->get('server');
$this->authorize('list-subusers', $server);
$this->setRequest($request)->injectJavascript();
return view('server.users.index', [
'subusers' => $this->repository->findWhere([['server_id', '=', $server->id]]),
]);
}
/**
* Displays a single subuser overview.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function view(Request $request): View
{
$server = $request->attributes->get('server');
$this->authorize('view-subuser', $server);
$subuser = $this->repository->getWithPermissions($request->attributes->get('subuser'));
$this->setRequest($request)->injectJavascript();
return view('server.users.view', [
'subuser' => $subuser,
'permlist' => Permission::getPermissions(),
'permissions' => $subuser->getRelation('permissions')->mapWithKeys(function ($item) {
return [$item->permission => true];
}),
]);
}
/**
* Handles editing a subuser.
*
* @param \Pterodactyl\Http\Requests\Server\Subuser\SubuserUpdateFormRequest $request
* @param string $uuid
* @param string $hash
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Illuminate\Auth\Access\AuthorizationException
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function update(SubuserUpdateFormRequest $request, string $uuid, string $hash): RedirectResponse
{
$this->subuserUpdateService->handle($request->attributes->get('subuser'), $request->input('permissions', []));
$this->alert->success(trans('server.users.user_updated'))->flash();
return redirect()->route('server.subusers.view', ['uuid' => $uuid, 'subuser' => $hash]);
}
/**
* Display new subuser creation page.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function create(Request $request): View
{
$server = $request->attributes->get('server');
$this->authorize('create-subuser', $server);
$this->setRequest($request)->injectJavascript();
return view('server.users.new', ['permissions' => Permission::getPermissions()]);
}
/**
* Handles creating a new subuser.
*
* @param \Pterodactyl\Http\Requests\Server\Subuser\SubuserStoreFormRequest $request
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Exception
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Subuser\ServerSubuserExistsException
* @throws \Pterodactyl\Exceptions\Service\Subuser\UserIsServerOwnerException
*/
public function store(SubuserStoreFormRequest $request): RedirectResponse
{
$server = $request->attributes->get('server');
$subuser = $this->subuserCreationService->handle($server, $request->input('email'), $request->input('permissions', []));
$this->alert->success(trans('server.users.user_assigned'))->flash();
return redirect()->route('server.subusers.view', [
'uuid' => $server->uuidShort,
'id' => $subuser->hashid,
]);
}
/**
* Handles deleting a subuser.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*
* @throws \Illuminate\Auth\Access\AuthorizationException
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function delete(Request $request): Response
{
$server = $request->attributes->get('server');
$this->authorize('delete-subuser', $server);
$this->subuserDeletionService->handle($request->attributes->get('subuser'));
return response('', 204);
}
}

View File

@ -1,79 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Server\Tasks;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Schedules\ProcessScheduleService;
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
class ActionController extends Controller
{
/**
* @var \Pterodactyl\Services\Schedules\ProcessScheduleService
*/
private $processScheduleService;
/**
* @var \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface
*/
private $repository;
/**
* ActionController constructor.
*
* @param \Pterodactyl\Services\Schedules\ProcessScheduleService $processScheduleService
* @param \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface $repository
*/
public function __construct(ProcessScheduleService $processScheduleService, ScheduleRepositoryInterface $repository)
{
$this->processScheduleService = $processScheduleService;
$this->repository = $repository;
}
/**
* Toggle a task to be active or inactive for a given server.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*
* @throws \Illuminate\Auth\Access\AuthorizationException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function toggle(Request $request): Response
{
$server = $request->attributes->get('server');
$schedule = $request->attributes->get('schedule');
$this->authorize('toggle-schedule', $server);
$this->repository->update($schedule->id, [
'is_active' => ! $schedule->is_active,
]);
return response('', 204);
}
/**
* Trigger a schedule to run now.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*
* @throws \Illuminate\Auth\Access\AuthorizationException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function trigger(Request $request): Response
{
$server = $request->attributes->get('server');
$this->authorize('toggle-schedule', $server);
$this->processScheduleService->handle(
$request->attributes->get('schedule')
);
return response('', 204);
}
}

View File

@ -1,198 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Server\Tasks;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Contracts\Extensions\HashidsInterface;
use Pterodactyl\Traits\Controllers\JavascriptInjection;
use Pterodactyl\Services\Schedules\ScheduleUpdateService;
use Pterodactyl\Services\Schedules\ScheduleCreationService;
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
use Pterodactyl\Http\Requests\Server\ScheduleCreationFormRequest;
class TaskManagementController extends Controller
{
use JavascriptInjection;
/**
* @var \Prologue\Alerts\AlertsMessageBag
*/
protected $alert;
/**
* @var \Pterodactyl\Services\Schedules\ScheduleCreationService
*/
protected $creationService;
/**
* @var \Pterodactyl\Contracts\Extensions\HashidsInterface
*/
protected $hashids;
/**
* @var \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface
*/
protected $repository;
/**
* @var \Pterodactyl\Services\Schedules\ScheduleUpdateService
*/
private $updateService;
/**
* TaskManagementController constructor.
*
* @param \Prologue\Alerts\AlertsMessageBag $alert
* @param \Pterodactyl\Contracts\Extensions\HashidsInterface $hashids
* @param \Pterodactyl\Services\Schedules\ScheduleCreationService $creationService
* @param \Pterodactyl\Services\Schedules\ScheduleUpdateService $updateService
* @param \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface $repository
*/
public function __construct(
AlertsMessageBag $alert,
HashidsInterface $hashids,
ScheduleCreationService $creationService,
ScheduleUpdateService $updateService,
ScheduleRepositoryInterface $repository
) {
$this->alert = $alert;
$this->creationService = $creationService;
$this->hashids = $hashids;
$this->repository = $repository;
$this->updateService = $updateService;
}
/**
* Display the task page listing.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function index(Request $request): View
{
$server = $request->attributes->get('server');
$this->authorize('list-schedules', $server);
$this->setRequest($request)->injectJavascript();
return view('server.schedules.index', [
'schedules' => $this->repository->findServerSchedules($server->id),
'actions' => [
'command' => trans('server.schedule.actions.command'),
'power' => trans('server.schedule.actions.power'),
],
]);
}
/**
* Display the task creation page.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function create(Request $request): View
{
$server = $request->attributes->get('server');
$this->authorize('create-schedule', $server);
$this->setRequest($request)->injectJavascript();
return view('server.schedules.new');
}
/**
* Handle request to store a new schedule and tasks in the database.
*
* @param \Pterodactyl\Http\Requests\Server\ScheduleCreationFormRequest $request
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Service\Schedule\Task\TaskIntervalTooLongException
*/
public function store(ScheduleCreationFormRequest $request): RedirectResponse
{
$server = $request->attributes->get('server');
$schedule = $this->creationService->handle($server, $request->normalize(), $request->getTasks());
$this->alert->success(trans('server.schedule.schedule_created'))->flash();
return redirect()->route('server.schedules.view', [
'server' => $server->uuidShort,
'schedule' => $schedule->hashid,
]);
}
/**
* Return a view to modify a schedule.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function view(Request $request): View
{
$server = $request->attributes->get('server');
$schedule = $request->attributes->get('schedule');
$this->authorize('view-schedule', $server);
$this->setRequest($request)->injectJavascript([
'tasks' => $schedule->getRelation('tasks')->map(function ($task) {
/* @var \Pterodactyl\Models\Task $task */
return collect($task->toArray())->only('action', 'time_offset', 'payload')->all();
}),
]);
return view('server.schedules.view', ['schedule' => $schedule]);
}
/**
* Update a specific parent task on the system.
*
* @param \Pterodactyl\Http\Requests\Server\ScheduleCreationFormRequest $request
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Schedule\Task\TaskIntervalTooLongException
*/
public function update(ScheduleCreationFormRequest $request): RedirectResponse
{
$server = $request->attributes->get('server');
$schedule = $request->attributes->get('schedule');
$this->updateService->handle($schedule, $request->normalize(), $request->getTasks());
$this->alert->success(trans('server.schedule.schedule_updated'))->flash();
return redirect()->route('server.schedules.view', [
'server' => $server->uuidShort,
'schedule' => $schedule->hashid,
]);
}
/**
* Delete a parent task from the Panel.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function delete(Request $request): Response
{
$server = $request->attributes->get('server');
$schedule = $request->attributes->get('schedule');
$this->authorize('delete-schedule', $server);
$this->repository->delete($schedule->id);
return response('', 204);
}
}

View File

@ -49,6 +49,7 @@ class Kernel extends HttpKernel
*/
protected $middleware = [
CheckForMaintenanceMode::class,
EncryptCookies::class,
ValidatePostSize::class,
TrimStrings::class,
ConvertEmptyStringsToNull::class,
@ -62,7 +63,6 @@ class Kernel extends HttpKernel
*/
protected $middlewareGroups = [
'web' => [
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class,
@ -73,7 +73,7 @@ class Kernel extends HttpKernel
RequireTwoFactorAuthentication::class,
],
'api' => [
'throttle:120,1',
'throttle:240,1',
ApiSubstituteBindings::class,
SetSessionDriver::class,
'api..key:' . ApiKey::TYPE_APPLICATION,
@ -81,9 +81,11 @@ class Kernel extends HttpKernel
AuthenticateIPAccess::class,
],
'client-api' => [
'throttle:60,1',
SubstituteClientApiBindings::class,
'throttle:240,1',
StartSession::class,
SetSessionDriver::class,
AuthenticateSession::class,
SubstituteClientApiBindings::class,
'api..key:' . ApiKey::TYPE_ACCOUNT,
AuthenticateIPAccess::class,
],

View File

@ -5,6 +5,7 @@ namespace Pterodactyl\Http\Middleware\Api;
use Closure;
use Cake\Chronos\Chronos;
use Illuminate\Http\Request;
use Pterodactyl\Models\User;
use Pterodactyl\Models\ApiKey;
use Illuminate\Auth\AuthManager;
use Illuminate\Contracts\Encryption\Encrypter;
@ -58,13 +59,43 @@ class AuthenticateKey
*/
public function handle(Request $request, Closure $next, int $keyType)
{
if (is_null($request->bearerToken())) {
if (is_null($request->bearerToken()) && is_null($request->user())) {
throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']);
}
$raw = $request->bearerToken();
$identifier = substr($raw, 0, ApiKey::IDENTIFIER_LENGTH);
$token = substr($raw, ApiKey::IDENTIFIER_LENGTH);
// This is a request coming through using cookies, we have an authenticated user not using
// an API key. Make some fake API key models and continue on through the process.
if (empty($raw) && $request->user() instanceof User) {
$model = (new ApiKey())->forceFill([
'user_id' => $request->user()->id,
'key_type' => ApiKey::TYPE_ACCOUNT,
]);
} else {
$model = $this->authenticateApiKey($raw, $keyType);
$this->auth->guard()->loginUsingId($model->user_id);
}
$request->attributes->set('api_key', $model);
return $next($request);
}
/**
* Authenticate an API key.
*
* @param string $key
* @param int $keyType
* @return \Pterodactyl\Models\ApiKey
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
protected function authenticateApiKey(string $key, int $keyType): ApiKey
{
$identifier = substr($key, 0, ApiKey::IDENTIFIER_LENGTH);
$token = substr($key, ApiKey::IDENTIFIER_LENGTH);
try {
$model = $this->repository->findFirstWhere([
@ -79,10 +110,8 @@ class AuthenticateKey
throw new AccessDeniedHttpException;
}
$this->auth->guard()->loginUsingId($model->user_id);
$request->attributes->set('api_key', $model);
$this->repository->withoutFreshModel()->update($model->id, ['last_used_at' => Chronos::now()]);
return $next($request);
return $model;
}
}

View File

@ -1,60 +0,0 @@
<?php
namespace Pterodactyl\Http\Middleware\Api\Client;
use Closure;
use Illuminate\Http\Request;
use Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class AuthenticateClientAccess
{
/**
* @var \Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService
*/
private $keyProviderService;
/**
* AuthenticateClientAccess constructor.
*
* @param \Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService $keyProviderService
*/
public function __construct(DaemonKeyProviderService $keyProviderService)
{
$this->keyProviderService = $keyProviderService;
}
/**
* Authenticate that the currently authenticated user has permission
* to access the specified server. This only checks that the user is an
* admin, owner, or a subuser. You'll need to do more specific checks in
* the API calls to determine if they can perform different actions.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/
public function handle(Request $request, Closure $next)
{
if (is_null($request->user())) {
throw new AccessDeniedHttpException('A request must be made using an authenticated client.');
}
/** @var \Pterodactyl\Models\Server $server */
$server = $request->route()->parameter('server');
try {
$token = $this->keyProviderService->handle($server, $request->user());
} catch (RecordNotFoundException $exception) {
throw new NotFoundHttpException('The requested server could not be located.');
}
$request->attributes->set('server_token', $token);
return $next($request);
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace Pterodactyl\Http\Middleware\Api\Client\Server;
use Closure;
use Illuminate\Http\Request;
use Pterodactyl\Models\Server;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class AuthenticateServerAccess
{
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
private $repository;
/**
* AuthenticateServerAccess constructor.
*
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
*/
public function __construct(ServerRepositoryInterface $repository)
{
$this->repository = $repository;
}
/**
* Authenticate that this server exists and is not suspended or marked as installing.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
$server = $request->route()->parameter('server');
if (! $server instanceof Server) {
throw new NotFoundHttpException(trans('exceptions.api.resource_not_found'));
}
if ($server->suspended) {
throw new AccessDeniedHttpException('Cannot access a server that is marked as being suspended.');
}
if (! $server->isInstalled()) {
throw new ConflictHttpException('Server has not completed the installation process.');
}
$request->attributes->set('server', $server);
return $next($request);
}
}

View File

@ -4,9 +4,11 @@ namespace Pterodactyl\Http\Middleware\Api\Client;
use Closure;
use Illuminate\Container\Container;
use Pterodactyl\Contracts\Extensions\HashidsInterface;
use Pterodactyl\Http\Middleware\Api\ApiSubstituteBindings;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
class SubstituteClientApiBindings extends ApiSubstituteBindings
{
@ -24,8 +26,13 @@ class SubstituteClientApiBindings extends ApiSubstituteBindings
// column rather than the default 'id'.
$this->router->bind('server', function ($value) use ($request) {
try {
$column = 'uuidShort';
if (preg_match('/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i', $value)) {
$column = 'uuid';
}
return Container::getInstance()->make(ServerRepositoryInterface::class)->findFirstWhere([
['uuidShort', '=', $value],
[$column, '=', $value],
]);
} catch (RecordNotFoundException $ex) {
$request->attributes->set('is_missing_model', true);
@ -34,6 +41,20 @@ class SubstituteClientApiBindings extends ApiSubstituteBindings
}
});
$this->router->bind('database', function ($value) use ($request) {
try {
$id = Container::getInstance()->make(HashidsInterface::class)->decodeFirst($value);
return Container::getInstance()->make(DatabaseRepositoryInterface::class)->findFirstWhere([
['id', '=', $id],
]);
} catch (RecordNotFoundException $exception) {
$request->attributes->set('is_missing_model', true);
return null;
}
});
return parent::handle($request, $next);
}
}

View File

@ -4,17 +4,10 @@ namespace Pterodactyl\Http\Middleware\Api;
use Closure;
use Illuminate\Http\Request;
use Barryvdh\Debugbar\LaravelDebugbar;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
class SetSessionDriver
{
/**
* @var \Illuminate\Contracts\Foundation\Application
*/
private $app;
/**
* @var \Illuminate\Contracts\Config\Repository
*/
@ -23,12 +16,10 @@ class SetSessionDriver
/**
* SetSessionDriver constructor.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @param \Illuminate\Contracts\Config\Repository $config
* @param \Illuminate\Contracts\Config\Repository $config
*/
public function __construct(Application $app, ConfigRepository $config)
public function __construct(ConfigRepository $config)
{
$this->app = $app;
$this->config = $config;
}
@ -41,10 +32,6 @@ class SetSessionDriver
*/
public function handle(Request $request, Closure $next)
{
if ($this->config->get('app.debug')) {
$this->app->make(LaravelDebugbar::class)->disable();
}
$this->config->set('session.driver', 'array');
return $next($request);

View File

@ -10,6 +10,7 @@
namespace Pterodactyl\Http\Middleware;
use Closure;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Prologue\Alerts\AlertsMessageBag;
@ -24,27 +25,12 @@ class RequireTwoFactorAuthentication
*/
private $alert;
/**
* The names of routes that should be accessible without 2FA enabled.
*
* @var array
*/
protected $except = [
'account.security',
'account.security.revoke',
'account.security.totp',
'account.security.totp.set',
'account.security.totp.disable',
'auth.totp',
'auth.logout',
];
/**
* The route to redirect a user to to enable 2FA.
*
* @var string
*/
protected $redirectRoute = 'account.security';
protected $redirectRoute = 'account';
/**
* RequireTwoFactorAuthentication constructor.
@ -69,7 +55,8 @@ class RequireTwoFactorAuthentication
return $next($request);
}
if (in_array($request->route()->getName(), $this->except)) {
$current = $request->route()->getName();
if (in_array($current, ['auth', 'account']) || Str::startsWith($current, ['auth.', 'account.'])) {
return $next($request);
}

View File

@ -5,7 +5,6 @@ namespace Pterodactyl\Http\Middleware\Server;
use Closure;
use Illuminate\Http\Request;
use Pterodactyl\Models\Server;
use Illuminate\Contracts\Session\Session;
use Illuminate\Contracts\Routing\ResponseFactory;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
@ -29,29 +28,21 @@ class AccessingValidServer
*/
private $response;
/**
* @var \Illuminate\Contracts\Session\Session
*/
private $session;
/**
* AccessingValidServer constructor.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @param \Illuminate\Contracts\Routing\ResponseFactory $response
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
* @param \Illuminate\Contracts\Session\Session $session
*/
public function __construct(
ConfigRepository $config,
ResponseFactory $response,
ServerRepositoryInterface $repository,
Session $session
ServerRepositoryInterface $repository
) {
$this->config = $config;
$this->repository = $repository;
$this->response = $response;
$this->session = $session;
}
/**
@ -61,7 +52,6 @@ class AccessingValidServer
* @param \Closure $next
* @return \Illuminate\Http\Response|mixed
*
* @throws \Illuminate\Auth\AuthenticationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
@ -90,10 +80,6 @@ class AccessingValidServer
return $this->response->view('errors.installing', [], 409);
}
// Store the server in the session.
// @todo remove from session. use request attributes.
$this->session->now('server_data.model', $server);
// Add server to the request attributes. This will replace sessions
// as files are updated.
$request->attributes->set('server', $server);

View File

@ -2,6 +2,8 @@
namespace Pterodactyl\Http\Requests\Admin\Servers\Databases;
use Illuminate\Validation\Rule;
use Illuminate\Database\Query\Builder;
use Pterodactyl\Http\Requests\Admin\AdminFormRequest;
class StoreServerDatabaseRequest extends AdminFormRequest
@ -14,7 +16,15 @@ class StoreServerDatabaseRequest extends AdminFormRequest
public function rules(): array
{
return [
'database' => 'required|string|min:1|max:24',
'database' => [
'required',
'string',
'min:1',
'max:24',
Rule::unique('databases')->where(function (Builder $query) {
$query->where('database_host_id', $this->input('database_host_id') ?? 0);
}),
],
'remote' => 'required|string|regex:/^[0-9%.]{1,15}$/',
'database_host_id' => 'required|integer|exists:database_hosts,id',
];

View File

@ -126,10 +126,6 @@ abstract class ApplicationApiRequest extends FormRequest
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
/**
* @return bool
*/
protected function passesAuthorization()
{
// If we have already validated we do not need to call this function

View File

@ -3,7 +3,6 @@
namespace Pterodactyl\Http\Requests\Api\Application\Locations;
use Pterodactyl\Models\Location;
use Pterodactyl\Http\Controllers\Api\Application\Locations\StoreLocationRequest;
class UpdateLocationRequest extends StoreLocationRequest
{
@ -31,6 +30,6 @@ class UpdateLocationRequest extends StoreLocationRequest
return collect(Location::getUpdateRulesForId($locationId))->only([
'short',
'long',
]);
])->toArray();
}
}

View File

@ -36,7 +36,7 @@ class StoreNodeRequest extends ApplicationApiRequest
'memory',
'memory_overallocate',
'disk',
'disk_overallocation',
'disk_overallocate',
'upload_size',
'daemonListen',
'daemonSFTP',

View File

@ -2,6 +2,8 @@
namespace Pterodactyl\Http\Requests\Api\Application\Servers\Databases;
use Illuminate\Validation\Rule;
use Illuminate\Database\Query\Builder;
use Pterodactyl\Services\Acl\Api\AdminAcl;
use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest;
@ -25,7 +27,15 @@ class StoreServerDatabaseRequest extends ApplicationApiRequest
public function rules(): array
{
return [
'database' => 'required|string|min:1|max:24',
'database' => [
'required',
'string',
'min:1',
'max:24',
Rule::unique('databases')->where(function (Builder $query) {
$query->where('database_host_id', $this->input('host') ?? 0);
}),
],
'remote' => 'required|string|regex:/^[0-9%.]{1,15}$/',
'host' => 'required|integer|exists:database_hosts,id',
];

View File

@ -3,6 +3,7 @@
namespace Pterodactyl\Http\Requests\Api\Application\Servers;
use Pterodactyl\Models\Server;
use Illuminate\Support\Collection;
class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
{
@ -17,15 +18,29 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
return [
'allocation' => $rules['allocation_id'],
'memory' => $rules['memory'],
'swap' => $rules['swap'],
'io' => $rules['io'],
'cpu' => $rules['cpu'],
'disk' => $rules['disk'],
'limits' => 'sometimes|array',
'limits.memory' => $this->requiredToOptional('memory', $rules['memory'], true),
'limits.swap' => $this->requiredToOptional('swap', $rules['swap'], true),
'limits.io' => $this->requiredToOptional('io', $rules['io'], true),
'limits.cpu' => $this->requiredToOptional('cpu', $rules['cpu'], true),
'limits.disk' => $this->requiredToOptional('disk', $rules['disk'], true),
// Legacy rules to maintain backwards compatable API support without requiring
// a major version bump.
//
// @see https://github.com/pterodactyl/panel/issues/1500
'memory' => $this->requiredToOptional('memory', $rules['memory']),
'swap' => $this->requiredToOptional('swap', $rules['swap']),
'io' => $this->requiredToOptional('io', $rules['io']),
'cpu' => $this->requiredToOptional('cpu', $rules['cpu']),
'disk' => $this->requiredToOptional('disk', $rules['disk']),
'add_allocations' => 'bail|array',
'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'],
@ -46,6 +61,15 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
$data['allocation_limit'] = $data['feature_limits']['allocations'];
unset($data['allocation'], $data['feature_limits']);
// Adjust the limits field to match what is expected by the model.
if (! empty($data['limits'])) {
foreach ($data['limits'] as $key => $value) {
$data[$key] = $value;
}
unset($data['limits']);
}
return $data;
}
@ -65,4 +89,30 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
'feature_limits.allocations' => 'Allocation Limit',
];
}
/**
* Converts existing rules for certain limits into a format that maintains backwards
* compatability with the old API endpoint while also supporting a more correct API
* call.
*
* @param string $field
* @param array $rules
* @param bool $limits
* @return array
*
* @see https://github.com/pterodactyl/panel/issues/1500
*/
protected function requiredToOptional(string $field, array $rules, bool $limits = false)
{
if (! in_array('required', $rules)) {
return $rules;
}
return (new Collection($rules))
->filter(function ($value) {
return $value !== 'required';
})
->prepend($limits ? 'required_with:limits' : 'required_without:limits')
->toArray();
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Client\Account;
use Pterodactyl\Models\User;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
use Pterodactyl\Exceptions\Http\Base\InvalidPasswordProvidedException;
class UpdateEmailRequest extends ClientApiRequest
{
/**
* @return bool
*
* @throws \Pterodactyl\Exceptions\Http\Base\InvalidPasswordProvidedException
*/
public function authorize(): bool
{
if (! parent::authorize()) {
return false;
}
// Verify password matches when changing password or email.
if (! password_verify($this->input('password'), $this->user()->password)) {
throw new InvalidPasswordProvidedException(trans('validation.internal.invalid_password'));
}
return true;
}
/**
* @return array
*/
public function rules(): array
{
$rules = User::getUpdateRulesForId($this->user()->id);
return ['email' => $rules['email']];
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Client\Account;
use Pterodactyl\Models\User;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
use Pterodactyl\Exceptions\Http\Base\InvalidPasswordProvidedException;
class UpdatePasswordRequest extends ClientApiRequest
{
/**
* @return bool
*
* @throws \Pterodactyl\Exceptions\Http\Base\InvalidPasswordProvidedException
*/
public function authorize(): bool
{
if (! parent::authorize()) {
return false;
}
// Verify password matches when changing password or email.
if (! password_verify($this->input('current_password'), $this->user()->password)) {
throw new InvalidPasswordProvidedException(trans('validation.internal.invalid_password'));
}
return true;
}
/**
* @return array
*/
public function rules(): array
{
$rules = User::getUpdateRulesForId($this->user()->id);
return ['password' => array_merge($rules['password'], ['confirmed'])];
}
}

View File

@ -2,18 +2,23 @@
namespace Pterodactyl\Http\Requests\Api\Client;
use Pterodactyl\Models\Server;
use Pterodactyl\Contracts\Http\ClientPermissionsRequest;
use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest;
abstract class ClientApiRequest extends ApplicationApiRequest
{
/**
* Determine if the current user is authorized to perform
* the requested action against the API.
* Determine if the current user is authorized to perform the requested action against the API.
*
* @return bool
*/
public function authorize(): bool
{
if ($this instanceof ClientPermissionsRequest || method_exists($this, 'permission')) {
return $this->user()->can($this->permission(), $this->getModel(Server::class));
}
return true;
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Databases;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Database;
use Pterodactyl\Contracts\Http\ClientPermissionsRequest;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
class DeleteDatabaseRequest extends ClientApiRequest implements ClientPermissionsRequest
{
/**
* @return string
*/
public function permission(): string
{
return 'delete-database';
}
/**
* @return bool
*/
public function resourceExists(): bool
{
return $this->getModel(Server::class)->id === $this->getModel(Database::class)->server_id;
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Databases;
use Pterodactyl\Contracts\Http\ClientPermissionsRequest;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
class GetDatabasesRequest extends ClientApiRequest implements ClientPermissionsRequest
{
/**
* @return string
*/
public function permission(): string
{
return 'view-databases';
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Databases;
use Pterodactyl\Contracts\Http\ClientPermissionsRequest;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
class StoreDatabaseRequest extends ClientApiRequest implements ClientPermissionsRequest
{
/**
* @return string
*/
public function permission(): string
{
return 'create-database';
}
/**
* @return array
*/
public function rules(): array
{
return [
'database' => 'required|alpha_dash|min:1|max:100',
'remote' => 'required|string|regex:/^[0-9%.]{1,15}$/',
];
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Files;
use Pterodactyl\Contracts\Http\ClientPermissionsRequest;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
class CopyFileRequest extends ClientApiRequest implements ClientPermissionsRequest
{
/**
* @return string
*/
public function permission(): string
{
return 'copy-files';
}
/**
* @return array
*/
public function rules(): array
{
return [
'location' => 'required|string',
];
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Files;
use Pterodactyl\Models\Server;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
class CreateFolderRequest extends ClientApiRequest
{
/**
* Checks that the authenticated user is allowed to create files on the server.
*
* @return bool
*/
public function authorize(): bool
{
return $this->user()->can('create-files', $this->getModel(Server::class));
}
/**
* @return array
*/
public function rules(): array
{
return [
'root' => 'sometimes|nullable|string',
'name' => 'required|string',
];
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Files;
use Pterodactyl\Contracts\Http\ClientPermissionsRequest;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
class DeleteFileRequest extends ClientApiRequest implements ClientPermissionsRequest
{
/**
* @return string
*/
public function permission(): string
{
return 'delete-files';
}
/**
* @return array
*/
public function rules(): array
{
return [
'location' => 'required|string',
];
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Files;
use Pterodactyl\Models\Server;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
class DownloadFileRequest extends ClientApiRequest
{
/**
* Ensure that the user making this request has permission to download files
* from this server.
*
* @return bool
*/
public function authorize(): bool
{
return $this->user()->can('download-files', $this->getModel(Server::class));
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Files;
use Pterodactyl\Contracts\Http\ClientPermissionsRequest;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
class GetFileContentsRequest extends ClientApiRequest implements ClientPermissionsRequest
{
/**
* Returns the permissions string indicating which permission should be used to
* validate that the authenticated user has permission to perform this action aganist
* the given resource (server).
*
* @return string
*/
public function permission(): string
{
return 'edit-files';
}
/**
* @return array
*/
public function rules(): array
{
return [
'file' => 'required|string',
];
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Files;
use Pterodactyl\Models\Server;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
class ListFilesRequest extends ClientApiRequest
{
/**
* Check that the user making this request to the API is authorized to list all
* of the files that exist for a given server.
*
* @return bool
*/
public function authorize(): bool
{
return $this->user()->can('list-files', $this->getModel(Server::class));
}
/**
* @return array
*/
public function rules(): array
{
return [
'directory' => 'sometimes|nullable|string',
];
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Files;
use Pterodactyl\Contracts\Http\ClientPermissionsRequest;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
class RenameFileRequest extends ClientApiRequest implements ClientPermissionsRequest
{
/**
* The permission the user is required to have in order to perform this
* request action.
*
* @return string
*/
public function permission(): string
{
return 'move-files';
}
/**
* @return array
*/
public function rules(): array
{
return [
'rename_from' => 'string|required',
'rename_to' => 'string|required',
];
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Files;
use Pterodactyl\Contracts\Http\ClientPermissionsRequest;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
class WriteFileContentRequest extends ClientApiRequest implements ClientPermissionsRequest
{
/**
* Returns the permissions string indicating which permission should be used to
* validate that the authenticated user has permission to perform this action aganist
* the given resource (server).
*
* @return string
*/
public function permission(): string
{
return 'save-files';
}
/**
* There is no rule here for the file contents since we just use the body content
* on the request to set the file contents. If nothing is passed that is fine since
* it just means we want to set the file to be empty.
*
* @return array
*/
public function rules(): array
{
return [
'file' => 'required|string',
];
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Network;
use Pterodactyl\Models\Server;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
class GetNetworkRequest extends ClientApiRequest
{
/**
* Check that the user has permission to view the allocations for
* this server.
*
* @return bool
*/
public function authorize(): bool
{
return $this->user()->can('view-allocations', $this->getModel(Server::class));
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Pterodactyl\Http\Requests\Auth;
use Illuminate\Foundation\Http\FormRequest;
class LoginCheckpointRequest extends FormRequest
{
/**
* Determine if the request is authorized.
*
* @return bool
*/
public function authorize(): bool
{
return true;
}
/**
* Rules to apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
'confirmation_token' => 'required|string',
'authentication_code' => 'required|numeric',
];
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Pterodactyl\Http\Requests\Auth;
use Illuminate\Foundation\Http\FormRequest;
class LoginRequest extends FormRequest
{
/**
* @return bool
*/
public function authorized(): bool
{
return true;
}
/**
* @return array
*/
public function rules(): array
{
return [
'user' => 'required|string|min:1',
'password' => 'required|string',
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Pterodactyl\Http\Requests\Auth;
use Illuminate\Foundation\Http\FormRequest;
class ResetPasswordRequest extends FormRequest
{
/**
* @return bool
*/
public function authorize(): bool
{
return true;
}
/**
* @return array
*/
public function rules(): array
{
return [
'token' => 'required|string',
'email' => 'required|email',
'password' => 'required|string|confirmed|min:8',
];
}
}

View File

@ -28,7 +28,7 @@ class AccountDataFormRequest extends FrontendUserFormRequest
// Verify password matches when changing password or email.
if (in_array($this->input('do_action'), ['password', 'email'])) {
if (! password_verify($this->input('current_password'), $this->user()->password)) {
throw new InvalidPasswordProvidedException(trans('base.account.invalid_password'));
throw new InvalidPasswordProvidedException(trans('validation.internal.invalid_password'));
}
}
@ -59,6 +59,7 @@ class AccountDataFormRequest extends FrontendUserFormRequest
'name_first' => array_get($modelRules, 'name_first'),
'name_last' => array_get($modelRules, 'name_last'),
'username' => array_get($modelRules, 'username'),
'language' => array_get($modelRules, 'language'),
];
break;
default:

View File

@ -0,0 +1,34 @@
<?php
namespace Pterodactyl\Http\ViewComposers;
use Illuminate\View\View;
use Pterodactyl\Services\Helpers\AssetHashService;
class AssetComposer
{
/**
* @var \Pterodactyl\Services\Helpers\AssetHashService
*/
private $assetHashService;
/**
* AssetComposer constructor.
*
* @param \Pterodactyl\Services\Helpers\AssetHashService $assetHashService
*/
public function __construct(AssetHashService $assetHashService)
{
$this->assetHashService = $assetHashService;
}
/**
* Provide access to the asset service in the views.
*
* @param \Illuminate\View\View $view
*/
public function compose(View $view)
{
$view->with('asset', $this->assetHashService);
}
}

View File

@ -110,8 +110,8 @@ class Node extends Model implements CleansAttributes, ValidableContract
'disk' => 'numeric|min:1',
'disk_overallocate' => 'numeric|min:-1',
'daemonBase' => 'regex:/^([\/][\d\w.\-\/]+)$/',
'daemonSFTP' => 'numeric|between:1024,65535',
'daemonListen' => 'numeric|between:1024,65535',
'daemonSFTP' => 'numeric|between:1,65535',
'daemonListen' => 'numeric|between:1,65535',
'maintenance_mode' => 'boolean',
'upload_size' => 'int|between:1,1024',
];
@ -132,6 +132,16 @@ class Node extends Model implements CleansAttributes, ValidableContract
'maintenance_mode' => false,
];
/**
* Get the connection address to use when making calls to this node.
*
* @return string
*/
public function getConnectionAddress(): string
{
return sprintf('%s://%s:%s', $this->scheme, $this->fqdn, $this->daemonListen);
}
/**
* Returns the configuration in JSON format.
*

View File

@ -63,7 +63,7 @@ class Server extends Model implements CleansAttributes, ValidableContract
'image' => 'required',
'startup' => 'required',
'database_limit' => 'present',
'allocation_limit' => 'present',
'allocation_limit' => 'sometimes',
];
/**
@ -87,7 +87,7 @@ class Server extends Model implements CleansAttributes, ValidableContract
'startup' => 'string',
'skip_scripts' => 'boolean',
'image' => 'string|max:255',
'installed' => 'boolean',
'installed' => 'in:0,1,2',
'database_limit' => 'nullable|integer|min:0',
'allocation_limit' => 'nullable|integer|min:0',
];
@ -143,6 +143,14 @@ class Server extends Model implements CleansAttributes, ValidableContract
return Schema::getColumnListing($this->getTable());
}
/**
* @return bool
*/
public function isInstalled(): bool
{
return $this->installed === 1;
}
/**
* Gets the user who owns the server.
*

View File

@ -5,6 +5,7 @@ namespace Pterodactyl\Models;
use Sofa\Eloquence\Eloquence;
use Sofa\Eloquence\Validable;
use Pterodactyl\Rules\Username;
use Illuminate\Support\Collection;
use Illuminate\Validation\Rules\In;
use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
@ -177,6 +178,16 @@ class User extends Model implements
return $rules;
}
/**
* Return the user model in a format that can be passed over to Vue templates.
*
* @return array
*/
public function toVueObject(): array
{
return (new Collection($this->toArray()))->except(['id', 'external_id'])->toArray();
}
/**
* Send the password reset notification.
*

View File

@ -3,7 +3,7 @@
namespace Pterodactyl\Providers;
use Illuminate\Support\ServiceProvider;
use Pterodactyl\Repositories\Daemon\FileRepository;
use Pterodactyl\Repositories\Wings\FileRepository;
use Pterodactyl\Repositories\Daemon\PowerRepository;
use Pterodactyl\Repositories\Eloquent\EggRepository;
use Pterodactyl\Repositories\Eloquent\NestRepository;

View File

@ -22,20 +22,21 @@ class RouteServiceProvider extends ServiceProvider
public function map()
{
Route::middleware(['web', 'auth', 'csrf'])
->namespace($this->namespace . '\Base')
->group(base_path('routes/base.php'));
->namespace($this->namespace . '\Base')
->group(base_path('routes/base.php'));
Route::middleware(['web', 'auth', 'admin', 'csrf'])->prefix('/admin')
->namespace($this->namespace . '\Admin')
->group(base_path('routes/admin.php'));
->namespace($this->namespace . '\Admin')
->group(base_path('routes/admin.php'));
Route::middleware(['web', 'csrf'])->prefix('/auth')
->namespace($this->namespace . '\Auth')
->group(base_path('routes/auth.php'));
->namespace($this->namespace . '\Auth')
->group(base_path('routes/auth.php'));
Route::middleware(['web', 'csrf', 'auth', 'server', 'subuser.auth', 'node.maintenance'])->prefix('/server/{server}')
->namespace($this->namespace . '\Server')
->group(base_path('routes/server.php'));
Route::middleware(['web', 'csrf', 'auth', 'server', 'subuser.auth', 'node.maintenance'])
->prefix('/api/server/{server}')
->namespace($this->namespace . '\Server')
->group(base_path('routes/server.php'));
Route::middleware(['api'])->prefix('/api/application')
->namespace($this->namespace . '\Api\Application')
@ -50,7 +51,7 @@ class RouteServiceProvider extends ServiceProvider
->group(base_path('routes/api-remote.php'));
Route::middleware(['web', 'daemon-old'])->prefix('/daemon')
->namespace($this->namespace . '\Daemon')
->group(base_path('routes/daemon.php'));
->namespace($this->namespace . '\Daemon')
->group(base_path('routes/daemon.php'));
}
}

View File

@ -3,6 +3,7 @@
namespace Pterodactyl\Providers;
use Illuminate\Support\ServiceProvider;
use Pterodactyl\Http\ViewComposers\AssetComposer;
use Pterodactyl\Http\ViewComposers\ServerListComposer;
use Pterodactyl\Http\ViewComposers\Server\ServerDataComposer;
@ -13,6 +14,8 @@ class ViewComposerServiceProvider extends ServiceProvider
*/
public function boot()
{
$this->app->make('view')->composer('*', AssetComposer::class);
$this->app->make('view')->composer('server.*', ServerDataComposer::class);
// Add data to make the sidebar work when viewing a server.

View File

@ -3,6 +3,7 @@
namespace Pterodactyl\Repositories\Daemon;
use stdClass;
use RuntimeException;
use Psr\Http\Message\ResponseInterface;
use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface;
@ -84,33 +85,20 @@ class FileRepository extends BaseRepository implements FileRepositoryInterface
{
$response = $this->getHttpClient()->request('GET', sprintf('server/directory/%s', rawurlencode($path)));
$contents = json_decode($response->getBody());
$files = $folders = [];
return json_decode($response->getBody());
}
foreach ($contents as $value) {
if ($value->directory) {
array_push($folders, [
'entry' => $value->name,
'directory' => trim($path, '/'),
'size' => null,
'date' => strtotime($value->modified),
'mime' => $value->mime,
]);
} elseif ($value->file) {
array_push($files, [
'entry' => $value->name,
'directory' => trim($path, '/'),
'extension' => str_replace('\\', '/', pathinfo($value->name, PATHINFO_EXTENSION)),
'size' => human_readable($value->size),
'date' => strtotime($value->modified),
'mime' => $value->mime,
]);
}
}
return [
'files' => $files,
'folders' => $folders,
];
/**
* Creates a new directory for the server in the given $path.
*
* @param string $name
* @param string $path
* @return \Psr\Http\Message\ResponseInterface
*
* @throws \RuntimeException
*/
public function createDirectory(string $name, string $path): ResponseInterface
{
throw new RuntimeException('Not implemented.');
}
}

View File

@ -76,7 +76,7 @@ class DatabaseRepository extends EloquentRepository implements DatabaseRepositor
*/
public function getDatabasesForServer(int $server): Collection
{
return $this->getBuilder()->where('server_id', $server)->get($this->getColumns());
return $this->getBuilder()->with('host')->where('server_id', $server)->get($this->getColumns());
}
/**

View File

@ -6,10 +6,8 @@ use Generator;
use Pterodactyl\Models\Node;
use Illuminate\Support\Collection;
use Pterodactyl\Repositories\Concerns\Searchable;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
class NodeRepository extends EloquentRepository implements NodeRepositoryInterface
{
@ -140,25 +138,6 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa
return $node;
}
/**
* Return a node with all of the servers attached to that node.
*
* @param int $id
* @return \Pterodactyl\Models\Node
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getNodeServers(int $id): Node
{
try {
return $this->getBuilder()->with([
'servers.user', 'servers.nest', 'servers.egg',
])->findOrFail($id, $this->getColumns());
} catch (ModelNotFoundException $exception) {
throw new RecordNotFoundException;
}
}
/**
* Return a collection of nodes for all locations to use in server creation UI.
*

View File

@ -2,6 +2,7 @@
namespace Pterodactyl\Repositories\Eloquent;
use Pterodactyl\Models\Node;
use Pterodactyl\Models\User;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\Server;
@ -358,4 +359,20 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
{
return $this->getBuilder()->where('suspended', true)->count();
}
/**
* Returns all of the servers that exist for a given node in a paginated response.
*
* @param int $node
* @param int $limit
*
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
public function loadAllServersForNode(int $node, int $limit): LengthAwarePaginator
{
return $this->getBuilder()
->with(['user', 'nest', 'egg'])
->where('node_id', '=', $node)
->paginate($limit);
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Pterodactyl\Repositories\Wings;
use GuzzleHttp\Client;
use Pterodactyl\Repositories\Daemon\BaseRepository;
use Pterodactyl\Contracts\Repository\Daemon\BaseRepositoryInterface;
abstract class BaseWingsRepository extends BaseRepository implements BaseRepositoryInterface
{
/**
* Return an instance of the Guzzle HTTP Client to be used for requests.
*
* @param array $headers
* @return \GuzzleHttp\Client
*/
public function getHttpClient(array $headers = []): Client
{
// We're just going to extend the parent client here since that logic is already quite
// sound and does everything we need it to aside from provide the correct base URL
// and authentication headers.
$client = parent::getHttpClient($headers);
return new Client(array_merge($client->getConfig(), [
'base_uri' => $this->getNode()->getConnectionAddress(),
'headers' => [
'Authorization' => 'Bearer ' . ($this->getToken() ?? $this->getNode()->daemonSecret),
'Accept' => 'application/json',
'Content-Type' => 'application/json',
],
]));
}
}

View File

@ -0,0 +1,173 @@
<?php
namespace Pterodactyl\Repositories\Wings;
use stdClass;
use Exception;
use Psr\Http\Message\ResponseInterface;
use Pterodactyl\Exceptions\Http\Server\FileSizeTooLargeException;
use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface;
class FileRepository extends BaseWingsRepository implements FileRepositoryInterface
{
/**
* Return stat information for a given file.
*
* @param string $path
* @return \stdClass
*
* @throws \Exception
* @throws \GuzzleHttp\Exception\TransferException
*/
public function getFileStat(string $path): stdClass
{
throw new Exception('Function not implemented.');
}
/**
* Return the contents of a given file.
*
* @param string $path
* @param int|null $notLargerThan the maximum content length in bytes
* @return string
*
* @throws \GuzzleHttp\Exception\TransferException
* @throws \Pterodactyl\Exceptions\Http\Server\FileSizeTooLargeException
*/
public function getContent(string $path, int $notLargerThan = null): string
{
$response = $this->getHttpClient()->get(
sprintf('/api/servers/%s/files/contents', $this->getServer()->uuid),
[
'query' => ['file' => $path],
]
);
$length = (int) $response->getHeader('Content-Length')[0] ?? 0;
if ($notLargerThan && $length > $notLargerThan) {
throw new FileSizeTooLargeException(
trans('server.files.exceptions.max_size')
);
}
return $response->getBody()->__toString();
}
/**
* Save new contents to a given file. This works for both creating and updating
* a file.
*
* @param string $path
* @param string $content
* @return \Psr\Http\Message\ResponseInterface
*
* @throws \GuzzleHttp\Exception\TransferException
*/
public function putContent(string $path, string $content): ResponseInterface
{
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/write', $this->getServer()->uuid),
[
'query' => ['file' => $path],
'body' => $content,
]
);
}
/**
* Return a directory listing for a given path.
*
* @param string $path
* @return array
*
* @throws \GuzzleHttp\Exception\TransferException
*/
public function getDirectory(string $path): array
{
$response = $this->getHttpClient()->get(
sprintf('/api/servers/%s/files/list-directory', $this->getServer()->uuid),
[
'query' => ['directory' => $path],
]
);
return json_decode($response->getBody(), true);
}
/**
* Creates a new directory for the server in the given $path.
*
* @param string $name
* @param string $path
* @return \Psr\Http\Message\ResponseInterface
*/
public function createDirectory(string $name, string $path): ResponseInterface
{
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/create-directory', $this->getServer()->uuid),
[
'json' => [
'name' => $name,
'path' => $path,
],
]
);
}
/**
* Renames or moves a file on the remote machine.
*
* @param string $from
* @param string $to
* @return \Psr\Http\Message\ResponseInterface
*/
public function renameFile(string $from, string $to): ResponseInterface
{
return $this->getHttpClient()->put(
sprintf('/api/servers/%s/files/rename', $this->getServer()->uuid),
[
'json' => [
'rename_from' => $from,
'rename_to' => $to,
],
]
);
}
/**
* Copy a given file and give it a unique name.
*
* @param string $location
* @return \Psr\Http\Message\ResponseInterface
*/
public function copyFile(string $location): ResponseInterface
{
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/copy', $this->getServer()->uuid),
[
'json' => [
'location' => $location,
],
]
);
}
/**
* Delete a file or folder for the server.
*
* @param string $location
* @return \Psr\Http\Message\ResponseInterface
*/
public function deleteFile(string $location): ResponseInterface
{
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/delete', $this->getServer()->uuid),
[
'json' => [
'location' => $location,
],
]
);
}
}

View File

@ -69,7 +69,7 @@ class DatabaseManagementService
$data['server_id'] = $server;
$data['database'] = sprintf('s%d_%s', $server, $data['database']);
$data['username'] = sprintf('u%d_%s', $server, str_random(10));
$data['password'] = $this->encrypter->encrypt(str_random(16));
$data['password'] = $this->encrypter->encrypt(str_random(24));
$this->database->beginTransaction();
try {

View File

@ -0,0 +1,141 @@
<?php
namespace Pterodactyl\Services\Helpers;
use Illuminate\Filesystem\FilesystemManager;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Cache\Repository as CacheRepository;
class AssetHashService
{
/**
* Location of the manifest file generated by gulp.
*/
public const MANIFEST_PATH = './assets/manifest.json';
/**
* @var \Illuminate\Contracts\Cache\Repository
*/
private $cache;
/**
* @var \Illuminate\Contracts\Filesystem\Filesystem
*/
private $filesystem;
/**
* @var \Illuminate\Contracts\Foundation\Application
*/
private $application;
/**
* @var null|array
*/
protected static $manifest;
/**
* AssetHashService constructor.
*
* @param \Illuminate\Contracts\Foundation\Application $application
* @param \Illuminate\Contracts\Cache\Repository $cache
* @param \Illuminate\Filesystem\FilesystemManager $filesystem
*/
public function __construct(Application $application, CacheRepository $cache, FilesystemManager $filesystem)
{
$this->application = $application;
$this->cache = $cache;
$this->filesystem = $filesystem->createLocalDriver(['root' => public_path()]);
}
/**
* Modify a URL to append the asset hash.
*
* @param string $resource
* @return string
*
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function url(string $resource): string
{
$file = last(explode('/', $resource));
$data = array_get($this->manifest(), $file, $file);
return str_replace($file, array_get($data, 'src', $file), $resource);
}
/**
* Return the data integrity hash for a resource.
*
* @param string $resource
* @return string
*
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function integrity(string $resource): string
{
$file = last(explode('/', $resource));
$data = array_get($this->manifest(), $file, $file);
return array_get($data, 'integrity', '');
}
/**
* Return a built CSS import using the provided URL.
*
* @param string $resource
* @return string
*
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function css(string $resource): string
{
return '<link href="' . $this->url($resource) . '"
rel="stylesheet preload"
as="style"
crossorigin="anonymous"
integrity="' . $this->integrity($resource) . '"
referrerpolicy="no-referrer">';
}
/**
* Return a built JS import using the provided URL.
*
* @param string $resource
* @return string
*
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function js(string $resource): string
{
return '<script src="' . $this->url($resource) . '"
integrity="' . $this->integrity($resource) . '"
crossorigin="anonymous"></script>';
}
/**
* Get the asset manifest and store it in the cache for quicker lookups.
*
* @return array
*
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
protected function manifest(): array
{
if (! is_null(self::$manifest)) {
return self::$manifest;
}
// Skip checking the cache if we are not in production.
if ($this->application->environment() === 'production') {
$stored = $this->cache->get('Core:AssetManifest');
if (! is_null($stored)) {
return self::$manifest = $stored;
}
}
$contents = json_decode($this->filesystem->get(self::MANIFEST_PATH), true);
$this->cache->put('Core:AssetManifest', $contents, 1440);
return self::$manifest = $contents;
}
}

View File

@ -1,22 +1,18 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Services\Users;
use Exception;
use RuntimeException;
use Pterodactyl\Models\User;
use PragmaRX\Google2FA\Google2FA;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
class TwoFactorSetupService
{
const VALID_BASE32_CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
/**
* @var \Illuminate\Contracts\Config\Repository
*/
@ -27,11 +23,6 @@ class TwoFactorSetupService
*/
private $encrypter;
/**
* @var \PragmaRX\Google2FA\Google2FA
*/
private $google2FA;
/**
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface
*/
@ -42,24 +33,22 @@ class TwoFactorSetupService
*
* @param \Illuminate\Contracts\Config\Repository $config
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
* @param \PragmaRX\Google2FA\Google2FA $google2FA
* @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository
*/
public function __construct(
ConfigRepository $config,
Encrypter $encrypter,
Google2FA $google2FA,
UserRepositoryInterface $repository
) {
$this->config = $config;
$this->encrypter = $encrypter;
$this->google2FA = $google2FA;
$this->repository = $repository;
}
/**
* Generate a 2FA token and store it in the database before returning the
* QR code image.
* QR code URL. This URL will need to be attached to a QR generating service in
* order to function.
*
* @param \Pterodactyl\Models\User $user
* @return string
@ -69,13 +58,26 @@ class TwoFactorSetupService
*/
public function handle(User $user): string
{
$secret = $this->google2FA->generateSecretKey($this->config->get('pterodactyl.auth.2fa.bytes'));
$image = $this->google2FA->getQRCodeGoogleUrl($this->config->get('app.name'), $user->email, $secret);
$secret = '';
try {
for ($i = 0; $i < $this->config->get('pterodactyl.auth.2fa.bytes', 16); $i++) {
$secret .= substr(self::VALID_BASE32_CHARACTERS, random_int(0, 31), 1);
}
} catch (Exception $exception) {
throw new RuntimeException($exception->getMessage(), 0, $exception);
}
$this->repository->withoutFreshModel()->update($user->id, [
'totp_secret' => $this->encrypter->encrypt($secret),
]);
return $image;
$company = $this->config->get('app.name');
return sprintf(
'otpauth://totp/%1$s:%2$s?secret=%3$s&issuer=%1$s',
rawurlencode($company),
rawurlencode($user->email),
rawurlencode($secret)
);
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Pterodactyl\Transformers\Api\Client;
use Pterodactyl\Models\User;
class AccountTransformer extends BaseClientTransformer
{
/**
* Return the resource name for the JSONAPI output.
*
* @return string
*/
public function getResourceName(): string
{
return 'user';
}
/**
* Return basic information about the currently logged in user.
*
* @param \Pterodactyl\Models\User $model
* @return array
*/
public function transform(User $model)
{
return [
'id' => $model->id,
'admin' => $model->root_admin,
'username' => $model->username,
'email' => $model->email,
'first_name' => $model->name_first,
'last_name' => $model->name_last,
'language' => $model->language,
];
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace Pterodactyl\Transformers\Api\Client;
use Pterodactyl\Models\Allocation;
class AllocationTransformer extends BaseClientTransformer
{
/**
* Return the resource name for the JSONAPI output.
*
* @return string
*/
public function getResourceName(): string
{
return 'allocation';
}
/**
* Return basic information about the currently logged in user.
*
* @param \Pterodactyl\Models\Allocation $model
* @return array
*/
public function transform(Allocation $model)
{
$model->loadMissing('server');
return [
'ip' => $model->ip,
'alias' => $model->ip_alias,
'port' => $model->port,
'default' => $model->getRelation('server')->allocation_id === $model->id,
];
}
}

View File

@ -0,0 +1,78 @@
<?php
namespace Pterodactyl\Transformers\Api\Client;
use Pterodactyl\Models\Database;
use League\Fractal\Resource\Item;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Contracts\Extensions\HashidsInterface;
class DatabaseTransformer extends BaseClientTransformer
{
protected $availableIncludes = ['password'];
/**
* @var \Illuminate\Contracts\Encryption\Encrypter
*/
private $encrypter;
/**
* @var \Pterodactyl\Contracts\Extensions\HashidsInterface
*/
private $hashids;
/**
* Handle dependency injection.
*
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
* @param \Pterodactyl\Contracts\Extensions\HashidsInterface $hashids
*/
public function handle(Encrypter $encrypter, HashidsInterface $hashids)
{
$this->encrypter = $encrypter;
$this->hashids = $hashids;
}
/**
* @return string
*/
public function getResourceName(): string
{
return Database::RESOURCE_NAME;
}
/**
* @param \Pterodactyl\Models\Database $model
* @return array
*/
public function transform(Database $model): array
{
$model->loadMissing('host');
return [
'id' => $this->hashids->encode($model->id),
'host' => [
'address' => $model->getRelation('host')->host,
'port' => $model->getRelation('host')->port,
],
'name' => $model->database,
'username' => $model->username,
'connections_from' => $model->remote,
];
}
/**
* Include the database password in the request.
*
* @param \Pterodactyl\Models\Database $model
* @return \League\Fractal\Resource\Item
*/
public function includePassword(Database $model): Item
{
return $this->item($model, function (Database $model) {
return [
'password' => $this->encrypter->decrypt($model->password),
];
}, 'database_password');
}
}

Some files were not shown because too many files have changed in this diff Show More