Merge pull request #701 from Pterodactyl/feature/add-frontend-server-configuration

Add database/startup/allocation management to server front-end views
This commit is contained in:
Dane Everitt 2017-10-27 20:38:16 -04:00 committed by GitHub
commit d73d580724
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
89 changed files with 3313 additions and 2047 deletions

View file

@ -12,15 +12,22 @@ This project follows [Semantic Versioning](http://semver.org) guidelines.
* New CLI command to disabled 2-Factor Authentication on an account if necessary. * New CLI command to disabled 2-Factor Authentication on an account if necessary.
* Ability to delete users and locations via the CLI. * Ability to delete users and locations via the CLI.
* You can now require 2FA for all users, admins only, or at will using a simple configuration in the Admin CP. * You can now require 2FA for all users, admins only, or at will using a simple configuration in the Admin CP.
* Added ability to export and import service options and their associated settings and environment variables via the Admin CP. * **Added ability to export and import service options and their associated settings and environment variables via the Admin CP.**
* Default allocation for a server can be changed on the front-end by users. This includes two new subuser permissions as well.
* Significant improvements to environment variable control for servers. Now ships with built-in abilities to define extra variables in the Panel's configuration file, or in-code for those heavily modifying the Panel.
* Quick link to server edit view in ACP on frontend when viewing servers.
* Databases created in the Panel now include `EXECUTE` privilege.
### Changed ### Changed
* **Services renamed to Nests. Service Options renamed to Eggs.** 🥚
* Theme colors and login pages updated to give a more unique feel to the project. * Theme colors and login pages updated to give a more unique feel to the project.
* Massive overhaul to the backend code that allows for much easier updating of core functionality as well as support for better testing. This overhaul also reduces complex code logic, and allows for faster response times in the application. * Massive overhaul to the backend code that allows for much easier updating of core functionality as well as support for better testing. This overhaul also reduces complex code logic, and allows for faster response times in the application.
* CLI commands updated to be easier to type, now stored in the `p:` namespace. * CLI commands updated to be easier to type, now stored in the `p:` namespace.
* Logout icon is now more universal and not just a power icon. * Logout icon is now more universal and not just a power icon.
* Administrative logout notice now uses SWAL rather than a generic javascript popup. * Administrative logout notice now uses SWAL rather than a generic javascript popup.
* Server creation page now only asks for a node to deploy to, rather than requiring a location and then a node. * Server creation page now only asks for a node to deploy to, rather than requiring a location and then a node.
* Database passwords are now hidden by default and will only show if clicked on. In addition, database view in ACP now indicates that passwords must be viewed on the front-end.
* Localhost cannot be used as a connection address in the environment configuration script. `127.0.0.1` is allowed.
### Fixed ### Fixed
* Unable to change the daemon secret for a server via the Admin CP. * Unable to change the daemon secret for a server via the Admin CP.
@ -28,6 +35,11 @@ This project follows [Semantic Versioning](http://semver.org) guidelines.
* Fixes a design-flaw in the allocation management part of nodes that would run a MySQL query for each port being allocated. This behavior is now changed to only execute one query to add multiple ports at once. * Fixes a design-flaw in the allocation management part of nodes that would run a MySQL query for each port being allocated. This behavior is now changed to only execute one query to add multiple ports at once.
* Attempting to create a server when no nodes are configured now redirects to the node creation page. * Attempting to create a server when no nodes are configured now redirects to the node creation page.
* Fixes missing library issue for teamspeak when used with mariadb. * Fixes missing library issue for teamspeak when used with mariadb.
* Fixes inability to change the default port on front-end when viewing a server.
* Fixes bug preventing deletion of nests that have other nests referencing them as children.
### Removed
* SFTP settings page now only displays connection address and username. Password setting was removed as it is no longer necessary with Daemon changes.
## v0.6.4 (Courageous Carniadactylus) ## v0.6.4 (Courageous Carniadactylus)
### Fixed ### Fixed

View file

@ -9,8 +9,35 @@
namespace Pterodactyl\Contracts\Repository; namespace Pterodactyl\Contracts\Repository;
use Illuminate\Support\Collection;
interface DatabaseRepositoryInterface extends RepositoryInterface interface DatabaseRepositoryInterface extends RepositoryInterface
{ {
const DEFAULT_CONNECTION_NAME = 'dynamic';
/**
* Set the connection name to execute statements against.
*
* @param string $connection
* @return $this
*/
public function setConnection(string $connection);
/**
* Return the connection to execute statements aganist.
*
* @return string
*/
public function getConnection(): string;
/**
* Return all of the databases belonging to a server.
*
* @param int $server
* @return \Illuminate\Support\Collection
*/
public function getDatabasesForServer(int $server): Collection;
/** /**
* Create a new database if it does not already exist on the host with * Create a new database if it does not already exist on the host with
* the provided details. * the provided details.
@ -26,58 +53,52 @@ interface DatabaseRepositoryInterface extends RepositoryInterface
/** /**
* Create a new database on a given connection. * Create a new database on a given connection.
* *
* @param string $database * @param string $database
* @param null|string $connection
* @return bool * @return bool
*/ */
public function createDatabase($database, $connection = null); public function createDatabase($database);
/** /**
* Create a new database user on a given connection. * Create a new database user on a given connection.
* *
* @param string $username * @param string $username
* @param string $remote * @param string $remote
* @param string $password * @param string $password
* @param null|string $connection
* @return bool * @return bool
*/ */
public function createUser($username, $remote, $password, $connection = null); public function createUser($username, $remote, $password);
/** /**
* Give a specific user access to a given database. * Give a specific user access to a given database.
* *
* @param string $database * @param string $database
* @param string $username * @param string $username
* @param string $remote * @param string $remote
* @param null|string $connection
* @return bool * @return bool
*/ */
public function assignUserToDatabase($database, $username, $remote, $connection = null); public function assignUserToDatabase($database, $username, $remote);
/** /**
* Flush the privileges for a given connection. * Flush the privileges for a given connection.
* *
* @param null|string $connection
* @return mixed * @return mixed
*/ */
public function flush($connection = null); public function flush();
/** /**
* Drop a given database on a specific connection. * Drop a given database on a specific connection.
* *
* @param string $database * @param string $database
* @param null|string $connection
* @return bool * @return bool
*/ */
public function dropDatabase($database, $connection = null); public function dropDatabase($database);
/** /**
* Drop a given user on a specific connection. * Drop a given user on a specific connection.
* *
* @param string $username * @param string $username
* @param string $remote * @param string $remote
* @param null|string $connection
* @return mixed * @return mixed
*/ */
public function dropUser($username, $remote, $connection = null); public function dropUser($username, $remote);
} }

View file

@ -9,6 +9,16 @@
namespace Pterodactyl\Contracts\Repository; namespace Pterodactyl\Contracts\Repository;
use Illuminate\Support\Collection;
interface EggVariableRepositoryInterface extends RepositoryInterface interface EggVariableRepositoryInterface extends RepositoryInterface
{ {
/**
* Return editable variables for a given egg. Editable variables must be set to
* user viewable in order to be picked up by this function.
*
* @param int $egg
* @return \Illuminate\Support\Collection
*/
public function getEditableVariables(int $egg): Collection;
} }

View file

@ -9,6 +9,7 @@
namespace Pterodactyl\Contracts\Repository; namespace Pterodactyl\Contracts\Repository;
use Pterodactyl\Models\Server;
use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface; use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface;
interface ServerRepositoryInterface extends RepositoryInterface, SearchableInterface interface ServerRepositoryInterface extends RepositoryInterface, SearchableInterface
@ -53,14 +54,26 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter
public function getVariablesWithValues($id, $returnAsObject = false); public function getVariablesWithValues($id, $returnAsObject = false);
/** /**
* Return enough data to be used for the creation of a server via the daemon. * Get the primary allocation for a given server. If a model is passed into
* the function, load the allocation relationship onto it. Otherwise, find and
* return the server from the database.
* *
* @param int $id * @param int|\Pterodactyl\Models\Server $server
* @return \Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model * @param bool $refresh
* @return \Pterodactyl\Models\Server
* *
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/ */
public function getDataForCreation($id); public function getPrimaryAllocation($server, bool $refresh = false): Server;
/**
* Return enough data to be used for the creation of a server via the daemon.
*
* @param \Pterodactyl\Models\Server $server
* @param bool $refresh
* @return \Pterodactyl\Models\Server
*/
public function getDataForCreation(Server $server, bool $refresh = false): Server;
/** /**
* Return a server as well as associated databases and their hosts. * Return a server as well as associated databases and their hosts.

View file

@ -28,9 +28,9 @@ class DisplayException extends PterodactylException
* @param string $message * @param string $message
* @param Throwable|null $previous * @param Throwable|null $previous
* @param string $level * @param string $level
* @internal param mixed $log * @param int $code
*/ */
public function __construct($message, Throwable $previous = null, $level = self::LEVEL_ERROR) public function __construct($message, Throwable $previous = null, $level = self::LEVEL_ERROR, $code = 0)
{ {
$this->level = $level; $this->level = $level;
@ -38,7 +38,7 @@ class DisplayException extends PterodactylException
Log::{$level}($previous); Log::{$level}($previous);
} }
parent::__construct($message); parent::__construct($message, $code, $previous);
} }
/** /**

View file

@ -0,0 +1,9 @@
<?php
namespace Pterodactyl\Exceptions\Service\Allocation;
use Pterodactyl\Exceptions\PterodactylException;
class AllocationDoesNotBelongToServerException extends PterodactylException
{
}

View file

@ -0,0 +1,85 @@
<?php
namespace Pterodactyl\Http\Controllers\API\Remote;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Auth\AuthenticationException;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Pterodactyl\Services\Sftp\AuthenticateUsingPasswordService;
use Pterodactyl\Http\Requests\API\Remote\SftpAuthenticationFormRequest;
class SftpController extends Controller
{
use ThrottlesLogins;
/**
* @var \Pterodactyl\Services\Sftp\AuthenticateUsingPasswordService
*/
private $authenticationService;
/**
* SftpController constructor.
*
* @param \Pterodactyl\Services\Sftp\AuthenticateUsingPasswordService $authenticationService
*/
public function __construct(AuthenticateUsingPasswordService $authenticationService)
{
$this->authenticationService = $authenticationService;
}
/**
* Authenticate a set of credentials and return the associated server details
* for a SFTP connection on the daemon.
*
* @param \Pterodactyl\Http\Requests\API\Remote\SftpAuthenticationFormRequest $request
* @return \Illuminate\Http\JsonResponse
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/
public function index(SftpAuthenticationFormRequest $request): JsonResponse
{
$connection = explode('.', $request->input('username'));
$this->incrementLoginAttempts($request);
if ($this->hasTooManyLoginAttempts($request)) {
return response()->json([
'error' => 'Logins throttled.',
], 429);
}
try {
$data = $this->authenticationService->handle(
array_get($connection, 0),
$request->input('password'),
object_get($request->attributes->get('node'), 'id', 0),
array_get($connection, 1)
);
$this->clearLoginAttempts($request);
} catch (AuthenticationException $exception) {
return response()->json([
'error' => 'Invalid credentials.',
], 403);
} catch (RecordNotFoundException $exception) {
return response()->json([
'error' => 'Invalid server.',
], 404);
}
return response()->json($data);
}
/**
* Get the throttle key for the given request.
*
* @param \Illuminate\Http\Request $request
* @return string
*/
protected function throttleKey(Request $request)
{
return strtolower(array_get(explode('.', $request->input('username')), 0) . '|' . $request->ip());
}
}

View file

@ -9,11 +9,15 @@
namespace Pterodactyl\Http\Controllers\Admin; namespace Pterodactyl\Http\Controllers\Admin;
use Pterodactyl\Models\DatabaseHost; use PDOException;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag; use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Database\DatabaseHostService; use Pterodactyl\Services\Databases\Hosts\HostUpdateService;
use Pterodactyl\Http\Requests\Admin\DatabaseHostFormRequest; use Pterodactyl\Http\Requests\Admin\DatabaseHostFormRequest;
use Pterodactyl\Services\Databases\Hosts\HostCreationService;
use Pterodactyl\Services\Databases\Hosts\HostDeletionService;
use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface;
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
@ -22,41 +26,57 @@ class DatabaseController extends Controller
/** /**
* @var \Prologue\Alerts\AlertsMessageBag * @var \Prologue\Alerts\AlertsMessageBag
*/ */
protected $alert; private $alert;
/**
* @var \Pterodactyl\Services\Databases\Hosts\HostCreationService
*/
private $creationService;
/**
* @var \Pterodactyl\Services\Databases\Hosts\HostDeletionService
*/
private $deletionService;
/** /**
* @var \Pterodactyl\Contracts\Repository\LocationRepositoryInterface * @var \Pterodactyl\Contracts\Repository\LocationRepositoryInterface
*/ */
protected $locationRepository; private $locationRepository;
/** /**
* @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface * @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface
*/ */
protected $repository; private $repository;
/** /**
* @var \Pterodactyl\Services\Database\DatabaseHostService * @var \Pterodactyl\Services\Databases\Hosts\HostUpdateService
*/ */
protected $service; private $updateService;
/** /**
* DatabaseController constructor. * DatabaseController constructor.
* *
* @param \Prologue\Alerts\AlertsMessageBag $alert * @param \Prologue\Alerts\AlertsMessageBag $alert
* @param \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface $repository * @param \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface $repository
* @param \Pterodactyl\Services\Database\DatabaseHostService $service * @param \Pterodactyl\Services\Databases\Hosts\HostCreationService $creationService
* @param \Pterodactyl\Services\Databases\Hosts\HostDeletionService $deletionService
* @param \Pterodactyl\Services\Databases\Hosts\HostUpdateService $updateService
* @param \Pterodactyl\Contracts\Repository\LocationRepositoryInterface $locationRepository * @param \Pterodactyl\Contracts\Repository\LocationRepositoryInterface $locationRepository
*/ */
public function __construct( public function __construct(
AlertsMessageBag $alert, AlertsMessageBag $alert,
DatabaseHostRepositoryInterface $repository, DatabaseHostRepositoryInterface $repository,
DatabaseHostService $service, HostCreationService $creationService,
HostDeletionService $deletionService,
HostUpdateService $updateService,
LocationRepositoryInterface $locationRepository LocationRepositoryInterface $locationRepository
) { ) {
$this->alert = $alert; $this->alert = $alert;
$this->creationService = $creationService;
$this->deletionService = $deletionService;
$this->repository = $repository; $this->repository = $repository;
$this->service = $service;
$this->locationRepository = $locationRepository; $this->locationRepository = $locationRepository;
$this->updateService = $updateService;
} }
/** /**
@ -64,7 +84,7 @@ class DatabaseController extends Controller
* *
* @return \Illuminate\View\View * @return \Illuminate\View\View
*/ */
public function index() public function index(): View
{ {
return view('admin.databases.index', [ return view('admin.databases.index', [
'locations' => $this->locationRepository->getAllWithNodes(), 'locations' => $this->locationRepository->getAllWithNodes(),
@ -80,7 +100,7 @@ class DatabaseController extends Controller
* *
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/ */
public function view($host) public function view($host): View
{ {
return view('admin.databases.view', [ return view('admin.databases.view', [
'locations' => $this->locationRepository->getAllWithNodes(), 'locations' => $this->locationRepository->getAllWithNodes(),
@ -94,42 +114,41 @@ class DatabaseController extends Controller
* @param \Pterodactyl\Http\Requests\Admin\DatabaseHostFormRequest $request * @param \Pterodactyl\Http\Requests\Admin\DatabaseHostFormRequest $request
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
* *
* @throws \Throwable * @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/ */
public function create(DatabaseHostFormRequest $request) public function create(DatabaseHostFormRequest $request): RedirectResponse
{ {
try { try {
$host = $this->service->create($request->normalize()); $host = $this->creationService->handle($request->normalize());
$this->alert->success('Successfully created a new database host on the system.')->flash(); } catch (PDOException $ex) {
return redirect()->route('admin.databases.view', $host->id);
} catch (\PDOException $ex) {
$this->alert->danger($ex->getMessage())->flash(); $this->alert->danger($ex->getMessage())->flash();
return redirect()->route('admin.databases');
} }
return redirect()->route('admin.databases'); $this->alert->success('Successfully created a new database host on the system.')->flash();
return redirect()->route('admin.databases.view', $host->id);
} }
/** /**
* Handle updating database host. * Handle updating database host.
* *
* @param \Pterodactyl\Http\Requests\Admin\DatabaseHostFormRequest $request * @param \Pterodactyl\Http\Requests\Admin\DatabaseHostFormRequest $request
* @param \Pterodactyl\Models\DatabaseHost $host * @param int $host
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
* *
* @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/ */
public function update(DatabaseHostFormRequest $request, DatabaseHost $host) public function update(DatabaseHostFormRequest $request, int $host): RedirectResponse
{ {
if ($request->input('action') === 'delete') {
return $this->delete($host);
}
try { try {
$host = $this->service->update($host->id, $request->normalize()); $host = $this->updateService->handle($host, $request->normalize());
$this->alert->success('Database host was updated successfully.')->flash(); $this->alert->success('Database host was updated successfully.')->flash();
} catch (\PDOException $ex) { } catch (PDOException $ex) {
$this->alert->danger($ex->getMessage())->flash(); $this->alert->danger($ex->getMessage())->flash();
} }
@ -139,14 +158,14 @@ class DatabaseController extends Controller
/** /**
* Handle request to delete a database host. * Handle request to delete a database host.
* *
* @param \Pterodactyl\Models\DatabaseHost $host * @param int $host
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
* *
* @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException
*/ */
public function delete(DatabaseHost $host) public function delete(int $host): RedirectResponse
{ {
$this->service->delete($host->id); $this->deletionService->handle($host);
$this->alert->success('The requested database host has been deleted from the system.')->flash(); $this->alert->success('The requested database host has been deleted from the system.')->flash();
return redirect()->route('admin.databases'); return redirect()->route('admin.databases');

View file

@ -11,6 +11,7 @@ namespace Pterodactyl\Http\Controllers\Admin;
use Javascript; use Javascript;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Pterodactyl\Models\User;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Prologue\Alerts\AlertsMessageBag; use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\DisplayException;
@ -22,12 +23,13 @@ use Pterodactyl\Services\Servers\ServerDeletionService;
use Pterodactyl\Services\Servers\ReinstallServerService; use Pterodactyl\Services\Servers\ReinstallServerService;
use Pterodactyl\Services\Servers\ContainerRebuildService; use Pterodactyl\Services\Servers\ContainerRebuildService;
use Pterodactyl\Services\Servers\BuildModificationService; use Pterodactyl\Services\Servers\BuildModificationService;
use Pterodactyl\Services\Database\DatabaseManagementService; use Pterodactyl\Services\Databases\DatabasePasswordService;
use Pterodactyl\Services\Servers\DetailsModificationService; use Pterodactyl\Services\Servers\DetailsModificationService;
use Pterodactyl\Services\Servers\StartupModificationService; use Pterodactyl\Services\Servers\StartupModificationService;
use Pterodactyl\Contracts\Repository\NestRepositoryInterface; use Pterodactyl\Contracts\Repository\NestRepositoryInterface;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository; use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository;
use Pterodactyl\Services\Databases\DatabaseManagementService;
use Illuminate\Contracts\Config\Repository as ConfigRepository; use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
@ -67,10 +69,15 @@ class ServersController extends Controller
protected $databaseRepository; protected $databaseRepository;
/** /**
* @var \Pterodactyl\Services\Database\DatabaseManagementService * @var \Pterodactyl\Services\Databases\DatabaseManagementService
*/ */
protected $databaseManagementService; protected $databaseManagementService;
/**
* @var \Pterodactyl\Services\Databases\DatabasePasswordService
*/
protected $databasePasswordService;
/** /**
* @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface * @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface
*/ */
@ -135,7 +142,8 @@ class ServersController extends Controller
* @param \Illuminate\Contracts\Config\Repository $config * @param \Illuminate\Contracts\Config\Repository $config
* @param \Pterodactyl\Services\Servers\ContainerRebuildService $containerRebuildService * @param \Pterodactyl\Services\Servers\ContainerRebuildService $containerRebuildService
* @param \Pterodactyl\Services\Servers\ServerCreationService $service * @param \Pterodactyl\Services\Servers\ServerCreationService $service
* @param \Pterodactyl\Services\Database\DatabaseManagementService $databaseManagementService * @param \Pterodactyl\Services\Databases\DatabaseManagementService $databaseManagementService
* @param \Pterodactyl\Services\Databases\DatabasePasswordService $databasePasswordService
* @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $databaseRepository * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $databaseRepository
* @param \Pterodactyl\Repositories\Eloquent\DatabaseHostRepository $databaseHostRepository * @param \Pterodactyl\Repositories\Eloquent\DatabaseHostRepository $databaseHostRepository
* @param \Pterodactyl\Services\Servers\ServerDeletionService $deletionService * @param \Pterodactyl\Services\Servers\ServerDeletionService $deletionService
@ -156,6 +164,7 @@ class ServersController extends Controller
ContainerRebuildService $containerRebuildService, ContainerRebuildService $containerRebuildService,
ServerCreationService $service, ServerCreationService $service,
DatabaseManagementService $databaseManagementService, DatabaseManagementService $databaseManagementService,
DatabasePasswordService $databasePasswordService,
DatabaseRepositoryInterface $databaseRepository, DatabaseRepositoryInterface $databaseRepository,
DatabaseHostRepository $databaseHostRepository, DatabaseHostRepository $databaseHostRepository,
ServerDeletionService $deletionService, ServerDeletionService $deletionService,
@ -173,9 +182,10 @@ class ServersController extends Controller
$this->buildModificationService = $buildModificationService; $this->buildModificationService = $buildModificationService;
$this->config = $config; $this->config = $config;
$this->containerRebuildService = $containerRebuildService; $this->containerRebuildService = $containerRebuildService;
$this->databaseManagementService = $databaseManagementService;
$this->databaseRepository = $databaseRepository;
$this->databaseHostRepository = $databaseHostRepository; $this->databaseHostRepository = $databaseHostRepository;
$this->databaseManagementService = $databaseManagementService;
$this->databasePasswordService = $databasePasswordService;
$this->databaseRepository = $databaseRepository;
$this->detailsModificationService = $detailsModificationService; $this->detailsModificationService = $detailsModificationService;
$this->deletionService = $deletionService; $this->deletionService = $deletionService;
$this->locationRepository = $locationRepository; $this->locationRepository = $locationRepository;
@ -561,10 +571,8 @@ class ServersController extends Controller
*/ */
public function saveStartup(Request $request, Server $server) public function saveStartup(Request $request, Server $server)
{ {
$this->startupModificationService->isAdmin()->handle( $this->startupModificationService->setUserLevel(User::USER_LEVEL_ADMIN);
$server, $this->startupModificationService->handle($server, $request->except('_token'));
$request->except('_token')
);
$this->alert->success(trans('admin/server.alerts.startup_changed'))->flash(); $this->alert->success(trans('admin/server.alerts.startup_changed'))->flash();
return redirect()->route('admin.servers.view.startup', $server->id); return redirect()->route('admin.servers.view.startup', $server->id);
@ -609,7 +617,7 @@ class ServersController extends Controller
['id', '=', $request->input('database')], ['id', '=', $request->input('database')],
]); ]);
$this->databaseManagementService->changePassword($database->id, str_random(20)); $this->databasePasswordService->handle($database, str_random(20));
return response('', 204); return response('', 204);
} }

View file

@ -0,0 +1,77 @@
<?php
namespace Pterodactyl\Http\Controllers\Server;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Traits\Controllers\JavascriptInjection;
use Pterodactyl\Services\Databases\DatabasePasswordService;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
class DatabaseController extends Controller
{
use JavascriptInjection;
/**
* @var \Pterodactyl\Services\Databases\DatabasePasswordService
*/
protected $passwordService;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface
*/
protected $repository;
/**
* DatabaseController constructor.
*
* @param \Pterodactyl\Services\Databases\DatabasePasswordService $passwordService
* @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository
*/
public function __construct(DatabasePasswordService $passwordService, DatabaseRepositoryInterface $repository)
{
$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();
return view('server.databases.index', [
'databases' => $this->repository->getDatabasesForServer($server->id),
]);
}
/**
* Handle a request to update the password for a specific database.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*
* @throws \Illuminate\Auth\Access\AuthorizationException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function update(Request $request): JsonResponse
{
$this->authorize('reset-db-password', $request->attributes->get('server'));
$password = str_random(20);
$this->passwordService->handle($request->attributes->get('database'), $password);
return response()->json(['password' => $password]);
}
}

View file

@ -1,185 +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;
use Log;
use Alert;
use Pterodactyl\Models;
use Illuminate\Http\Request;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Exceptions\DisplayValidationException;
class ServerController extends Controller
{
/**
* Returns the allocation overview for a server.
*
* @param \Illuminate\Http\Request $request
* @param string $uuid
* @return \Illuminate\View\View
*/
public function getAllocation(Request $request, $uuid)
{
$server = Models\Server::byUuid($uuid);
$this->authorize('view-allocation', $server);
$server->js();
return view('server.settings.allocation', [
'server' => $server->load(['allocations' => function ($query) {
$query->orderBy('ip', 'asc');
$query->orderBy('port', 'asc');
}]),
'node' => $server->node,
]);
}
/**
* Returns the startup overview for a server.
*
* @param \Illuminate\Http\Request $request
* @param string $uuid
* @return \Illuminate\View\View
*/
public function getStartup(Request $request, $uuid)
{
$server = Models\Server::byUuid($uuid);
$this->authorize('view-startup', $server);
$server->load(['node', 'allocation', 'variables']);
$variables = Models\EggVariable::where('option_id', $server->option_id)->get();
$replacements = [
'{{SERVER_MEMORY}}' => $server->memory,
'{{SERVER_IP}}' => $server->allocation->ip,
'{{SERVER_PORT}}' => $server->allocation->port,
];
$processed = str_replace(array_keys($replacements), array_values($replacements), $server->startup);
foreach ($variables as $var) {
if ($var->user_viewable) {
$serverVar = $server->variables->where('variable_id', $var->id)->first();
$var->server_set_value = $serverVar->variable_value ?? $var->default_value;
} else {
$var->server_set_value = '[hidden]';
}
$processed = str_replace('{{' . $var->env_variable . '}}', $var->server_set_value, $processed);
}
$server->js();
return view('server.settings.startup', [
'server' => $server,
'node' => $server->node,
'variables' => $variables->where('user_viewable', 1),
'service' => $server->service,
'processedStartup' => $processed,
]);
}
/**
* Returns the database overview for a server.
*
* @param \Illuminate\Http\Request $request
* @param string $uuid
* @return \Illuminate\View\View
*/
public function getDatabases(Request $request, $uuid)
{
$server = Models\Server::byUuid($uuid);
$this->authorize('view-databases', $server);
$server->load('node', 'databases.host');
$server->js();
return view('server.settings.databases', [
'server' => $server,
'node' => $server->node,
'databases' => $server->databases,
]);
}
/**
* Returns the SFTP overview for a server.
*
* @param \Illuminate\Http\Request $request
* @param string $uuid
* @return \Illuminate\View\View
*/
public function getSFTP(Request $request, $uuid)
{
$server = Models\Server::byUuid($uuid);
$this->authorize('view-sftp', $server);
$server->js();
return view('server.settings.sftp', [
'server' => $server,
'node' => $server->node,
]);
}
/**
* Handles changing the SFTP password for a server.
*
* @param \Illuminate\Http\Request $request
* @param string $uuid
* @return \Illuminate\Http\RedirectResponse
*/
public function postSettingsSFTP(Request $request, $uuid)
{
$server = Models\Server::byUuid($uuid);
$this->authorize('reset-sftp', $server);
try {
$repo = new ServerRepository;
$repo->updateSFTPPassword($server->id, $request->input('sftp_pass'));
Alert::success('Successfully updated this servers SFTP password.')->flash();
} catch (DisplayValidationException $ex) {
return redirect()->route('server.settings.sftp', $uuid)->withErrors(json_decode($ex->getMessage()));
} catch (DisplayException $ex) {
Alert::danger($ex->getMessage())->flash();
} catch (\Exception $ex) {
Log::error($ex);
Alert::danger('An unknown error occured while attempting to update this server\'s SFTP settings.')->flash();
}
return redirect()->route('server.settings.sftp', $uuid);
}
/**
* Handles changing the startup settings for a server.
*
* @param \Illuminate\Http\Request $request
* @param string $uuid
* @return \Illuminate\Http\RedirectResponse
*/
public function postSettingsStartup(Request $request, $uuid)
{
$server = Models\Server::byUuid($uuid);
$this->authorize('edit-startup', $server);
try {
$repo = new ServerRepository;
$repo->updateStartup($server->id, $request->except('_token'));
Alert::success('Server startup variables were successfully updated.')->flash();
} catch (DisplayValidationException $ex) {
return redirect()->route('server.settings.startup', $uuid)->withErrors(json_decode($ex->getMessage()));
} catch (DisplayException $ex) {
Alert::danger($ex->getMessage())->flash();
} catch (\Exception $ex) {
Log::error($ex);
Alert::danger('An unhandled exception occured while attemping to update startup variables for this server. Please try again.')->flash();
}
return redirect()->route('server.settings.startup', $uuid);
}
}

View file

@ -0,0 +1,97 @@
<?php
namespace Pterodactyl\Http\Controllers\Server\Settings;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
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

@ -0,0 +1,26 @@
<?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
*/
public function index(Request $request): View
{
$this->setRequest($request)->injectJavascript();
return view('server.settings.sftp');
}
}

View file

@ -0,0 +1,94 @@
<?php
namespace Pterodactyl\Http\Controllers\Server\Settings;
use Illuminate\Http\Request;
use Pterodactyl\Models\User;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Http\Controllers\Controller;
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)
{
$server = $request->attributes->get('server');
$this->authorize('view-startup', $server);
$this->injectJavascript();
$data = $this->commandViewService->handle($server->id);
return view('server.settings.startup', [
'variables' => $data->get('variables'),
'server_values' => $data->get('server_values'),
'startup' => $data->get('startup'),
]);
}
/**
* Handle request to update the startup variables for a server. Authorization
* is handled in the form request.
*
* @param \Pterodactyl\Http\Requests\Server\UpdateStartupParametersFormRequest $request
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function update(UpdateStartupParametersFormRequest $request): RedirectResponse
{
$this->modificationService->setUserLevel(User::USER_LEVEL_USER);
$this->modificationService->handle($request->attributes->get('server'), $request->normalize());
$this->alert->success(trans('server.config.startup.edited'))->flash();
return redirect()->route('server.settings.startup', ['server' => $request->attributes->get('server')->uuidShort]);
}
}

View file

@ -5,6 +5,9 @@ namespace Pterodactyl\Http;
use Pterodactyl\Http\Middleware\DaemonAuthenticate; use Pterodactyl\Http\Middleware\DaemonAuthenticate;
use Illuminate\Foundation\Http\Kernel as HttpKernel; use Illuminate\Foundation\Http\Kernel as HttpKernel;
use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\Routing\Middleware\SubstituteBindings;
use Pterodactyl\Http\Middleware\Server\SubuserBelongsToServer;
use Pterodactyl\Http\Middleware\Server\DatabaseBelongsToServer;
use Pterodactyl\Http\Middleware\Server\ScheduleBelongsToServer;
class Kernel extends HttpKernel class Kernel extends HttpKernel
{ {
@ -63,7 +66,6 @@ class Kernel extends HttpKernel
'guest' => \Pterodactyl\Http\Middleware\RedirectIfAuthenticated::class, 'guest' => \Pterodactyl\Http\Middleware\RedirectIfAuthenticated::class,
'server' => \Pterodactyl\Http\Middleware\ServerAuthenticate::class, 'server' => \Pterodactyl\Http\Middleware\ServerAuthenticate::class,
'subuser.auth' => \Pterodactyl\Http\Middleware\SubuserAccessAuthenticate::class, 'subuser.auth' => \Pterodactyl\Http\Middleware\SubuserAccessAuthenticate::class,
'subuser' => \Pterodactyl\Http\Middleware\Server\SubuserAccess::class,
'admin' => \Pterodactyl\Http\Middleware\AdminAuthenticate::class, 'admin' => \Pterodactyl\Http\Middleware\AdminAuthenticate::class,
'daemon-old' => DaemonAuthenticate::class, 'daemon-old' => DaemonAuthenticate::class,
'csrf' => \Pterodactyl\Http\Middleware\VerifyCsrfToken::class, 'csrf' => \Pterodactyl\Http\Middleware\VerifyCsrfToken::class,
@ -71,6 +73,13 @@ class Kernel extends HttpKernel
'can' => \Illuminate\Auth\Middleware\Authorize::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'recaptcha' => \Pterodactyl\Http\Middleware\VerifyReCaptcha::class, 'recaptcha' => \Pterodactyl\Http\Middleware\VerifyReCaptcha::class,
'schedule' => \Pterodactyl\Http\Middleware\Server\ScheduleAccess::class,
// Server specific middleware (used for authenticating access to resources)
//
// These are only used for individual server authentication, and not gloabl
// actions from other resources. They are defined in the route files.
'server..database' => DatabaseBelongsToServer::class,
'server..subuser' => SubuserBelongsToServer::class,
'server..schedule' => ScheduleBelongsToServer::class,
]; ];
} }

View file

@ -75,7 +75,7 @@ class DaemonAuthenticate
throw new HttpException(403); throw new HttpException(403);
} }
$request->attributes->set('node.model', $node); $request->attributes->set('node', $node);
return $next($request); return $next($request);
} }

View file

@ -0,0 +1,51 @@
<?php
namespace Pterodactyl\Http\Middleware\Server;
use Closure;
use Illuminate\Http\Request;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class DatabaseBelongsToServer
{
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface
*/
protected $repository;
/**
* DatabaseAccess constructor.
*
* @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository
*/
public function __construct(DatabaseRepositoryInterface $repository)
{
$this->repository = $repository;
}
/**
* Check if a database being requested belongs to the currently loaded server.
* If it does not, throw a 404 error, otherwise continue on with the request
* and set an attribute with the database.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function handle(Request $request, Closure $next)
{
$server = $request->attributes->get('server');
$database = $this->repository->find($request->input('database'));
if ($database->server_id !== $server->id) {
throw new NotFoundHttpException;
}
$request->attributes->set('database', $database);
return $next($request);
}
}

View file

@ -14,7 +14,7 @@ use Illuminate\Contracts\Session\Session;
use Pterodactyl\Contracts\Extensions\HashidsInterface; use Pterodactyl\Contracts\Extensions\HashidsInterface;
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface; use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
class ScheduleAccess class ScheduleBelongsToServer
{ {
/** /**
* @var \Pterodactyl\Contracts\Extensions\HashidsInterface * @var \Pterodactyl\Contracts\Extensions\HashidsInterface

View file

@ -15,7 +15,7 @@ use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class SubuserAccess class SubuserBelongsToServer
{ {
/** /**
* @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface * @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface

View file

@ -105,8 +105,13 @@ class ServerAuthenticate
} }
// Store the server in the session. // Store the server in the session.
// @todo remove from session. use request attributes.
$this->session->now('server_data.model', $server); $this->session->now('server_data.model', $server);
// Add server to the request attributes. This will replace sessions
// as files are updated.
$request->attributes->set('server', $server);
return $next($request); return $next($request);
} }
} }

View file

@ -60,6 +60,7 @@ class SubuserAccessAuthenticate
try { try {
$token = $this->keyProviderService->handle($server->id, $request->user()->id); $token = $this->keyProviderService->handle($server->id, $request->user()->id);
$this->session->now('server_data.token', $token); $this->session->now('server_data.token', $token);
$request->attributes->set('server_token', $token);
} catch (RecordNotFoundException $exception) { } catch (RecordNotFoundException $exception) {
throw new AuthenticationException('This account does not have permission to access this server.'); throw new AuthenticationException('This account does not have permission to access this server.');
} }

View file

@ -0,0 +1,44 @@
<?php
namespace Pterodactyl\Http\Requests\API\Remote;
use Pterodactyl\Http\Requests\Request;
class SftpAuthenticationFormRequest extends Request
{
/**
* Authenticate the request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Rules to apply to the request.
*
* @return array
*/
public function rules()
{
return [
'username' => 'required|string',
'password' => 'required|string',
];
}
/**
* Return only the fields that we are interested in from the request.
* This will include empty fields as a null value.
*
* @return array
*/
public function normalize()
{
return $this->only(
array_keys($this->rules())
);
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace Pterodactyl\Http\Requests\Server;
use Pterodactyl\Http\Requests\FrontendUserFormRequest;
use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface;
class UpdateStartupParametersFormRequest extends FrontendUserFormRequest
{
/**
* @var array
*/
private $validationAttributes = [];
/**
* Determine if the user has permission to update the startup parameters
* for this server.
*
* @return bool
*/
public function authorize()
{
if (! parent::authorize()) {
return false;
}
return $this->user()->can('edit-startup', $this->attributes->get('server'));
}
/**
* Validate that all of the required fields were passed and that the environment
* variable values meet the defined criteria for those fields.
*
* @return array
*/
public function rules()
{
$repository = $this->container->make(EggVariableRepositoryInterface::class);
$variables = $repository->getEditableVariables($this->attributes->get('server')->egg_id);
$rules = $variables->mapWithKeys(function ($variable) {
$this->validationAttributes['environment.' . $variable->env_variable] = $variable->name;
return ['environment.' . $variable->env_variable => $variable->rules];
})->toArray();
return array_merge($rules, [
'environment' => 'required|array',
]);
}
/**
* Return attributes to provide better naming conventions for error messages.
*
* @return array
*/
public function attributes()
{
return $this->validationAttributes;
}
}

View file

@ -1,32 +1,25 @@
<?php <?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\ViewComposers\Server; namespace Pterodactyl\Http\ViewComposers\Server;
use Illuminate\View\View; use Illuminate\View\View;
use Illuminate\Contracts\Session\Session; use Illuminate\Http\Request;
class ServerDataComposer class ServerDataComposer
{ {
/** /**
* @var \Illuminate\Contracts\Session\Session * @var \Illuminate\Http\Request
*/ */
protected $session; protected $request;
/** /**
* ServerDataComposer constructor. * ServerDataComposer constructor.
* *
* @param \Illuminate\Contracts\Session\Session $session * @param \Illuminate\Http\Request $request
*/ */
public function __construct(Session $session) public function __construct(Request $request)
{ {
$this->session = $session; $this->request = $request;
} }
/** /**
@ -36,10 +29,10 @@ class ServerDataComposer
*/ */
public function compose(View $view) public function compose(View $view)
{ {
$data = $this->session->get('server_data'); $server = $this->request->get('server');
$view->with('server', array_get($data, 'model')); $view->with('server', $server);
$view->with('node', object_get($data['model'], 'node')); $view->with('node', object_get($server, 'node'));
$view->with('daemon_token', array_get($data, 'token')); $view->with('daemon_token', $this->request->get('server_token'));
} }
} }

View file

@ -64,6 +64,16 @@ class Allocation extends Model implements CleansAttributes, ValidableContract
'server_id' => 'nullable|exists:servers,id', 'server_id' => 'nullable|exists:servers,id',
]; ];
/**
* Return a hashid encoded string to represent the ID of the allocation.
*
* @return string
*/
public function getHashidAttribute()
{
return app()->make('hashids')->encode($this->id);
}
/** /**
* Accessor to automatically provide the IP alias if defined. * Accessor to automatically provide the IP alias if defined.
* *

View file

@ -20,6 +20,8 @@ class Node extends Model implements CleansAttributes, ValidableContract
{ {
use Eloquence, Notifiable, Validable; use Eloquence, Notifiable, Validable;
const DAEMON_SECRET_LENGTH = 36;
/** /**
* The table associated with the model. * The table associated with the model.
* *
@ -144,13 +146,23 @@ class Node extends Model implements CleansAttributes, ValidableContract
], ],
], ],
'docker' => [ 'docker' => [
'container' => [
'user' => null,
],
'network' => [
'name' => 'pterodactyl_nw',
],
'socket' => '/var/run/docker.sock', 'socket' => '/var/run/docker.sock',
'autoupdate_images' => true, 'autoupdate_images' => true,
], ],
'sftp' => [ 'sftp' => [
'path' => $this->daemonBase, 'path' => $this->daemonBase,
'ip' => '0.0.0.0',
'port' => $this->daemonSFTP, 'port' => $this->daemonSFTP,
'container' => 'ptdl-sftp', 'keypair' => [
'bits' => 2048,
'e' => 65537,
],
], ],
'logger' => [ 'logger' => [
'path' => 'logs/', 'path' => 'logs/',

View file

@ -86,7 +86,8 @@ class Permission extends Model implements CleansAttributes, ValidableContract
'delete-subuser' => null, 'delete-subuser' => null,
], ],
'server' => [ 'server' => [
'set-connection' => null, 'view-allocations' => null,
'edit-allocation' => null,
'view-startup' => null, 'view-startup' => null,
'edit-startup' => null, 'edit-startup' => null,
], ],

View file

@ -29,19 +29,12 @@ class Server extends Model implements CleansAttributes, ValidableContract
*/ */
protected $table = 'servers'; protected $table = 'servers';
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = ['sftp_password'];
/** /**
* The attributes that should be mutated to dates. * The attributes that should be mutated to dates.
* *
* @var array * @var array
*/ */
protected $dates = ['deleted_at']; protected $dates = [self::CREATED_AT, self::UPDATED_AT, 'deleted_at'];
/** /**
* Always eager load these relationships on the model. * Always eager load these relationships on the model.
@ -55,7 +48,7 @@ class Server extends Model implements CleansAttributes, ValidableContract
* *
* @var array * @var array
*/ */
protected $guarded = ['id', 'installed', 'created_at', 'updated_at', 'deleted_at']; protected $guarded = ['id', 'installed', self::CREATED_AT, self::UPDATED_AT, 'deleted_at'];
/** /**
* @var array * @var array
@ -73,8 +66,6 @@ class Server extends Model implements CleansAttributes, ValidableContract
'node_id' => 'required', 'node_id' => 'required',
'allocation_id' => 'required', 'allocation_id' => 'required',
'pack_id' => 'sometimes', 'pack_id' => 'sometimes',
'auto_deploy' => 'sometimes',
'custom_id' => 'sometimes',
'skip_scripts' => 'sometimes', 'skip_scripts' => 'sometimes',
]; ];
@ -95,10 +86,7 @@ class Server extends Model implements CleansAttributes, ValidableContract
'nest_id' => 'exists:nests,id', 'nest_id' => 'exists:nests,id',
'egg_id' => 'exists:eggs,id', 'egg_id' => 'exists:eggs,id',
'pack_id' => 'nullable|numeric|min:0', 'pack_id' => 'nullable|numeric|min:0',
'custom_container' => 'nullable|string',
'startup' => 'nullable|string', 'startup' => 'nullable|string',
'auto_deploy' => 'accepted',
'custom_id' => 'numeric|unique:servers,id',
'skip_scripts' => 'boolean', 'skip_scripts' => 'boolean',
]; ];
@ -132,7 +120,6 @@ class Server extends Model implements CleansAttributes, ValidableContract
*/ */
protected $searchableColumns = [ protected $searchableColumns = [
'name' => 10, 'name' => 10,
'username' => 10,
'uuidShort' => 9, 'uuidShort' => 9,
'uuid' => 8, 'uuid' => 8,
'pack.name' => 7, 'pack.name' => 7,

View file

@ -32,6 +32,9 @@ class User extends Model implements
{ {
use Authenticatable, Authorizable, CanResetPassword, Eloquence, Notifiable, Validable; use Authenticatable, Authorizable, CanResetPassword, Eloquence, Notifiable, Validable;
const USER_LEVEL_USER = 0;
const USER_LEVEL_ADMIN = 1;
/** /**
* Level of servers to display when using access() on a user. * Level of servers to display when using access() on a user.
* *

View file

@ -10,6 +10,7 @@
namespace Pterodactyl\Repositories\Eloquent; namespace Pterodactyl\Repositories\Eloquent;
use Pterodactyl\Models\Database; use Pterodactyl\Models\Database;
use Illuminate\Support\Collection;
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Illuminate\Database\DatabaseManager; use Illuminate\Database\DatabaseManager;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
@ -17,6 +18,11 @@ use Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException;
class DatabaseRepository extends EloquentRepository implements DatabaseRepositoryInterface class DatabaseRepository extends EloquentRepository implements DatabaseRepositoryInterface
{ {
/**
* @var string
*/
protected $connection = self::DEFAULT_CONNECTION_NAME;
/** /**
* @var \Illuminate\Database\DatabaseManager * @var \Illuminate\Database\DatabaseManager
*/ */
@ -45,6 +51,40 @@ class DatabaseRepository extends EloquentRepository implements DatabaseRepositor
return Database::class; return Database::class;
} }
/**
* Set the connection name to execute statements against.
*
* @param string $connection
* @return $this
*/
public function setConnection(string $connection)
{
$this->connection = $connection;
return $this;
}
/**
* Return the connection to execute statements aganist.
*
* @return string
*/
public function getConnection(): string
{
return $this->connection;
}
/**
* Return all of the databases belonging to a server.
*
* @param int $server
* @return \Illuminate\Support\Collection
*/
public function getDatabasesForServer(int $server): Collection
{
return $this->getBuilder()->where('server_id', $server)->get($this->getColumns());
}
/** /**
* {@inheritdoc} * {@inheritdoc}
* @return bool|\Illuminate\Database\Eloquent\Model * @return bool|\Illuminate\Database\Eloquent\Model
@ -67,80 +107,64 @@ class DatabaseRepository extends EloquentRepository implements DatabaseRepositor
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function createDatabase($database, $connection = null) public function createDatabase($database)
{ {
return $this->runStatement( return $this->runStatement(sprintf('CREATE DATABASE IF NOT EXISTS `%s`', $database));
sprintf('CREATE DATABASE IF NOT EXISTS `%s`', $database),
$connection
);
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function createUser($username, $remote, $password, $connection = null) public function createUser($username, $remote, $password)
{ {
return $this->runStatement( return $this->runStatement(sprintf('CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\'', $username, $remote, $password));
sprintf('CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\'', $username, $remote, $password),
$connection
);
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function assignUserToDatabase($database, $username, $remote, $connection = null) public function assignUserToDatabase($database, $username, $remote)
{ {
return $this->runStatement( return $this->runStatement(sprintf(
sprintf( 'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX, EXECUTE ON `%s`.* TO `%s`@`%s`',
'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX, EXECUTE ON `%s`.* TO `%s`@`%s`', $database,
$database, $username,
$username, $remote
$remote ));
),
$connection
);
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function flush($connection = null) public function flush()
{ {
return $this->runStatement('FLUSH PRIVILEGES', $connection); return $this->runStatement('FLUSH PRIVILEGES');
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function dropDatabase($database, $connection = null) public function dropDatabase($database)
{ {
return $this->runStatement( return $this->runStatement(sprintf('DROP DATABASE IF EXISTS `%s`', $database));
sprintf('DROP DATABASE IF EXISTS `%s`', $database),
$connection
);
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function dropUser($username, $remote, $connection = null) public function dropUser($username, $remote)
{ {
return $this->runStatement( return $this->runStatement(sprintf('DROP USER IF EXISTS `%s`@`%s`', $username, $remote));
sprintf('DROP USER IF EXISTS `%s`@`%s`', $username, $remote),
$connection
);
} }
/** /**
* Run the provided statement against the database on a given connection. * Run the provided statement against the database on a given connection.
* *
* @param string $statement * @param string $statement
* @param null|string $connection
* @return bool * @return bool
*/ */
protected function runStatement($statement, $connection = null) protected function runStatement($statement)
{ {
return $this->database->connection($connection)->statement($statement); return $this->database->connection($this->getConnection())->statement($statement);
} }
} }

View file

@ -9,6 +9,7 @@
namespace Pterodactyl\Repositories\Eloquent; namespace Pterodactyl\Repositories\Eloquent;
use Illuminate\Support\Collection;
use Pterodactyl\Models\EggVariable; use Pterodactyl\Models\EggVariable;
use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface; use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface;
@ -21,4 +22,20 @@ class EggVariableRepository extends EloquentRepository implements EggVariableRep
{ {
return EggVariable::class; return EggVariable::class;
} }
/**
* Return editable variables for a given egg. Editable variables must be set to
* user viewable in order to be picked up by this function.
*
* @param int $egg
* @return \Illuminate\Support\Collection
*/
public function getEditableVariables(int $egg): Collection
{
return $this->getBuilder()->where([
['egg_id', '=', $egg],
['user_viewable', '=', 1],
['user_editable', '=', 1],
])->get($this->getColumns());
}
} }

View file

@ -67,8 +67,8 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
Assert::integerish($id, 'First argument passed to findWithVariables must be integer, received %s.'); Assert::integerish($id, 'First argument passed to findWithVariables must be integer, received %s.');
$instance = $this->getBuilder()->with('egg.variables', 'variables') $instance = $this->getBuilder()->with('egg.variables', 'variables')
->where($this->getModel()->getKeyName(), '=', $id) ->where($this->getModel()->getKeyName(), '=', $id)
->first($this->getColumns()); ->first($this->getColumns());
if (is_null($instance)) { if (is_null($instance)) {
throw new RecordNotFoundException(); throw new RecordNotFoundException();
@ -77,6 +77,36 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
return $instance; return $instance;
} }
/**
* Get the primary allocation for a given server. If a model is passed into
* the function, load the allocation relationship onto it. Otherwise, find and
* return the server from the database.
*
* @param int|\Pterodactyl\Models\Server $server
* @param bool $refresh
* @return \Pterodactyl\Models\Server
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getPrimaryAllocation($server, bool $refresh = false): Server
{
$instance = $server;
if (! $instance instanceof Server) {
Assert::integerish($server, 'First argument passed to getPrimaryAllocation must be instance of \Pterodactyl\Models\Server or integer, received %s.');
$instance = $this->getBuilder()->find($server, $this->getColumns());
}
if (! $instance) {
throw new RecordNotFoundException;
}
if (! $instance->relationLoaded('allocation') || $refresh) {
$instance->load('allocation');
}
return $instance;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
@ -107,16 +137,21 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
} }
/** /**
* {@inheritdoc} * Return enough data to be used for the creation of a server via the daemon.
*
* @param \Pterodactyl\Models\Server $server
* @param bool $refresh
* @return \Pterodactyl\Models\Server
*/ */
public function getDataForCreation($id) public function getDataForCreation(Server $server, bool $refresh = false): Server
{ {
$instance = $this->getBuilder()->with(['allocation', 'allocations', 'pack', 'egg'])->find($id, $this->getColumns()); foreach (['allocation', 'allocations', 'pack', 'egg'] as $relation) {
if (! $instance) { if (! $server->relationLoaded($relation) || $refresh) {
throw new RecordNotFoundException(); $server->load($relation);
}
} }
return $instance; return $server;
} }
/** /**

View file

@ -0,0 +1,110 @@
<?php
namespace Pterodactyl\Services\Allocations;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Allocation;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
use Pterodactyl\Exceptions\Service\Allocation\AllocationDoesNotBelongToServerException;
use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonRepositoryInterface;
class SetDefaultAllocationService
{
/**
* @var \Illuminate\Database\ConnectionInterface
*/
private $connection;
/**
* @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface
*/
private $daemonRepository;
/**
* @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface
*/
private $repository;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
private $serverRepository;
/**
* SetDefaultAllocationService constructor.
*
* @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $repository
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonRepository
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $serverRepository
*/
public function __construct(
AllocationRepositoryInterface $repository,
ConnectionInterface $connection,
DaemonRepositoryInterface $daemonRepository,
ServerRepositoryInterface $serverRepository
) {
$this->connection = $connection;
$this->daemonRepository = $daemonRepository;
$this->repository = $repository;
$this->serverRepository = $serverRepository;
}
/**
* Update the default allocation for a server only if that allocation is currently
* assigned to the specified server.
*
* @param int|\Pterodactyl\Models\Server $server
* @param int $allocation
* @return \Pterodactyl\Models\Allocation
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Allocation\AllocationDoesNotBelongToServerException
*/
public function handle($server, int $allocation): Allocation
{
if (! $server instanceof Server) {
$server = $this->serverRepository->find($server);
}
$allocations = $this->repository->findWhere([['server_id', '=', $server->id]]);
$model = $allocations->filter(function ($model) use ($allocation) {
return $model->id === $allocation;
})->first();
if (! $model instanceof Allocation) {
throw new AllocationDoesNotBelongToServerException;
}
$this->connection->beginTransaction();
$this->serverRepository->withoutFresh()->update($server->id, ['allocation_id' => $model->id]);
// Update on the daemon.
try {
$this->daemonRepository->setAccessServer($server->uuid)->setNode($server->node_id)->update([
'build' => [
'default' => [
'ip' => $model->ip,
'port' => $model->port,
],
'ports|overwrite' => $allocations->groupBy('ip')->map(function ($item) {
return $item->pluck('port');
})->toArray(),
],
]);
$this->connection->commit();
} catch (RequestException $exception) {
$this->connection->rollBack();
throw new DaemonConnectionException($exception);
}
return $model;
}
}

View file

@ -1,148 +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\Services\Database;
use Illuminate\Database\DatabaseManager;
use Pterodactyl\Exceptions\DisplayException;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Extensions\DynamicDatabaseConnection;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
class DatabaseHostService
{
/**
* @var \Illuminate\Database\DatabaseManager
*/
protected $database;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface
*/
protected $databaseRepository;
/**
* @var \Pterodactyl\Extensions\DynamicDatabaseConnection
*/
protected $dynamic;
/**
* @var \Illuminate\Contracts\Encryption\Encrypter
*/
protected $encrypter;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface
*/
protected $repository;
/**
* DatabaseHostService constructor.
*
* @param \Illuminate\Database\DatabaseManager $database
* @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $databaseRepository
* @param \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface $repository
* @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
*/
public function __construct(
DatabaseManager $database,
DatabaseRepositoryInterface $databaseRepository,
DatabaseHostRepositoryInterface $repository,
DynamicDatabaseConnection $dynamic,
Encrypter $encrypter
) {
$this->database = $database;
$this->databaseRepository = $databaseRepository;
$this->dynamic = $dynamic;
$this->encrypter = $encrypter;
$this->repository = $repository;
}
/**
* Create a new database host and persist it to the database.
*
* @param array $data
* @return \Pterodactyl\Models\DatabaseHost
*
* @throws \Throwable
* @throws \PDOException
*/
public function create(array $data)
{
$this->database->beginTransaction();
$host = $this->repository->create([
'password' => $this->encrypter->encrypt(array_get($data, 'password')),
'name' => array_get($data, 'name'),
'host' => array_get($data, 'host'),
'port' => array_get($data, 'port'),
'username' => array_get($data, 'username'),
'max_databases' => null,
'node_id' => array_get($data, 'node_id'),
]);
// Check Access
$this->dynamic->set('dynamic', $host);
$this->database->connection('dynamic')->select('SELECT 1 FROM dual');
$this->database->commit();
return $host;
}
/**
* Update a database host and persist to the database.
*
* @param int $id
* @param array $data
* @return mixed
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function update($id, array $data)
{
$this->database->beginTransaction();
if (! empty(array_get($data, 'password'))) {
$data['password'] = $this->encrypter->encrypt($data['password']);
} else {
unset($data['password']);
}
$host = $this->repository->update($id, $data);
$this->dynamic->set('dynamic', $host);
$this->database->connection('dynamic')->select('SELECT 1 FROM dual');
$this->database->commit();
return $host;
}
/**
* Delete a database host if it has no active databases attached to it.
*
* @param int $id
* @return bool|null
*
* @throws \Pterodactyl\Exceptions\DisplayException
*/
public function delete($id)
{
$count = $this->databaseRepository->findCountWhere([['database_host_id', '=', $id]]);
if ($count > 0) {
throw new DisplayException(trans('exceptions.databases.delete_has_databases'));
}
return $this->repository->delete($id);
}
}

View file

@ -7,8 +7,9 @@
* https://opensource.org/licenses/MIT * https://opensource.org/licenses/MIT
*/ */
namespace Pterodactyl\Services\Database; namespace Pterodactyl\Services\Databases;
use Pterodactyl\Models\Database;
use Illuminate\Database\DatabaseManager; use Illuminate\Database\DatabaseManager;
use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Extensions\DynamicDatabaseConnection; use Pterodactyl\Extensions\DynamicDatabaseConnection;
@ -79,28 +80,26 @@ class DatabaseManagementService
$database = $this->repository->createIfNotExists($data); $database = $this->repository->createIfNotExists($data);
$this->dynamic->set('dynamic', $data['database_host_id']); $this->dynamic->set('dynamic', $data['database_host_id']);
$this->repository->createDatabase($database->database, 'dynamic'); $this->repository->createDatabase($database->database);
$this->repository->createUser( $this->repository->createUser(
$database->username, $database->username,
$database->remote, $database->remote,
$this->encrypter->decrypt($database->password), $this->encrypter->decrypt($database->password)
'dynamic'
); );
$this->repository->assignUserToDatabase( $this->repository->assignUserToDatabase(
$database->database, $database->database,
$database->username, $database->username,
$database->remote, $database->remote
'dynamic'
); );
$this->repository->flush('dynamic'); $this->repository->flush();
$this->database->commit(); $this->database->commit();
} catch (\Exception $ex) { } catch (\Exception $ex) {
try { try {
if (isset($database)) { if (isset($database) && $database instanceof Database) {
$this->repository->dropDatabase($database->database, 'dynamic'); $this->repository->dropDatabase($database->database);
$this->repository->dropUser($database->username, $database->remote, 'dynamic'); $this->repository->dropUser($database->username, $database->remote);
$this->repository->flush('dynamic'); $this->repository->flush();
} }
} catch (\Exception $exTwo) { } catch (\Exception $exTwo) {
// ignore an exception // ignore an exception
@ -113,62 +112,22 @@ class DatabaseManagementService
return $database; return $database;
} }
/**
* Change the password for a specific user and database combination.
*
* @param int $id
* @param string $password
* @return bool
*
* @throws \Exception
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function changePassword($id, $password)
{
$database = $this->repository->find($id);
$this->dynamic->set('dynamic', $database->database_host_id);
$this->database->beginTransaction();
try {
$updated = $this->repository->withoutFresh()->update($id, [
'password' => $this->encrypter->encrypt($password),
]);
$this->repository->dropUser($database->username, $database->remote, 'dynamic');
$this->repository->createUser($database->username, $database->remote, $password, 'dynamic');
$this->repository->assignUserToDatabase(
$database->database,
$database->username,
$database->remote,
'dynamic'
);
$this->repository->flush('dynamic');
$this->database->commit();
} catch (\Exception $ex) {
$this->database->rollBack();
throw $ex;
}
return $updated;
}
/** /**
* Delete a database from the given host server. * Delete a database from the given host server.
* *
* @param int $id * @param int $id
* @return bool|null * @return bool|null
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/ */
public function delete($id) public function delete($id)
{ {
$database = $this->repository->find($id); $database = $this->repository->find($id);
$this->dynamic->set('dynamic', $database->database_host_id); $this->dynamic->set('dynamic', $database->database_host_id);
$this->repository->dropDatabase($database->database, 'dynamic'); $this->repository->dropDatabase($database->database);
$this->repository->dropUser($database->username, $database->remote, 'dynamic'); $this->repository->dropUser($database->username, $database->remote);
$this->repository->flush('dynamic'); $this->repository->flush();
return $this->repository->delete($id); return $this->repository->delete($id);
} }

View file

@ -0,0 +1,86 @@
<?php
namespace Pterodactyl\Services\Databases;
use Pterodactyl\Models\Database;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Extensions\DynamicDatabaseConnection;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
class DatabasePasswordService
{
/**
* @var \Illuminate\Database\ConnectionInterface
*/
private $connection;
/**
* @var \Pterodactyl\Extensions\DynamicDatabaseConnection
*/
private $dynamic;
/**
* @var \Illuminate\Contracts\Encryption\Encrypter
*/
private $encrypter;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface
*/
private $repository;
/**
* DatabasePasswordService constructor.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository
* @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
*/
public function __construct(
ConnectionInterface $connection,
DatabaseRepositoryInterface $repository,
DynamicDatabaseConnection $dynamic,
Encrypter $encrypter
) {
$this->connection = $connection;
$this->dynamic = $dynamic;
$this->encrypter = $encrypter;
$this->repository = $repository;
}
/**
* Updates a password for a given database.
*
* @param \Pterodactyl\Models\Database|int $database
* @param string $password
* @return bool
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function handle($database, string $password): bool
{
if (! $database instanceof Database) {
$database = $this->repository->find($database);
}
$this->dynamic->set('dynamic', $database->database_host_id);
$this->connection->beginTransaction();
$updated = $this->repository->withoutFresh()->update($database->id, [
'password' => $this->encrypter->encrypt($password),
]);
$this->repository->dropUser($database->username, $database->remote);
$this->repository->createUser($database->username, $database->remote, $password);
$this->repository->assignUserToDatabase($database->database, $database->username, $database->remote);
$this->repository->flush();
unset($password);
$this->connection->commit();
return $updated;
}
}

View file

@ -0,0 +1,92 @@
<?php
namespace Pterodactyl\Services\Databases\Hosts;
use Pterodactyl\Models\DatabaseHost;
use Illuminate\Database\DatabaseManager;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Extensions\DynamicDatabaseConnection;
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
class HostCreationService
{
/**
* @var \Illuminate\Database\ConnectionInterface
*/
private $connection;
/**
* @var \Illuminate\Database\DatabaseManager
*/
private $databaseManager;
/**
* @var \Pterodactyl\Extensions\DynamicDatabaseConnection
*/
private $dynamic;
/**
* @var \Illuminate\Contracts\Encryption\Encrypter
*/
private $encrypter;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface
*/
private $repository;
/**
* HostCreationService constructor.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Illuminate\Database\DatabaseManager $databaseManager
* @param \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface $repository
* @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
*/
public function __construct(
ConnectionInterface $connection,
DatabaseManager $databaseManager,
DatabaseHostRepositoryInterface $repository,
DynamicDatabaseConnection $dynamic,
Encrypter $encrypter
) {
$this->connection = $connection;
$this->databaseManager = $databaseManager;
$this->dynamic = $dynamic;
$this->encrypter = $encrypter;
$this->repository = $repository;
}
/**
* Create a new database host on the Panel.
*
* @param array $data
* @return \Pterodactyl\Models\DatabaseHost
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function handle(array $data): DatabaseHost
{
$this->connection->beginTransaction();
$host = $this->repository->create([
'password' => $this->encrypter->encrypt(array_get($data, 'password')),
'name' => array_get($data, 'name'),
'host' => array_get($data, 'host'),
'port' => array_get($data, 'port'),
'username' => array_get($data, 'username'),
'max_databases' => null,
'node_id' => array_get($data, 'node_id'),
]);
// Confirm access using the provided credentials before saving data.
$this->dynamic->set('dynamic', $host);
$this->databaseManager->connection('dynamic')->select('SELECT 1 FROM dual');
$this->connection->commit();
return $host;
}
}

View file

@ -0,0 +1,53 @@
<?php
namespace Pterodactyl\Services\Databases\Hosts;
use Pterodactyl\Exceptions\Service\HasActiveServersException;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
class HostDeletionService
{
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface
*/
private $databaseRepository;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface
*/
private $repository;
/**
* HostDeletionService constructor.
*
* @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $databaseRepository
* @param \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface $repository
*/
public function __construct(
DatabaseRepositoryInterface $databaseRepository,
DatabaseHostRepositoryInterface $repository
) {
$this->databaseRepository = $databaseRepository;
$this->repository = $repository;
}
/**
* Delete a specified host from the Panel if no databases are
* attached to it.
*
* @param int $host
* @return int
*
* @throws \Pterodactyl\Exceptions\Service\HasActiveServersException
*/
public function handle(int $host): int
{
$count = $this->databaseRepository->findCountWhere([['database_host_id', '=', $host]]);
if ($count > 0) {
throw new HasActiveServersException(trans('exceptions.databases.delete_has_databases'));
}
return $this->repository->delete($host);
}
}

View file

@ -0,0 +1,96 @@
<?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\Databases\Hosts;
use Pterodactyl\Models\DatabaseHost;
use Illuminate\Database\DatabaseManager;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Extensions\DynamicDatabaseConnection;
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
class HostUpdateService
{
/**
* @var \Illuminate\Database\ConnectionInterface
*/
private $connection;
/**
* @var \Illuminate\Database\DatabaseManager
*/
private $databaseManager;
/**
* @var \Pterodactyl\Extensions\DynamicDatabaseConnection
*/
private $dynamic;
/**
* @var \Illuminate\Contracts\Encryption\Encrypter
*/
private $encrypter;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface
*/
private $repository;
/**
* DatabaseHostService constructor.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Illuminate\Database\DatabaseManager $databaseManager
* @param \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface $repository
* @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
*/
public function __construct(
ConnectionInterface $connection,
DatabaseManager $databaseManager,
DatabaseHostRepositoryInterface $repository,
DynamicDatabaseConnection $dynamic,
Encrypter $encrypter
) {
$this->connection = $connection;
$this->databaseManager = $databaseManager;
$this->dynamic = $dynamic;
$this->encrypter = $encrypter;
$this->repository = $repository;
}
/**
* Update a database host and persist to the database.
*
* @param int $hostId
* @param array $data
* @return mixed
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function handle(int $hostId, array $data): DatabaseHost
{
if (! empty(array_get($data, 'password'))) {
$data['password'] = $this->encrypter->encrypt($data['password']);
} else {
unset($data['password']);
}
$this->connection->beginTransaction();
$host = $this->repository->update($hostId, $data);
$this->dynamic->set('dynamic', $host);
$this->databaseManager->connection('dynamic')->select('SELECT 1 FROM dual');
$this->connection->commit();
return $host;
}
}

View file

@ -1,42 +1,37 @@
<?php <?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\Servers; namespace Pterodactyl\Services\Servers;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class EnvironmentService class EnvironmentService
{ {
const ENVIRONMENT_CASTS = [
'STARTUP' => 'startup',
'P_SERVER_LOCATION' => 'location.short',
'P_SERVER_UUID' => 'uuid',
];
/** /**
* @var array * @var array
*/ */
protected $additional = []; private $additional = [];
/**
* @var \Illuminate\Contracts\Config\Repository
*/
private $config;
/** /**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/ */
protected $repository; private $repository;
/** /**
* EnvironmentService constructor. * EnvironmentService constructor.
* *
* @param \Illuminate\Contracts\Config\Repository $config
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
*/ */
public function __construct(ServerRepositoryInterface $repository) public function __construct(ConfigRepository $config, ServerRepositoryInterface $repository)
{ {
$this->config = $config;
$this->repository = $repository; $this->repository = $repository;
} }
@ -46,42 +41,70 @@ class EnvironmentService
* *
* @param string $key * @param string $key
* @param callable $closure * @param callable $closure
* @return $this
*/ */
public function setEnvironmentKey($key, callable $closure) public function setEnvironmentKey(string $key, callable $closure)
{ {
$this->additional[] = [$key, $closure]; $this->additional[$key] = $closure;
}
return $this; /**
* Return the dynamically added additional keys.
*
* @return array
*/
public function getEnvironmentKeys(): array
{
return $this->additional;
} }
/** /**
* Take all of the environment variables configured for this server and return * Take all of the environment variables configured for this server and return
* them in an easy to process format. * them in an easy to process format.
* *
* @param int|\Pterodactyl\Models\Server $server * @param \Pterodactyl\Models\Server $server
* @return array * @return array
* *
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/ */
public function process($server) public function handle(Server $server): array
{ {
if (! $server instanceof Server) {
$server = $this->repository->find($server);
}
$variables = $this->repository->getVariablesWithValues($server->id); $variables = $this->repository->getVariablesWithValues($server->id);
// Process static environment variables defined in this file. // Process environment variables defined in this file. This is done first
foreach (self::ENVIRONMENT_CASTS as $key => $object) { // in order to allow run-time and config defined variables to take
// priority over built-in values.
foreach ($this->getEnvironmentMappings() as $key => $object) {
$variables[$key] = object_get($server, $object); $variables[$key] = object_get($server, $object);
} }
// Process variables set in the configuration file.
foreach ($this->config->get('pterodactyl.environment_mappings', []) as $key => $object) {
if (is_callable($object)) {
$variables[$key] = call_user_func($object, $server);
} else {
$variables[$key] = object_get($server, $object);
}
}
// Process dynamically included environment variables. // Process dynamically included environment variables.
foreach ($this->additional as $item) { foreach ($this->additional as $key => $closure) {
$variables[$item[0]] = call_user_func($item[1], $server); $variables[$key] = call_user_func($closure, $server);
} }
return $variables; return $variables;
} }
/**
* Return a mapping of Panel default environment variables.
*
* @return array
*/
final private function getEnvironmentMappings(): array
{
return [
'STARTUP' => 'startup',
'P_SERVER_LOCATION' => 'location.short',
'P_SERVER_UUID' => 'uuid',
];
}
} }

View file

@ -19,12 +19,12 @@ class ServerConfigurationStructureService
/** /**
* @var \Pterodactyl\Services\Servers\EnvironmentService * @var \Pterodactyl\Services\Servers\EnvironmentService
*/ */
protected $environment; private $environment;
/** /**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/ */
protected $repository; private $repository;
/** /**
* ServerConfigurationStructureService constructor. * ServerConfigurationStructureService constructor.
@ -41,19 +41,21 @@ class ServerConfigurationStructureService
} }
/** /**
* @param int|\Pterodactyl\Models\Server $server * Return a configuration array for a specific server when passed a server model.
*
* @param \Pterodactyl\Models\Server $server
* @return array * @return array
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/ */
public function handle($server): array public function handle(Server $server): array
{ {
if (! $server instanceof Server || array_diff(self::REQUIRED_RELATIONS, $server->getRelations())) { if (array_diff(self::REQUIRED_RELATIONS, $server->getRelations())) {
$server = $this->repository->getDataForCreation(is_digit($server) ? $server : $server->id); $server = $this->repository->getDataForCreation($server);
} }
return [ return [
'uuid' => $server->uuid, 'uuid' => $server->uuid,
'user' => $server->username,
'build' => [ 'build' => [
'default' => [ 'default' => [
'ip' => $server->allocation->ip, 'ip' => $server->allocation->ip,
@ -62,7 +64,7 @@ class ServerConfigurationStructureService
'ports' => $server->allocations->groupBy('ip')->map(function ($item) { 'ports' => $server->allocations->groupBy('ip')->map(function ($item) {
return $item->pluck('port'); return $item->pluck('port');
})->toArray(), })->toArray(),
'env' => $this->environment->process($server), 'env' => $this->environment->handle($server),
'memory' => (int) $server->memory, 'memory' => (int) $server->memory,
'swap' => (int) $server->swap, 'swap' => (int) $server->swap,
'io' => (int) $server->io, 'io' => (int) $server->io,
@ -70,7 +72,6 @@ class ServerConfigurationStructureService
'disk' => (int) $server->disk, 'disk' => (int) $server->disk,
'image' => $server->image, 'image' => $server->image,
], ],
'keys' => [],
'service' => [ 'service' => [
'egg' => $server->egg->uuid, 'egg' => $server->egg->uuid,
'pack' => object_get($server, 'pack.uuid'), 'pack' => object_get($server, 'pack.uuid'),

View file

@ -1,18 +1,12 @@
<?php <?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\Servers; namespace Pterodactyl\Services\Servers;
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
use Pterodactyl\Models\Node;
use Pterodactyl\Models\User;
use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Exception\RequestException;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Services\Nodes\NodeCreationService;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
@ -26,52 +20,47 @@ class ServerCreationService
/** /**
* @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface
*/ */
protected $allocationRepository; private $allocationRepository;
/** /**
* @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService * @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService
*/ */
protected $configurationStructureService; private $configurationStructureService;
/** /**
* @var \Illuminate\Database\ConnectionInterface * @var \Illuminate\Database\ConnectionInterface
*/ */
protected $connection; private $connection;
/** /**
* @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface
*/ */
protected $daemonServerRepository; private $daemonServerRepository;
/** /**
* @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface
*/ */
protected $nodeRepository; private $nodeRepository;
/** /**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/ */
protected $repository; private $repository;
/** /**
* @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface
*/ */
protected $serverVariableRepository; private $serverVariableRepository;
/** /**
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface
*/ */
protected $userRepository; private $userRepository;
/**
* @var \Pterodactyl\Services\Servers\UsernameGenerationService
*/
protected $usernameService;
/** /**
* @var \Pterodactyl\Services\Servers\VariableValidatorService * @var \Pterodactyl\Services\Servers\VariableValidatorService
*/ */
protected $validatorService; private $validatorService;
/** /**
* CreationService constructor. * CreationService constructor.
@ -84,7 +73,6 @@ class ServerCreationService
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
* @param \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface $serverVariableRepository * @param \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface $serverVariableRepository
* @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $userRepository * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $userRepository
* @param \Pterodactyl\Services\Servers\UsernameGenerationService $usernameService
* @param \Pterodactyl\Services\Servers\VariableValidatorService $validatorService * @param \Pterodactyl\Services\Servers\VariableValidatorService $validatorService
*/ */
public function __construct( public function __construct(
@ -96,7 +84,6 @@ class ServerCreationService
ServerRepositoryInterface $repository, ServerRepositoryInterface $repository,
ServerVariableRepositoryInterface $serverVariableRepository, ServerVariableRepositoryInterface $serverVariableRepository,
UserRepositoryInterface $userRepository, UserRepositoryInterface $userRepository,
UsernameGenerationService $usernameService,
VariableValidatorService $validatorService VariableValidatorService $validatorService
) { ) {
$this->allocationRepository = $allocationRepository; $this->allocationRepository = $allocationRepository;
@ -107,7 +94,6 @@ class ServerCreationService
$this->repository = $repository; $this->repository = $repository;
$this->serverVariableRepository = $serverVariableRepository; $this->serverVariableRepository = $serverVariableRepository;
$this->userRepository = $userRepository; $this->userRepository = $userRepository;
$this->usernameService = $usernameService;
$this->validatorService = $validatorService; $this->validatorService = $validatorService;
} }
@ -124,35 +110,30 @@ class ServerCreationService
public function create(array $data) public function create(array $data)
{ {
// @todo auto-deployment // @todo auto-deployment
$validator = $this->validatorService->isAdmin()->setFields($data['environment'])->validate($data['egg_id']);
$uniqueShort = str_random(8);
$this->connection->beginTransaction(); $this->connection->beginTransaction();
$server = $this->repository->create([ $server = $this->repository->create([
'uuid' => Uuid::uuid4()->toString(), 'uuid' => Uuid::uuid4()->toString(),
'uuidShort' => $uniqueShort, 'uuidShort' => str_random(8),
'node_id' => $data['node_id'], 'node_id' => array_get($data, 'node_id'),
'name' => $data['name'], 'name' => array_get($data, 'name'),
'description' => $data['description'], 'description' => array_get($data, 'description'),
'skip_scripts' => isset($data['skip_scripts']), 'skip_scripts' => isset($data['skip_scripts']),
'suspended' => false, 'suspended' => false,
'owner_id' => $data['owner_id'], 'owner_id' => array_get($data, 'owner_id'),
'memory' => $data['memory'], 'memory' => array_get($data, 'memory'),
'swap' => $data['swap'], 'swap' => array_get($data, 'swap'),
'disk' => $data['disk'], 'disk' => array_get($data, 'disk'),
'io' => $data['io'], 'io' => array_get($data, 'io'),
'cpu' => $data['cpu'], 'cpu' => array_get($data, 'cpu'),
'oom_disabled' => isset($data['oom_disabled']), 'oom_disabled' => isset($data['oom_disabled']),
'allocation_id' => $data['allocation_id'], 'allocation_id' => array_get($data, 'allocation_id'),
'nest_id' => $data['nest_id'], 'nest_id' => array_get($data, 'nest_id'),
'egg_id' => $data['egg_id'], 'egg_id' => array_get($data, 'egg_id'),
'pack_id' => (! isset($data['pack_id']) || $data['pack_id'] == 0) ? null : $data['pack_id'], 'pack_id' => (! isset($data['pack_id']) || $data['pack_id'] == 0) ? null : $data['pack_id'],
'startup' => $data['startup'], 'startup' => array_get($data, 'startup'),
'daemonSecret' => str_random(NodeCreationService::DAEMON_SECRET_LENGTH), 'daemonSecret' => str_random(Node::DAEMON_SECRET_LENGTH),
'image' => $data['docker_image'], 'image' => array_get($data, 'docker_image'),
'username' => $this->usernameService->generate($data['name'], $uniqueShort),
'sftp_password' => null,
]); ]);
// Process allocations and assign them to the server in the database. // Process allocations and assign them to the server in the database.
@ -164,17 +145,21 @@ class ServerCreationService
$this->allocationRepository->assignAllocationsToServer($server->id, $records); $this->allocationRepository->assignAllocationsToServer($server->id, $records);
// Process the passed variables and store them in the database. // Process the passed variables and store them in the database.
$records = []; $this->validatorService->setUserLevel(User::USER_LEVEL_ADMIN);
foreach ($validator->getResults() as $result) { $results = $this->validatorService->handle(array_get($data, 'egg_id'), array_get($data, 'environment', []));
$records[] = [
'server_id' => $server->id,
'variable_id' => $result['id'],
'variable_value' => $result['value'],
];
}
$this->serverVariableRepository->insert($records); $records = $results->map(function ($result) use ($server) {
$structure = $this->configurationStructureService->handle($server->id); return [
'server_id' => $server->id,
'variable_id' => $result->id,
'variable_value' => $result->value,
];
})->toArray();
if (! empty($records)) {
$this->serverVariableRepository->insert($records);
}
$structure = $this->configurationStructureService->handle($server);
// Create the server on the daemon & commit it to the database. // Create the server on the daemon & commit it to the database.
try { try {

View file

@ -14,7 +14,7 @@ use Pterodactyl\Models\Server;
use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Exception\RequestException;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Services\Database\DatabaseManagementService; use Pterodactyl\Services\Databases\DatabaseManagementService;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface;
@ -32,7 +32,7 @@ class ServerDeletionService
protected $daemonServerRepository; protected $daemonServerRepository;
/** /**
* @var \Pterodactyl\Services\Database\DatabaseManagementService * @var \Pterodactyl\Services\Databases\DatabaseManagementService
*/ */
protected $databaseManagementService; protected $databaseManagementService;
@ -62,7 +62,7 @@ class ServerDeletionService
* @param \Illuminate\Database\ConnectionInterface $connection * @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonServerRepository * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonServerRepository
* @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $databaseRepository * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $databaseRepository
* @param \Pterodactyl\Services\Database\DatabaseManagementService $databaseManagementService * @param \Pterodactyl\Services\Databases\DatabaseManagementService $databaseManagementService
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
* @param \Illuminate\Log\Writer $writer * @param \Illuminate\Log\Writer $writer
*/ */

View file

@ -0,0 +1,55 @@
<?php
namespace Pterodactyl\Services\Servers;
use Illuminate\Support\Collection;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class StartupCommandViewService
{
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
private $repository;
/**
* StartupCommandViewService constructor.
*
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
*/
public function __construct(ServerRepositoryInterface $repository)
{
$this->repository = $repository;
}
/**
* Generate a startup command for a server and return all of the user-viewable variables
* as well as thier assigned values.
*
* @param int $server
* @return \Illuminate\Support\Collection
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function handle(int $server): Collection
{
$response = $this->repository->getVariablesWithValues($server, true);
$server = $this->repository->getPrimaryAllocation($response->server);
$find = ['{{SERVER_MEMORY}}', '{{SERVER_IP}}', '{{SERVER_PORT}}'];
$replace = [$server->memory, $server->allocation->ip, $server->allocation->port];
$variables = $server->egg->variables->each(function ($variable) use (&$find, &$replace, $response) {
$find[] = '{{' . $variable->env_variable . '}}';
$replace[] = $variable->user_viewable ? $response->data[$variable->env_variable] : '[hidden]';
})->filter(function ($variable) {
return $variable->user_viewable === 1;
});
return collect([
'startup' => str_replace($find, $replace, $server->startup),
'variables' => $variables,
'server_values' => $response->data,
]);
}
}

View file

@ -1,58 +1,50 @@
<?php <?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\Servers; namespace Pterodactyl\Services\Servers;
use Pterodactyl\Models\User;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Exception\RequestException;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Traits\Services\HasUserLevels;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface;
use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface;
class StartupModificationService class StartupModificationService
{ {
/** use HasUserLevels;
* @var bool
*/
protected $admin = false;
/** /**
* @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface
*/ */
protected $daemonServerRepository; private $daemonServerRepository;
/** /**
* @var \Illuminate\Database\ConnectionInterface * @var \Illuminate\Database\ConnectionInterface
*/ */
protected $connection; private $connection;
/** /**
* @var \Pterodactyl\Services\Servers\EnvironmentService * @var \Pterodactyl\Services\Servers\EnvironmentService
*/ */
protected $environmentService; private $environmentService;
/** /**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/ */
protected $repository; private $repository;
/** /**
* @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface
*/ */
protected $serverVariableRepository; private $serverVariableRepository;
/** /**
* @var \Pterodactyl\Services\Servers\VariableValidatorService * @var \Pterodactyl\Services\Servers\VariableValidatorService
*/ */
protected $validatorService; private $validatorService;
/** /**
* StartupModificationService constructor. * StartupModificationService constructor.
@ -80,91 +72,81 @@ class StartupModificationService
$this->validatorService = $validatorService; $this->validatorService = $validatorService;
} }
/**
* Determine if this function should run at an administrative level.
*
* @param bool $bool
* @return $this
*/
public function isAdmin($bool = true)
{
$this->admin = $bool;
return $this;
}
/** /**
* Process startup modification for a server. * Process startup modification for a server.
* *
* @param int|\Pterodactyl\Models\Server $server * @param \Pterodactyl\Models\Server $server
* @param array $data * @param array $data
* *
* @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/ */
public function handle($server, array $data) public function handle(Server $server, array $data)
{ {
if (! $server instanceof Server) { $this->connection->beginTransaction();
$server = $this->repository->find($server); if (! is_null(array_get($data, 'environment'))) {
$this->validatorService->setUserLevel($this->getUserLevel());
$results = $this->validatorService->handle(array_get($data, 'egg_id', $server->egg_id), array_get($data, 'environment', []));
$results->each(function ($result) use ($server) {
$this->serverVariableRepository->withoutFresh()->updateOrCreate([
'server_id' => $server->id,
'variable_id' => $result->id,
], [
'variable_value' => $result->value,
]);
});
} }
$daemonData = ['build' => [
'env|overwrite' => $this->environmentService->handle($server),
]];
if ($this->isUserLevel(User::USER_LEVEL_ADMIN)) {
$this->updateAdministrativeSettings($data, $server, $daemonData);
}
try {
$this->daemonServerRepository->setNode($server->node_id)->setAccessServer($server->uuid)->update($daemonData);
} catch (RequestException $exception) {
$this->connection->rollBack();
throw new DaemonConnectionException($exception);
}
$this->connection->commit();
}
/**
* Update certain administrative settings for a server in the DB.
*
* @param array $data
* @param \Pterodactyl\Models\Server $server
* @param array $daemonData
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
private function updateAdministrativeSettings(array $data, Server &$server, array &$daemonData)
{
$server = $this->repository->update($server->id, [
'installed' => 0,
'startup' => array_get($data, 'startup', $server->startup),
'nest_id' => array_get($data, 'nest_id', $server->nest_id),
'egg_id' => array_get($data, 'egg_id', $server->egg_id),
'pack_id' => array_get($data, 'pack_id', $server->pack_id) > 0 ? array_get($data, 'pack_id', $server->pack_id) : null,
'skip_scripts' => isset($data['skip_scripts']),
]);
if ( if (
$server->nest_id != array_get($data, 'nest_id', $server->nest_id) || $server->nest_id != array_get($data, 'nest_id', $server->nest_id) ||
$server->egg_id != array_get($data, 'egg_id', $server->egg_id) || $server->egg_id != array_get($data, 'egg_id', $server->egg_id) ||
$server->pack_id != array_get($data, 'pack_id', $server->pack_id) $server->pack_id != array_get($data, 'pack_id', $server->pack_id)
) { ) {
$hasServiceChanges = true; $daemonData['service'] = array_merge(
} $this->repository->withColumns(['id', 'egg_id', 'pack_id'])->getDaemonServiceData($server->id),
['skip_scripts' => isset($data['skip_scripts'])]
$this->connection->beginTransaction(); );
if (isset($data['environment'])) {
$validator = $this->validatorService->isAdmin($this->admin)
->setFields($data['environment'])
->validate(array_get($data, 'egg_id', $server->egg_id));
foreach ($validator->getResults() as $result) {
$this->serverVariableRepository->withoutFresh()->updateOrCreate([
'server_id' => $server->id,
'variable_id' => $result['id'],
], [
'variable_value' => $result['value'],
]);
}
}
$daemonData = [
'build' => [
'env|overwrite' => $this->environmentService->process($server),
],
];
if ($this->admin) {
$server = $this->repository->update($server->id, [
'installed' => 0,
'startup' => array_get($data, 'startup', $server->startup),
'nest_id' => array_get($data, 'nest_id', $server->nest_id),
'egg_id' => array_get($data, 'egg_id', $server->egg_id),
'pack_id' => array_get($data, 'pack_id', $server->pack_id) > 0 ? array_get($data, 'pack_id', $server->pack_id) : null,
'skip_scripts' => isset($data['skip_scripts']),
]);
if (isset($hasServiceChanges)) {
$daemonData['service'] = array_merge(
$this->repository->withColumns(['id', 'egg_id', 'pack_id'])->getDaemonServiceData($server->id),
['skip_scripts' => isset($data['skip_scripts'])]
);
}
}
try {
$this->daemonServerRepository->setNode($server->node_id)->setAccessServer($server->uuid)->update($daemonData);
$this->connection->commit();
} catch (RequestException $exception) {
$response = $exception->getResponse();
throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [
'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(),
]), $exception, 'warning');
} }
} }
} }

View file

@ -1,40 +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\Services\Servers;
class UsernameGenerationService
{
/**
* Generate a unique username to be used for SFTP connections and identification
* of the server docker container on the host system.
*
* @param string $name
* @param null $identifier
* @return string
*/
public function generate($name, $identifier = null)
{
if (is_null($identifier) || ! ctype_alnum($identifier)) {
$unique = str_random(8);
} else {
if (strlen($identifier) < 8) {
$unique = $identifier . str_random((8 - strlen($identifier)));
} else {
$unique = substr($identifier, 0, 8);
}
}
// Filter the Server Name
$name = trim(preg_replace('/[^A-Za-z0-9]+/', '', $name), '_');
$name = (strlen($name) < 1) ? str_random(6) : $name;
return strtolower(substr($name, 0, 6) . '_' . $unique);
}
}

View file

@ -9,6 +9,9 @@
namespace Pterodactyl\Services\Servers; namespace Pterodactyl\Services\Servers;
use Pterodactyl\Models\User;
use Illuminate\Support\Collection;
use Pterodactyl\Traits\Services\HasUserLevels;
use Pterodactyl\Exceptions\DisplayValidationException; use Pterodactyl\Exceptions\DisplayValidationException;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Illuminate\Contracts\Validation\Factory as ValidationFactory; use Illuminate\Contracts\Validation\Factory as ValidationFactory;
@ -17,20 +20,7 @@ use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface;
class VariableValidatorService class VariableValidatorService
{ {
/** use HasUserLevels;
* @var bool
*/
protected $isAdmin = false;
/**
* @var array
*/
protected $fields = [];
/**
* @var array
*/
protected $results = [];
/** /**
* @var \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface * @var \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface
@ -72,56 +62,26 @@ class VariableValidatorService
$this->validator = $validator; $this->validator = $validator;
} }
/**
* Set the fields with populated data to validate.
*
* @param array $fields
* @return $this
*/
public function setFields(array $fields)
{
$this->fields = $fields;
return $this;
}
/**
* Set this function to be running at the administrative level.
*
* @param bool $bool
* @return $this
*/
public function isAdmin($bool = true)
{
$this->isAdmin = $bool;
return $this;
}
/** /**
* Validate all of the passed data aganist the given service option variables. * Validate all of the passed data aganist the given service option variables.
* *
* @param int $option * @param int $egg
* @return $this * @param array $fields
* @return \Illuminate\Support\Collection
*/ */
public function validate($option) public function handle(int $egg, array $fields = []): Collection
{ {
$variables = $this->optionVariableRepository->findWhere([['egg_id', '=', $option]]); $variables = $this->optionVariableRepository->findWhere([['egg_id', '=', $egg]]);
if (count($variables) === 0) {
$this->results = [];
return $this; return $variables->map(function ($item) use ($fields) {
} // Skip doing anything if user is not an admin and
// variable is not user viewable or editable.
$variables->each(function ($item) { if (! $this->isUserLevel(User::USER_LEVEL_ADMIN) && (! $item->user_editable || ! $item->user_viewable)) {
// Skip doing anything if user is not an admin and variable is not user viewable return false;
// or editable.
if (! $this->isAdmin && (! $item->user_editable || ! $item->user_viewable)) {
return;
} }
$validator = $this->validator->make([ $validator = $this->validator->make([
'variable_value' => array_key_exists($item->env_variable, $this->fields) ? $this->fields[$item->env_variable] : null, 'variable_value' => array_get($fields, $item->env_variable),
], [ ], [
'variable_value' => $item->rules, 'variable_value' => $item->rules,
]); ]);
@ -136,23 +96,13 @@ class VariableValidatorService
)); ));
} }
$this->results[] = [ return (object) [
'id' => $item->id, 'id' => $item->id,
'key' => $item->env_variable, 'key' => $item->env_variable,
'value' => $this->fields[$item->env_variable], 'value' => array_get($fields, $item->env_variable),
]; ];
})->filter(function ($item) {
return is_object($item);
}); });
return $this;
}
/**
* Return the final results after everything has been validated.
*
* @return array
*/
public function getResults()
{
return $this->results;
} }
} }

View file

@ -0,0 +1,90 @@
<?php
namespace Pterodactyl\Services\Sftp;
use Illuminate\Auth\AuthenticationException;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class AuthenticateUsingPasswordService
{
/**
* @var \Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService
*/
private $keyProviderService;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
private $repository;
/**
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface
*/
private $userRepository;
/**
* AuthenticateUsingPasswordService constructor.
*
* @param \Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService $keyProviderService
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
* @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $userRepository
*/
public function __construct(
DaemonKeyProviderService $keyProviderService,
ServerRepositoryInterface $repository,
UserRepositoryInterface $userRepository
) {
$this->keyProviderService = $keyProviderService;
$this->repository = $repository;
$this->userRepository = $userRepository;
}
/**
* Attempt to authenticate a provded username and password and determine if they
* have permission to access a given server. This function does not account for
* subusers currently. Only administrators and server owners can login to access
* their files at this time.
*
* Server must exist on the node that the API call is being made from in order for a
* valid response to be provided.
*
* @param string $username
* @param string $password
* @param string|null $server
* @param int $node
* @return array
*
* @throws \Illuminate\Auth\AuthenticationException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function handle(string $username, string $password, int $node, string $server = null): array
{
if (is_null($server)) {
throw new RecordNotFoundException;
}
try {
$user = $this->userRepository->withColumns(['id', 'root_admin', 'password'])->findFirstWhere([['username', '=', $username]]);
if (! password_verify($password, $user->password)) {
throw new AuthenticationException;
}
} catch (RecordNotFoundException $exception) {
throw new AuthenticationException;
}
$server = $this->repository->withColumns(['id', 'node_id', 'owner_id', 'uuid'])->getByUuid($server);
if ($server->node_id !== $node || (! $user->root_admin && $server->owner_id !== $user->id)) {
throw new RecordNotFoundException;
}
return [
'server' => $server->uuid,
'token' => $this->keyProviderService->handle($server->id, $user->id),
];
}
}

View file

@ -10,32 +10,46 @@
namespace Pterodactyl\Traits\Controllers; namespace Pterodactyl\Traits\Controllers;
use Javascript; use Javascript;
use Illuminate\Http\Request;
trait JavascriptInjection trait JavascriptInjection
{ {
/** /**
* @var \Illuminate\Contracts\Session\Session * @var \Illuminate\Http\Request
*/ */
protected $session; private $request;
/**
* Set the request object to use when injecting JS.
*
* @param \Illuminate\Http\Request $request
* @return $this
*/
public function setRequest(Request $request)
{
$this->request = $request;
return $this;
}
/** /**
* Injects server javascript into the page to be used by other services. * Injects server javascript into the page to be used by other services.
* *
* @param array $args * @param array $args
* @param bool $overwrite * @param bool $overwrite
* @return mixed * @return array
*/ */
public function injectJavascript($args = [], $overwrite = false) public function injectJavascript($args = [], $overwrite = false)
{ {
$server = $this->session->get('server_data.model'); $request = $this->request ?? app()->make(Request::class);
$token = $this->session->get('server_data.token'); $server = $request->attributes->get('server');
$token = $request->attributes->get('server_token');
$response = array_merge([ $response = array_merge([
'server' => [ 'server' => [
'uuid' => $server->uuid, 'uuid' => $server->uuid,
'uuidShort' => $server->uuidShort, 'uuidShort' => $server->uuidShort,
'daemonSecret' => $token, 'daemonSecret' => $token,
'username' => $server->username,
], ],
'node' => [ 'node' => [
'fqdn' => $server->node->fqdn, 'fqdn' => $server->node->fqdn,

View file

@ -0,0 +1,44 @@
<?php
namespace Pterodactyl\Traits\Services;
use Pterodactyl\Models\User;
trait HasUserLevels
{
/**
* @var int
*/
private $userLevel = User::USER_LEVEL_USER;
/**
* Set the access level for running this function.
*
* @param int $level
*/
public function setUserLevel(int $level)
{
$this->userLevel = $level;
}
/**
* Determine which level this function is running at.
*
* @return int
*/
public function getUserLevel(): int
{
return $this->userLevel;
}
/**
* Determine if the current user level is set to a specific level.
*
* @param int $level
* @return bool
*/
public function isUserLevel(int $level): bool
{
return $this->getUserLevel() === $level;
}
}

View file

@ -184,4 +184,19 @@ return [
'daemon/*', 'daemon/*',
'remote/*', 'remote/*',
], ],
/*
|--------------------------------------------------------------------------
| Dynamic Environment Variables
|--------------------------------------------------------------------------
|
| Place dynamic environment variables here that should be auto-appended
| to server environment fields when the server is created or updated.
|
| Items should be in 'key' => 'value' format, where key is the environment
| variable name, and value is the server-object key. For example:
|
| 'P_SERVER_CREATED_AT' => 'created_at'
*/
'environment_variables' => [],
]; ];

View file

@ -29,9 +29,10 @@ $factory->define(Pterodactyl\Models\Server::class, function (Faker\Generator $fa
'io' => 500, 'io' => 500,
'cpu' => 0, 'cpu' => 0,
'oom_disabled' => 0, 'oom_disabled' => 0,
'allocation_id' => $faker->randomNumber(),
'nest_id' => $faker->randomNumber(),
'egg_id' => $faker->randomNumber(),
'pack_id' => null, 'pack_id' => null,
'username' => $faker->userName,
'sftp_password' => null,
'installed' => 1, 'installed' => 1,
'created_at' => \Carbon\Carbon::now(), 'created_at' => \Carbon\Carbon::now(),
'updated_at' => \Carbon\Carbon::now(), 'updated_at' => \Carbon\Carbon::now(),
@ -39,6 +40,8 @@ $factory->define(Pterodactyl\Models\Server::class, function (Faker\Generator $fa
}); });
$factory->define(Pterodactyl\Models\User::class, function (Faker\Generator $faker) { $factory->define(Pterodactyl\Models\User::class, function (Faker\Generator $faker) {
static $password;
return [ return [
'id' => $faker->unique()->randomNumber(), 'id' => $faker->unique()->randomNumber(),
'external_id' => null, 'external_id' => null,
@ -47,7 +50,7 @@ $factory->define(Pterodactyl\Models\User::class, function (Faker\Generator $fake
'email' => $faker->safeEmail, 'email' => $faker->safeEmail,
'name_first' => $faker->firstName, 'name_first' => $faker->firstName,
'name_last' => $faker->lastName, 'name_last' => $faker->lastName,
'password' => bcrypt('password'), 'password' => $password ?: $password = bcrypt('password'),
'language' => 'en', 'language' => 'en',
'root_admin' => false, 'root_admin' => false,
'use_totp' => false, 'use_totp' => false,
@ -173,6 +176,21 @@ $factory->define(Pterodactyl\Models\DatabaseHost::class, function (Faker\Generat
]; ];
}); });
$factory->define(Pterodactyl\Models\Database::class, function (Faker\Generator $faker) {
static $password;
return [
'id' => $faker->unique()->randomNumber(),
'server_id' => $faker->randomNumber(),
'database_host_id' => $faker->randomNumber(),
'database' => str_random(10),
'username' => str_random(10),
'password' => $password ?: bcrypt('test123'),
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
'updated_at' => \Carbon\Carbon::now()->toDateTimeString(),
];
});
$factory->define(Pterodactyl\Models\Schedule::class, function (Faker\Generator $faker) { $factory->define(Pterodactyl\Models\Schedule::class, function (Faker\Generator $faker) {
return [ return [
'id' => $faker->unique()->randomNumber(), 'id' => $faker->unique()->randomNumber(),

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class RemoveLegacySFTPInformation extends Migration
{
/**
* Run the migrations.
*/
public function up()
{
Schema::table('servers', function (Blueprint $table) {
$table->dropUnique(['username']);
$table->dropColumn('username');
$table->dropColumn('sftp_password');
});
}
/**
* Reverse the migrations.
*/
public function down()
{
Schema::table('servers', function (Blueprint $table) {
$table->string('username')->nullable()->after('image')->unique();
$table->text('sftp_password')->after('image');
});
}
}

File diff suppressed because one or more lines are too long

View file

@ -20,11 +20,12 @@ return [
'subusers' => 'Subusers', 'subusers' => 'Subusers',
'schedules' => 'Schedules', 'schedules' => 'Schedules',
'configuration' => 'Configuration', 'configuration' => 'Configuration',
'port_allocations' => 'Port Allocations', 'port_allocations' => 'Allocation Settings',
'sftp_settings' => 'SFTP Settings', 'sftp_settings' => 'SFTP Settings',
'startup_parameters' => 'Startup Parameters', 'startup_parameters' => 'Startup Parameters',
'databases' => 'Databases', 'databases' => 'Databases',
'edit_file' => 'Edit File', 'edit_file' => 'Edit File',
'admin' => 'Manage', 'admin_header' => 'ADMINISTRATIVE',
'admin' => 'Server Configuration',
], ],
]; ];

View file

@ -189,9 +189,13 @@ return [
'title' => 'Delete Subuser', 'title' => 'Delete Subuser',
'description' => 'Allows a user to delete other subusers on the server.', 'description' => 'Allows a user to delete other subusers on the server.',
], ],
'set_connection' => [ 'view_allocations' => [
'title' => 'Set Default Connection', 'title' => 'View Allocations',
'description' => 'Allows user to set the default connection used for a server as well as view avaliable ports.', 'description' => 'Allows user to view all of the IPs and ports assigned to a server.',
],
'edit_allocation' => [
'title' => 'Edit Default Connection',
'description' => 'Allows user to change the default connection allocation to use for a server.',
], ],
'view_startup' => [ 'view_startup' => [
'title' => 'View Startup Command', 'title' => 'View Startup Command',
@ -288,8 +292,8 @@ return [
'command' => 'Startup Command', 'command' => 'Startup Command',
'edit_params' => 'Edit Parameters', 'edit_params' => 'Edit Parameters',
'update' => 'Update Startup Parameters', 'update' => 'Update Startup Parameters',
'startup_var' => 'Startup Command Variable',
'startup_regex' => 'Input Rules', 'startup_regex' => 'Input Rules',
'edited' => 'Startup variables have been successfully edited. They will take effect the next time this server is started.',
], ],
'sftp' => [ 'sftp' => [
'header' => 'SFTP Configuration', 'header' => 'SFTP Configuration',
@ -297,12 +301,12 @@ return [
'change_pass' => 'Change SFTP Password', 'change_pass' => 'Change SFTP Password',
'details' => 'SFTP Details', 'details' => 'SFTP Details',
'conn_addr' => 'Connection Address', 'conn_addr' => 'Connection Address',
'warning' => 'Ensure that your client is set to use SFTP and not FTP or FTPS for connections, there is a difference between the protocols.', 'warning' => 'The SFTP password is your account password. Ensure that your client is set to use SFTP and not FTP or FTPS for connections, there is a difference between the protocols.',
], ],
'database' => [ 'database' => [
'header' => 'Databases', 'header' => 'Databases',
'header_sub' => 'All databases available for this server.', 'header_sub' => 'All databases available for this server.',
'your_dbs' => 'Your Databases', 'your_dbs' => 'Configured Databases',
'host' => 'MySQL Host', 'host' => 'MySQL Host',
'reset_password' => 'Reset Password', 'reset_password' => 'Reset Password',
'no_dbs' => 'There are no databases listed for this server.', 'no_dbs' => 'There are no databases listed for this server.',

View file

@ -79,9 +79,8 @@
</div> </div>
<div class="box-footer"> <div class="box-footer">
{!! csrf_field() !!} {!! csrf_field() !!}
{!! method_field('PATCH') !!} <button name="_method" value="DELETE" class="btn btn-sm btn-danger pull-left muted muted-hover"><i class="fa fa-trash-o"></i></button>
<button name="action" value="delete" class="btn btn-sm btn-danger pull-left muted muted-hover"><i class="fa fa-trash-o"></i></button> <button name="_method" value="PATCH" class="btn btn-sm btn-primary pull-right">Save</button>
<button name="action" value="edit" class="btn btn-sm btn-primary pull-right">Save</button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -42,7 +42,6 @@
<th>ID</th> <th>ID</th>
<th>Server Name</th> <th>Server Name</th>
<th>Owner</th> <th>Owner</th>
<th>Username</th>
<th>Node</th> <th>Node</th>
<th>Connection</th> <th>Connection</th>
<th></th> <th></th>
@ -52,7 +51,6 @@
<td><code>{{ $server->uuidShort }}</code></td> <td><code>{{ $server->uuidShort }}</code></td>
<td><a href="{{ route('admin.servers.view', $server->id) }}">{{ $server->name }}</a></td> <td><a href="{{ route('admin.servers.view', $server->id) }}">{{ $server->name }}</a></td>
<td><a href="{{ route('admin.users.view', $server->user->id) }}">{{ $server->user->username }}</a></td> <td><a href="{{ route('admin.users.view', $server->user->id) }}">{{ $server->user->username }}</a></td>
<td>{{ $server->username }}</td>
<td><a href="{{ route('admin.nodes.view', $server->node->id) }}">{{ $server->node->name }}</a></td> <td><a href="{{ route('admin.nodes.view', $server->node->id) }}">{{ $server->node->name }}</a></td>
<td> <td>
<code>{{ $server->allocation->alias }}:{{ $server->allocation->port }}</code> <code>{{ $server->allocation->alias }}:{{ $server->allocation->port }}</code>

View file

@ -40,6 +40,9 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-7"> <div class="col-sm-7">
<div class="alert alert-info">
Database passwords can be viewed when <a href="{{ route('server.databases.index', ['server' => $server->uuidShort]) }}">visiting this server</a> on the front-end.
</div>
<div class="box box-primary"> <div class="box box-primary">
<div class="box-header with-border"> <div class="box-header with-border">
<h3 class="box-title">Active Databases</h3> <h3 class="box-title">Active Databases</h3>
@ -128,7 +131,7 @@
}, function () { }, function () {
$.ajax({ $.ajax({
method: 'DELETE', method: 'DELETE',
url: Router.route('admin.servers.view.database.delete', { id: '{{ $server->id }}', database: self.data('id') }), url: Router.route('admin.servers.view.database.delete', { server: '{{ $server->id }}', database: self.data('id') }),
headers: { 'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content') }, headers: { 'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content') },
}).done(function () { }).done(function () {
self.parent().parent().slideUp(); self.parent().parent().slideUp();
@ -149,7 +152,7 @@
$(this).addClass('disabled').find('i').addClass('fa-spin'); $(this).addClass('disabled').find('i').addClass('fa-spin');
$.ajax({ $.ajax({
type: 'PATCH', type: 'PATCH',
url: Router.route('admin.servers.view.database', { id: '{{ $server->id }}' }), url: Router.route('admin.servers.view.database', { server: '{{ $server->id }}' }),
headers: { 'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content') }, headers: { 'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content') },
data: { database: $(this).data('id') }, data: { database: $(this).data('id') },
}).done(function (data) { }).done(function (data) {

View file

@ -55,14 +55,6 @@
<td>Docker Container ID</td> <td>Docker Container ID</td>
<td data-attr="container-id"><i class="fa fa-fw fa-refresh fa-spin"></i></td> <td data-attr="container-id"><i class="fa fa-fw fa-refresh fa-spin"></i></td>
</tr> </tr>
<tr>
<td>Docker User ID</td>
<td data-attr="container-user"><i class="fa fa-fw fa-refresh fa-spin"></i></td>
</tr>
<tr>
<td>Docker Container Name</td>
<td>{{ $server->username }}</td>
</tr>
<tr> <tr>
<td>Service</td> <td>Service</td>
<td> <td>

View file

@ -155,7 +155,18 @@
</a> </a>
</li> </li>
@endcan @endcan
@if(Gate::allows('view-startup', $server) || Gate::allows('view-sftp', $server) || Gate::allows('view-databases', $server) || Gate::allows('view-allocation', $server)) @can('view-databases', $server)
<li
@if(starts_with(Route::currentRouteName(), 'server.databases'))
class="active"
@endif
>
<a href="{{ route('server.databases.index', $server->uuidShort)}}">
<i class="fa fa-database"></i> <span>@lang('navigation.server.databases')</span>
</a>
</li>
@endcan
@if(Gate::allows('view-startup', $server) || Gate::allows('view-sftp', $server) || Gate::allows('view-allocation', $server))
<li class="treeview <li class="treeview
@if(starts_with(Route::currentRouteName(), 'server.settings')) @if(starts_with(Route::currentRouteName(), 'server.settings'))
active active
@ -178,15 +189,13 @@
@can('view-startup', $server) @can('view-startup', $server)
<li class="{{ Route::currentRouteName() !== 'server.settings.startup' ?: 'active' }}"><a href="{{ route('server.settings.startup', $server->uuidShort) }}"><i class="fa fa-angle-right"></i> @lang('navigation.server.startup_parameters')</a></li> <li class="{{ Route::currentRouteName() !== 'server.settings.startup' ?: 'active' }}"><a href="{{ route('server.settings.startup', $server->uuidShort) }}"><i class="fa fa-angle-right"></i> @lang('navigation.server.startup_parameters')</a></li>
@endcan @endcan
@can('view-databases', $server)
<li class="{{ Route::currentRouteName() !== 'server.settings.databases' ?: 'active' }}"><a href="{{ route('server.settings.databases', $server->uuidShort) }}"><i class="fa fa-angle-right"></i> @lang('navigation.server.databases')</a></li>
@endcan
</ul> </ul>
</li> </li>
@endif @endif
@if(Auth::user()->root_admin) @if(Auth::user()->root_admin)
<li class="header">@lang('navigation.server.admin_header')</li>
<li> <li>
<a href="{{ route('admin.servers.view', $server->id) }}"> <a href="{{ route('admin.servers.view', $server->id) }}" target="_blank">
<i class="fa fa-cog"></i> <span>@lang('navigation.server.admin')</span> <i class="fa fa-cog"></i> <span>@lang('navigation.server.admin')</span>
</a> </a>
</li> </li>

View file

@ -16,7 +16,7 @@
<div id="terminal" style="width:100%;max-height: none !important;"></div> <div id="terminal" style="width:100%;max-height: none !important;"></div>
<div id="terminal_input" class="form-group no-margin"> <div id="terminal_input" class="form-group no-margin">
<div class="input-group"> <div class="input-group">
<div class="input-group-addon terminal_input--prompt">{{ $server->username }}:~$</div> <div class="input-group-addon terminal_input--prompt">container:~/$</div>
<input type="text" class="form-control terminal_input--input"> <input type="text" class="form-control terminal_input--input">
</div> </div>
</div> </div>

View file

@ -25,6 +25,11 @@
<div class="box"> <div class="box">
<div class="box-header with-border"> <div class="box-header with-border">
<h3 class="box-title">@lang('server.config.database.your_dbs')</h3> <h3 class="box-title">@lang('server.config.database.your_dbs')</h3>
@if(auth()->user()->root_admin)
<div class="box-tools">
<a href="{{ route('admin.servers.view.database', ['server' => $server->id]) }}" target="_blank" class="btn btn-sm btn-success">Create New</a>
</div>
@endif
</div> </div>
@if(count($databases) > 0) @if(count($databases) > 0)
<div class="box-body table-responsive no-padding"> <div class="box-body table-responsive no-padding">
@ -41,7 +46,14 @@
<tr> <tr>
<td class="middle">{{ $database->database }}</td> <td class="middle">{{ $database->database }}</td>
<td class="middle">{{ $database->username }}</td> <td class="middle">{{ $database->username }}</td>
<td class="middle"><code data-attr="set-password">{{ Crypt::decrypt($database->password) }}</code></td> <td class="middle">
<code class="toggle-display" style="cursor:pointer" data-toggle="tooltip" data-placement="right" title="Click to Reveal">
<i class="fa fa-key"></i> &bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;
</code>
<code class="hidden" data-attr="set-password">
{{ Crypt::decrypt($database->password) }}
</code>
</td>
<td class="middle"><code>{{ $database->host->host }}:{{ $database->host->port }}</code></td> <td class="middle"><code>{{ $database->host->host }}:{{ $database->host->port }}</code></td>
@can('reset-db-password', $server) @can('reset-db-password', $server)
<td> <td>
@ -55,7 +67,7 @@
</div> </div>
@else @else
<div class="box-body"> <div class="box-body">
<div class="callout callout-info callout-nomargin"> <div class="alert alert-info no-margin-bottom">
@lang('server.config.database.no_dbs') @lang('server.config.database.no_dbs')
@if(Auth::user()->root_admin === 1) @if(Auth::user()->root_admin === 1)
<a href="{{ route('admin.servers.view', [ <a href="{{ route('admin.servers.view', [
@ -75,37 +87,44 @@
@parent @parent
{!! Theme::js('js/frontend/server.socket.js') !!} {!! Theme::js('js/frontend/server.socket.js') !!}
<script> <script>
@can('reset-db-password', $server) $(function () {
$('[data-action="reset-password"]').click(function (e) { $('[data-toggle="tooltip"]').tooltip()
e.preventDefault();
var block = $(this);
$(this).addClass('disabled').find('i').addClass('fa-spin');
$.ajax({
type: 'POST',
url: Router.route('server.ajax.reset-database-password', { server: Pterodactyl.server.uuidShort }),
headers: {
'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content'),
},
data: {
database: $(this).data('id')
}
}).done(function (data) {
block.parent().parent().find('[data-attr="set-password"]').html(data);
}).fail(function(jqXHR, textStatus, errorThrown) {
console.error(jqXHR);
var error = 'An error occured while trying to process this request.';
if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {
error = jqXHR.responseJSON.error;
}
swal({
type: 'error',
title: 'Whoops!',
text: error
});
}).always(function () {
block.removeClass('disabled').find('i').removeClass('fa-spin');
});
}); });
@endcan $('.toggle-display').on('click', function () {
$(this).parent().find('code[data-attr="set-password"]').removeClass('hidden');
$(this).hide();
});
@can('reset-db-password', $server)
$('[data-action="reset-password"]').click(function (e) {
e.preventDefault();
var block = $(this);
$(this).addClass('disabled').find('i').addClass('fa-spin');
$.ajax({
type: 'PATCH',
url: Router.route('server.databases.password', { server: Pterodactyl.server.uuidShort }),
headers: {
'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content'),
},
data: {
database: $(this).data('id')
}
}).done(function (data) {
block.parent().parent().find('[data-attr="set-password"]').html(data.password);
}).fail(function(jqXHR) {
console.error(jqXHR);
var error = 'An error occured while trying to process this request.';
if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {
error = jqXHR.responseJSON.error;
}
swal({
type: 'error',
title: 'Whoops!',
text: error
});
}).always(function () {
block.removeClass('disabled').find('i').removeClass('fa-spin');
});
});
@endcan
</script> </script>
@endsection @endsection

View file

@ -30,7 +30,7 @@
<div id="terminal" style="width:100%;"></div> <div id="terminal" style="width:100%;"></div>
<div id="terminal_input" class="form-group no-margin"> <div id="terminal_input" class="form-group no-margin">
<div class="input-group"> <div class="input-group">
<div class="input-group-addon terminal_input--prompt">{{ $server->username }}:~$</div> <div class="input-group-addon terminal_input--prompt">container:~/$</div>
<input type="text" class="form-control terminal_input--input"> <input type="text" class="form-control terminal_input--input">
</div> </div>
</div> </div>

View file

@ -35,7 +35,7 @@
<th>@lang('strings.port')</th> <th>@lang('strings.port')</th>
<th></th> <th></th>
</tr> </tr>
@foreach ($server->allocations as $allocation) @foreach ($allocations as $allocation)
<tr> <tr>
<td> <td>
<code>{{ $allocation->ip }}</code> <code>{{ $allocation->ip }}</code>
@ -50,9 +50,9 @@
<td><code>{{ $allocation->port }}</code></td> <td><code>{{ $allocation->port }}</code></td>
<td class="col-xs-2 middle"> <td class="col-xs-2 middle">
@if($allocation->id === $server->allocation_id) @if($allocation->id === $server->allocation_id)
<span class="label label-success" data-allocation="{{ $allocation->id }}">@lang('strings.primary')</span> <a class="btn btn-xs btn-success disabled" data-action="set-default" data-allocation="{{ $allocation->hashid }}" role="button">@lang('strings.primary')</a>
@else @else
<span class="label label-default" data-action="set-connection" data-allocation="{{ $allocation->id }}">@lang('strings.make_primary')</span> <a class="btn btn-xs btn-default" data-action="set-default" data-allocation="{{ $allocation->hashid }}" role="button">@lang('strings.make_primary')</a>
@endif @endif
</td> </td>
</tr> </tr>
@ -60,6 +60,9 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div id="toggleActivityOverlay" class="overlay hidden">
<i class="fa fa-refresh fa-spin"></i>
</div>
</div> </div>
</div> </div>
<div class="col-sm-4"> <div class="col-sm-4">
@ -79,37 +82,39 @@
@parent @parent
{!! Theme::js('js/frontend/server.socket.js') !!} {!! Theme::js('js/frontend/server.socket.js') !!}
<script> <script>
@can('reset-db-password', $server) $(document).ready(function () {
$('[data-action="reset-database-password"]').click(function (e) { @can('edit-allocation', $server)
e.preventDefault(); (function triggerClickHandler() {
var block = $(this); $('a[data-action="set-default"]:not(.disabled)').click(function (e) {
$(this).find('i').addClass('fa-spin'); $('#toggleActivityOverlay').removeClass('hidden');
$.ajax({ e.preventDefault();
type: 'POST', var self = $(this);
url: Router.route('server.ajax.reset-database-password', { server: Pterodactyl.server.uuidShort }), $.ajax({
headers: { type: 'PATCH',
'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content'), url: Router.route('server.settings.allocation', { server: Pterodactyl.server.uuidShort }),
}, headers: {
data: { 'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content'),
'database': $(this).data('id') },
} data: {
}).done(function (data) { 'allocation': $(this).data('allocation')
block.parent().find('code').html(data); }
}).fail(function(jqXHR, textStatus, errorThrown) { }).done(function () {
console.error(jqXHR); self.parents().eq(2).find('a[role="button"]').removeClass('btn-success disabled').addClass('btn-default').html('{{ trans('strings.make_primary') }}');
var error = 'An error occured while trying to process this request.'; self.removeClass('btn-default').addClass('btn-success disabled').html('{{ trans('strings.primary') }}');
if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') { }).fail(function(jqXHR) {
error = jqXHR.responseJSON.error; console.error(jqXHR);
} var error = 'An error occured while trying to process this request.';
swal({ if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {
type: 'error', error = jqXHR.responseJSON.error;
title: 'Whoops!', }
text: error swal({type: 'error', title: 'Whoops!', text: error});
}).always(function () {
triggerClickHandler();
$('#toggleActivityOverlay').addClass('hidden');
})
}); });
}).always(function () { })();
block.find('i').removeClass('fa-spin'); @endcan
});
}); });
@endcan
</script> </script>
@endsection @endsection

View file

@ -21,37 +21,7 @@
@section('content') @section('content')
<div class="row"> <div class="row">
<div class="col-sm-6"> <div class="col-xs-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">@lang('server.config.sftp.change_pass')</h3>
</div>
@can('reset-sftp', $server)
<form action="{{ route('server.settings.sftp', $server->uuidShort) }}" method="post">
<div class="box-body">
<div class="form-group">
<label for="sftp_pass" class="control-label">@lang('base.account.new_password')</label>
<div>
<input type="password" class="form-control" name="sftp_pass" />
<p class="text-muted"><small>@lang('auth.password_requirements')</small></p>
</div>
</div>
</div>
<div class="box-footer">
{!! csrf_field() !!}
<input type="submit" class="btn btn-primary btn-sm" value="@lang('base.account.update_pass')" />
</div>
</form>
@else
<div class="box-body">
<div class="callout callout-warning callout-nomargin">
<p>@lang('auth.not_authorized')</p>
</div>
</div>
@endcan
</div>
</div>
<div class="col-sm-6">
<div class="box"> <div class="box">
<div class="box-header with-border"> <div class="box-header with-border">
<h3 class="box-title">@lang('server.config.sftp.details')</h3> <h3 class="box-title">@lang('server.config.sftp.details')</h3>
@ -66,20 +36,12 @@
<div class="form-group"> <div class="form-group">
<label for="password" class="control-label">@lang('strings.username')</label> <label for="password" class="control-label">@lang('strings.username')</label>
<div> <div>
<input type="text" class="form-control" readonly value="{{ $server->username }}" /> <input type="text" class="form-control" readonly value="{{ auth()->user()->username }}.{{ $server->uuidShort }}" />
</div> </div>
</div> </div>
@can('view-sftp-password', $server)
<div class="form-group">
<label for="password" class="control-label">@lang('base.account.current_password')</label>
<div>
<input type="text" class="form-control" readonly @if(! is_null($server->sftp_password))value="{{ Crypt::decrypt($server->sftp_password) }}"@endif />
</div>
</div>
@endcan
</div> </div>
<div class="box-footer"> <div class="box-footer">
<p class="small text-muted">@lang('server.config.sftp.warning')</p> <p class="small text-muted no-margin-bottom">@lang('server.config.sftp.warning')</p>
</div> </div>
</div> </div>
</div> </div>

View file

@ -21,26 +21,20 @@
@section('content') @section('content')
<div class="row"> <div class="row">
<form action="{{ route('server.settings.startup', $server->uuidShort) }}" method="POST"> <div class="col-xs-12">
<div class="col-xs-12"> <div class="box">
<div class="box"> <div class="box-header with-border">
<div class="box-header with-border"> <h3 class="box-title">@lang('server.config.startup.command')</h3>
<h3 class="box-title">@lang('server.config.startup.command')</h3> </div>
<div class="box-body">
<div class="form-group no-margin-bottom">
<input type="text" class="form-control" readonly value="{{ $startup }}" />
</div> </div>
<div class="box-body">
<div class="form-group">
<input type="text" class="form-control" readonly value="{{ $processedStartup }}" />
</div>
</div>
@can('edit-startup', $server)
<div class="box-footer">
{!! csrf_field() !!}
<input type="submit" class="btn btn-primary btn-sm pull-right" value="@lang('server.config.startup.update')" />
</div>
@endcan
</div> </div>
</div> </div>
@can('edit-startup', $server) </div>
@can('edit-startup', $server)
<form action="{{ route('server.settings.startup', $server->uuidShort) }}" method="POST">
@foreach($variables as $v) @foreach($variables as $v)
<div class="col-xs-12 col-md-4 col-sm-6"> <div class="col-xs-12 col-md-4 col-sm-6">
<div class="box"> <div class="box">
@ -50,11 +44,11 @@
<div class="box-body"> <div class="box-body">
<input <input
@if($v->user_editable) @if($v->user_editable)
name="env_{{ $v->id }}" name="environment[{{ $v->env_variable }}]"
@else @else
readonly readonly
@endif @endif
class="form-control" type="text" value="{{ old('env_' . $v->id, $v->server_set_value) }}" /> class="form-control" type="text" value="{{ old('environment.' . $v->env_variable, $server_values[$v->env_variable]) }}" />
<p class="small text-muted">{{ $v->description }}</p> <p class="small text-muted">{{ $v->description }}</p>
<p class="no-margin"> <p class="no-margin">
@if($v->required && $v->user_editable ) @if($v->required && $v->user_editable )
@ -68,14 +62,22 @@
</p> </p>
</div> </div>
<div class="box-footer"> <div class="box-footer">
<p class="no-margin text-muted small"><strong>@lang('server.config.startup.startup_var'):</strong> <code>{{ $v->env_variable }}</code></p>
<p class="no-margin text-muted small"><strong>@lang('server.config.startup.startup_regex'):</strong> <code>{{ $v->rules }}</code></p> <p class="no-margin text-muted small"><strong>@lang('server.config.startup.startup_regex'):</strong> <code>{{ $v->rules }}</code></p>
</div> </div>
</div> </div>
</div> </div>
@endforeach @endforeach
@endcan <div class="col-xs-12">
</form> <div class="box box-primary">
<div class="box-footer">
{!! csrf_field() !!}
{!! method_field('PATCH') !!}
<input type="submit" class="btn btn-primary btn-sm pull-right" value="@lang('server.config.startup.update')" />
</div>
</div>
</div>
</form>
@endcan
</div> </div>
@endsection @endsection

View file

@ -38,6 +38,7 @@ Route::group(['prefix' => 'databases'], function () {
Route::post('/', 'DatabaseController@create'); Route::post('/', 'DatabaseController@create');
Route::patch('/view/{host}', 'DatabaseController@update'); Route::patch('/view/{host}', 'DatabaseController@update');
Route::delete('/view/{host}', 'DatabaseController@delete');
}); });
/* /*

View file

@ -12,3 +12,7 @@ Route::group(['prefix' => '/eggs'], function () {
Route::get('/', 'EggRetrievalController@index')->name('api.remote.eggs'); Route::get('/', 'EggRetrievalController@index')->name('api.remote.eggs');
Route::get('/{uuid}', 'EggRetrievalController@download')->name('api.remote.eggs.download'); Route::get('/{uuid}', 'EggRetrievalController@download')->name('api.remote.eggs.download');
}); });
Route::group(['prefix' => '/sftp'], function () {
Route::post('/', 'SftpController@index')->name('api.remote.sftp');
});

View file

@ -18,13 +18,27 @@ Route::get('/console', 'ConsoleController@console')->name('server.console');
| |
*/ */
Route::group(['prefix' => 'settings'], function () { Route::group(['prefix' => 'settings'], function () {
Route::get('/databases', 'ServerController@getDatabases')->name('server.settings.databases'); Route::get('/allocation', 'Settings\AllocationController@index')->name('server.settings.allocation');
Route::get('/sftp', 'ServerController@getSFTP')->name('server.settings.sftp'); Route::patch('/allocation', 'Settings\AllocationController@update');
Route::get('/startup', 'ServerController@getStartup')->name('server.settings.startup');
Route::get('/allocation', 'ServerController@getAllocation')->name('server.settings.allocation');
Route::post('/sftp', 'ServerController@postSettingsSFTP'); Route::get('/sftp', 'Settings\SftpController@index')->name('server.settings.sftp');
Route::post('/startup', 'ServerController@postSettingsStartup');
Route::get('/startup', 'Settings\StartupController@index')->name('server.settings.startup');
Route::patch('/startup', 'Settings\StartupController@update');
});
/*
|--------------------------------------------------------------------------
| Server Database Controller Routes
|--------------------------------------------------------------------------
|
| Endpoint: /server/{server}/databases
|
*/
Route::group(['prefix' => 'databases'], function () {
Route::get('/', 'DatabaseController@index')->name('server.databases.index');
Route::patch('/password', 'DatabaseController@update')->middleware('server..database')->name('server.databases.password');
}); });
/* /*
@ -56,13 +70,13 @@ Route::group(['prefix' => 'files'], function () {
Route::group(['prefix' => 'users'], function () { Route::group(['prefix' => 'users'], function () {
Route::get('/', 'SubuserController@index')->name('server.subusers'); Route::get('/', 'SubuserController@index')->name('server.subusers');
Route::get('/new', 'SubuserController@create')->name('server.subusers.new'); Route::get('/new', 'SubuserController@create')->name('server.subusers.new');
Route::get('/view/{subuser}', 'SubuserController@view')->middleware('subuser')->name('server.subusers.view'); Route::get('/view/{subuser}', 'SubuserController@view')->middleware('server..subuser')->name('server.subusers.view');
Route::post('/new', 'SubuserController@store'); Route::post('/new', 'SubuserController@store');
Route::patch('/view/{subuser}', 'SubuserController@update')->middleware('subuser'); Route::patch('/view/{subuser}', 'SubuserController@update')->middleware('server..subuser');
Route::delete('/view/{subuser}/delete', 'SubuserController@delete')->middleware('subuser')->name('server.subusers.delete'); Route::delete('/view/{subuser}/delete', 'SubuserController@delete')->middleware('server..subuser')->name('server.subusers.delete');
}); });
/* /*
@ -76,24 +90,12 @@ Route::group(['prefix' => 'users'], function () {
Route::group(['prefix' => 'schedules'], function () { Route::group(['prefix' => 'schedules'], function () {
Route::get('/', 'Tasks\TaskManagementController@index')->name('server.schedules'); Route::get('/', 'Tasks\TaskManagementController@index')->name('server.schedules');
Route::get('/new', 'Tasks\TaskManagementController@create')->name('server.schedules.new'); Route::get('/new', 'Tasks\TaskManagementController@create')->name('server.schedules.new');
Route::get('/view/{schedule}', 'Tasks\TaskManagementController@view')->middleware('schedule')->name('server.schedules.view'); Route::get('/view/{schedule}', 'Tasks\TaskManagementController@view')->middleware('server..schedule')->name('server.schedules.view');
Route::post('/new', 'Tasks\TaskManagementController@store'); Route::post('/new', 'Tasks\TaskManagementController@store');
Route::patch('/view/{schedule}', 'Tasks\TaskManagementController@update')->middleware('schedule'); Route::patch('/view/{schedule}', 'Tasks\TaskManagementController@update')->middleware('server..schedule');
Route::patch('/view/{schedule}/toggle', 'Tasks\TaskToggleController@index')->middleware('schedule')->name('server.schedules.toggle'); Route::patch('/view/{schedule}/toggle', 'Tasks\TaskToggleController@index')->middleware('server..schedule')->name('server.schedules.toggle');
Route::delete('/view/{schedule}/delete', 'Tasks\TaskManagementController@delete')->middleware('schedule')->name('server.schedules.delete'); Route::delete('/view/{schedule}/delete', 'Tasks\TaskManagementController@delete')->middleware('server..schedule')->name('server.schedules.delete');
});
/*
|--------------------------------------------------------------------------
| Server Ajax Controller Routes
|--------------------------------------------------------------------------
|
| Endpoint: /server/{server}/ajax
|
*/
Route::group(['prefix' => 'ajax'], function () {
Route::post('/settings/reset-database-password', 'AjaxController@postResetDatabasePassword')->name('server.ajax.reset-database-password');
}); });

View file

@ -0,0 +1,49 @@
<?php
namespace Tests\Traits;
use Mockery;
use Mockery\MockInterface;
use GuzzleHttp\Exception\RequestException;
trait MocksRequestException
{
/**
* @var \GuzzleHttp\Exception\RequestException|\Mockery\Mock
*/
private $exception;
/**
* @var mixed
*/
private $exceptionResponse;
/**
* Configure the exception mock to work with the Panel's default exception
* handler actions.
*/
public function configureExceptionMock()
{
$this->getExceptionMock()->shouldReceive('getResponse')->andReturn($this->exceptionResponse);
}
/**
* Return a mocked instance of the request exception.
*
* @return \Mockery\MockInterface
*/
private function getExceptionMock(): MockInterface
{
return $this->exception ?? $this->exception = Mockery::mock(RequestException::class);
}
/**
* Set the exception response.
*
* @param mixed $response
*/
protected function setExceptionResponse($response)
{
$this->exceptionResponse = $response;
}
}

View file

@ -13,8 +13,10 @@ use Mockery as m;
use Tests\TestCase; use Tests\TestCase;
use Prologue\Alerts\AlertsMessageBag; use Prologue\Alerts\AlertsMessageBag;
use Tests\Assertions\ControllerAssertionsTrait; use Tests\Assertions\ControllerAssertionsTrait;
use Pterodactyl\Services\Database\DatabaseHostService;
use Pterodactyl\Http\Controllers\Admin\DatabaseController; use Pterodactyl\Http\Controllers\Admin\DatabaseController;
use Pterodactyl\Services\Databases\Hosts\HostUpdateService;
use Pterodactyl\Services\Databases\Hosts\HostCreationService;
use Pterodactyl\Services\Databases\Hosts\HostDeletionService;
use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface;
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
@ -23,29 +25,34 @@ class DatabaseControllerTest extends TestCase
use ControllerAssertionsTrait; use ControllerAssertionsTrait;
/** /**
* @var \Prologue\Alerts\AlertsMessageBag * @var \Prologue\Alerts\AlertsMessageBag|\Mockery\Mock
*/ */
protected $alert; private $alert;
/** /**
* @var \Pterodactyl\Http\Controllers\Admin\DatabaseController * @var \Pterodactyl\Services\Databases\Hosts\HostCreationService|\Mockery\Mock
*/ */
protected $controller; private $creationService;
/** /**
* @var \Pterodactyl\Contracts\Repository\LocationRepositoryInterface * @var \Pterodactyl\Services\Databases\Hosts\HostDeletionService|\Mockery\Mock
*/ */
protected $locationRepository; private $deletionService;
/** /**
* @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface * @var \Pterodactyl\Contracts\Repository\LocationRepositoryInterface|\Mockery\Mock
*/ */
protected $repository; private $locationRepository;
/** /**
* @var \Pterodactyl\Services\Database\DatabaseHostService * @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface|\Mockery\Mock
*/ */
protected $service; private $repository;
/**
* @var \Pterodactyl\Services\Databases\Hosts\HostUpdateService|\Mockery\Mock
*/
private $updateService;
/** /**
* Setup tests. * Setup tests.
@ -55,16 +62,11 @@ class DatabaseControllerTest extends TestCase
parent::setUp(); parent::setUp();
$this->alert = m::mock(AlertsMessageBag::class); $this->alert = m::mock(AlertsMessageBag::class);
$this->creationService = m::mock(HostCreationService::class);
$this->deletionService = m::mock(HostDeletionService::class);
$this->locationRepository = m::mock(LocationRepositoryInterface::class); $this->locationRepository = m::mock(LocationRepositoryInterface::class);
$this->repository = m::mock(DatabaseHostRepositoryInterface::class); $this->repository = m::mock(DatabaseHostRepositoryInterface::class);
$this->service = m::mock(DatabaseHostService::class); $this->updateService = m::mock(HostUpdateService::class);
$this->controller = new DatabaseController(
$this->alert,
$this->repository,
$this->service,
$this->locationRepository
);
} }
/** /**
@ -75,7 +77,7 @@ class DatabaseControllerTest extends TestCase
$this->locationRepository->shouldReceive('getAllWithNodes')->withNoArgs()->once()->andReturn('getAllWithNodes'); $this->locationRepository->shouldReceive('getAllWithNodes')->withNoArgs()->once()->andReturn('getAllWithNodes');
$this->repository->shouldReceive('getWithViewDetails')->withNoArgs()->once()->andReturn('getWithViewDetails'); $this->repository->shouldReceive('getWithViewDetails')->withNoArgs()->once()->andReturn('getWithViewDetails');
$response = $this->controller->index(); $response = $this->getController()->index();
$this->assertIsViewResponse($response); $this->assertIsViewResponse($response);
$this->assertViewNameEquals('admin.databases.index', $response); $this->assertViewNameEquals('admin.databases.index', $response);
@ -93,7 +95,7 @@ class DatabaseControllerTest extends TestCase
$this->locationRepository->shouldReceive('getAllWithNodes')->withNoArgs()->once()->andReturn('getAllWithNodes'); $this->locationRepository->shouldReceive('getAllWithNodes')->withNoArgs()->once()->andReturn('getAllWithNodes');
$this->repository->shouldReceive('getWithServers')->with(1)->once()->andReturn('getWithServers'); $this->repository->shouldReceive('getWithServers')->with(1)->once()->andReturn('getWithServers');
$response = $this->controller->view(1); $response = $this->getController()->view(1);
$this->assertIsViewResponse($response); $this->assertIsViewResponse($response);
$this->assertViewNameEquals('admin.databases.view', $response); $this->assertViewNameEquals('admin.databases.view', $response);
@ -102,4 +104,21 @@ class DatabaseControllerTest extends TestCase
$this->assertViewKeyEquals('locations', 'getAllWithNodes', $response); $this->assertViewKeyEquals('locations', 'getAllWithNodes', $response);
$this->assertViewKeyEquals('host', 'getWithServers', $response); $this->assertViewKeyEquals('host', 'getWithServers', $response);
} }
/**
* Return an instance of the DatabaseController with mock dependencies.
*
* @return \Pterodactyl\Http\Controllers\Admin\DatabaseController
*/
private function getController(): DatabaseController
{
return new DatabaseController(
$this->alert,
$this->repository,
$this->creationService,
$this->deletionService,
$this->updateService,
$this->locationRepository
);
}
} }

View file

@ -96,7 +96,7 @@ class DatabaseRepositoryTest extends TestCase
public function testCreateDatabaseStatement() public function testCreateDatabaseStatement()
{ {
$query = sprintf('CREATE DATABASE IF NOT EXISTS `%s`', 'test_database'); $query = sprintf('CREATE DATABASE IF NOT EXISTS `%s`', 'test_database');
$this->repository->shouldReceive('runStatement')->with($query, 'test')->once()->andReturn(true); $this->repository->shouldReceive('runStatement')->with($query)->once()->andReturn(true);
$this->assertTrue($this->repository->createDatabase('test_database', 'test')); $this->assertTrue($this->repository->createDatabase('test_database', 'test'));
} }
@ -107,7 +107,7 @@ class DatabaseRepositoryTest extends TestCase
public function testCreateUserStatement() public function testCreateUserStatement()
{ {
$query = sprintf('CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\'', 'test', '%', 'password'); $query = sprintf('CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\'', 'test', '%', 'password');
$this->repository->shouldReceive('runStatement')->with($query, 'test')->once()->andReturn(true); $this->repository->shouldReceive('runStatement')->with($query)->once()->andReturn(true);
$this->assertTrue($this->repository->createUser('test', '%', 'password', 'test')); $this->assertTrue($this->repository->createUser('test', '%', 'password', 'test'));
} }
@ -118,7 +118,7 @@ class DatabaseRepositoryTest extends TestCase
public function testUserAssignmentToDatabaseStatement() public function testUserAssignmentToDatabaseStatement()
{ {
$query = sprintf('GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX, EXECUTE ON `%s`.* TO `%s`@`%s`', 'test_database', 'test', '%'); $query = sprintf('GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX, EXECUTE ON `%s`.* TO `%s`@`%s`', 'test_database', 'test', '%');
$this->repository->shouldReceive('runStatement')->with($query, 'test')->once()->andReturn(true); $this->repository->shouldReceive('runStatement')->with($query)->once()->andReturn(true);
$this->assertTrue($this->repository->assignUserToDatabase('test_database', 'test', '%', 'test')); $this->assertTrue($this->repository->assignUserToDatabase('test_database', 'test', '%', 'test'));
} }
@ -128,7 +128,7 @@ class DatabaseRepositoryTest extends TestCase
*/ */
public function testFlushStatement() public function testFlushStatement()
{ {
$this->repository->shouldReceive('runStatement')->with('FLUSH PRIVILEGES', 'test')->once()->andReturn(true); $this->repository->shouldReceive('runStatement')->with('FLUSH PRIVILEGES')->once()->andReturn(true);
$this->assertTrue($this->repository->flush('test')); $this->assertTrue($this->repository->flush('test'));
} }
@ -139,7 +139,7 @@ class DatabaseRepositoryTest extends TestCase
public function testDropDatabaseStatement() public function testDropDatabaseStatement()
{ {
$query = sprintf('DROP DATABASE IF EXISTS `%s`', 'test_database'); $query = sprintf('DROP DATABASE IF EXISTS `%s`', 'test_database');
$this->repository->shouldReceive('runStatement')->with($query, 'test')->once()->andReturn(true); $this->repository->shouldReceive('runStatement')->with($query)->once()->andReturn(true);
$this->assertTrue($this->repository->dropDatabase('test_database', 'test')); $this->assertTrue($this->repository->dropDatabase('test_database', 'test'));
} }
@ -150,7 +150,7 @@ class DatabaseRepositoryTest extends TestCase
public function testDropUserStatement() public function testDropUserStatement()
{ {
$query = sprintf('DROP USER IF EXISTS `%s`@`%s`', 'test', '%'); $query = sprintf('DROP USER IF EXISTS `%s`@`%s`', 'test', '%');
$this->repository->shouldReceive('runStatement')->with($query, 'test')->once()->andReturn(true); $this->repository->shouldReceive('runStatement')->with($query)->once()->andReturn(true);
$this->assertTrue($this->repository->dropUser('test', '%', 'test')); $this->assertTrue($this->repository->dropUser('test', '%', 'test'));
} }

View file

@ -0,0 +1,156 @@
<?php
namespace Tests\Unit\Services\Allocations;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Allocation;
use Tests\Traits\MocksRequestException;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Exceptions\PterodactylException;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Services\Allocations\SetDefaultAllocationService;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonRepositoryInterface;
class SetDefaultAllocationServiceTest extends TestCase
{
use MocksRequestException;
/**
* @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock
*/
private $connection;
/**
* @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface|\Mockery\Mock
*/
private $daemonRepository;
/**
* @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface|\Mockery\Mock
*/
private $repository;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock
*/
private $serverRepository;
/**
* Setup tests.
*/
public function setUp()
{
parent::setUp();
$this->connection = m::mock(ConnectionInterface::class);
$this->daemonRepository = m::mock(DaemonRepositoryInterface::class);
$this->repository = m::mock(AllocationRepositoryInterface::class);
$this->serverRepository = m::mock(ServerRepositoryInterface::class);
}
/**
* Test that an allocation can be updated.
*
* @dataProvider useModelDataProvider
*/
public function testAllocationIsUpdated(bool $useModel)
{
$allocations = factory(Allocation::class)->times(2)->make();
$model = factory(Server::class)->make();
if (! $useModel) {
$this->serverRepository->shouldReceive('find')->with(1234)->once()->andReturn($model);
}
$this->repository->shouldReceive('findWhere')->with([['server_id', '=', $model->id]])->once()->andReturn($allocations);
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->serverRepository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf();
$this->serverRepository->shouldReceive('update')->with($model->id, [
'allocation_id' => $allocations->first()->id,
])->once()->andReturnNull();
$this->daemonRepository->shouldReceive('setAccessServer')->with($model->uuid)->once()->andReturnSelf();
$this->daemonRepository->shouldReceive('setNode')->with($model->node_id)->once()->andReturnSelf();
$this->daemonRepository->shouldReceive('update')->with([
'build' => [
'default' => [
'ip' => $allocations->first()->ip,
'port' => $allocations->first()->port,
],
'ports|overwrite' => $allocations->groupBy('ip')->map(function ($item) {
return $item->pluck('port');
})->toArray(),
],
])->once()->andReturnNull();
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$response = $this->getService()->handle($useModel ? $model : 1234, $allocations->first()->id);
$this->assertNotEmpty($response);
$this->assertSame($allocations->first(), $response);
}
/**
* Test that an allocation that doesn't belong to a server throws an exception.
*
* @expectedException \Pterodactyl\Exceptions\Service\Allocation\AllocationDoesNotBelongToServerException
*/
public function testAllocationNotBelongingToServerThrowsException()
{
$model = factory(Server::class)->make();
$this->repository->shouldReceive('findWhere')->with([['server_id', '=', $model->id]])->once()->andReturn(collect());
$this->getService()->handle($model, 1234);
}
/**
* Test that an exception thrown by guzzle is handled properly.
*/
public function testExceptionThrownByGuzzleIsHandled()
{
$this->configureExceptionMock();
$allocation = factory(Allocation::class)->make();
$model = factory(Server::class)->make();
$this->repository->shouldReceive('findWhere')->with([['server_id', '=', $model->id]])->once()->andReturn(collect([$allocation]));
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->serverRepository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf();
$this->serverRepository->shouldReceive('update')->with($model->id, [
'allocation_id' => $allocation->id,
])->once()->andReturnNull();
$this->daemonRepository->shouldReceive('setAccessServer->setNode->update')->once()->andThrow($this->getExceptionMock());
$this->connection->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull();
try {
$this->getService()->handle($model, $allocation->id);
} catch (PterodactylException $exception) {
$this->assertInstanceOf(DaemonConnectionException::class, $exception);
$this->assertInstanceOf(RequestException::class, $exception->getPrevious());
}
}
/**
* Data provider to determine if a model should be passed or an int.
*
* @return array
*/
public function useModelDataProvider(): array
{
return [[false], [true]];
}
/**
* Return an instance of the service with mocked dependencies.
*
* @return \Pterodactyl\Services\Allocations\SetDefaultAllocationService
*/
private function getService(): SetDefaultAllocationService
{
return new SetDefaultAllocationService($this->repository, $this->connection, $this->daemonRepository, $this->serverRepository);
}
}

View file

@ -1,202 +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 Tests\Unit\Services\Administrative;
use Mockery as m;
use Tests\TestCase;
use Illuminate\Database\DatabaseManager;
use Pterodactyl\Exceptions\DisplayException;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Extensions\DynamicDatabaseConnection;
use Pterodactyl\Services\Database\DatabaseHostService;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
class DatabaseHostServiceTest extends TestCase
{
/**
* @var \Illuminate\Database\DatabaseManager
*/
protected $database;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface
*/
protected $databaseRepository;
/**
* @var \Pterodactyl\Extensions\DynamicDatabaseConnection
*/
protected $dynamic;
/**
* @var \Illuminate\Contracts\Encryption\Encrypter
*/
protected $encrypter;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface
*/
protected $repository;
/**
* @var \Pterodactyl\Services\Database\DatabaseHostService
*/
protected $service;
/**
* Setup tests.
*/
public function setUp()
{
parent::setUp();
$this->database = m::mock(DatabaseManager::class);
$this->databaseRepository = m::mock(DatabaseRepositoryInterface::class);
$this->dynamic = m::mock(DynamicDatabaseConnection::class);
$this->encrypter = m::mock(Encrypter::class);
$this->repository = m::mock(DatabaseHostRepositoryInterface::class);
$this->service = new DatabaseHostService(
$this->database,
$this->databaseRepository,
$this->repository,
$this->dynamic,
$this->encrypter
);
}
/**
* Test that creating a host returns the correct data.
*/
public function testHostIsCreated()
{
$data = [
'password' => 'raw-password',
'name' => 'HostName',
'host' => '127.0.0.1',
'port' => 3306,
'username' => 'someusername',
'node_id' => null,
];
$finalData = (object) array_replace($data, ['password' => 'enc-password']);
$this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->encrypter->shouldReceive('encrypt')->with('raw-password')->once()->andReturn('enc-password');
$this->repository->shouldReceive('create')->with([
'password' => 'enc-password',
'name' => 'HostName',
'host' => '127.0.0.1',
'port' => 3306,
'username' => 'someusername',
'max_databases' => null,
'node_id' => null,
])->once()->andReturn($finalData);
$this->dynamic->shouldReceive('set')->with('dynamic', $finalData)->once()->andReturnNull();
$this->database->shouldReceive('connection')->with('dynamic')->once()->andReturnSelf()
->shouldReceive('select')->with('SELECT 1 FROM dual')->once()->andReturnNull();
$this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$response = $this->service->create($data);
$this->assertNotNull($response);
$this->assertTrue(is_object($response), 'Assert that response is an object.');
$this->assertEquals('enc-password', $response->password);
$this->assertEquals('HostName', $response->name);
$this->assertEquals('127.0.0.1', $response->host);
$this->assertEquals(3306, $response->port);
$this->assertEquals('someusername', $response->username);
$this->assertNull($response->node_id);
}
/**
* Test that passing a password will store an encrypted version in the DB.
*/
public function testHostIsUpdatedWithPasswordProvided()
{
$finalData = (object) ['password' => 'enc-pass', 'host' => '123.456.78.9'];
$this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->encrypter->shouldReceive('encrypt')->with('raw-pass')->once()->andReturn('enc-pass');
$this->repository->shouldReceive('update')->with(1, [
'password' => 'enc-pass',
'host' => '123.456.78.9',
])->once()->andReturn($finalData);
$this->dynamic->shouldReceive('set')->with('dynamic', $finalData)->once()->andReturnNull();
$this->database->shouldReceive('connection')->with('dynamic')->once()->andReturnSelf()
->shouldReceive('select')->with('SELECT 1 FROM dual')->once()->andReturnNull();
$this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$response = $this->service->update(1, ['password' => 'raw-pass', 'host' => '123.456.78.9']);
$this->assertNotNull($response);
$this->assertEquals('enc-pass', $response->password);
$this->assertEquals('123.456.78.9', $response->host);
}
/**
* Test that passing no or empty password will skip storing it.
*/
public function testHostIsUpdatedWithoutPassword()
{
$finalData = (object) ['host' => '123.456.78.9'];
$this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->encrypter->shouldNotReceive('encrypt');
$this->repository->shouldReceive('update')->with(1, ['host' => '123.456.78.9'])->once()->andReturn($finalData);
$this->dynamic->shouldReceive('set')->with('dynamic', $finalData)->once()->andReturnNull();
$this->database->shouldReceive('connection')->with('dynamic')->once()->andReturnSelf()
->shouldReceive('select')->with('SELECT 1 FROM dual')->once()->andReturnNull();
$this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$response = $this->service->update(1, ['password' => '', 'host' => '123.456.78.9']);
$this->assertNotNull($response);
$this->assertEquals('123.456.78.9', $response->host);
}
/**
* Test that a database host can be deleted.
*/
public function testHostIsDeleted()
{
$this->databaseRepository->shouldReceive('findCountWhere')->with([['database_host_id', '=', 1]])->once()->andReturn(0);
$this->repository->shouldReceive('delete')->with(1)->once()->andReturn(true);
$response = $this->service->delete(1);
$this->assertTrue($response, 'Assert that response is true.');
}
/**
* Test exception is thrown when there are databases attached to a host.
*/
public function testExceptionIsThrownIfHostHasDatabases()
{
$this->databaseRepository->shouldReceive('findCountWhere')->with([['database_host_id', '=', 1]])->once()->andReturn(2);
try {
$this->service->delete(1);
} catch (DisplayException $exception) {
$this->assertEquals(trans('exceptions.databases.delete_has_databases'), $exception->getMessage());
}
}
}

View file

@ -1,344 +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 Tests\Unit\Services\Database;
use Exception;
use Mockery as m;
use Tests\TestCase;
use phpmock\phpunit\PHPMock;
use Illuminate\Database\DatabaseManager;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Extensions\DynamicDatabaseConnection;
use Pterodactyl\Services\Database\DatabaseManagementService;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
class DatabaseManagementServiceTest extends TestCase
{
use PHPMock;
const TEST_DATA = [
'server_id' => 1,
'database' => 'd1_dbname',
'remote' => '%',
'username' => 'u1_str_random',
'password' => 'enc_password',
'database_host_id' => 3,
];
/**
* @var \Illuminate\Database\DatabaseManager
*/
protected $database;
/**
* @var \Pterodactyl\Extensions\DynamicDatabaseConnection
*/
protected $dynamic;
/**
* @var \Illuminate\Contracts\Encryption\Encrypter
*/
protected $encrypter;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface
*/
protected $repository;
/**
* @var \Pterodactyl\Services\Database\DatabaseManagementService
*/
protected $service;
/**
* Setup tests.
*/
public function setUp()
{
parent::setUp();
$this->database = m::mock(DatabaseManager::class);
$this->dynamic = m::mock(DynamicDatabaseConnection::class);
$this->encrypter = m::mock(Encrypter::class);
$this->repository = m::mock(DatabaseRepositoryInterface::class);
$this->getFunctionMock('\\Pterodactyl\\Services\\Database', 'str_random')
->expects($this->any())->willReturn('str_random');
$this->service = new DatabaseManagementService(
$this->database,
$this->dynamic,
$this->repository,
$this->encrypter
);
}
/**
* Test that a new database can be created that is linked to a specific host.
*/
public function testCreateANewDatabaseThatIsLinkedToAHost()
{
$this->encrypter->shouldReceive('encrypt')->with('str_random')->once()->andReturn('enc_password');
$this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('createIfNotExists')
->with(self::TEST_DATA)
->once()
->andReturn((object) self::TEST_DATA);
$this->dynamic->shouldReceive('set')
->with('dynamic', self::TEST_DATA['database_host_id'])
->once()
->andReturnNull();
$this->repository->shouldReceive('createDatabase')->with(
self::TEST_DATA['database'],
'dynamic'
)->once()->andReturnNull();
$this->encrypter->shouldReceive('decrypt')->with('enc_password')->once()->andReturn('str_random');
$this->repository->shouldReceive('createUser')->with(
self::TEST_DATA['username'],
self::TEST_DATA['remote'],
'str_random',
'dynamic'
)->once()->andReturnNull();
$this->repository->shouldReceive('assignUserToDatabase')->with(
self::TEST_DATA['database'],
self::TEST_DATA['username'],
self::TEST_DATA['remote'],
'dynamic'
)->once()->andReturnNull();
$this->repository->shouldReceive('flush')->with('dynamic')->once()->andReturnNull();
$this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$response = $this->service->create(1, [
'database' => 'dbname',
'remote' => '%',
'database_host_id' => 3,
]);
$this->assertNotEmpty($response);
$this->assertTrue(is_object($response), 'Assert that response is an object.');
$this->assertEquals(self::TEST_DATA['database'], $response->database);
$this->assertEquals(self::TEST_DATA['remote'], $response->remote);
$this->assertEquals(self::TEST_DATA['username'], $response->username);
$this->assertEquals(self::TEST_DATA['password'], $response->password);
$this->assertEquals(self::TEST_DATA['database_host_id'], $response->database_host_id);
}
/**
* Test that an exception before the database is created and returned does not attempt any actions.
*
* @expectedException \Exception
*/
public function testExceptionBeforeDatabaseIsCreatedShouldNotAttemptAnyRollBackOperations()
{
$this->encrypter->shouldReceive('encrypt')->with('str_random')->once()->andReturn('enc_password');
$this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('createIfNotExists')
->with(self::TEST_DATA)
->once()
->andThrow(new Exception('Test Message'));
$this->repository->shouldNotReceive('dropDatabase');
$this->database->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull();
$this->service->create(1, [
'database' => 'dbname',
'remote' => '%',
'database_host_id' => 3,
]);
}
/**
* Test that an exception after database creation attempts to clean up previous operations.
*
* @expectedException \Exception
*/
public function testExceptionAfterDatabaseCreationShouldAttemptRollBackOperations()
{
$this->encrypter->shouldReceive('encrypt')->with('str_random')->once()->andReturn('enc_password');
$this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('createIfNotExists')
->with(self::TEST_DATA)
->once()
->andReturn((object) self::TEST_DATA);
$this->dynamic->shouldReceive('set')
->with('dynamic', self::TEST_DATA['database_host_id'])
->once()
->andReturnNull();
$this->repository->shouldReceive('createDatabase')->with(
self::TEST_DATA['database'],
'dynamic'
)->once()->andThrow(new Exception('Test Message'));
$this->repository->shouldReceive('dropDatabase')
->with(self::TEST_DATA['database'], 'dynamic')
->once()
->andReturnNull();
$this->repository->shouldReceive('dropUser')->with(
self::TEST_DATA['username'],
self::TEST_DATA['remote'],
'dynamic'
)->once()->andReturnNull();
$this->repository->shouldReceive('flush')->with('dynamic')->once()->andReturnNull();
$this->database->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull();
$this->service->create(1, [
'database' => 'dbname',
'remote' => '%',
'database_host_id' => 3,
]);
}
/**
* Test that an exception thrown during a rollback operation is silently handled and not returned.
*/
public function testExceptionThrownDuringRollBackProcessShouldNotBeThrownToCallingFunction()
{
$this->encrypter->shouldReceive('encrypt')->with('str_random')->once()->andReturn('enc_password');
$this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('createIfNotExists')
->with(self::TEST_DATA)
->once()
->andReturn((object) self::TEST_DATA);
$this->dynamic->shouldReceive('set')
->with('dynamic', self::TEST_DATA['database_host_id'])
->once()
->andReturnNull();
$this->repository->shouldReceive('createDatabase')->with(
self::TEST_DATA['database'],
'dynamic'
)->once()->andThrow(new Exception('Test One'));
$this->repository->shouldReceive('dropDatabase')->with(self::TEST_DATA['database'], 'dynamic')
->once()->andThrow(new Exception('Test Two'));
$this->database->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull();
try {
$this->service->create(1, [
'database' => 'dbname',
'remote' => '%',
'database_host_id' => 3,
]);
} catch (Exception $ex) {
$this->assertInstanceOf(Exception::class, $ex);
$this->assertEquals('Test One', $ex->getMessage());
}
}
/**
* Test that a password can be changed for a given database.
*/
public function testDatabasePasswordShouldBeChanged()
{
$this->repository->shouldReceive('find')->with(1)->once()->andReturn((object) self::TEST_DATA);
$this->dynamic->shouldReceive('set')
->with('dynamic', self::TEST_DATA['database_host_id'])
->once()
->andReturnNull();
$this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->encrypter->shouldReceive('encrypt')->with('new_password')->once()->andReturn('new_enc_password');
$this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('update')->with(1, [
'password' => 'new_enc_password',
])->andReturn(true);
$this->repository->shouldReceive('dropUser')->with(
self::TEST_DATA['username'],
self::TEST_DATA['remote'],
'dynamic'
)->once()->andReturnNull();
$this->repository->shouldReceive('createUser')->with(
self::TEST_DATA['username'],
self::TEST_DATA['remote'],
'new_password',
'dynamic'
)->once()->andReturnNull();
$this->repository->shouldReceive('assignUserToDatabase')->with(
self::TEST_DATA['database'],
self::TEST_DATA['username'],
self::TEST_DATA['remote'],
'dynamic'
)->once()->andReturnNull();
$this->repository->shouldReceive('flush')->with('dynamic')->once()->andReturnNull();
$this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$response = $this->service->changePassword(1, 'new_password');
$this->assertTrue($response);
}
/**
* Test that an exception thrown while changing a password will attempt a rollback.
*
* @expectedException \Exception
*/
public function testExceptionThrownWhileChangingDatabasePasswordShouldRollBack()
{
$this->repository->shouldReceive('find')->with(1)->once()->andReturn((object) self::TEST_DATA);
$this->dynamic->shouldReceive('set')
->with('dynamic', self::TEST_DATA['database_host_id'])
->once()
->andReturnNull();
$this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->encrypter->shouldReceive('encrypt')->with('new_password')->once()->andReturn('new_enc_password');
$this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('update')->with(1, [
'password' => 'new_enc_password',
])->andReturn(true);
$this->repository->shouldReceive('dropUser')->with(
self::TEST_DATA['username'],
self::TEST_DATA['remote'],
'dynamic'
)->once()->andThrow(new Exception());
$this->database->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull();
$this->service->changePassword(1, 'new_password');
}
/**
* Test that a database can be deleted.
*/
public function testDatabaseShouldBeDeleted()
{
$this->repository->shouldReceive('find')->with(1)->once()->andReturn((object) self::TEST_DATA);
$this->dynamic->shouldReceive('set')
->with('dynamic', self::TEST_DATA['database_host_id'])
->once()
->andReturnNull();
$this->repository->shouldReceive('dropDatabase')
->with(self::TEST_DATA['database'], 'dynamic')
->once()
->andReturnNull();
$this->repository->shouldReceive('dropUser')->with(
self::TEST_DATA['username'],
self::TEST_DATA['remote'],
'dynamic'
)->once()->andReturnNull();
$this->repository->shouldReceive('flush')->with('dynamic')->once()->andReturnNull();
$this->repository->shouldReceive('delete')->with(1)->once()->andReturn(1);
$response = $this->service->delete(1);
$this->assertEquals(1, $response);
}
}

View file

@ -0,0 +1,99 @@
<?php
namespace Tests\Unit\Services\Databases;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\Database;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Extensions\DynamicDatabaseConnection;
use Pterodactyl\Services\Databases\DatabasePasswordService;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
class DatabasePasswordServiceTest extends TestCase
{
/**
* @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock
*/
private $connection;
/**
* @var \Pterodactyl\Extensions\DynamicDatabaseConnection|\Mockery\Mock
*/
private $dynamic;
/**
* @var \Illuminate\Contracts\Encryption\Encrypter|\Mockery\Mock
*/
private $encrypter;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface|\Mockery\Mock
*/
private $repository;
/**
* Setup tests.
*/
public function setUp()
{
parent::setUp();
$this->connection = m::mock(ConnectionInterface::class);
$this->dynamic = m::mock(DynamicDatabaseConnection::class);
$this->encrypter = m::mock(Encrypter::class);
$this->repository = m::mock(DatabaseRepositoryInterface::class);
}
/**
* Test that a password can be updated.
*
* @dataProvider useModelDataProvider
*/
public function testPasswordIsChanged(bool $useModel)
{
$model = factory(Database::class)->make();
if (! $useModel) {
$this->repository->shouldReceive('find')->with(1234)->once()->andReturn($model);
}
$this->dynamic->shouldReceive('set')->with('dynamic', $model->database_host_id)->once()->andReturnNull();
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->encrypter->shouldReceive('encrypt')->with('test123')->once()->andReturn('enc123');
$this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf();
$this->repository->shouldReceive('update')->with($model->id, ['password' => 'enc123'])->once()->andReturn(true);
$this->repository->shouldReceive('dropUser')->with($model->username, $model->remote)->once()->andReturnNull();
$this->repository->shouldReceive('createUser')->with($model->username, $model->remote, 'test123')->once()->andReturnNull();
$this->repository->shouldReceive('assignUserToDatabase')->with($model->database, $model->username, $model->remote)->once()->andReturnNull();
$this->repository->shouldReceive('flush')->withNoArgs()->once()->andReturnNull();
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$response = $this->getService()->handle($useModel ? $model : 1234, 'test123');
$this->assertNotEmpty($response);
$this->assertTrue($response);
}
/**
* Data provider to determine if a model should be passed or an int.
*
* @return array
*/
public function useModelDataProvider(): array
{
return [[false], [true]];
}
/**
* Return an instance of the service with mocked dependencies.
*
* @return \Pterodactyl\Services\Databases\DatabasePasswordService
*/
private function getService(): DatabasePasswordService
{
return new DatabasePasswordService($this->connection, $this->repository, $this->dynamic, $this->encrypter);
}
}

View file

@ -0,0 +1,101 @@
<?php
namespace Tests\Unit\Services\Databases\Hosts;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\DatabaseHost;
use Illuminate\Database\DatabaseManager;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Extensions\DynamicDatabaseConnection;
use Pterodactyl\Services\Databases\Hosts\HostCreationService;
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
class HostCreationServiceTest extends TestCase
{
/**
* @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock
*/
private $connection;
/**
* @var \Illuminate\Database\DatabaseManager|\Mockery\Mock
*/
private $databaseManager;
/**
* @var \Pterodactyl\Extensions\DynamicDatabaseConnection|\Mockery\Mock
*/
private $dynamic;
/**
* @var \Illuminate\Contracts\Encryption\Encrypter|\Mockery\Mock
*/
private $encrypter;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface|\Mockery\Mock
*/
private $repository;
/**
* Setup tests.
*/
public function setUp()
{
parent::setUp();
$this->connection = m::mock(ConnectionInterface::class);
$this->databaseManager = m::mock(DatabaseManager::class);
$this->dynamic = m::mock(DynamicDatabaseConnection::class);
$this->encrypter = m::mock(Encrypter::class);
$this->repository = m::mock(DatabaseHostRepositoryInterface::class);
}
/**
* Test that a database host can be created.
*/
public function testDatabaseHostIsCreated()
{
$model = factory(DatabaseHost::class)->make();
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->encrypter->shouldReceive('encrypt')->with('test123')->once()->andReturn('enc123');
$this->repository->shouldReceive('create')->with(m::subset([
'password' => 'enc123',
'username' => $model->username,
'node_id' => $model->node_id,
]))->once()->andReturn($model);
$this->dynamic->shouldReceive('set')->with('dynamic', $model)->once()->andReturnNull();
$this->databaseManager->shouldReceive('connection')->with('dynamic')->once()->andReturnSelf();
$this->databaseManager->shouldReceive('select')->with('SELECT 1 FROM dual')->once()->andReturnNull();
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$response = $this->getService()->handle([
'password' => 'test123',
'username' => $model->username,
'node_id' => $model->node_id,
]);
$this->assertNotEmpty($response);
$this->assertSame($model, $response);
}
/**
* Return an instance of the service with mocked dependencies.
*
* @return \Pterodactyl\Services\Databases\Hosts\HostCreationService
*/
private function getService(): HostCreationService
{
return new HostCreationService(
$this->connection,
$this->databaseManager,
$this->repository,
$this->dynamic,
$this->encrypter
);
}
}

View file

@ -0,0 +1,85 @@
<?php
namespace Tests\Unit\Services\Databases\Hosts;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Exceptions\PterodactylException;
use Pterodactyl\Exceptions\Service\HasActiveServersException;
use Pterodactyl\Services\Databases\Hosts\HostDeletionService;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
class HostDeletionServiceTest extends TestCase
{
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface|\Mockery\Mock
*/
private $databaseRepository;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface|\Mockery\Mock
*/
private $repository;
/**
* Setup tests.
*/
public function setUp()
{
parent::setUp();
$this->databaseRepository = m::mock(DatabaseRepositoryInterface::class);
$this->repository = m::mock(DatabaseHostRepositoryInterface::class);
}
/**
* Test that a host can be deleted.
*/
public function testHostIsDeleted()
{
$this->databaseRepository->shouldReceive('findCountWhere')->with([['database_host_id', '=', 1234]])->once()->andReturn(0);
$this->repository->shouldReceive('delete')->with(1234)->once()->andReturn(1);
$response = $this->getService()->handle(1234);
$this->assertNotEmpty($response);
$this->assertSame(1, $response);
}
/**
* Test that an exception is thrown if a host with databases is deleted.
*
* @dataProvider databaseCountDataProvider
*/
public function testExceptionIsThrownIfDeletingHostWithDatabases(int $count)
{
$this->databaseRepository->shouldReceive('findCountWhere')->with([['database_host_id', '=', 1234]])->once()->andReturn($count);
try {
$this->getService()->handle(1234);
} catch (PterodactylException $exception) {
$this->assertInstanceOf(HasActiveServersException::class, $exception);
$this->assertEquals(trans('exceptions.databases.delete_has_databases'), $exception->getMessage());
}
}
/**
* Data provider to ensure exceptions are thrown for any value > 0.
*
* @return array
*/
public function databaseCountDataProvider(): array
{
return [[1], [2], [10]];
}
/**
* Return an instance of the service with mocked dependencies.
*
* @return \Pterodactyl\Services\Databases\Hosts\HostDeletionService
*/
private function getService(): HostDeletionService
{
return new HostDeletionService($this->databaseRepository, $this->repository);
}
}

View file

@ -0,0 +1,112 @@
<?php
namespace Tests\Unit\Services\Databases\Hosts;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\DatabaseHost;
use Illuminate\Database\DatabaseManager;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Extensions\DynamicDatabaseConnection;
use Pterodactyl\Services\Databases\Hosts\HostUpdateService;
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
class HostUpdateServiceTest extends TestCase
{
/**
* @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock
*/
private $connection;
/**
* @var \Illuminate\Database\DatabaseManager|\Mockery\Mock
*/
private $databaseManager;
/**
* @var \Pterodactyl\Extensions\DynamicDatabaseConnection|\Mockery\Mock
*/
private $dynamic;
/**
* @var \Illuminate\Contracts\Encryption\Encrypter|\Mockery\Mock
*/
private $encrypter;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface|\Mockery\Mock
*/
private $repository;
/**
* Setup tests.
*/
public function setUp()
{
parent::setUp();
$this->connection = m::mock(ConnectionInterface::class);
$this->databaseManager = m::mock(DatabaseManager::class);
$this->dynamic = m::mock(DynamicDatabaseConnection::class);
$this->encrypter = m::mock(Encrypter::class);
$this->repository = m::mock(DatabaseHostRepositoryInterface::class);
}
/**
* Test that a password is encrypted before storage if provided.
*/
public function testPasswordIsEncryptedWhenProvided()
{
$model = factory(DatabaseHost::class)->make();
$this->encrypter->shouldReceive('encrypt')->with('test123')->once()->andReturn('enc123');
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('update')->with(1234, ['password' => 'enc123'])->once()->andReturn($model);
$this->dynamic->shouldReceive('set')->with('dynamic', $model)->once()->andReturnNull();
$this->databaseManager->shouldReceive('connection')->with('dynamic')->once()->andReturnSelf();
$this->databaseManager->shouldReceive('select')->with('SELECT 1 FROM dual')->once()->andReturnNull();
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$response = $this->getService()->handle(1234, ['password' => 'test123']);
$this->assertNotEmpty($response);
$this->assertSame($model, $response);
}
/**
* Test that updates still occur when no password is provided.
*/
public function testUpdateOccursWhenNoPasswordIsProvided()
{
$model = factory(DatabaseHost::class)->make();
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('update')->with(1234, ['username' => 'test'])->once()->andReturn($model);
$this->dynamic->shouldReceive('set')->with('dynamic', $model)->once()->andReturnNull();
$this->databaseManager->shouldReceive('connection')->with('dynamic')->once()->andReturnSelf();
$this->databaseManager->shouldReceive('select')->with('SELECT 1 FROM dual')->once()->andReturnNull();
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$response = $this->getService()->handle(1234, ['password' => '', 'username' => 'test']);
$this->assertNotEmpty($response);
$this->assertSame($model, $response);
}
/**
* Return an instance of the service with mocked dependencies.
*
* @return \Pterodactyl\Services\Databases\Hosts\HostUpdateService
*/
private function getService(): HostUpdateService
{
return new HostUpdateService(
$this->connection,
$this->databaseManager,
$this->repository,
$this->dynamic,
$this->encrypter
);
}
}

View file

@ -1,11 +1,4 @@
<?php <?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 Tests\Unit\Services\Servers; namespace Tests\Unit\Services\Servers;
@ -13,25 +6,23 @@ use Mockery as m;
use Tests\TestCase; use Tests\TestCase;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Pterodactyl\Models\Location; use Pterodactyl\Models\Location;
use Illuminate\Contracts\Config\Repository;
use Pterodactyl\Services\Servers\EnvironmentService; use Pterodactyl\Services\Servers\EnvironmentService;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class EnvironmentServiceTest extends TestCase class EnvironmentServiceTest extends TestCase
{ {
/** const CONFIG_MAPPING = 'pterodactyl.environment_mappings';
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
protected $repository;
/** /**
* @var \Pterodactyl\Services\Servers\EnvironmentService * @var \Illuminate\Contracts\Config\Repository|\Mockery\Mock
*/ */
protected $service; private $config;
/** /**
* @var \Pterodactyl\Models\Server * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock
*/ */
protected $server; private $repository;
/** /**
* Setup tests. * Setup tests.
@ -40,24 +31,23 @@ class EnvironmentServiceTest extends TestCase
{ {
parent::setUp(); parent::setUp();
$this->config = m::mock(Repository::class);
$this->repository = m::mock(ServerRepositoryInterface::class); $this->repository = m::mock(ServerRepositoryInterface::class);
$this->server = factory(Server::class)->make([
'location' => factory(Location::class)->make(),
]);
$this->service = new EnvironmentService($this->repository);
} }
/** /**
* Test that set environment key function returns an instance of the class. * Test that set environment key stores the key into a retreviable array.
*/ */
public function testSettingEnvironmentKeyShouldReturnInstanceOfSelf() public function testSettingEnvironmentKeyPersistsItInArray()
{ {
$instance = $this->service->setEnvironmentKey('TEST_KEY', function () { $service = $this->getService();
$service->setEnvironmentKey('TEST_KEY', function () {
return true; return true;
}); });
$this->assertInstanceOf(EnvironmentService::class, $instance); $this->assertNotEmpty($service->getEnvironmentKeys());
$this->assertArrayHasKey('TEST_KEY', $service->getEnvironmentKeys());
} }
/** /**
@ -65,22 +55,17 @@ class EnvironmentServiceTest extends TestCase
*/ */
public function testProcessShouldReturnDefaultEnvironmentVariablesForAServer() public function testProcessShouldReturnDefaultEnvironmentVariablesForAServer()
{ {
$this->repository->shouldReceive('getVariablesWithValues')->with($this->server->id)->once()->andReturn([ $model = $this->getServerModel();
$this->config->shouldReceive('get')->with(self::CONFIG_MAPPING, [])->once()->andReturn([]);
$this->repository->shouldReceive('getVariablesWithValues')->with($model->id)->once()->andReturn([
'TEST_VARIABLE' => 'Test Variable', 'TEST_VARIABLE' => 'Test Variable',
]); ]);
$response = $this->service->process($this->server); $response = $this->getService()->handle($model);
$this->assertNotEmpty($response);
$this->assertEquals(count(EnvironmentService::ENVIRONMENT_CASTS) + 1, count($response), 'Assert response contains correct amount of items.'); $this->assertEquals(4, count($response));
$this->assertTrue(is_array($response), 'Assert that response is an array.');
$this->assertArrayHasKey('TEST_VARIABLE', $response); $this->assertArrayHasKey('TEST_VARIABLE', $response);
$this->assertEquals('Test Variable', $response['TEST_VARIABLE']); $this->assertSame('Test Variable', $response['TEST_VARIABLE']);
foreach (EnvironmentService::ENVIRONMENT_CASTS as $key => $value) {
$this->assertArrayHasKey($key, $response);
$this->assertEquals(object_get($this->server, $value), $response[$key]);
}
} }
/** /**
@ -88,43 +73,106 @@ class EnvironmentServiceTest extends TestCase
*/ */
public function testProcessShouldReturnKeySetAtRuntime() public function testProcessShouldReturnKeySetAtRuntime()
{ {
$this->repository->shouldReceive('getVariablesWithValues')->with($this->server->id)->once()->andReturn([]); $model = $this->getServerModel();
$this->config->shouldReceive('get')->with(self::CONFIG_MAPPING, [])->once()->andReturn([]);
$this->repository->shouldReceive('getVariablesWithValues')->with($model->id)->once()->andReturn([]);
$response = $this->service->setEnvironmentKey('TEST_VARIABLE', function ($server) { $service = $this->getService();
$service->setEnvironmentKey('TEST_VARIABLE', function ($server) {
return $server->uuidShort; return $server->uuidShort;
})->process($this->server); });
$this->assertTrue(is_array($response), 'Assert response is an array.'); $response = $service->handle($model);
$this->assertNotEmpty($response);
$this->assertArrayHasKey('TEST_VARIABLE', $response); $this->assertArrayHasKey('TEST_VARIABLE', $response);
$this->assertEquals($this->server->uuidShort, $response['TEST_VARIABLE']); $this->assertSame($model->uuidShort, $response['TEST_VARIABLE']);
} }
/** /**
* Test that duplicate variables provided at run-time override the defaults. * Test that duplicate variables provided in config override the defaults.
*/
public function testProcessShouldAllowOverwritingVaraiblesWithConfigurationFile()
{
$model = $this->getServerModel();
$this->repository->shouldReceive('getVariablesWithValues')->with($model->id)->once()->andReturn([]);
$this->config->shouldReceive('get')->with(self::CONFIG_MAPPING, [])->once()->andReturn([
'P_SERVER_UUID' => 'name',
]);
$response = $this->getService()->handle($model);
$this->assertNotEmpty($response);
$this->assertSame(3, count($response));
$this->assertArrayHasKey('P_SERVER_UUID', $response);
$this->assertSame($model->name, $response['P_SERVER_UUID']);
}
/**
* Test that config based environment variables can be done using closures.
*/
public function testVariablesSetInConfigurationAllowForClosures()
{
$model = $this->getServerModel();
$this->config->shouldReceive('get')->with(self::CONFIG_MAPPING, [])->once()->andReturn([
'P_SERVER_UUID' => function ($server) {
return $server->id * 2;
},
]);
$this->repository->shouldReceive('getVariablesWithValues')->with($model->id)->once()->andReturn([]);
$response = $this->getService()->handle($model);
$this->assertNotEmpty($response);
$this->assertSame(3, count($response));
$this->assertArrayHasKey('P_SERVER_UUID', $response);
$this->assertSame($model->id * 2, $response['P_SERVER_UUID']);
}
/**
* Test that duplicate variables provided at run-time override the defaults and those
* that are defined in the configuration file.
*/ */
public function testProcessShouldAllowOverwritingDefaultVariablesWithRuntimeProvided() public function testProcessShouldAllowOverwritingDefaultVariablesWithRuntimeProvided()
{ {
$this->repository->shouldReceive('getVariablesWithValues')->with($this->server->id)->once()->andReturn([]); $model = $this->getServerModel();
$this->config->shouldReceive('get')->with(self::CONFIG_MAPPING, [])->once()->andReturn([
'P_SERVER_UUID' => 'overwritten-config',
]);
$this->repository->shouldReceive('getVariablesWithValues')->with($model->id)->once()->andReturn([]);
$response = $this->service->setEnvironmentKey('P_SERVER_UUID', function ($server) { $service = $this->getService();
$service->setEnvironmentKey('P_SERVER_UUID', function ($model) {
return 'overwritten'; return 'overwritten';
})->process($this->server); });
$this->assertTrue(is_array($response), 'Assert response is an array.'); $response = $service->handle($model);
$this->assertNotEmpty($response);
$this->assertSame(3, count($response));
$this->assertArrayHasKey('P_SERVER_UUID', $response); $this->assertArrayHasKey('P_SERVER_UUID', $response);
$this->assertEquals('overwritten', $response['P_SERVER_UUID']); $this->assertSame('overwritten', $response['P_SERVER_UUID']);
} }
/** /**
* Test that function can run when an ID is provided rather than a server model. * Return an instance of the service with mocked dependencies.
*
* @return \Pterodactyl\Services\Servers\EnvironmentService
*/ */
public function testProcessShouldAcceptAnIntegerInPlaceOfAServerModel() private function getService(): EnvironmentService
{ {
$this->repository->shouldReceive('find')->with($this->server->id)->once()->andReturn($this->server); return new EnvironmentService($this->config, $this->repository);
$this->repository->shouldReceive('getVariablesWithValues')->with($this->server->id)->once()->andReturn([]); }
$response = $this->service->process($this->server->id); /**
* Return a server model with a location relationship to be used in the tests.
$this->assertTrue(is_array($response), 'Assert that response is an array.'); *
* @return \Pterodactyl\Models\Server
*/
private function getServerModel(): Server
{
return factory(Server::class)->make([
'location' => factory(Location::class)->make(),
]);
} }
} }

View file

@ -0,0 +1,100 @@
<?php
namespace Tests\Unit\Services\Servers;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\Egg;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Allocation;
use Pterodactyl\Services\Servers\EnvironmentService;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Services\Servers\ServerConfigurationStructureService;
class ServerConfigurationStructureServiceTest extends TestCase
{
/**
* @var \Pterodactyl\Services\Servers\EnvironmentService|\Mockery\Mock
*/
private $environment;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock
*/
private $repository;
/**
* Setup tests.
*/
public function setUp()
{
parent::setUp();
$this->environment = m::mock(EnvironmentService::class);
$this->repository = m::mock(ServerRepositoryInterface::class);
}
/**
* Test that a configuration is returned in the proper format when passed a
* server model that is missing required relationships.
*/
public function testCorrectStructureIsReturned()
{
$model = factory(Server::class)->make();
$model->allocation = factory(Allocation::class)->make();
$model->allocations = collect(factory(Allocation::class)->times(2)->make());
$model->egg = factory(Egg::class)->make();
$portListing = $model->allocations->groupBy('ip')->map(function ($item) {
return $item->pluck('port');
})->toArray();
$this->repository->shouldReceive('getDataForCreation')->with($model)->once()->andReturn($model);
$this->environment->shouldReceive('handle')->with($model)->once()->andReturn(['environment_array']);
$response = $this->getService()->handle($model);
$this->assertNotEmpty($response);
$this->assertArrayNotHasKey('user', $response);
$this->assertArrayNotHasKey('keys', $response);
$this->assertArrayHasKey('uuid', $response);
$this->assertArrayHasKey('build', $response);
$this->assertArrayHasKey('service', $response);
$this->assertArrayHasKey('rebuild', $response);
$this->assertArrayHasKey('suspended', $response);
$this->assertArraySubset([
'default' => [
'ip' => $model->allocation->ip,
'port' => $model->allocation->port,
],
], $response['build'], true, 'Assert server default allocation is correct.');
$this->assertArraySubset(['ports' => $portListing], $response['build'], true, 'Assert server ports are correct.');
$this->assertArraySubset([
'env' => ['environment_array'],
'swap' => (int) $model->swap,
'io' => (int) $model->io,
'cpu' => (int) $model->cpu,
'disk' => (int) $model->disk,
'image' => $model->image,
], $response['build'], true, 'Assert server build data is correct.');
$this->assertArraySubset([
'egg' => $model->egg->uuid,
'pack' => null,
'skip_scripts' => $model->skip_scripts,
], $response['service']);
$this->assertFalse($response['rebuild']);
$this->assertSame((int) $model->suspended, $response['suspended']);
}
/**
* Return an instance of the service with mocked dependencies.
*
* @return \Pterodactyl\Services\Servers\ServerConfigurationStructureService
*/
private function getService(): ServerConfigurationStructureService
{
return new ServerConfigurationStructureService($this->repository, $this->environment);
}
}

View file

@ -1,24 +1,18 @@
<?php <?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 Tests\Unit\Services\Servers; namespace Tests\Unit\Services\Servers;
use Mockery as m; use Mockery as m;
use Tests\TestCase; use Tests\TestCase;
use phpmock\phpunit\PHPMock; use Pterodactyl\Models\User;
use Tests\Traits\MocksUuids; use Tests\Traits\MocksUuids;
use Pterodactyl\Models\Server;
use Tests\Traits\MocksRequestException;
use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Exception\RequestException;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Exceptions\PterodactylException; use Pterodactyl\Exceptions\PterodactylException;
use Pterodactyl\Services\Servers\ServerCreationService; use Pterodactyl\Services\Servers\ServerCreationService;
use Pterodactyl\Services\Servers\VariableValidatorService; use Pterodactyl\Services\Servers\VariableValidatorService;
use Pterodactyl\Services\Servers\UsernameGenerationService;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
@ -33,91 +27,57 @@ use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonS
*/ */
class ServerCreationServiceTest extends TestCase class ServerCreationServiceTest extends TestCase
{ {
use MocksUuids, PHPMock; use MocksRequestException, MocksUuids;
/** /**
* @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface|\Mockery\Mock * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface|\Mockery\Mock
*/ */
protected $allocationRepository; private $allocationRepository;
/** /**
* @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService|\Mockery\Mock * @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService|\Mockery\Mock
*/ */
protected $configurationStructureService; private $configurationStructureService;
/** /**
* @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock * @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock
*/ */
protected $connection; private $connection;
/** /**
* @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface|\Mockery\Mock * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface|\Mockery\Mock
*/ */
protected $daemonServerRepository; private $daemonServerRepository;
/**
* @var array
*/
protected $data = [
'node_id' => 1,
'name' => 'SomeName',
'description' => null,
'owner_id' => 1,
'memory' => 128,
'disk' => 128,
'swap' => 0,
'io' => 500,
'cpu' => 0,
'allocation_id' => 1,
'allocation_additional' => [2, 3],
'environment' => [
'TEST_VAR_1' => 'var1-value',
],
'nest_id' => 1,
'egg_id' => 1,
'startup' => 'startup-param',
'docker_image' => 'some/image',
];
/** /**
* @var \GuzzleHttp\Exception\RequestException|\Mockery\Mock * @var \GuzzleHttp\Exception\RequestException|\Mockery\Mock
*/ */
protected $exception; private $exception;
/** /**
* @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface|\Mockery\Mock * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface|\Mockery\Mock
*/ */
protected $nodeRepository; private $nodeRepository;
/** /**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock
*/ */
protected $repository; private $repository;
/** /**
* @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface|\Mockery\Mock * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface|\Mockery\Mock
*/ */
protected $serverVariableRepository; private $serverVariableRepository;
/**
* @var \Pterodactyl\Services\Servers\ServerCreationService
*/
protected $service;
/** /**
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface|\Mockery\Mock * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface|\Mockery\Mock
*/ */
protected $userRepository; private $userRepository;
/**
* @var \Pterodactyl\Services\Servers\UsernameGenerationService|\Mockery\Mock
*/
protected $usernameService;
/** /**
* @var \Pterodactyl\Services\Servers\VariableValidatorService|\Mockery\Mock * @var \Pterodactyl\Services\Servers\VariableValidatorService|\Mockery\Mock
*/ */
protected $validatorService; private $validatorService;
/** /**
* Setup tests. * Setup tests.
@ -135,13 +95,88 @@ class ServerCreationServiceTest extends TestCase
$this->repository = m::mock(ServerRepositoryInterface::class); $this->repository = m::mock(ServerRepositoryInterface::class);
$this->serverVariableRepository = m::mock(ServerVariableRepositoryInterface::class); $this->serverVariableRepository = m::mock(ServerVariableRepositoryInterface::class);
$this->userRepository = m::mock(UserRepositoryInterface::class); $this->userRepository = m::mock(UserRepositoryInterface::class);
$this->usernameService = m::mock(UsernameGenerationService::class);
$this->validatorService = m::mock(VariableValidatorService::class); $this->validatorService = m::mock(VariableValidatorService::class);
}
$this->getFunctionMock('\\Pterodactyl\\Services\\Servers', 'str_random') /**
->expects($this->any())->willReturn('random_string'); * Test core functionality of the creation process.
*/
public function testCreateShouldHitAllOfTheNecessaryServicesAndStoreTheServer()
{
$model = factory(Server::class)->make([
'uuid' => $this->getKnownUuid(),
]);
$this->service = new ServerCreationService( $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('create')->with(m::subset([
'uuid' => $this->getKnownUuid(),
'node_id' => $model->node_id,
'owner_id' => $model->owner_id,
'nest_id' => $model->nest_id,
'egg_id' => $model->egg_id,
]))->once()->andReturn($model);
$this->allocationRepository->shouldReceive('assignAllocationsToServer')->with($model->id, [$model->allocation_id])->once()->andReturnNull();
$this->validatorService->shouldReceive('setUserLevel')->with(User::USER_LEVEL_ADMIN)->once()->andReturnNull();
$this->validatorService->shouldReceive('handle')->with($model->egg_id, [])->once()->andReturn(
collect([(object) ['id' => 123, 'value' => 'var1-value']])
);
$this->serverVariableRepository->shouldReceive('insert')->with([
[
'server_id' => $model->id,
'variable_id' => 123,
'variable_value' => 'var1-value',
],
])->once()->andReturnNull();
$this->configurationStructureService->shouldReceive('handle')->with($model)->once()->andReturn(['test' => 'struct']);
$this->daemonServerRepository->shouldReceive('setNode')->with($model->node_id)->once()->andReturnSelf();
$this->daemonServerRepository->shouldReceive('create')->with(['test' => 'struct'], ['start_on_completion' => false])->once()->andReturnNull();
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$response = $this->getService()->create($model->toArray());
$this->assertSame($model, $response);
}
/**
* Test handling of node timeout or other daemon error.
*/
public function testExceptionShouldBeThrownIfTheRequestFails()
{
$this->configureExceptionMock();
$model = factory(Server::class)->make([
'uuid' => $this->getKnownUuid(),
]);
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('create')->once()->andReturn($model);
$this->allocationRepository->shouldReceive('assignAllocationsToServer')->once()->andReturnNull();
$this->validatorService->shouldReceive('setUserLevel')->once()->andReturnNull();
$this->validatorService->shouldReceive('handle')->once()->andReturn(collect([]));
$this->configurationStructureService->shouldReceive('handle')->once()->andReturn([]);
$this->daemonServerRepository->shouldReceive('setNode')->with($model->node_id)->once()->andThrow($this->exception);
$this->connection->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull();
try {
$this->getService()->create($model->toArray());
} catch (PterodactylException $exception) {
$this->assertInstanceOf(DaemonConnectionException::class, $exception);
$this->assertInstanceOf(RequestException::class, $exception->getPrevious());
}
}
/**
* Return an instance of the service with mocked dependencies.
*
* @return \Pterodactyl\Services\Servers\ServerCreationService
*/
private function getService(): ServerCreationService
{
return new ServerCreationService(
$this->allocationRepository, $this->allocationRepository,
$this->connection, $this->connection,
$this->daemonServerRepository, $this->daemonServerRepository,
@ -150,84 +185,7 @@ class ServerCreationServiceTest extends TestCase
$this->repository, $this->repository,
$this->serverVariableRepository, $this->serverVariableRepository,
$this->userRepository, $this->userRepository,
$this->usernameService,
$this->validatorService $this->validatorService
); );
} }
/**
* Test core functionality of the creation process.
*/
public function testCreateShouldHitAllOfTheNecessaryServicesAndStoreTheServer()
{
$this->validatorService->shouldReceive('isAdmin')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('setFields')->with($this->data['environment'])->once()->andReturnSelf()
->shouldReceive('validate')->with($this->data['egg_id'])->once()->andReturnSelf();
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->usernameService->shouldReceive('generate')->with($this->data['name'], 'random_string')
->once()->andReturn('user_name');
$this->repository->shouldReceive('create')->with(m::subset([
'uuid' => $this->getKnownUuid(),
'node_id' => $this->data['node_id'],
'owner_id' => 1,
'nest_id' => 1,
'egg_id' => 1,
]))->once()->andReturn((object) [
'node_id' => 1,
'id' => 1,
]);
$this->allocationRepository->shouldReceive('assignAllocationsToServer')->with(1, [1, 2, 3])->once()->andReturnNull();
$this->validatorService->shouldReceive('getResults')->withNoArgs()->once()->andReturn([[
'id' => 1,
'key' => 'TEST_VAR_1',
'value' => 'var1-value',
]]);
$this->serverVariableRepository->shouldReceive('insert')->with([[
'server_id' => 1,
'variable_id' => 1,
'variable_value' => 'var1-value',
]])->once()->andReturnNull();
$this->configurationStructureService->shouldReceive('handle')->with(1)->once()->andReturn(['test' => 'struct']);
$this->daemonServerRepository->shouldReceive('setNode')->with(1)->once()->andReturnSelf()
->shouldReceive('create')->with(['test' => 'struct'], ['start_on_completion' => false])->once()->andReturnNull();
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$response = $this->service->create($this->data);
$this->assertEquals(1, $response->id);
$this->assertEquals(1, $response->node_id);
}
/**
* Test handling of node timeout or other daemon error.
*/
public function testExceptionShouldBeThrownIfTheRequestFails()
{
$this->validatorService->shouldReceive('isAdmin->setFields->validate->getResults')->once()->andReturn([]);
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->usernameService->shouldReceive('generate')->once()->andReturn('user_name');
$this->repository->shouldReceive('create')->once()->andReturn((object) [
'node_id' => 1,
'id' => 1,
]);
$this->allocationRepository->shouldReceive('assignAllocationsToServer')->once()->andReturnNull();
$this->serverVariableRepository->shouldReceive('insert')->with([])->once()->andReturnNull();
$this->configurationStructureService->shouldReceive('handle')->once()->andReturnNull();
$this->daemonServerRepository->shouldReceive('setNode->create')->once()->andThrow($this->exception);
$this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnNull();
$this->connection->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull();
try {
$this->service->create($this->data);
} catch (PterodactylException $exception) {
$this->assertInstanceOf(DaemonConnectionException::class, $exception);
}
}
} }

View file

@ -18,7 +18,7 @@ use GuzzleHttp\Exception\RequestException;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Services\Servers\ServerDeletionService; use Pterodactyl\Services\Servers\ServerDeletionService;
use Pterodactyl\Services\Database\DatabaseManagementService; use Pterodactyl\Services\Databases\DatabaseManagementService;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface;
@ -36,7 +36,7 @@ class ServerDeletionServiceTest extends TestCase
protected $daemonServerRepository; protected $daemonServerRepository;
/** /**
* @var \Pterodactyl\Services\Database\DatabaseManagementService * @var \Pterodactyl\Services\Databases\DatabaseManagementService
*/ */
protected $databaseManagementService; protected $databaseManagementService;

View file

@ -11,6 +11,7 @@ namespace Tests\Unit\Services\Servers;
use Mockery as m; use Mockery as m;
use Tests\TestCase; use Tests\TestCase;
use Pterodactyl\Models\User;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Services\Servers\EnvironmentService; use Pterodactyl\Services\Servers\EnvironmentService;
@ -25,37 +26,32 @@ class StartupModificationServiceTest extends TestCase
/** /**
* @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface|\Mockery\Mock * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface|\Mockery\Mock
*/ */
protected $daemonServerRepository; private $daemonServerRepository;
/** /**
* @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock * @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock
*/ */
protected $connection; private $connection;
/** /**
* @var \Pterodactyl\Services\Servers\EnvironmentService|\Mockery\Mock * @var \Pterodactyl\Services\Servers\EnvironmentService|\Mockery\Mock
*/ */
protected $environmentService; private $environmentService;
/** /**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock
*/ */
protected $repository; private $repository;
/** /**
* @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface|\Mockery\Mock * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface|\Mockery\Mock
*/ */
protected $serverVariableRepository; private $serverVariableRepository;
/**
* @var \Pterodactyl\Services\Servers\StartupModificationService
*/
protected $service;
/** /**
* @var \Pterodactyl\Services\Servers\VariableValidatorService|\Mockery\Mock * @var \Pterodactyl\Services\Servers\VariableValidatorService|\Mockery\Mock
*/ */
protected $validatorService; private $validatorService;
/** /**
* Setup tests. * Setup tests.
@ -70,8 +66,97 @@ class StartupModificationServiceTest extends TestCase
$this->repository = m::mock(ServerRepositoryInterface::class); $this->repository = m::mock(ServerRepositoryInterface::class);
$this->serverVariableRepository = m::mock(ServerVariableRepositoryInterface::class); $this->serverVariableRepository = m::mock(ServerVariableRepositoryInterface::class);
$this->validatorService = m::mock(VariableValidatorService::class); $this->validatorService = m::mock(VariableValidatorService::class);
}
$this->service = new StartupModificationService( /**
* Test startup modification as a non-admin user.
*/
public function testStartupModifiedAsNormalUser()
{
$model = factory(Server::class)->make();
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->validatorService->shouldReceive('setUserLevel')->with(User::USER_LEVEL_USER)->once()->andReturnNull();
$this->validatorService->shouldReceive('handle')->with(123, ['test' => 'abcd1234'])->once()->andReturn(
collect([(object) ['id' => 1, 'value' => 'stored-value']])
);
$this->serverVariableRepository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf();
$this->serverVariableRepository->shouldReceive('updateOrCreate')->with([
'server_id' => $model->id,
'variable_id' => 1,
], ['variable_value' => 'stored-value'])->once()->andReturnNull();
$this->environmentService->shouldReceive('handle')->with($model)->once()->andReturn(['env']);
$this->daemonServerRepository->shouldReceive('setNode')->with($model->node_id)->once()->andReturnSelf();
$this->daemonServerRepository->shouldReceive('setAccessServer')->with($model->uuid)->once()->andReturnSelf();
$this->daemonServerRepository->shouldReceive('update')->with([
'build' => ['env|overwrite' => ['env']],
])->once()->andReturnSelf();
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$this->getService()->handle($model, ['egg_id' => 123, 'environment' => ['test' => 'abcd1234']]);
$this->assertTrue(true);
}
/**
* Test startup modification as an admin user.
*/
public function testStartupModificationAsAdminUser()
{
$model = factory(Server::class)->make([
'egg_id' => 123,
]);
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->validatorService->shouldReceive('setUserLevel')->with(User::USER_LEVEL_ADMIN)->once()->andReturnNull();
$this->validatorService->shouldReceive('handle')->with(456, ['test' => 'abcd1234'])->once()->andReturn(
collect([(object) ['id' => 1, 'value' => 'stored-value']])
);
$this->serverVariableRepository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf();
$this->serverVariableRepository->shouldReceive('updateOrCreate')->with([
'server_id' => $model->id,
'variable_id' => 1,
], ['variable_value' => 'stored-value'])->once()->andReturnNull();
$this->environmentService->shouldReceive('handle')->with($model)->once()->andReturn(['env']);
$this->repository->shouldReceive('update')->with($model->id, m::subset([
'installed' => 0,
'egg_id' => 456,
'pack_id' => 789,
]))->once()->andReturn($model);
$this->repository->shouldReceive('withColumns->getDaemonServiceData')->with($model->id)->once()->andReturn([]);
$this->daemonServerRepository->shouldReceive('setNode')->with($model->node_id)->once()->andReturnSelf();
$this->daemonServerRepository->shouldReceive('setAccessServer')->with($model->uuid)->once()->andReturnSelf();
$this->daemonServerRepository->shouldReceive('update')->with([
'build' => [
'env|overwrite' => ['env'],
],
'service' => [
'skip_scripts' => false,
],
])->once()->andReturnSelf();
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$service = $this->getService();
$service->setUserLevel(User::USER_LEVEL_ADMIN);
$service->handle($model, ['egg_id' => 456, 'pack_id' => 789, 'environment' => ['test' => 'abcd1234']]);
$this->assertTrue(true);
}
/**
* Return an instance of the service with mocked dependencies.
*
* @return \Pterodactyl\Services\Servers\StartupModificationService
*/
private function getService(): StartupModificationService
{
return new StartupModificationService(
$this->connection, $this->connection,
$this->daemonServerRepository, $this->daemonServerRepository,
$this->environmentService, $this->environmentService,
@ -80,16 +165,4 @@ class StartupModificationServiceTest extends TestCase
$this->validatorService $this->validatorService
); );
} }
/**
* Test startup is modified when user is not an administrator.
*
* @todo this test works, but not for the right reasons...
*/
public function testStartupIsModifiedAsNonAdmin()
{
$model = factory(Server::class)->make();
$this->assertTrue(true);
}
} }

View file

@ -1,109 +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 Tests\Unit\Services\Servers;
use Tests\TestCase;
use phpmock\phpunit\PHPMock;
use Pterodactyl\Services\Servers\UsernameGenerationService;
class UsernameGenerationServiceTest extends TestCase
{
use PHPMock;
/**
* @var UsernameGenerationService
*/
protected $service;
/**
* Setup tests.
*/
public function setUp()
{
parent::setUp();
$this->service = new UsernameGenerationService();
$this->getFunctionMock('\\Pterodactyl\\Services\\Servers', 'str_random')
->expects($this->any())->willReturnCallback(function ($count) {
return str_pad('', $count, '0');
});
}
/**
* Test that a valid username is returned and is the correct length.
*/
public function testShouldReturnAValidUsernameWithASelfGeneratedIdentifier()
{
$response = $this->service->generate('testname');
$this->assertEquals('testna_00000000', $response);
}
/**
* Test that a name and identifier provided returns the expected username.
*/
public function testShouldReturnAValidUsernameWithAnIdentifierProvided()
{
$response = $this->service->generate('testname', 'identifier');
$this->assertEquals('testna_identifi', $response);
}
/**
* Test that the identifier is extended to 8 characters if it is shorter.
*/
public function testShouldExtendIdentifierToBe8CharactersIfItIsShorter()
{
$response = $this->service->generate('testname', 'xyz');
$this->assertEquals('testna_xyz00000', $response);
}
/**
* Test that special characters are removed from the username.
*/
public function testShouldStripSpecialCharactersFromName()
{
$response = $this->service->generate('te!st_n$ame', 'identifier');
$this->assertEquals('testna_identifi', $response);
}
/**
* Test that an empty name is replaced with 6 random characters.
*/
public function testEmptyNamesShouldBeReplacedWithRandomCharacters()
{
$response = $this->service->generate('');
$this->assertEquals('000000_00000000', $response);
}
/**
* Test that a name consisting entirely of special characters is handled.
*/
public function testNameOfOnlySpecialCharactersIsHandledProperly()
{
$response = $this->service->generate('$%#*#(@#(#*$&#(#!#@');
$this->assertEquals('000000_00000000', $response);
}
/**
* Test that passing a name shorter than 6 characters returns the entire name.
*/
public function testNameShorterThan6CharactersShouldBeRenderedEntirely()
{
$response = $this->service->generate('test', 'identifier');
$this->assertEquals('test_identifi', $response);
}
}

View file

@ -11,8 +11,11 @@ namespace Tests\Unit\Services\Servers;
use Mockery as m; use Mockery as m;
use Tests\TestCase; use Tests\TestCase;
use Pterodactyl\Models\User;
use Illuminate\Support\Collection;
use Pterodactyl\Models\EggVariable; use Pterodactyl\Models\EggVariable;
use Illuminate\Contracts\Validation\Factory; use Illuminate\Contracts\Validation\Factory;
use Pterodactyl\Exceptions\PterodactylException;
use Pterodactyl\Exceptions\DisplayValidationException; use Pterodactyl\Exceptions\DisplayValidationException;
use Pterodactyl\Services\Servers\VariableValidatorService; use Pterodactyl\Services\Servers\VariableValidatorService;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
@ -22,35 +25,25 @@ use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface;
class VariableValidatorServiceTest extends TestCase class VariableValidatorServiceTest extends TestCase
{ {
/** /**
* @var \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface * @var \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface|\Mockery\Mock
*/ */
protected $optionVariableRepository; protected $optionVariableRepository;
/** /**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock
*/ */
protected $serverRepository; protected $serverRepository;
/** /**
* @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface|\Mockery\Mock
*/ */
protected $serverVariableRepository; protected $serverVariableRepository;
/** /**
* @var \Pterodactyl\Services\Servers\VariableValidatorService * @var \Illuminate\Contracts\Validation\Factory|\Mockery\Mock
*/
protected $service;
/**
* @var \Illuminate\Validation\Factory
*/ */
protected $validator; protected $validator;
/**
* @var \Illuminate\Support\Collection
*/
protected $variables;
/** /**
* Setup tests. * Setup tests.
*/ */
@ -58,56 +51,10 @@ class VariableValidatorServiceTest extends TestCase
{ {
parent::setUp(); parent::setUp();
$this->variables = collect(
[
factory(EggVariable::class)->states('editable', 'viewable')->make(),
factory(EggVariable::class)->states('viewable')->make(),
factory(EggVariable::class)->states('editable')->make(),
factory(EggVariable::class)->make(),
]
);
$this->optionVariableRepository = m::mock(EggVariableRepositoryInterface::class); $this->optionVariableRepository = m::mock(EggVariableRepositoryInterface::class);
$this->serverRepository = m::mock(ServerRepositoryInterface::class); $this->serverRepository = m::mock(ServerRepositoryInterface::class);
$this->serverVariableRepository = m::mock(ServerVariableRepositoryInterface::class); $this->serverVariableRepository = m::mock(ServerVariableRepositoryInterface::class);
$this->validator = m::mock(Factory::class); $this->validator = m::mock(Factory::class);
$this->service = new VariableValidatorService(
$this->optionVariableRepository,
$this->serverRepository,
$this->serverVariableRepository,
$this->validator
);
}
/**
* Test that setting fields returns an instance of the class.
*/
public function testSettingFieldsShouldReturnInstanceOfSelf()
{
$response = $this->service->setFields([]);
$this->assertInstanceOf(VariableValidatorService::class, $response);
}
/**
* Test that setting administrator value returns an instance of the class.
*/
public function testSettingAdminShouldReturnInstanceOfSelf()
{
$response = $this->service->isAdmin();
$this->assertInstanceOf(VariableValidatorService::class, $response);
}
/**
* Test that getting the results returns an array of values.
*/
public function testGettingResultsReturnsAnArrayOfValues()
{
$response = $this->service->getResults();
$this->assertTrue(is_array($response));
} }
/** /**
@ -115,13 +62,11 @@ class VariableValidatorServiceTest extends TestCase
*/ */
public function testEmptyResultSetShouldBeReturnedIfNoVariablesAreFound() public function testEmptyResultSetShouldBeReturnedIfNoVariablesAreFound()
{ {
$this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn([]); $this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn(collect([]));
$response = $this->service->validate(1); $response = $this->getService()->handle(1, []);
$this->assertEmpty($response);
$this->assertInstanceOf(VariableValidatorService::class, $response); $this->assertInstanceOf(Collection::class, $response);
$this->assertTrue(is_array($response->getResults()));
$this->assertEmpty($response->getResults());
} }
/** /**
@ -129,31 +74,34 @@ class VariableValidatorServiceTest extends TestCase
*/ */
public function testValidatorShouldNotProcessVariablesSetAsNotUserEditableWhenAdminFlagIsNotPassed() public function testValidatorShouldNotProcessVariablesSetAsNotUserEditableWhenAdminFlagIsNotPassed()
{ {
$this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn($this->variables); $variables = $this->getVariableCollection();
$this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn($variables);
$this->validator->shouldReceive('make')->with([ $this->validator->shouldReceive('make')->with([
'variable_value' => 'Test_SomeValue_0', 'variable_value' => 'Test_SomeValue_0',
], [ ], [
'variable_value' => $this->variables[0]->rules, 'variable_value' => $variables[0]->rules,
])->once()->andReturnSelf() ])->once()->andReturnSelf();
->shouldReceive('fails')->withNoArgs()->once()->andReturn(false); $this->validator->shouldReceive('fails')->withNoArgs()->once()->andReturn(false);
$response = $this->service->setFields([ $response = $this->getService()->handle(1, [
$this->variables[0]->env_variable => 'Test_SomeValue_0', $variables[0]->env_variable => 'Test_SomeValue_0',
$this->variables[1]->env_variable => 'Test_SomeValue_1', $variables[1]->env_variable => 'Test_SomeValue_1',
$this->variables[2]->env_variable => 'Test_SomeValue_2', $variables[2]->env_variable => 'Test_SomeValue_2',
$this->variables[3]->env_variable => 'Test_SomeValue_3', $variables[3]->env_variable => 'Test_SomeValue_3',
])->validate(1)->getResults(); ]);
$this->assertEquals(1, count($response), 'Assert response has a single item in array.'); $this->assertNotEmpty($response);
$this->assertArrayHasKey('0', $response); $this->assertInstanceOf(Collection::class, $response);
$this->assertArrayHasKey('id', $response[0]); $this->assertEquals(1, $response->count(), 'Assert response has a single item in collection.');
$this->assertArrayHasKey('key', $response[0]);
$this->assertArrayHasKey('value', $response[0]);
$this->assertEquals($this->variables[0]->id, $response[0]['id']); $variable = $response->first();
$this->assertEquals($this->variables[0]->env_variable, $response[0]['key']); $this->assertObjectHasAttribute('id', $variable);
$this->assertEquals('Test_SomeValue_0', $response[0]['value']); $this->assertObjectHasAttribute('key', $variable);
$this->assertObjectHasAttribute('value', $variable);
$this->assertSame($variables[0]->id, $variable->id);
$this->assertSame($variables[0]->env_variable, $variable->key);
$this->assertSame('Test_SomeValue_0', $variable->value);
} }
/** /**
@ -161,36 +109,39 @@ class VariableValidatorServiceTest extends TestCase
*/ */
public function testValidatorShouldProcessAllVariablesWhenAdminFlagIsSet() public function testValidatorShouldProcessAllVariablesWhenAdminFlagIsSet()
{ {
$this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn($this->variables); $variables = $this->getVariableCollection();
$this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn($variables);
foreach ($this->variables as $key => $variable) { foreach ($variables as $key => $variable) {
$this->validator->shouldReceive('make')->with([ $this->validator->shouldReceive('make')->with([
'variable_value' => 'Test_SomeValue_' . $key, 'variable_value' => 'Test_SomeValue_' . $key,
], [ ], [
'variable_value' => $this->variables[$key]->rules, 'variable_value' => $variables[$key]->rules,
])->andReturnSelf() ])->once()->andReturnSelf();
->shouldReceive('fails')->withNoArgs()->once()->andReturn(false); $this->validator->shouldReceive('fails')->withNoArgs()->once()->andReturn(false);
} }
$response = $this->service->isAdmin()->setFields([ $service = $this->getService();
$this->variables[0]->env_variable => 'Test_SomeValue_0', $service->setUserLevel(User::USER_LEVEL_ADMIN);
$this->variables[1]->env_variable => 'Test_SomeValue_1', $response = $service->handle(1, [
$this->variables[2]->env_variable => 'Test_SomeValue_2', $variables[0]->env_variable => 'Test_SomeValue_0',
$this->variables[3]->env_variable => 'Test_SomeValue_3', $variables[1]->env_variable => 'Test_SomeValue_1',
])->validate(1)->getResults(); $variables[2]->env_variable => 'Test_SomeValue_2',
$variables[3]->env_variable => 'Test_SomeValue_3',
]);
$this->assertEquals(4, count($response), 'Assert response has all four items in array.'); $this->assertNotEmpty($response);
$this->assertInstanceOf(Collection::class, $response);
$this->assertEquals(4, $response->count(), 'Assert response has all four items in collection.');
foreach ($response as $key => $values) { $response->each(function ($variable, $key) use ($variables) {
$this->assertArrayHasKey($key, $response); $this->assertObjectHasAttribute('id', $variable);
$this->assertArrayHasKey('id', $response[$key]); $this->assertObjectHasAttribute('key', $variable);
$this->assertArrayHasKey('key', $response[$key]); $this->assertObjectHasAttribute('value', $variable);
$this->assertArrayHasKey('value', $response[$key]); $this->assertSame($variables[$key]->id, $variable->id);
$this->assertSame($variables[$key]->env_variable, $variable->key);
$this->assertEquals($this->variables[$key]->id, $response[$key]['id']); $this->assertSame('Test_SomeValue_' . $key, $variable->value);
$this->assertEquals($this->variables[$key]->env_variable, $response[$key]['key']); });
$this->assertEquals('Test_SomeValue_' . $key, $response[$key]['value']);
}
} }
/** /**
@ -198,31 +149,63 @@ class VariableValidatorServiceTest extends TestCase
*/ */
public function testValidatorShouldThrowExceptionWhenAValidationErrorIsEncountered() public function testValidatorShouldThrowExceptionWhenAValidationErrorIsEncountered()
{ {
$this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn($this->variables); $variables = $this->getVariableCollection();
$this->optionVariableRepository->shouldReceive('findWhere')->with([['egg_id', '=', 1]])->andReturn($variables);
$this->validator->shouldReceive('make')->with([ $this->validator->shouldReceive('make')->with([
'variable_value' => null, 'variable_value' => null,
], [ ], [
'variable_value' => $this->variables[0]->rules, 'variable_value' => $variables[0]->rules,
])->once()->andReturnSelf() ])->once()->andReturnSelf();
->shouldReceive('fails')->withNoArgs()->once()->andReturn(true); $this->validator->shouldReceive('fails')->withNoArgs()->once()->andReturn(true);
$this->validator->shouldReceive('errors')->withNoArgs()->once()->andReturnSelf() $this->validator->shouldReceive('errors')->withNoArgs()->once()->andReturnSelf();
->shouldReceive('toArray')->withNoArgs()->once()->andReturn([]); $this->validator->shouldReceive('toArray')->withNoArgs()->once()->andReturn([]);
try { try {
$this->service->setFields([ $this->getService()->handle(1, [$variables[0]->env_variable => null]);
$this->variables[0]->env_variable => null, } catch (PterodactylException $exception) {
])->validate(1); $this->assertInstanceOf(DisplayValidationException::class, $exception);
} catch (DisplayValidationException $exception) {
$decoded = json_decode($exception->getMessage());
$decoded = json_decode($exception->getMessage());
$this->assertEquals(0, json_last_error(), 'Assert that response is decodable JSON.'); $this->assertEquals(0, json_last_error(), 'Assert that response is decodable JSON.');
$this->assertObjectHasAttribute('notice', $decoded); $this->assertObjectHasAttribute('notice', $decoded);
$this->assertEquals( $this->assertEquals(
trans('admin/server.exceptions.bad_variable', ['name' => $this->variables[0]->name]), trans('admin/server.exceptions.bad_variable', ['name' => $variables[0]->name]),
$decoded->notice[0] $decoded->notice[0]
); );
} }
} }
/**
* Return a collection of fake variables to use for testing.
*
* @return \Illuminate\Support\Collection
*/
private function getVariableCollection(): Collection
{
return collect(
[
factory(EggVariable::class)->states('editable', 'viewable')->make(),
factory(EggVariable::class)->states('viewable')->make(),
factory(EggVariable::class)->states('editable')->make(),
factory(EggVariable::class)->make(),
]
);
}
/**
* Return an instance of the service with mocked dependencies.
*
* @return \Pterodactyl\Services\Servers\VariableValidatorService
*/
private function getService(): VariableValidatorService
{
return new VariableValidatorService(
$this->optionVariableRepository,
$this->serverRepository,
$this->serverVariableRepository,
$this->validator
);
}
} }

View file

@ -0,0 +1,181 @@
<?php
namespace Tests\Unit\Services\Sftp;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\User;
use Pterodactyl\Models\Server;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Services\Sftp\AuthenticateUsingPasswordService;
class AuthenticateUsingPasswordServiceTest extends TestCase
{
/**
* @var \Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService|\Mockery\Mock
*/
private $keyProviderService;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock
*/
private $repository;
/**
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface|\Mockery\Mock
*/
private $userRepository;
/**
* Setup tests.
*/
public function setUp()
{
parent::setUp();
$this->keyProviderService = m::mock(DaemonKeyProviderService::class);
$this->repository = m::mock(ServerRepositoryInterface::class);
$this->userRepository = m::mock(UserRepositoryInterface::class);
}
/**
* Test that an account can be authenticated.
*/
public function testNonAdminAccountIsAuthenticated()
{
$user = factory(User::class)->make(['root_admin' => 0]);
$server = factory(Server::class)->make(['node_id' => 1, 'owner_id' => $user->id]);
$this->userRepository->shouldReceive('withColumns')->with(['id', 'root_admin', 'password'])->once()->andReturnSelf();
$this->userRepository->shouldReceive('findFirstWhere')->with([['username', '=', $user->username]])->once()->andReturn($user);
$this->repository->shouldReceive('withColumns')->with(['id', 'node_id', 'owner_id', 'uuid'])->once()->andReturnSelf();
$this->repository->shouldReceive('getByUuid')->with($server->uuidShort)->once()->andReturn($server);
$this->keyProviderService->shouldReceive('handle')->with($server->id, $user->id)->once()->andReturn('server_token');
$response = $this->getService()->handle($user->username, 'password', 1, $server->uuidShort);
$this->assertNotEmpty($response);
$this->assertArrayHasKey('server', $response);
$this->assertArrayHasKey('token', $response);
$this->assertSame($server->uuid, $response['server']);
$this->assertSame('server_token', $response['token']);
}
/**
* Test that an administrative user can access servers that they are not
* set as the owner of.
*/
public function testAdminAccountIsAuthenticated()
{
$user = factory(User::class)->make(['root_admin' => 1]);
$server = factory(Server::class)->make(['node_id' => 1, 'owner_id' => $user->id + 1]);
$this->userRepository->shouldReceive('withColumns')->with(['id', 'root_admin', 'password'])->once()->andReturnSelf();
$this->userRepository->shouldReceive('findFirstWhere')->with([['username', '=', $user->username]])->once()->andReturn($user);
$this->repository->shouldReceive('withColumns')->with(['id', 'node_id', 'owner_id', 'uuid'])->once()->andReturnSelf();
$this->repository->shouldReceive('getByUuid')->with($server->uuidShort)->once()->andReturn($server);
$this->keyProviderService->shouldReceive('handle')->with($server->id, $user->id)->once()->andReturn('server_token');
$response = $this->getService()->handle($user->username, 'password', 1, $server->uuidShort);
$this->assertNotEmpty($response);
$this->assertArrayHasKey('server', $response);
$this->assertArrayHasKey('token', $response);
$this->assertSame($server->uuid, $response['server']);
$this->assertSame('server_token', $response['token']);
}
/**
* Test exception gets thrown if no server is passed into the function.
*
* @expectedException \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function testExceptionIsThrownIfNoServerIsProvided()
{
$this->getService()->handle('username', 'password', 1);
}
/**
* Test that an exception is thrown if the user account exists but the wrong
* credentials are passed.
*
* @expectedException \Illuminate\Auth\AuthenticationException
*/
public function testExceptionIsThrownIfUserDetailsAreIncorrect()
{
$user = factory(User::class)->make();
$this->userRepository->shouldReceive('withColumns')->with(['id', 'root_admin', 'password'])->once()->andReturnSelf();
$this->userRepository->shouldReceive('findFirstWhere')->with([['username', '=', $user->username]])->once()->andReturn($user);
$this->getService()->handle($user->username, 'wrongpassword', 1, '1234');
}
/**
* Test that an exception is thrown if no user account is found.
*
* @expectedException \Illuminate\Auth\AuthenticationException
*/
public function testExceptionIsThrownIfNoUserAccountIsFound()
{
$this->userRepository->shouldReceive('withColumns')->with(['id', 'root_admin', 'password'])->once()->andReturnSelf();
$this->userRepository->shouldReceive('findFirstWhere')->with([['username', '=', 'something']])->once()->andThrow(new RecordNotFoundException);
$this->getService()->handle('something', 'password', 1, '1234');
}
/**
* Test that an exception is thrown if the user is not the owner of the server
* and is not an administrator.
*
* @expectedException \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function testExceptionIsThrownIfUserDoesNotOwnServer()
{
$user = factory(User::class)->make(['root_admin' => 0]);
$server = factory(Server::class)->make(['node_id' => 1, 'owner_id' => $user->id + 1]);
$this->userRepository->shouldReceive('withColumns')->with(['id', 'root_admin', 'password'])->once()->andReturnSelf();
$this->userRepository->shouldReceive('findFirstWhere')->with([['username', '=', $user->username]])->once()->andReturn($user);
$this->repository->shouldReceive('withColumns')->with(['id', 'node_id', 'owner_id', 'uuid'])->once()->andReturnSelf();
$this->repository->shouldReceive('getByUuid')->with($server->uuidShort)->once()->andReturn($server);
$this->getService()->handle($user->username, 'password', 1, $server->uuidShort);
}
/**
* Test that an exception is thrown if the requested server does not belong to
* the node that the request is made from.
*
* @expectedException \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function testExceptionIsThrownIfServerDoesNotExistOnCurrentNode()
{
$user = factory(User::class)->make(['root_admin' => 0]);
$server = factory(Server::class)->make(['node_id' => 2, 'owner_id' => $user->id]);
$this->userRepository->shouldReceive('withColumns')->with(['id', 'root_admin', 'password'])->once()->andReturnSelf();
$this->userRepository->shouldReceive('findFirstWhere')->with([['username', '=', $user->username]])->once()->andReturn($user);
$this->repository->shouldReceive('withColumns')->with(['id', 'node_id', 'owner_id', 'uuid'])->once()->andReturnSelf();
$this->repository->shouldReceive('getByUuid')->with($server->uuidShort)->once()->andReturn($server);
$this->getService()->handle($user->username, 'password', 1, $server->uuidShort);
}
/**
* Return an instance of the service with mocked dependencies.
*
* @return \Pterodactyl\Services\Sftp\AuthenticateUsingPasswordService
*/
private function getService(): AuthenticateUsingPasswordService
{
return new AuthenticateUsingPasswordService($this->keyProviderService, $this->repository, $this->userRepository);
}
}