diff --git a/app/Contracts/Repository/Daemon/ConfigurationRepositoryInterface.php b/app/Contracts/Repository/Daemon/ConfigurationRepositoryInterface.php new file mode 100644 index 000000000..c56dde57a --- /dev/null +++ b/app/Contracts/Repository/Daemon/ConfigurationRepositoryInterface.php @@ -0,0 +1,36 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Pterodactyl\Contracts\Repository\Daemon; + +interface ConfigurationRepositoryInterface extends BaseRepositoryInterface +{ + /** + * Update the configuration details for the specified node using data from the database. + * + * @param array $overrides + * @return \Psr\Http\Message\ResponseInterface + */ + public function update(array $overrides = []); +} diff --git a/app/Contracts/Repository/NodeRepositoryInterface.php b/app/Contracts/Repository/NodeRepositoryInterface.php index 1dcdad8e1..51c6540ea 100644 --- a/app/Contracts/Repository/NodeRepositoryInterface.php +++ b/app/Contracts/Repository/NodeRepositoryInterface.php @@ -28,6 +28,52 @@ use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface; interface NodeRepositoryInterface extends RepositoryInterface, SearchableInterface { + /** + * Return the usage stats for a single node. + * + * @param int $id + * @return array + */ + public function getUsageStats($id); + + /** + * Return all available nodes with a searchable interface. + * + * @param int $count + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator + */ + public function getNodeListingData($count = 25); + + /** + * Return a single node with location and server information. + * + * @param int $id + * @return mixed + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function getSingleNode($id); + + /** + * Return a node with all of the associated allocations and servers that are attached to said allocations. + * + * @param int $id + * @return mixed + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function getNodeAllocations($id); + + /** + * Return a node with all of the servers attached to that node. + * + * @param int $id + * @return mixed + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function getNodeServers($id); + /** * Return a collection of nodes beloning to a specific location for use on frontend display. * diff --git a/app/Contracts/Repository/RepositoryInterface.php b/app/Contracts/Repository/RepositoryInterface.php index 1f498b6ea..ad600817b 100644 --- a/app/Contracts/Repository/RepositoryInterface.php +++ b/app/Contracts/Repository/RepositoryInterface.php @@ -116,6 +116,14 @@ interface RepositoryInterface */ public function findFirstWhere(array $fields); + /** + * Return a count of records matching the passed arguments. + * + * @param array $fields + * @return int + */ + public function findCountWhere(array $fields); + /** * Update a given ID with the passed array of fields. * diff --git a/app/Http/Controllers/Admin/DatabaseController.php b/app/Http/Controllers/Admin/DatabaseController.php index a383558be..4f61c8482 100644 --- a/app/Http/Controllers/Admin/DatabaseController.php +++ b/app/Http/Controllers/Admin/DatabaseController.php @@ -132,6 +132,7 @@ class DatabaseController extends Controller * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ public function update(DatabaseHostFormRequest $request, DatabaseHost $host) { diff --git a/app/Http/Controllers/Admin/NodesController.php b/app/Http/Controllers/Admin/NodesController.php index ce02febac..76b83caf1 100644 --- a/app/Http/Controllers/Admin/NodesController.php +++ b/app/Http/Controllers/Admin/NodesController.php @@ -24,48 +24,112 @@ namespace Pterodactyl\Http\Controllers\Admin; -use DB; +use Illuminate\Cache\Repository as CacheRepository; +use Illuminate\Contracts\Translation\Translator; use Log; use Alert; use Cache; use Javascript; -use Pterodactyl\Models; +use Prologue\Alerts\AlertsMessageBag; +use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Http\Requests\Admin\NodeFormRequest; +use Pterodactyl\Models\Allocation; +use Pterodactyl\Models\Node; use Illuminate\Http\Request; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Repositories\NodeRepository; use Pterodactyl\Exceptions\DisplayValidationException; +use Pterodactyl\Services\Nodes\CreationService; +use Pterodactyl\Services\Nodes\DeletionService; +use Pterodactyl\Services\Nodes\UpdateService; class NodesController extends Controller { + /** + * @var \Prologue\Alerts\AlertsMessageBag + */ + protected $alert; + + /** + * @var \Illuminate\Cache\Repository + */ + protected $cache; + + /** + * @var \Pterodactyl\Services\Nodes\CreationService + */ + protected $creationService; + + /** + * @var \Pterodactyl\Services\Nodes\DeletionService + */ + protected $deletionService; + + /** + * @var \Pterodactyl\Contracts\Repository\LocationRepositoryInterface + */ + protected $locationRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + */ + protected $repository; + + /** + * @var \Illuminate\Contracts\Translation\Translator + */ + protected $translator; + + /** + * @var \Pterodactyl\Services\Nodes\UpdateService + */ + protected $updateService; + + public function __construct( + AlertsMessageBag $alert, + CacheRepository $cache, + CreationService $creationService, + DeletionService $deletionService, + LocationRepositoryInterface $locationRepository, + NodeRepositoryInterface $repository, + Translator $translator, + UpdateService $updateService + ) { + $this->alert = $alert; + $this->cache = $cache; + $this->creationService = $creationService; + $this->deletionService = $deletionService; + $this->locationRepository = $locationRepository; + $this->repository = $repository; + $this->translator = $translator; + $this->updateService = $updateService; + } + /** * Displays the index page listing all nodes on the panel. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ public function index(Request $request) { - $nodes = Models\Node::with('location')->withCount('servers'); - - if (! is_null($request->input('query'))) { - $nodes->search($request->input('query')); - } - - return view('admin.nodes.index', ['nodes' => $nodes->paginate(25)]); + return view('admin.nodes.index', [ + 'nodes' => $this->repository->search($request->input('query'))->getNodeListingData(), + ]); } /** * Displays create new node page. * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\View\View|\Illuminate\Http\RedirectResponse + * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View */ - public function create(Request $request) + public function create() { - $locations = Models\Location::all(); - if (! $locations->count()) { - Alert::warning('You must add a location before you can add a new node.')->flash(); + $locations = $this->locationRepository->all(); + if (count($locations) < 1) { + $this->alert->warning($this->translator->trans('admin/node.notices.location_required'))->flash(); return redirect()->route('admin.locations'); } @@ -76,117 +140,68 @@ class NodesController extends Controller /** * Post controller to create a new node on the system. * - * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Http\Requests\Admin\NodeFormRequest $request * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function store(Request $request) + public function store(NodeFormRequest $request) { - try { - $repo = new NodeRepository; - $node = $repo->create(array_merge( - $request->only([ - 'public', 'disk_overallocate', - 'memory_overallocate', 'behind_proxy', - ]), - $request->intersect([ - 'name', 'location_id', 'fqdn', - 'scheme', 'memory', 'disk', - 'daemonBase', 'daemonSFTP', 'daemonListen', - ]) - )); - Alert::success('Successfully created new node that can be configured automatically on your remote machine by visiting the configuration tab. Before you can add any servers you need to first assign some IP addresses and ports by adding an allocation.')->flash(); + $node = $this->creationService->handle($request->normalize()); + $this->alert->info($this->translator->trans('admin/node.notices.node_created'))->flash(); - return redirect()->route('admin.nodes.view.allocation', $node->id); - } catch (DisplayValidationException $e) { - return redirect()->route('admin.nodes.new')->withErrors(json_decode($e->getMessage()))->withInput(); - } catch (DisplayException $e) { - Alert::danger($e->getMessage())->flash(); - } catch (\Exception $e) { - Log::error($e); - Alert::danger('An unhandled exception occured while attempting to add this node. Please try again.')->flash(); - } - - return redirect()->route('admin.nodes.new')->withInput(); + return redirect()->route('admin.nodes.view.allocation', $node->id); } /** * Shows the index overview page for a specific node. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param int $node * @return \Illuminate\View\View */ - public function viewIndex(Request $request, $id) + public function viewIndex($node) { - $node = Models\Node::with('location')->withCount('servers')->findOrFail($id); - $stats = collect( - Models\Server::select( - DB::raw('SUM(memory) as memory, SUM(disk) as disk') - )->where('node_id', $node->id)->first() - )->mapWithKeys(function ($item, $key) use ($node) { - if ($node->{$key . '_overallocate'} > 0) { - $withover = $node->{$key} * (1 + ($node->{$key . '_overallocate'} / 100)); - } else { - $withover = $node->{$key}; - } - - $percent = ($item / $withover) * 100; - - return [$key => [ - 'value' => number_format($item), - 'max' => number_format($withover), - 'percent' => $percent, - 'css' => ($percent <= 75) ? 'green' : (($percent > 90) ? 'red' : 'yellow'), - ]]; - })->toArray(); - - return view('admin.nodes.view.index', ['node' => $node, 'stats' => $stats]); + return view('admin.nodes.view.index', [ + 'node' => $this->repository->getSingleNode($node), + 'stats' => $this->repository->getUsageStats($node), + ]); } /** * Shows the settings page for a specific node. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Models\Node $node * @return \Illuminate\View\View */ - public function viewSettings(Request $request, $id) + public function viewSettings(Node $node) { return view('admin.nodes.view.settings', [ - 'node' => Models\Node::findOrFail($id), - 'locations' => Models\Location::all(), + 'node' => $node, + 'locations' => $this->locationRepository->all(), ]); } /** * Shows the configuration page for a specific node. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Models\Node $node * @return \Illuminate\View\View */ - public function viewConfiguration(Request $request, $id) + public function viewConfiguration(Node $node) { - return view('admin.nodes.view.configuration', [ - 'node' => Models\Node::findOrFail($id), - ]); + return view('admin.nodes.view.configuration', ['node' => $node]); } /** * Shows the allocation page for a specific node. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param int $node * @return \Illuminate\View\View */ - public function viewAllocation(Request $request, $id) + public function viewAllocation($node) { - $node = Models\Node::findOrFail($id); - $node->setRelation('allocations', $node->allocations()->orderBy('ip', 'asc')->orderBy('port', 'asc')->with('server')->paginate(50)); - - Javascript::put([ - 'node' => collect($node)->only(['id']), - ]); + $node = $this->repository->getNodeAllocations($node); + Javascript::put(['node' => collect($node)->only(['id'])]); return view('admin.nodes.view.allocation', ['node' => $node]); } @@ -194,69 +209,48 @@ class NodesController extends Controller /** * Shows the server listing page for a specific node. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param int $node * @return \Illuminate\View\View */ - public function viewServers(Request $request, $id) + public function viewServers($node) { - $node = Models\Node::with('servers.user', 'servers.service', 'servers.option')->findOrFail($id); + $node = $this->repository->getNodeServers($node); Javascript::put([ 'node' => collect($node->makeVisible('daemonSecret'))->only(['scheme', 'fqdn', 'daemonListen', 'daemonSecret']), ]); - return view('admin.nodes.view.servers', [ - 'node' => $node, - ]); + return view('admin.nodes.view.servers', ['node' => $node]); } /** * Updates settings for a node. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Http\Requests\Admin\NodeFormRequest $request + * @param \Pterodactyl\Models\Node $node * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function updateSettings(Request $request, $id) + public function updateSettings(NodeFormRequest $request, Node $node) { - $repo = new NodeRepository; + $this->updateService->handle($node, $request->normalize()); + $this->alert->success($this->translator->trans('admin/node.notices.node_updated'))->flash(); - try { - $node = $repo->update($id, array_merge( - $request->only([ - 'public', 'disk_overallocate', - 'memory_overallocate', 'behind_proxy', - ]), - $request->intersect([ - 'name', 'location_id', 'fqdn', - 'scheme', 'memory', 'disk', 'upload_size', - 'reset_secret', 'daemonSFTP', 'daemonListen', - ]) - )); - Alert::success('Successfully updated this node\'s information. If you changed any daemon settings you will need to restart it now.')->flash(); - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.nodes.view.settings', $id)->withErrors(json_decode($ex->getMessage()))->withInput(); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception occured while attempting to edit this node. Please try again.')->flash(); - } - - return redirect()->route('admin.nodes.view.settings', $id)->withInput(); + return redirect()->route('admin.nodes.view.settings', $node->id)->withInput(); } /** * Removes a single allocation from a node. * - * @param \Illuminate\Http\Request $request - * @param int $node - * @param int $allocation + * @param \Illuminate\Http\Request $request + * @param int $node + * @param int $allocation * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse */ public function allocationRemoveSingle(Request $request, $node, $allocation) { - $query = Models\Allocation::where('node_id', $node)->whereNull('server_id')->where('id', $allocation)->delete(); + $query = Allocation::where('node_id', $node)->whereNull('server_id')->where('id', $allocation)->delete(); if ($query < 1) { return response()->json([ 'error' => 'Unable to find an allocation matching those details to delete.', @@ -269,13 +263,16 @@ class NodesController extends Controller /** * Remove all allocations for a specific IP at once on a node. * - * @param \Illuminate\Http\Request $request - * @param int $node + * @param \Illuminate\Http\Request $request + * @param int $node * @return \Illuminate\Http\RedirectResponse */ public function allocationRemoveBlock(Request $request, $node) { - $query = Models\Allocation::where('node_id', $node)->whereNull('server_id')->where('ip', $request->input('ip'))->delete(); + $query = Allocation::where('node_id', $node) + ->whereNull('server_id') + ->where('ip', $request->input('ip')) + ->delete(); if ($query < 1) { Alert::danger('There was an error while attempting to delete allocations on that IP.')->flash(); } else { @@ -288,8 +285,8 @@ class NodesController extends Controller /** * Sets an alias for a specific allocation on a node. * - * @param \Illuminate\Http\Request $request - * @param int $node + * @param \Illuminate\Http\Request $request + * @param int $node * @return \Illuminate\Http\Response */ public function allocationSetAlias(Request $request, $node) @@ -299,7 +296,7 @@ class NodesController extends Controller } try { - $update = Models\Allocation::findOrFail($request->input('allocation_id')); + $update = Allocation::findOrFail($request->input('allocation_id')); $update->ip_alias = (empty($request->input('alias'))) ? null : $request->input('alias'); $update->save(); @@ -312,8 +309,8 @@ class NodesController extends Controller /** * Creates new allocations on a node. * - * @param \Illuminate\Http\Request $request - * @param int $node + * @param \Illuminate\Http\Request $request + * @param int $node * @return \Illuminate\Http\RedirectResponse */ public function createAllocation(Request $request, $node) @@ -324,12 +321,16 @@ class NodesController extends Controller $repo->addAllocations($node, $request->intersect(['allocation_ip', 'allocation_alias', 'allocation_ports'])); Alert::success('Successfully added new allocations!')->flash(); } catch (DisplayValidationException $ex) { - return redirect()->route('admin.nodes.view.allocation', $node)->withErrors(json_decode($ex->getMessage()))->withInput(); + return redirect() + ->route('admin.nodes.view.allocation', $node) + ->withErrors(json_decode($ex->getMessage())) + ->withInput(); } catch (DisplayException $ex) { Alert::danger($ex->getMessage())->flash(); } catch (\Exception $ex) { Log::error($ex); - Alert::danger('An unhandled exception occured while attempting to add allocations this node. This error has been logged.')->flash(); + Alert::danger('An unhandled exception occured while attempting to add allocations this node. This error has been logged.') + ->flash(); } return redirect()->route('admin.nodes.view.allocation', $node); @@ -338,42 +339,29 @@ class NodesController extends Controller /** * Deletes a node from the system. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param $node * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException */ - public function delete(Request $request, $id) + public function delete($node) { - $repo = new NodeRepository; + $this->deletionService->handle($node); + $this->alert->success($this->translator->trans('admin/node.notices.node_deleted'))->flash(); - try { - $repo->delete($id); - Alert::success('Successfully deleted the requested node from the panel.')->flash(); - - return redirect()->route('admin.nodes'); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception occured while attempting to delete this node. Please try again.')->flash(); - } - - return redirect()->route('admin.nodes.view', $id); + return redirect()->route('admin.nodes'); } /** * Returns the configuration token to auto-deploy a node. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Models\Node $node * @return \Illuminate\Http\JsonResponse */ - public function setToken(Request $request, $id) + public function setToken(Node $node) { - $node = Models\Node::findOrFail($id); - - $token = str_random(32); - Cache::tags(['Node:Configuration'])->put($token, $node->id, 5); + $token = bin2hex(random_bytes(16)); + $this->cache->tags(['Node:Configuration'])->put($token, $node->id, 5); return response()->json(['token' => $token]); } diff --git a/app/Http/Requests/Admin/NodeFormRequest.php b/app/Http/Requests/Admin/NodeFormRequest.php new file mode 100644 index 000000000..97080c3bf --- /dev/null +++ b/app/Http/Requests/Admin/NodeFormRequest.php @@ -0,0 +1,62 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Pterodactyl\Http\Requests\Admin; + +use Pterodactyl\Models\Node; + +class NodeFormRequest extends AdminFormRequest +{ + /** + * Get rules to apply to data in this request. + */ + public function rules() + { + if ($this->method() === 'PATCH') { + return Node::getUpdateRulesForId($this->route()->parameter('node')->id); + } + + return Node::getCreateRules(); + } + + /** + * Run validation after the rules above have been applied. + * + * @param \Illuminate\Validation\Validator $validator + */ + public function withValidator($validator) + { + $validator->after(function ($validator) { + // Check that the FQDN is a valid IP address. + if (! filter_var(gethostbyname($this->input('fqdn')), FILTER_VALIDATE_IP)) { + $validator->errors()->add('fqdn', trans('admin/node.validation.fqdn_not_resolvable')); + } + + // Check that if using HTTPS the FQDN is not an IP address. + if (filter_var($this->input('fqdn'), FILTER_VALIDATE_IP) && $this->input('scheme') === 'https') { + $validator->errors()->add('fqdn', trans('admin/node.validation.fqdn_required_for_ssl')); + } + }); + } +} diff --git a/app/Models/Node.php b/app/Models/Node.php index 9c454cfcb..3f43a28a3 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -25,13 +25,15 @@ namespace Pterodactyl\Models; use GuzzleHttp\Client; +use Sofa\Eloquence\Eloquence; +use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; use Illuminate\Notifications\Notifiable; -use Nicolaslopezj\Searchable\SearchableTrait; +use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class Node extends Model +class Node extends Model implements ValidableContract { - use Notifiable, SearchableTrait; + use Eloquence, Notifiable, Validable; /** * The table associated with the model. @@ -47,20 +49,20 @@ class Node extends Model */ protected $hidden = ['daemonSecret']; - /** - * Cast values to correct type. - * - * @var array - */ - protected $casts = [ - 'public' => 'integer', - 'location_id' => 'integer', - 'memory' => 'integer', - 'disk' => 'integer', - 'daemonListen' => 'integer', - 'daemonSFTP' => 'integer', - 'behind_proxy' => 'boolean', - ]; + /** + * Cast values to correct type. + * + * @var array + */ + protected $casts = [ + 'public' => 'integer', + 'location_id' => 'integer', + 'memory' => 'integer', + 'disk' => 'integer', + 'daemonListen' => 'integer', + 'daemonSFTP' => 'integer', + 'behind_proxy' => 'boolean', + ]; /** * Fields that are mass assignable. @@ -81,22 +83,67 @@ class Node extends Model * * @var array */ - protected $searchable = [ - 'columns' => [ - 'nodes.name' => 10, - 'nodes.fqdn' => 8, - 'locations.short' => 4, - 'locations.long' => 4, - ], - 'joins' => [ - 'locations' => ['locations.id', 'nodes.location_id'], - ], - ]; + protected $searchableColumns = [ + 'name' => 10, + 'fqdn' => 8, + 'location.short' => 4, + 'location.long' => 4, + ]; + + /** + * @var array + */ + protected static $applicationRules = [ + 'name' => 'required', + 'location_id' => 'required', + 'fqdn' => 'required', + 'scheme' => 'required', + 'memory' => 'required', + 'memory_overallocate' => 'required', + 'disk' => 'required', + 'disk_overallocate' => 'required', + 'daemonBase' => 'sometimes|required', + 'daemonSFTP' => 'required', + 'daemonListen' => 'required', + ]; + + /** + * @var array + */ + protected static $dataIntegrityRules = [ + 'name' => 'regex:/^([\w .-]{1,100})$/', + 'location_id' => 'exists:locations,id', + 'public' => 'boolean', + 'fqdn' => 'string', + 'behind_proxy' => 'boolean', + 'memory' => 'numeric|min:1', + 'memory_overallocate' => 'numeric|min:-1', + 'disk' => 'numeric|min:1', + 'disk_overallocate' => 'numeric|min:-1', + 'daemonBase' => 'regex:/^([\/][\d\w.\-\/]+)$/', + 'daemonSFTP' => 'numeric|between:1024,65535', + 'daemonListen' => 'numeric|between:1024,65535', + ]; + + /** + * Default values for specific columns that are generally not changed on base installs. + * + * @var array + */ + protected $attributes = [ + 'public' => true, + 'behind_proxy' => false, + 'memory_overallocate' => 0, + 'disk_overallocate' => 0, + 'daemonBase' => '/srv/daemon-data', + 'daemonSFTP' => 2022, + 'daemonListen' => 8080, + ]; /** * Return an instance of the Guzzle client for this specific node. * - * @param array $headers + * @param array $headers * @return \GuzzleHttp\Client */ public function guzzleClient($headers = []) @@ -112,7 +159,7 @@ class Node extends Model /** * Returns the configuration in JSON format. * - * @param bool $pretty + * @param bool $pretty * @return string */ public function getConfigurationAsJson($pretty = false) diff --git a/app/Models/Server.php b/app/Models/Server.php index 5a147c3e9..988712014 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -67,6 +67,9 @@ class Server extends Model implements ValidableContract */ protected $guarded = ['id', 'installed', 'created_at', 'updated_at', 'deleted_at']; + /** + * @var array + */ protected static $applicationRules = [ 'owner_id' => 'required', 'name' => 'required', @@ -83,6 +86,9 @@ class Server extends Model implements ValidableContract 'skip_scripts' => 'sometimes', ]; + /** + * @var array + */ protected static $dataIntegrityRules = [ 'owner_id' => 'exists:users,id', 'name' => 'regex:/^([\w .-]{1,200})$/', @@ -132,22 +138,15 @@ class Server extends Model implements ValidableContract * * @var array */ - protected $searchable = [ - 'columns' => [ - 'servers.name' => 10, - 'servers.username' => 10, - 'servers.uuidShort' => 9, - 'servers.uuid' => 8, - 'packs.name' => 7, - 'users.email' => 6, - 'users.username' => 6, - 'nodes.name' => 2, - ], - 'joins' => [ - 'packs' => ['packs.id', 'servers.pack_id'], - 'users' => ['users.id', 'servers.owner_id'], - 'nodes' => ['nodes.id', 'servers.node_id'], - ], + protected $searchableColumns = [ + 'name' => 10, + 'username' => 10, + 'uuidShort' => 9, + 'uuid' => 8, + 'pack.name' => 7, + 'user.email' => 6, + 'user.username' => 6, + 'node.name' => 2, ]; /** @@ -155,10 +154,11 @@ class Server extends Model implements ValidableContract * DO NOT USE THIS TO MODIFY SERVER DETAILS OR SAVE THOSE DETAILS. * YOU WILL OVERWRITE THE SECRET KEY AND BREAK THINGS. * - * @param string $uuid - * @param array $with - * @param array $withCount + * @param string $uuid + * @param array $with + * @param array $withCount * @return \Pterodactyl\Models\Server + * @throws \Exception * @todo Remove $with and $withCount due to cache issues, they aren't used anyways. */ public static function byUuid($uuid, array $with = [], array $withCount = []) diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php index 07fc2d28c..38c163717 100644 --- a/app/Providers/RepositoryServiceProvider.php +++ b/app/Providers/RepositoryServiceProvider.php @@ -28,6 +28,8 @@ use Illuminate\Support\ServiceProvider; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; use Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; @@ -36,6 +38,8 @@ use Pterodactyl\Contracts\Repository\OptionVariableRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; +use Pterodactyl\Repositories\Daemon\ConfigurationRepository; +use Pterodactyl\Repositories\Daemon\ServerRepository as DaemonServerRepository; use Pterodactyl\Repositories\Eloquent\AllocationRepository; use Pterodactyl\Repositories\Eloquent\ApiKeyRepository; use Pterodactyl\Repositories\Eloquent\ApiPermissionRepository; @@ -71,9 +75,7 @@ class RepositoryServiceProvider extends ServiceProvider $this->app->bind(UserRepositoryInterface::class, UserRepository::class); // Daemon Repositories - $this->app->bind( - \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface::class, - \Pterodactyl\Repositories\Daemon\ServerRepository::class - ); + $this->app->bind(ConfigurationRepositoryInterface::class, ConfigurationRepository::class); + $this->app->bind(DaemonServerRepositoryInterface::class, DaemonServerRepository::class); } } diff --git a/app/Repositories/Daemon/BaseRepository.php b/app/Repositories/Daemon/BaseRepository.php index c56b2e428..8a637e9f2 100644 --- a/app/Repositories/Daemon/BaseRepository.php +++ b/app/Repositories/Daemon/BaseRepository.php @@ -51,6 +51,7 @@ class BaseRepository implements BaseRepositoryInterface public function setNode($id) { + // @todo accept a model $this->node = $this->nodeRepository->find($id); return $this; diff --git a/app/Repositories/Daemon/ConfigurationRepository.php b/app/Repositories/Daemon/ConfigurationRepository.php new file mode 100644 index 000000000..14f9436d9 --- /dev/null +++ b/app/Repositories/Daemon/ConfigurationRepository.php @@ -0,0 +1,63 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Pterodactyl\Repositories\Daemon; + +use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface; + +class ConfigurationRepository extends BaseRepository implements ConfigurationRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function update(array $overrides = []) + { + $node = $this->getNode(); + $structure = [ + 'web' => [ + 'listen' => $node->daemonListen, + 'ssl' => [ + 'enabled' => (! $node->behind_proxy && $node->scheme === 'https'), + ], + ], + 'sftp' => [ + 'path' => $node->daemonBase, + 'port' => $node->daemonSFTP, + ], + 'remote' => [ + 'base' => $this->config->get('app.url'), + ], + 'uploads' => [ + 'size_limit' => $node->upload_size, + ], + 'keys' => [ + $node->daemonSecret, + ], + ]; + + return $this->getHttpClient()->request('PATCH', '/config', [ + 'json' => array_merge($structure, $overrides), + ]); + } +} diff --git a/app/Repositories/Eloquent/EloquentRepository.php b/app/Repositories/Eloquent/EloquentRepository.php index fa39848a9..c73f0935c 100644 --- a/app/Repositories/Eloquent/EloquentRepository.php +++ b/app/Repositories/Eloquent/EloquentRepository.php @@ -105,6 +105,14 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf return $instance; } + /** + * {@inheritdoc}. + */ + public function findCountWhere(array $fields) + { + return $this->getBuilder()->where($fields)->count($this->getColumns()); + } + /** * {@inheritdoc} */ diff --git a/app/Repositories/Eloquent/LocationRepository.php b/app/Repositories/Eloquent/LocationRepository.php index 50d400730..0c04f39ea 100644 --- a/app/Repositories/Eloquent/LocationRepository.php +++ b/app/Repositories/Eloquent/LocationRepository.php @@ -28,8 +28,9 @@ use Pterodactyl\Models\Location; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; +use Pterodactyl\Repositories\Eloquent\Attributes\SearchableRepository; -class LocationRepository extends EloquentRepository implements LocationRepositoryInterface +class LocationRepository extends SearchableRepository implements LocationRepositoryInterface { /** * @var string @@ -44,21 +45,6 @@ class LocationRepository extends EloquentRepository implements LocationRepositor return Location::class; } - /** - * {@inheritdoc} - */ - public function search($term) - { - if (empty($term)) { - return $this; - } - - $clone = clone $this; - $clone->searchTerm = $term; - - return $clone; - } - /** * {@inheritdoc} */ diff --git a/app/Repositories/Eloquent/NodeRepository.php b/app/Repositories/Eloquent/NodeRepository.php index 7a53ddac5..2e18b1d4a 100644 --- a/app/Repositories/Eloquent/NodeRepository.php +++ b/app/Repositories/Eloquent/NodeRepository.php @@ -25,6 +25,7 @@ namespace Pterodactyl\Repositories\Eloquent; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Models\Node; use Pterodactyl\Repositories\Eloquent\Attributes\SearchableRepository; @@ -38,6 +39,104 @@ class NodeRepository extends SearchableRepository implements NodeRepositoryInter return Node::class; } + /** + * {@inheritdoc} + */ + public function getUsageStats($id) + { + $node = $this->getBuilder()->select( + 'nodes.disk_overallocate', 'nodes.memory_overallocate', 'nodes.disk', 'nodes.memory', + $this->getBuilder()->raw('SUM(servers.memory) as sum_memory, SUM(servers.disk) as sum_disk') + )->join('servers', 'servers.node_id', '=', 'nodes.id') + ->where('nodes.id', $id) + ->first(); + + return collect(['disk' => $node->sum_disk, 'memory' => $node->sum_memory]) + ->mapWithKeys(function ($value, $key) use ($node) { + $maxUsage = $node->{$key}; + if ($node->{$key . '_overallocate'} > 0) { + $maxUsage = $node->{$key} * (1 + ($node->{$key . '_overallocate'} / 100)); + } + + $percent = ($value / $maxUsage) * 100; + + return [ + $key => [ + 'value' => number_format($value), + 'max' => number_format($maxUsage), + 'percent' => $percent, + 'css' => ($percent <= 75) ? 'green' : (($percent > 90) ? 'red' : 'yellow'), + ], + ]; + }) + ->toArray(); + } + + /** + * {@inheritdoc} + */ + public function getNodeListingData($count = 25) + { + $instance = $this->getBuilder()->with('location')->withCount('servers'); + + if ($this->searchTerm) { + $instance->search($this->searchTerm); + } + + return $instance->paginate($count, $this->getColumns()); + } + + /** + * {@inheritdoc} + */ + public function getSingleNode($id) + { + $instance = $this->getBuilder()->with('location')->withCount('servers')->find($id, $this->getColumns()); + + if (! $instance) { + throw new RecordNotFoundException(); + } + + return $instance; + } + + /** + * {@inheritdoc} + */ + public function getNodeAllocations($id) + { + $instance = $this->getBuilder()->find($id, $this->getColumns()); + + if (! $instance) { + throw new RecordNotFoundException(); + } + + $instance->setRelation( + 'allocations', + $this->getModel()->allocations()->orderBy('ip', 'asc') + ->orderBy('port', 'asc') + ->with('server') + ->paginate(50) + ); + + return $instance; + } + + /** + * {@inheritdoc} + */ + public function getNodeServers($id) + { + $instance = $this->getBuilder()->with('servers.user', 'servers.service', 'servers.option') + ->find($id, $this->getColumns()); + + if (! $instance) { + throw new RecordNotFoundException(); + } + + return $instance; + } + /** * {@inheritdoc} */ diff --git a/app/Repositories/Old/APIRepository.php b/app/Repositories/Old/APIRepository.php deleted file mode 100644 index 10af25155..000000000 --- a/app/Repositories/Old/APIRepository.php +++ /dev/null @@ -1,207 +0,0 @@ -. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -namespace Pterodactyl\Repositories; - -use DB; -use Auth; -use Crypt; -use Validator; -use IPTools\Network; -use Pterodactyl\Models\User; -use Pterodactyl\Models\APIKey as Key; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Models\APIPermission as Permission; -use Pterodactyl\Exceptions\DisplayValidationException; - -class APIRepository -{ - /** - * Holder for listing of allowed IPs when creating a new key. - * - * @var array - */ - protected $allowed = []; - - /** - * The eloquent model for a user. - * - * @var \Pterodactyl\Models\User - */ - protected $user; - - /** - * Constructor for API Repository. - * - * @param null|\Pterodactyl\Models\User $user - * @return void - */ - public function __construct(User $user = null) - { - $this->user = is_null($user) ? Auth::user() : $user; - if (is_null($this->user)) { - throw new \Exception('Unable to initialize user for API repository instance.'); - } - } - - /** - * Create a New API Keypair on the system. - * - * @param array $data - * @return string - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function create(array $data) - { - $validator = Validator::make($data, [ - 'memo' => 'string|max:500', - 'allowed_ips' => 'sometimes|string', - 'permissions' => 'sometimes|required|array', - 'admin_permissions' => 'sometimes|required|array', - ]); - - $validator->after(function ($validator) use ($data) { - if (array_key_exists('allowed_ips', $data) && ! empty($data['allowed_ips'])) { - foreach (explode("\n", $data['allowed_ips']) as $ip) { - $ip = trim($ip); - try { - Network::parse($ip); - array_push($this->allowed, $ip); - } catch (\Exception $ex) { - $validator->errors()->add('allowed_ips', 'Could not parse IP <' . $ip . '> because it is in an invalid format.'); - } - } - } - }); - - // Run validator, throw catchable and displayable exception if it fails. - // Exception includes a JSON result of failed validation rules. - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - DB::beginTransaction(); - try { - $secretKey = str_random(16) . '.' . str_random(7) . '.' . str_random(7); - $key = Key::create([ - 'user_id' => $this->user->id, - 'public' => str_random(16), - 'secret' => Crypt::encrypt($secretKey), - 'allowed_ips' => empty($this->allowed) ? null : json_encode($this->allowed), - 'memo' => $data['memo'], - 'expires_at' => null, - ]); - - $totalPermissions = 0; - $pNodes = Permission::permissions(); - - if (isset($data['permissions'])) { - foreach ($data['permissions'] as $permission) { - $parts = explode('-', $permission); - - if (count($parts) !== 2) { - continue; - } - - list($block, $search) = $parts; - - if (! array_key_exists($block, $pNodes['_user'])) { - continue; - } - - if (! in_array($search, $pNodes['_user'][$block])) { - continue; - } - - $totalPermissions++; - Permission::create([ - 'key_id' => $key->id, - 'permission' => 'user.' . $permission, - ]); - } - } - - if ($this->user->isRootAdmin() && isset($data['admin_permissions'])) { - unset($pNodes['_user']); - - foreach ($data['admin_permissions'] as $permission) { - $parts = explode('-', $permission); - - if (count($parts) !== 2) { - continue; - } - - list($block, $search) = $parts; - - if (! array_key_exists($block, $pNodes)) { - continue; - } - - if (! in_array($search, $pNodes[$block])) { - continue; - } - - $totalPermissions++; - Permission::create([ - 'key_id' => $key->id, - 'permission' => $permission, - ]); - } - } - - if ($totalPermissions < 1) { - throw new DisplayException('No valid permissions were passed.'); - } - - DB::commit(); - - return $secretKey; - } catch (\Exception $ex) { - DB::rollBack(); - throw $ex; - } - } - - /** - * Revokes an API key and associated permissions. - * - * @param string $key - * @return void - * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException - */ - public function revoke($key) - { - DB::transaction(function () use ($key) { - $model = Key::with('permissions')->where('public', $key)->where('user_id', $this->user->id)->firstOrFail(); - foreach ($model->permissions as &$permission) { - $permission->delete(); - } - - $model->delete(); - }); - } -} diff --git a/app/Repositories/Old/DatabaseRepository.php b/app/Repositories/Old/DatabaseRepository.php deleted file mode 100644 index 1e4bc75af..000000000 --- a/app/Repositories/Old/DatabaseRepository.php +++ /dev/null @@ -1,173 +0,0 @@ -. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -namespace Pterodactyl\Repositories; - -use DB; -use Crypt; -use Validator; -use Pterodactyl\Models\Server; -use Pterodactyl\Models\Database; -use Pterodactyl\Models\DatabaseHost; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\DisplayValidationException; - -class DatabaseRepository -{ - /** - * Adds a new database to a specified database host server. - * - * @param int $id - * @param array $data - * @return \Pterodactyl\Models\Database - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function create($id, array $data) - { - $server = Server::findOrFail($id); - - $validator = Validator::make($data, [ - 'host' => 'required|exists:database_hosts,id', - 'database' => 'required|regex:/^\w{1,100}$/', - 'connection' => 'required|regex:/^[0-9%.]{1,15}$/', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - $host = DatabaseHost::findOrFail($data['host']); - DB::beginTransaction(); - - try { - $database = Database::firstOrNew([ - 'server_id' => $server->id, - 'database_host_id' => $data['host'], - 'database' => sprintf('s%d_%s', $server->id, $data['database']), - ]); - - if ($database->exists) { - throw new DisplayException('A database with those details already exists in the system.'); - } - - $database->username = sprintf('s%d_%s', $server->id, str_random(10)); - $database->remote = $data['connection']; - $database->password = Crypt::encrypt(str_random(20)); - - $database->save(); - } catch (\Exception $ex) { - DB::rollBack(); - throw $ex; - } - - try { - $host->setDynamicConnection(); - - DB::connection('dynamic')->statement(sprintf('CREATE DATABASE IF NOT EXISTS `%s`', $database->database)); - DB::connection('dynamic')->statement(sprintf( - 'CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\'', - $database->username, $database->remote, Crypt::decrypt($database->password) - )); - DB::connection('dynamic')->statement(sprintf( - 'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX ON `%s`.* TO `%s`@`%s`', - $database->database, $database->username, $database->remote - )); - - DB::connection('dynamic')->statement('FLUSH PRIVILEGES'); - - // Save Everything - DB::commit(); - - return $database; - } catch (\Exception $ex) { - try { - DB::connection('dynamic')->statement(sprintf('DROP DATABASE IF EXISTS `%s`', $database->database)); - DB::connection('dynamic')->statement(sprintf('DROP USER IF EXISTS `%s`@`%s`', $database->username, $database->remote)); - DB::connection('dynamic')->statement('FLUSH PRIVILEGES'); - } catch (\Exception $ex) { - } - - DB::rollBack(); - throw $ex; - } - } - - /** - * Updates the password for a given database. - * - * @param int $id - * @param string $password - * @return void - * - * @todo Fix logic behind resetting passwords. - */ - public function password($id, $password) - { - $database = Database::with('host')->findOrFail($id); - $database->host->setDynamicConnection(); - - DB::transaction(function () use ($database, $password) { - $database->password = Crypt::encrypt($password); - - // We have to do the whole delete user, create user thing rather than - // SET PASSWORD ... because MariaDB and PHP statements ends up inserting - // a corrupted password. A way around this is strtoupper(sha1(sha1($password, true))) - // but no garuntees that will work correctly with every system. - DB::connection('dynamic')->statement(sprintf('DROP USER IF EXISTS `%s`@`%s`', $database->username, $database->remote)); - DB::connection('dynamic')->statement(sprintf( - 'CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\'', - $database->username, $database->remote, $password - )); - DB::connection('dynamic')->statement(sprintf( - 'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX ON `%s`.* TO `%s`@`%s`', - $database->database, $database->username, $database->remote - )); - DB::connection('dynamic')->statement('FLUSH PRIVILEGES'); - - $database->save(); - }); - } - - /** - * Drops a database from the associated database host. - * - * @param int $id - * @return void - */ - public function drop($id) - { - $database = Database::with('host')->findOrFail($id); - $database->host->setDynamicConnection(); - - DB::transaction(function () use ($database) { - DB::connection('dynamic')->statement(sprintf('DROP DATABASE IF EXISTS `%s`', $database->database)); - DB::connection('dynamic')->statement(sprintf('DROP USER IF EXISTS `%s`@`%s`', $database->username, $database->remote)); - DB::connection('dynamic')->statement('FLUSH PRIVILEGES'); - - $database->delete(); - }); - } -} diff --git a/app/Repositories/Old/LocationRepository.php b/app/Repositories/Old/LocationRepository.php deleted file mode 100644 index 5f08cfc17..000000000 --- a/app/Repositories/Old/LocationRepository.php +++ /dev/null @@ -1,104 +0,0 @@ -. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -namespace Pterodactyl\Repositories; - -use Validator; -use Pterodactyl\Models\Location; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\DisplayValidationException; - -class LocationRepository -{ - /** - * Creates a new location on the system. - * - * @param array $data - * @return \Pterodactyl\Models\Location - * - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function create(array $data) - { - $validator = Validator::make($data, [ - 'short' => 'required|string|between:1,60|unique:locations,short', - 'long' => 'required|string|between:1,255', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - return Location::create([ - 'long' => $data['long'], - 'short' => $data['short'], - ]); - } - - /** - * Modifies a location. - * - * @param int $id - * @param array $data - * @return \Pterodactyl\Models\Location - * - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function update($id, array $data) - { - $location = Location::findOrFail($id); - - $validator = Validator::make($data, [ - 'short' => 'sometimes|required|string|between:1,60|unique:locations,short,' . $location->id, - 'long' => 'sometimes|required|string|between:1,255', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - $location->fill($data)->save(); - - return $location; - } - - /** - * Deletes a location from the system. - * - * @param int $id - * @return void - * - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function delete($id) - { - $location = Location::withCount('nodes')->findOrFail($id); - - if ($location->nodes_count > 0) { - throw new DisplayException('Cannot delete a location that has nodes assigned to it.'); - } - - $location->delete(); - } -} diff --git a/app/Services/Nodes/CreationService.php b/app/Services/Nodes/CreationService.php new file mode 100644 index 000000000..b33817f32 --- /dev/null +++ b/app/Services/Nodes/CreationService.php @@ -0,0 +1,62 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Pterodactyl\Services\Nodes; + +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; + +class CreationService +{ + const DAEMON_SECRET_LENGTH = 18; + + /** + * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + */ + protected $repository; + + /** + * CreationService constructor. + * + * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository + */ + public function __construct(NodeRepositoryInterface $repository) + { + $this->repository = $repository; + } + + /** + * Create a new node on the panel. + * + * @param array $data + * @return \Pterodactyl\Models\Node + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function handle(array $data) + { + $data['daemonSecret'] = bin2hex(random_bytes(self::DAEMON_SECRET_LENGTH)); + + return $this->repository->create($data); + } +} diff --git a/app/Services/Nodes/DeletionService.php b/app/Services/Nodes/DeletionService.php new file mode 100644 index 000000000..1b57c5915 --- /dev/null +++ b/app/Services/Nodes/DeletionService.php @@ -0,0 +1,88 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Pterodactyl\Services\Nodes; + +use Illuminate\Contracts\Translation\Translator; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\Node; + +class DeletionService +{ + /** + * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + + /** + * @var \Illuminate\Contracts\Translation\Translator + */ + protected $translator; + + /** + * DeletionService constructor. + * + * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $serverRepository + * @param \Illuminate\Contracts\Translation\Translator $translator + */ + public function __construct( + NodeRepositoryInterface $repository, + ServerRepositoryInterface $serverRepository, + Translator $translator + ) { + $this->repository = $repository; + $this->serverRepository = $serverRepository; + $this->translator = $translator; + } + + /** + * Delete a node from the panel if no servers are attached to it. + * + * @param int|\Pterodactyl\Models\Node $node + * @return bool|null + * + * @throws \Pterodactyl\Exceptions\DisplayException + */ + public function handle($node) + { + if ($node instanceof Node) { + $node = $node->id; + } + + $servers = $this->serverRepository->withColumns('id')->findCountWhere([['node_id', '=', $node]]); + if ($servers > 0) { + throw new DisplayException($this->translator->trans('admin/exceptions.node.servers_attached')); + } + + return $this->repository->delete($node); + } +} diff --git a/app/Services/Nodes/UpdateService.php b/app/Services/Nodes/UpdateService.php new file mode 100644 index 000000000..583367931 --- /dev/null +++ b/app/Services/Nodes/UpdateService.php @@ -0,0 +1,104 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Pterodactyl\Services\Nodes; + +use GuzzleHttp\Exception\RequestException; +use Illuminate\Log\Writer; +use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\Node; + +class UpdateService +{ + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface + */ + protected $configRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + */ + protected $repository; + + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + + /** + * UpdateService constructor. + * + * @param \Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface $configurationRepository + * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository + * @param \Illuminate\Log\Writer $writer + */ + public function __construct( + ConfigurationRepositoryInterface $configurationRepository, + NodeRepositoryInterface $repository, + Writer $writer + ) { + $this->configRepository = $configurationRepository; + $this->repository = $repository; + $this->writer = $writer; + } + + /** + * Update the configuration values for a given node on the machine. + * + * @param int|\Pterodactyl\Models\Node $node + * @param array $data + * @return mixed + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function handle($node, array $data) + { + if (! $node instanceof Node) { + $node = $this->repository->find($node); + } + + if (! is_null(array_get($data, 'reset_secret'))) { + $data['daemonSecret'] = bin2hex(random_bytes(CreationService::DAEMON_SECRET_LENGTH)); + unset($data['reset_secret']); + } + + $updateResponse = $this->repository->withoutFresh()->update($node->id, $data); + + try { + $this->configRepository->setNode($node->id)->setAccessToken($node->daemonSecret)->update(); + } catch (RequestException $exception) { + $response = $exception->getResponse(); + $this->writer->warning($exception); + + throw new DisplayException(trans('admin/exceptions.node.daemon_off_config_updated', [ + 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), + ])); + } + + return $updateResponse; + } +} diff --git a/app/Services/Users/DeletionService.php b/app/Services/Users/DeletionService.php index 5bf6a5b01..3d3077859 100644 --- a/app/Services/Users/DeletionService.php +++ b/app/Services/Users/DeletionService.php @@ -74,15 +74,15 @@ class DeletionService */ public function handle($user) { - if (! $user instanceof User) { - $user = $this->repository->find($user); + if ($user instanceof User) { + $user = $user->id; } - $servers = $this->serverRepository->findWhere([['owner_id', '=', $user->id]]); + $servers = $this->serverRepository->findWhere([['owner_id', '=', $user]]); if (count($servers) > 0) { throw new DisplayException($this->translator->trans('admin/user.exceptions.user_has_servers')); } - return $this->repository->delete($user->id); + return $this->repository->delete($user); } } diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index e517d5801..c16652f02 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -15,7 +15,7 @@ $factory->define(Pterodactyl\Models\Server::class, function (Faker\Generator $faker) { return [ - 'id' => $faker->randomNumber(), + 'id' => $faker->unique()->randomNumber(), 'uuid' => $faker->uuid, 'uuidShort' => str_random(8), 'name' => $faker->firstName, @@ -40,7 +40,7 @@ $factory->define(Pterodactyl\Models\Server::class, function (Faker\Generator $fa $factory->define(Pterodactyl\Models\User::class, function (Faker\Generator $faker) { return [ - 'id' => $faker->randomNumber(), + 'id' => $faker->unique()->randomNumber(), 'external_id' => null, 'uuid' => $faker->uuid, 'username' => $faker->userName, @@ -56,19 +56,21 @@ $factory->define(Pterodactyl\Models\User::class, function (Faker\Generator $fake $factory->state(Pterodactyl\Models\User::class, 'admin', function () { return [ - 'root_admin' => true, + 'root_admin' => true, ]; }); $factory->define(Pterodactyl\Models\Location::class, function (Faker\Generator $faker) { return [ - 'short' => $faker->domainWord, - 'long' => $faker->catchPhrase, - ]; + 'id' => $faker->unique()->randomNumber(), + 'short' => $faker->domainWord, + 'long' => $faker->catchPhrase, + ]; }); $factory->define(Pterodactyl\Models\Node::class, function (Faker\Generator $faker) { return [ + 'id' => $faker->unique()->randomNumber(), 'public' => true, 'name' => $faker->firstName, 'fqdn' => $faker->ipv4, @@ -88,7 +90,7 @@ $factory->define(Pterodactyl\Models\Node::class, function (Faker\Generator $fake $factory->define(Pterodactyl\Models\ServiceVariable::class, function (Faker\Generator $faker) { return [ - 'id' => $faker->randomNumber(), + 'id' => $faker->unique()->randomNumber(), 'name' => $faker->firstName, 'description' => $faker->sentence(), 'env_variable' => strtoupper(str_replace(' ', '_', $faker->words(2, true))), @@ -98,7 +100,7 @@ $factory->define(Pterodactyl\Models\ServiceVariable::class, function (Faker\Gene 'rules' => 'required|string', 'created_at' => \Carbon\Carbon::now(), 'updated_at' => \Carbon\Carbon::now(), - ]; + ]; }); $factory->state(Pterodactyl\Models\ServiceVariable::class, 'viewable', function () { diff --git a/database/migrations/2017_08_05_115800_CascadeNullValuesForDatabaseHostWhenNodeIsDeleted.php b/database/migrations/2017_08_05_115800_CascadeNullValuesForDatabaseHostWhenNodeIsDeleted.php new file mode 100644 index 000000000..137384a8d --- /dev/null +++ b/database/migrations/2017_08_05_115800_CascadeNullValuesForDatabaseHostWhenNodeIsDeleted.php @@ -0,0 +1,34 @@ +dropForeign(['node_id']); + $table->foreign('node_id')->references('id')->on('nodes')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('database_hosts', function (Blueprint $table) { + $table->dropForeign(['node_id']); + $table->foreign('node_id')->references('id')->on('nodes'); + }); + } +} diff --git a/database/migrations/2017_08_05_144104_AllowNegativeValuesForOverallocation.php b/database/migrations/2017_08_05_144104_AllowNegativeValuesForOverallocation.php new file mode 100644 index 000000000..60eadcafc --- /dev/null +++ b/database/migrations/2017_08_05_144104_AllowNegativeValuesForOverallocation.php @@ -0,0 +1,34 @@ +integer('disk_overallocate')->default(0)->nullable(false)->change(); + $table->integer('memory_overallocate')->default(0)->nullable(false)->change(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('nodes', function (Blueprint $table) { + $table->mediumInteger('disk_overallocate')->unsigned()->nullable()->change(); + $table->mediumInteger('memory_overallocate')->unsigned()->nullable()->change(); + }); + } +} diff --git a/resources/lang/en/admin/exceptions.php b/resources/lang/en/admin/exceptions.php new file mode 100644 index 000000000..1a5bcaa37 --- /dev/null +++ b/resources/lang/en/admin/exceptions.php @@ -0,0 +1,31 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +return [ + 'daemon_connection_failed' => 'There was an exception while attempting to communicate with the daemon resulting in a HTTP/:code response code. This exception has been logged.', + 'node' => [ + 'servers_attached' => 'A node must have no servers linked to it in order to be deleted.', + 'daemon_off_config_updated' => 'The daemon configuration has been updated, however there was an error encountered while attempting to automatically update the configuration file on the Daemon. You will need to manually update the configuration file (core.json) for the daemon to apply these changes. The daemon responded with a HTTP/:code response code and the error has been logged.', + ], +]; diff --git a/resources/lang/en/admin/node.php b/resources/lang/en/admin/node.php new file mode 100644 index 000000000..fc5b0b1ca --- /dev/null +++ b/resources/lang/en/admin/node.php @@ -0,0 +1,36 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +return [ + 'validation' => [ + 'fqdn_not_resolvable' => 'The FQDN or IP address provided does not resolve to a valid IP address.', + 'fqdn_required_for_ssl' => 'A fully qualified domain name that resolves to a public IP address is required in order to use SSL for this node.', + ], + 'notices' => [ + 'node_deleted' => 'Node has been successfully removed from the panel.', + 'location_required' => 'You must have at least one location configured before you can add a node to this panel.', + 'node_created' => 'Successfully created new node. You can automatically configure the daemon on this machine by visiting the \'Configuration\' tab. Before you can add any servers you must first allocate at least one IP address and port.', + 'node_updated' => 'Node information has been updated. If any daemon settings were changed you will need to reboot it for those changes to take effect.', + ], +]; diff --git a/resources/themes/pterodactyl/admin/nodes/view/settings.blade.php b/resources/themes/pterodactyl/admin/nodes/view/settings.blade.php index b0624af28..a5b50d40d 100644 --- a/resources/themes/pterodactyl/admin/nodes/view/settings.blade.php +++ b/resources/themes/pterodactyl/admin/nodes/view/settings.blade.php @@ -218,6 +218,7 @@
diff --git a/routes/admin.php b/routes/admin.php index 157109c13..31bc47a3a 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -137,21 +137,22 @@ Route::group(['prefix' => 'servers'], function () { Route::group(['prefix' => 'nodes'], function () { Route::get('/', 'NodesController@index')->name('admin.nodes'); Route::get('/new', 'NodesController@create')->name('admin.nodes.new'); - Route::get('/view/{id}', 'NodesController@viewIndex')->name('admin.nodes.view'); - Route::get('/view/{id}/settings', 'NodesController@viewSettings')->name('admin.nodes.view.settings'); - Route::get('/view/{id}/configuration', 'NodesController@viewConfiguration')->name('admin.nodes.view.configuration'); - Route::get('/view/{id}/allocation', 'NodesController@viewAllocation')->name('admin.nodes.view.allocation'); - Route::get('/view/{id}/servers', 'NodesController@viewServers')->name('admin.nodes.view.servers'); - Route::get('/view/{id}/settings/token', 'NodesController@setToken')->name('admin.nodes.view.configuration.token'); + Route::get('/view/{node}', 'NodesController@viewIndex')->name('admin.nodes.view'); + Route::get('/view/{node}/settings', 'NodesController@viewSettings')->name('admin.nodes.view.settings'); + Route::get('/view/{node}/configuration', 'NodesController@viewConfiguration')->name('admin.nodes.view.configuration'); + Route::get('/view/{node}/allocation', 'NodesController@viewAllocation')->name('admin.nodes.view.allocation'); + Route::get('/view/{node}/servers', 'NodesController@viewServers')->name('admin.nodes.view.servers'); + Route::get('/view/{node}/settings/token', 'NodesController@setToken')->name('admin.nodes.view.configuration.token'); Route::post('/new', 'NodesController@store'); - Route::post('/view/{id}/settings', 'NodesController@updateSettings'); - Route::post('/view/{id}/allocation', 'NodesController@createAllocation'); - Route::post('/view/{id}/allocation/remove', 'NodesController@allocationRemoveBlock')->name('admin.nodes.view.allocation.removeBlock'); - Route::post('/view/{id}/allocation/alias', 'NodesController@allocationSetAlias')->name('admin.nodes.view.allocation.setAlias'); + Route::post('/view/{node}/allocation', 'NodesController@createAllocation'); + Route::post('/view/{node}/allocation/remove', 'NodesController@allocationRemoveBlock')->name('admin.nodes.view.allocation.removeBlock'); + Route::post('/view/{node}/allocation/alias', 'NodesController@allocationSetAlias')->name('admin.nodes.view.allocation.setAlias'); - Route::delete('/view/{id}/delete', 'NodesController@delete')->name('admin.nodes.view.delete'); - Route::delete('/view/{id}/allocation/remove/{allocation}', 'NodesController@allocationRemoveSingle')->name('admin.nodes.view.allocation.removeSingle'); + Route::patch('/view/{node}/settings', 'NodesController@updateSettings'); + + Route::delete('/view/{node}/delete', 'NodesController@delete')->name('admin.nodes.view.delete'); + Route::delete('/view/{node}/allocation/remove/{allocation}', 'NodesController@allocationRemoveSingle')->name('admin.nodes.view.allocation.removeSingle'); }); /* diff --git a/tests/TestCase.php b/tests/TestCase.php index d8c7f6ff2..664e4a9c3 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,7 +2,6 @@ namespace Tests; -use Mockery as m; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase @@ -12,6 +11,5 @@ abstract class TestCase extends BaseTestCase public function setUp() { parent::setUp(); - m::close(); } } diff --git a/tests/Unit/Services/Nodes/CreationServiceTest.php b/tests/Unit/Services/Nodes/CreationServiceTest.php new file mode 100644 index 000000000..5932e7095 --- /dev/null +++ b/tests/Unit/Services/Nodes/CreationServiceTest.php @@ -0,0 +1,74 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Tests\Unit\Services\Nodes; + +use Mockery as m; +use phpmock\phpunit\PHPMock; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Services\Nodes\CreationService; +use Tests\TestCase; + +class CreationServiceTest extends TestCase +{ + use PHPMock; + + /** + * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Nodes\CreationService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->repository = m::mock(NodeRepositoryInterface::class); + + $this->service = new CreationService($this->repository); + } + + /** + * Test that a node is created and a daemon secret token is created. + */ + public function testNodeIsCreatedAndDaemonSecretIsGenerated() + { + $this->getFunctionMock('\\Pterodactyl\\Services\\Nodes', 'bin2hex') + ->expects($this->once())->willReturn('hexResult'); + + $this->repository->shouldReceive('create')->with([ + 'name' => 'NodeName', + 'daemonSecret' => 'hexResult', + ])->once()->andReturnNull(); + + $this->assertNull($this->service->handle(['name' => 'NodeName'])); + } +} diff --git a/tests/Unit/Services/Nodes/DeletionServiceTest.php b/tests/Unit/Services/Nodes/DeletionServiceTest.php new file mode 100644 index 000000000..266fbc379 --- /dev/null +++ b/tests/Unit/Services/Nodes/DeletionServiceTest.php @@ -0,0 +1,121 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Tests\Unit\Services\Nodes; + +use Illuminate\Contracts\Translation\Translator; +use Mockery as m; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Models\Node; +use Pterodactyl\Services\Nodes\DeletionService; +use Tests\TestCase; + +class DeletionServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + + /** + * @var \Illuminate\Contracts\Translation\Translator + */ + protected $translator; + + /** + * @var \Pterodactyl\Services\Nodes\DeletionService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->repository = m::mock(NodeRepositoryInterface::class); + $this->serverRepository = m::mock(ServerRepositoryInterface::class); + $this->translator = m::mock(Translator::class); + + $this->service = new DeletionService( + $this->repository, + $this->serverRepository, + $this->translator + ); + } + + /** + * Test that a node is deleted if there are no servers attached to it. + */ + public function testNodeIsDeletedIfNoServersAreAttached() + { + $this->serverRepository->shouldReceive('withColumns')->with('id')->once()->andReturnSelf() + ->shouldReceive('findCountWhere')->with([['node_id', '=', 1]])->once()->andReturn(0); + $this->repository->shouldReceive('delete')->with(1)->once()->andReturn(true); + + $this->assertTrue( + $this->service->handle(1), + 'Assert that deletion returns a positive boolean value.' + ); + } + + /** + * Test that an exception is thrown if servers are attached to the node. + * + * @expectedException \Pterodactyl\Exceptions\DisplayException + */ + public function testExceptionIsThrownIfServersAreAttachedToNode() + { + $this->serverRepository->shouldReceive('withColumns')->with('id')->once()->andReturnSelf() + ->shouldReceive('findCountWhere')->with([['node_id', '=', 1]])->once()->andReturn(1); + $this->translator->shouldReceive('trans')->with('admin/exceptions.node.servers_attached')->once()->andReturnNull(); + $this->repository->shouldNotReceive('delete'); + + $this->service->handle(1); + } + + /** + * Test that a model can be passed into the handle function rather than an ID. + */ + public function testModelCanBePassedToFunctionInPlaceOfNodeId() + { + $node = factory(Node::class)->make(); + + $this->serverRepository->shouldReceive('withColumns')->with('id')->once()->andReturnSelf() + ->shouldReceive('findCountWhere')->with([['node_id', '=', $node->id]])->once()->andReturn(0); + $this->repository->shouldReceive('delete')->with($node->id)->once()->andReturn(true); + + $this->assertTrue( + $this->service->handle($node->id), + 'Assert that deletion returns a positive boolean value.' + ); + } +} diff --git a/tests/Unit/Services/Nodes/UpdateServiceTest.php b/tests/Unit/Services/Nodes/UpdateServiceTest.php new file mode 100644 index 000000000..9bccf2d43 --- /dev/null +++ b/tests/Unit/Services/Nodes/UpdateServiceTest.php @@ -0,0 +1,182 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Tests\Unit\Services\Nodes; + +use Exception; +use GuzzleHttp\Exception\RequestException; +use Illuminate\Log\Writer; +use Mockery as m; +use phpmock\phpunit\PHPMock; +use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\Node; +use Pterodactyl\Services\Nodes\CreationService; +use Pterodactyl\Services\Nodes\UpdateService; +use Tests\TestCase; + +class UpdateServiceTest extends TestCase +{ + use PHPMock; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface + */ + protected $configRepository; + + /** + * @var \GuzzleHttp\Exception\RequestException + */ + protected $exception; + + /** + * @var \Pterodactyl\Models\Node + */ + protected $node; + + /** + * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Nodes\UpdateService + */ + protected $service; + + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->node = factory(Node::class)->make(); + + $this->configRepository = m::mock(ConfigurationRepositoryInterface::class); + $this->exception = m::mock(RequestException::class); + $this->repository = m::mock(NodeRepositoryInterface::class); + $this->writer = m::mock(Writer::class); + + $this->service = new UpdateService( + $this->configRepository, + $this->repository, + $this->writer + ); + } + + /** + * Test that the daemon secret is reset when `reset_secret` is passed in the data. + */ + public function testNodeIsUpdatedAndDaemonSecretIsReset() + { + $this->getFunctionMock('\\Pterodactyl\\Services\\Nodes', 'random_bytes') + ->expects($this->once())->willReturnCallback(function ($bytes) { + $this->assertEquals(CreationService::DAEMON_SECRET_LENGTH, $bytes); + + return '\00'; + }); + + $this->getFunctionMock('\\Pterodactyl\\Services\\Nodes', 'bin2hex') + ->expects($this->once())->willReturn('hexResponse'); + + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($this->node->id, [ + 'name' => 'NewName', + 'daemonSecret' => 'hexResponse', + ])->andReturn(true); + + $this->configRepository->shouldReceive('setNode')->with($this->node->id)->once()->andReturnSelf() + ->shouldReceive('setAccessToken')->with($this->node->daemonSecret)->once()->andReturnSelf() + ->shouldReceive('update')->withNoArgs()->once()->andReturnNull(); + + $this->assertTrue($this->service->handle($this->node, ['name' => 'NewName', 'reset_secret' => true])); + } + + /** + * Test that daemon secret is not modified when no variable is passed in data. + */ + public function testNodeIsUpdatedAndDaemonSecretIsNotChanged() + { + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($this->node->id, [ + 'name' => 'NewName', + ])->andReturn(true); + + $this->configRepository->shouldReceive('setNode')->with($this->node->id)->once()->andReturnSelf() + ->shouldReceive('setAccessToken')->with($this->node->daemonSecret)->once()->andReturnSelf() + ->shouldReceive('update')->withNoArgs()->once()->andReturnNull(); + + $this->assertTrue($this->service->handle($this->node, ['name' => 'NewName'])); + } + + /** + * Test that an exception caused by the daemon is handled properly. + */ + public function testExceptionCausedByDaemonIsHandled() + { + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($this->node->id, [ + 'name' => 'NewName', + ])->andReturn(true); + + $this->configRepository->shouldReceive('setNode')->with($this->node->id)->once()->andThrow($this->exception); + $this->writer->shouldReceive('warning')->with($this->exception)->once()->andReturnNull(); + $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('getStatusCode')->withNoArgs()->once()->andReturn(400); + + try { + $this->service->handle($this->node, ['name' => 'NewName']); + } catch (Exception $exception) { + $this->assertInstanceOf(DisplayException::class, $exception); + $this->assertEquals( + trans('admin/exceptions.node.daemon_off_config_updated', ['code' => 400]), $exception->getMessage() + ); + } + } + + /** + * Test that an ID can be passed in place of a model. + */ + public function testFunctionCanAcceptANodeIdInPlaceOfModel() + { + $this->repository->shouldReceive('find')->with($this->node->id)->once()->andReturn($this->node); + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($this->node->id, [ + 'name' => 'NewName', + ])->andReturn(true); + + $this->configRepository->shouldReceive('setNode')->with($this->node->id)->once()->andReturnSelf() + ->shouldReceive('setAccessToken')->with($this->node->daemonSecret)->once()->andReturnSelf() + ->shouldReceive('update')->withNoArgs()->once()->andReturnNull(); + + $this->assertTrue($this->service->handle($this->node->id, ['name' => 'NewName'])); + } +} diff --git a/tests/Unit/Services/Users/DeletionServiceTest.php b/tests/Unit/Services/Users/DeletionServiceTest.php index 6f21096e4..85f7400b8 100644 --- a/tests/Unit/Services/Users/DeletionServiceTest.php +++ b/tests/Unit/Services/Users/DeletionServiceTest.php @@ -85,7 +85,7 @@ class DeletionServiceTest extends TestCase $this->repository->shouldReceive('delete')->with($this->user->id)->once()->andReturn(true); $this->assertTrue( - $this->service->handle($this->user), + $this->service->handle($this->user->id), 'Assert that service responds true.' ); } @@ -100,20 +100,19 @@ class DeletionServiceTest extends TestCase $this->serverRepository->shouldReceive('findWhere')->with([['owner_id', '=', $this->user->id]])->once()->andReturn(['item']); $this->translator->shouldReceive('trans')->with('admin/user.exceptions.user_has_servers')->once()->andReturnNull(); - $this->service->handle($this->user); + $this->service->handle($this->user->id); } /** * Test that the function supports passing in a model or an ID. */ - public function testIntegerCanBePassedInPlaceOfUserModel() + public function testModelCanBePassedInPlaceOfUserId() { - $this->repository->shouldReceive('find')->with($this->user->id)->once()->andReturn($this->user); $this->serverRepository->shouldReceive('findWhere')->with([['owner_id', '=', $this->user->id]])->once()->andReturn([]); $this->repository->shouldReceive('delete')->with($this->user->id)->once()->andReturn(true); $this->assertTrue( - $this->service->handle($this->user->id), + $this->service->handle($this->user), 'Assert that service responds true.' ); }