Add support for node management actions using new services

This commit is contained in:
Dane Everitt 2017-08-05 17:20:07 -05:00
parent 4391defb9f
commit c1a078bdcf
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
33 changed files with 1375 additions and 745 deletions

View file

@ -0,0 +1,36 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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 = []);
}

View file

@ -28,6 +28,52 @@ use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface;
interface NodeRepositoryInterface extends RepositoryInterface, 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. * Return a collection of nodes beloning to a specific location for use on frontend display.
* *

View file

@ -116,6 +116,14 @@ interface RepositoryInterface
*/ */
public function findFirstWhere(array $fields); 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. * Update a given ID with the passed array of fields.
* *

View file

@ -132,6 +132,7 @@ class DatabaseController extends Controller
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
* *
* @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/ */
public function update(DatabaseHostFormRequest $request, DatabaseHost $host) public function update(DatabaseHostFormRequest $request, DatabaseHost $host)
{ {

View file

@ -24,48 +24,112 @@
namespace Pterodactyl\Http\Controllers\Admin; namespace Pterodactyl\Http\Controllers\Admin;
use DB; use Illuminate\Cache\Repository as CacheRepository;
use Illuminate\Contracts\Translation\Translator;
use Log; use Log;
use Alert; use Alert;
use Cache; use Cache;
use Javascript; 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 Illuminate\Http\Request;
use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Repositories\NodeRepository; use Pterodactyl\Repositories\NodeRepository;
use Pterodactyl\Exceptions\DisplayValidationException; use Pterodactyl\Exceptions\DisplayValidationException;
use Pterodactyl\Services\Nodes\CreationService;
use Pterodactyl\Services\Nodes\DeletionService;
use Pterodactyl\Services\Nodes\UpdateService;
class NodesController extends Controller 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. * Displays the index page listing all nodes on the panel.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View * @return \Illuminate\View\View
*/ */
public function index(Request $request) public function index(Request $request)
{ {
$nodes = Models\Node::with('location')->withCount('servers'); return view('admin.nodes.index', [
'nodes' => $this->repository->search($request->input('query'))->getNodeListingData(),
if (! is_null($request->input('query'))) { ]);
$nodes->search($request->input('query'));
}
return view('admin.nodes.index', ['nodes' => $nodes->paginate(25)]);
} }
/** /**
* Displays create new node page. * Displays create new node page.
* *
* @param \Illuminate\Http\Request $request * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
* @return \Illuminate\View\View|\Illuminate\Http\RedirectResponse
*/ */
public function create(Request $request) public function create()
{ {
$locations = Models\Location::all(); $locations = $this->locationRepository->all();
if (! $locations->count()) { if (count($locations) < 1) {
Alert::warning('You must add a location before you can add a new node.')->flash(); $this->alert->warning($this->translator->trans('admin/node.notices.location_required'))->flash();
return redirect()->route('admin.locations'); return redirect()->route('admin.locations');
} }
@ -76,117 +140,68 @@ class NodesController extends Controller
/** /**
* Post controller to create a new node on the system. * 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 * @return \Illuminate\Http\RedirectResponse
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/ */
public function store(Request $request) public function store(NodeFormRequest $request)
{ {
try { $node = $this->creationService->handle($request->normalize());
$repo = new NodeRepository; $this->alert->info($this->translator->trans('admin/node.notices.node_created'))->flash();
$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. <strong>Before you can add any servers you need to first assign some IP addresses and ports by adding an allocation.</strong>')->flash();
return redirect()->route('admin.nodes.view.allocation', $node->id); 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();
} }
/** /**
* Shows the index overview page for a specific node. * Shows the index overview page for a specific node.
* *
* @param \Illuminate\Http\Request $request * @param int $node
* @param int $id
* @return \Illuminate\View\View * @return \Illuminate\View\View
*/ */
public function viewIndex(Request $request, $id) public function viewIndex($node)
{ {
$node = Models\Node::with('location')->withCount('servers')->findOrFail($id); return view('admin.nodes.view.index', [
$stats = collect( 'node' => $this->repository->getSingleNode($node),
Models\Server::select( 'stats' => $this->repository->getUsageStats($node),
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]);
} }
/** /**
* Shows the settings page for a specific node. * Shows the settings page for a specific node.
* *
* @param \Illuminate\Http\Request $request * @param \Pterodactyl\Models\Node $node
* @param int $id
* @return \Illuminate\View\View * @return \Illuminate\View\View
*/ */
public function viewSettings(Request $request, $id) public function viewSettings(Node $node)
{ {
return view('admin.nodes.view.settings', [ return view('admin.nodes.view.settings', [
'node' => Models\Node::findOrFail($id), 'node' => $node,
'locations' => Models\Location::all(), 'locations' => $this->locationRepository->all(),
]); ]);
} }
/** /**
* Shows the configuration page for a specific node. * Shows the configuration page for a specific node.
* *
* @param \Illuminate\Http\Request $request * @param \Pterodactyl\Models\Node $node
* @param int $id
* @return \Illuminate\View\View * @return \Illuminate\View\View
*/ */
public function viewConfiguration(Request $request, $id) public function viewConfiguration(Node $node)
{ {
return view('admin.nodes.view.configuration', [ return view('admin.nodes.view.configuration', ['node' => $node]);
'node' => Models\Node::findOrFail($id),
]);
} }
/** /**
* Shows the allocation page for a specific node. * Shows the allocation page for a specific node.
* *
* @param \Illuminate\Http\Request $request * @param int $node
* @param int $id
* @return \Illuminate\View\View * @return \Illuminate\View\View
*/ */
public function viewAllocation(Request $request, $id) public function viewAllocation($node)
{ {
$node = Models\Node::findOrFail($id); $node = $this->repository->getNodeAllocations($node);
$node->setRelation('allocations', $node->allocations()->orderBy('ip', 'asc')->orderBy('port', 'asc')->with('server')->paginate(50)); Javascript::put(['node' => collect($node)->only(['id'])]);
Javascript::put([
'node' => collect($node)->only(['id']),
]);
return view('admin.nodes.view.allocation', ['node' => $node]); 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. * Shows the server listing page for a specific node.
* *
* @param \Illuminate\Http\Request $request * @param int $node
* @param int $id
* @return \Illuminate\View\View * @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([ Javascript::put([
'node' => collect($node->makeVisible('daemonSecret'))->only(['scheme', 'fqdn', 'daemonListen', 'daemonSecret']), 'node' => collect($node->makeVisible('daemonSecret'))->only(['scheme', 'fqdn', 'daemonListen', 'daemonSecret']),
]); ]);
return view('admin.nodes.view.servers', [ return view('admin.nodes.view.servers', ['node' => $node]);
'node' => $node,
]);
} }
/** /**
* Updates settings for a node. * Updates settings for a node.
* *
* @param \Illuminate\Http\Request $request * @param \Pterodactyl\Http\Requests\Admin\NodeFormRequest $request
* @param int $id * @param \Pterodactyl\Models\Node $node
* @return \Illuminate\Http\RedirectResponse * @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 { return redirect()->route('admin.nodes.view.settings', $node->id)->withInput();
$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();
} }
/** /**
* Removes a single allocation from a node. * Removes a single allocation from a node.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @param int $node * @param int $node
* @param int $allocation * @param int $allocation
* @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
*/ */
public function allocationRemoveSingle(Request $request, $node, $allocation) 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) { if ($query < 1) {
return response()->json([ return response()->json([
'error' => 'Unable to find an allocation matching those details to delete.', '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. * Remove all allocations for a specific IP at once on a node.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @param int $node * @param int $node
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
*/ */
public function allocationRemoveBlock(Request $request, $node) 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) { if ($query < 1) {
Alert::danger('There was an error while attempting to delete allocations on that IP.')->flash(); Alert::danger('There was an error while attempting to delete allocations on that IP.')->flash();
} else { } else {
@ -288,8 +285,8 @@ class NodesController extends Controller
/** /**
* Sets an alias for a specific allocation on a node. * Sets an alias for a specific allocation on a node.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @param int $node * @param int $node
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function allocationSetAlias(Request $request, $node) public function allocationSetAlias(Request $request, $node)
@ -299,7 +296,7 @@ class NodesController extends Controller
} }
try { 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->ip_alias = (empty($request->input('alias'))) ? null : $request->input('alias');
$update->save(); $update->save();
@ -312,8 +309,8 @@ class NodesController extends Controller
/** /**
* Creates new allocations on a node. * Creates new allocations on a node.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @param int $node * @param int $node
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
*/ */
public function createAllocation(Request $request, $node) 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'])); $repo->addAllocations($node, $request->intersect(['allocation_ip', 'allocation_alias', 'allocation_ports']));
Alert::success('Successfully added new allocations!')->flash(); Alert::success('Successfully added new allocations!')->flash();
} catch (DisplayValidationException $ex) { } 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) { } catch (DisplayException $ex) {
Alert::danger($ex->getMessage())->flash(); Alert::danger($ex->getMessage())->flash();
} catch (\Exception $ex) { } catch (\Exception $ex) {
Log::error($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); return redirect()->route('admin.nodes.view.allocation', $node);
@ -338,42 +339,29 @@ class NodesController extends Controller
/** /**
* Deletes a node from the system. * Deletes a node from the system.
* *
* @param \Illuminate\Http\Request $request * @param $node
* @param int $id
* @return \Illuminate\Http\RedirectResponse * @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 { return redirect()->route('admin.nodes');
$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);
} }
/** /**
* Returns the configuration token to auto-deploy a node. * Returns the configuration token to auto-deploy a node.
* *
* @param \Illuminate\Http\Request $request * @param \Pterodactyl\Models\Node $node
* @param int $id
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
*/ */
public function setToken(Request $request, $id) public function setToken(Node $node)
{ {
$node = Models\Node::findOrFail($id); $token = bin2hex(random_bytes(16));
$this->cache->tags(['Node:Configuration'])->put($token, $node->id, 5);
$token = str_random(32);
Cache::tags(['Node:Configuration'])->put($token, $node->id, 5);
return response()->json(['token' => $token]); return response()->json(['token' => $token]);
} }

View file

@ -0,0 +1,62 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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'));
}
});
}
}

View file

@ -25,13 +25,15 @@
namespace Pterodactyl\Models; namespace Pterodactyl\Models;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use Sofa\Eloquence\Eloquence;
use Sofa\Eloquence\Validable;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable; 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. * The table associated with the model.
@ -47,20 +49,20 @@ class Node extends Model
*/ */
protected $hidden = ['daemonSecret']; protected $hidden = ['daemonSecret'];
/** /**
* Cast values to correct type. * Cast values to correct type.
* *
* @var array * @var array
*/ */
protected $casts = [ protected $casts = [
'public' => 'integer', 'public' => 'integer',
'location_id' => 'integer', 'location_id' => 'integer',
'memory' => 'integer', 'memory' => 'integer',
'disk' => 'integer', 'disk' => 'integer',
'daemonListen' => 'integer', 'daemonListen' => 'integer',
'daemonSFTP' => 'integer', 'daemonSFTP' => 'integer',
'behind_proxy' => 'boolean', 'behind_proxy' => 'boolean',
]; ];
/** /**
* Fields that are mass assignable. * Fields that are mass assignable.
@ -81,22 +83,67 @@ class Node extends Model
* *
* @var array * @var array
*/ */
protected $searchable = [ protected $searchableColumns = [
'columns' => [ 'name' => 10,
'nodes.name' => 10, 'fqdn' => 8,
'nodes.fqdn' => 8, 'location.short' => 4,
'locations.short' => 4, 'location.long' => 4,
'locations.long' => 4, ];
],
'joins' => [ /**
'locations' => ['locations.id', 'nodes.location_id'], * @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. * Return an instance of the Guzzle client for this specific node.
* *
* @param array $headers * @param array $headers
* @return \GuzzleHttp\Client * @return \GuzzleHttp\Client
*/ */
public function guzzleClient($headers = []) public function guzzleClient($headers = [])
@ -112,7 +159,7 @@ class Node extends Model
/** /**
* Returns the configuration in JSON format. * Returns the configuration in JSON format.
* *
* @param bool $pretty * @param bool $pretty
* @return string * @return string
*/ */
public function getConfigurationAsJson($pretty = false) public function getConfigurationAsJson($pretty = false)

View file

@ -67,6 +67,9 @@ class Server extends Model implements ValidableContract
*/ */
protected $guarded = ['id', 'installed', 'created_at', 'updated_at', 'deleted_at']; protected $guarded = ['id', 'installed', 'created_at', 'updated_at', 'deleted_at'];
/**
* @var array
*/
protected static $applicationRules = [ protected static $applicationRules = [
'owner_id' => 'required', 'owner_id' => 'required',
'name' => 'required', 'name' => 'required',
@ -83,6 +86,9 @@ class Server extends Model implements ValidableContract
'skip_scripts' => 'sometimes', 'skip_scripts' => 'sometimes',
]; ];
/**
* @var array
*/
protected static $dataIntegrityRules = [ protected static $dataIntegrityRules = [
'owner_id' => 'exists:users,id', 'owner_id' => 'exists:users,id',
'name' => 'regex:/^([\w .-]{1,200})$/', 'name' => 'regex:/^([\w .-]{1,200})$/',
@ -132,22 +138,15 @@ class Server extends Model implements ValidableContract
* *
* @var array * @var array
*/ */
protected $searchable = [ protected $searchableColumns = [
'columns' => [ 'name' => 10,
'servers.name' => 10, 'username' => 10,
'servers.username' => 10, 'uuidShort' => 9,
'servers.uuidShort' => 9, 'uuid' => 8,
'servers.uuid' => 8, 'pack.name' => 7,
'packs.name' => 7, 'user.email' => 6,
'users.email' => 6, 'user.username' => 6,
'users.username' => 6, 'node.name' => 2,
'nodes.name' => 2,
],
'joins' => [
'packs' => ['packs.id', 'servers.pack_id'],
'users' => ['users.id', 'servers.owner_id'],
'nodes' => ['nodes.id', 'servers.node_id'],
],
]; ];
/** /**
@ -155,10 +154,11 @@ class Server extends Model implements ValidableContract
* DO NOT USE THIS TO MODIFY SERVER DETAILS OR SAVE THOSE DETAILS. * DO NOT USE THIS TO MODIFY SERVER DETAILS OR SAVE THOSE DETAILS.
* YOU WILL OVERWRITE THE SECRET KEY AND BREAK THINGS. * YOU WILL OVERWRITE THE SECRET KEY AND BREAK THINGS.
* *
* @param string $uuid * @param string $uuid
* @param array $with * @param array $with
* @param array $withCount * @param array $withCount
* @return \Pterodactyl\Models\Server * @return \Pterodactyl\Models\Server
* @throws \Exception
* @todo Remove $with and $withCount due to cache issues, they aren't used anyways. * @todo Remove $with and $withCount due to cache issues, they aren't used anyways.
*/ */
public static function byUuid($uuid, array $with = [], array $withCount = []) public static function byUuid($uuid, array $with = [], array $withCount = [])

View file

@ -28,6 +28,8 @@ use Illuminate\Support\ServiceProvider;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface;
use Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface; 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\DatabaseHostRepositoryInterface;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface;
@ -36,6 +38,8 @@ use Pterodactyl\Contracts\Repository\OptionVariableRepositoryInterface;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface;
use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; 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\AllocationRepository;
use Pterodactyl\Repositories\Eloquent\ApiKeyRepository; use Pterodactyl\Repositories\Eloquent\ApiKeyRepository;
use Pterodactyl\Repositories\Eloquent\ApiPermissionRepository; use Pterodactyl\Repositories\Eloquent\ApiPermissionRepository;
@ -71,9 +75,7 @@ class RepositoryServiceProvider extends ServiceProvider
$this->app->bind(UserRepositoryInterface::class, UserRepository::class); $this->app->bind(UserRepositoryInterface::class, UserRepository::class);
// Daemon Repositories // Daemon Repositories
$this->app->bind( $this->app->bind(ConfigurationRepositoryInterface::class, ConfigurationRepository::class);
\Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface::class, $this->app->bind(DaemonServerRepositoryInterface::class, DaemonServerRepository::class);
\Pterodactyl\Repositories\Daemon\ServerRepository::class
);
} }
} }

View file

@ -51,6 +51,7 @@ class BaseRepository implements BaseRepositoryInterface
public function setNode($id) public function setNode($id)
{ {
// @todo accept a model
$this->node = $this->nodeRepository->find($id); $this->node = $this->nodeRepository->find($id);
return $this; return $this;

View file

@ -0,0 +1,63 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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),
]);
}
}

View file

@ -105,6 +105,14 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf
return $instance; return $instance;
} }
/**
* {@inheritdoc}.
*/
public function findCountWhere(array $fields)
{
return $this->getBuilder()->where($fields)->count($this->getColumns());
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */

View file

@ -28,8 +28,9 @@ use Pterodactyl\Models\Location;
use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; 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 * @var string
@ -44,21 +45,6 @@ class LocationRepository extends EloquentRepository implements LocationRepositor
return Location::class; return Location::class;
} }
/**
* {@inheritdoc}
*/
public function search($term)
{
if (empty($term)) {
return $this;
}
$clone = clone $this;
$clone->searchTerm = $term;
return $clone;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */

View file

@ -25,6 +25,7 @@
namespace Pterodactyl\Repositories\Eloquent; namespace Pterodactyl\Repositories\Eloquent;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Pterodactyl\Models\Node; use Pterodactyl\Models\Node;
use Pterodactyl\Repositories\Eloquent\Attributes\SearchableRepository; use Pterodactyl\Repositories\Eloquent\Attributes\SearchableRepository;
@ -38,6 +39,104 @@ class NodeRepository extends SearchableRepository implements NodeRepositoryInter
return Node::class; 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} * {@inheritdoc}
*/ */

View file

@ -1,207 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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();
});
}
}

View file

@ -1,173 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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();
});
}
}

View file

@ -1,104 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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();
}
}

View file

@ -0,0 +1,62 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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);
}
}

View file

@ -0,0 +1,88 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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);
}
}

View file

@ -0,0 +1,104 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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;
}
}

View file

@ -74,15 +74,15 @@ class DeletionService
*/ */
public function handle($user) public function handle($user)
{ {
if (! $user instanceof User) { if ($user instanceof User) {
$user = $this->repository->find($user); $user = $user->id;
} }
$servers = $this->serverRepository->findWhere([['owner_id', '=', $user->id]]); $servers = $this->serverRepository->findWhere([['owner_id', '=', $user]]);
if (count($servers) > 0) { if (count($servers) > 0) {
throw new DisplayException($this->translator->trans('admin/user.exceptions.user_has_servers')); throw new DisplayException($this->translator->trans('admin/user.exceptions.user_has_servers'));
} }
return $this->repository->delete($user->id); return $this->repository->delete($user);
} }
} }

View file

@ -15,7 +15,7 @@
$factory->define(Pterodactyl\Models\Server::class, function (Faker\Generator $faker) { $factory->define(Pterodactyl\Models\Server::class, function (Faker\Generator $faker) {
return [ return [
'id' => $faker->randomNumber(), 'id' => $faker->unique()->randomNumber(),
'uuid' => $faker->uuid, 'uuid' => $faker->uuid,
'uuidShort' => str_random(8), 'uuidShort' => str_random(8),
'name' => $faker->firstName, '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) { $factory->define(Pterodactyl\Models\User::class, function (Faker\Generator $faker) {
return [ return [
'id' => $faker->randomNumber(), 'id' => $faker->unique()->randomNumber(),
'external_id' => null, 'external_id' => null,
'uuid' => $faker->uuid, 'uuid' => $faker->uuid,
'username' => $faker->userName, '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 () { $factory->state(Pterodactyl\Models\User::class, 'admin', function () {
return [ return [
'root_admin' => true, 'root_admin' => true,
]; ];
}); });
$factory->define(Pterodactyl\Models\Location::class, function (Faker\Generator $faker) { $factory->define(Pterodactyl\Models\Location::class, function (Faker\Generator $faker) {
return [ return [
'short' => $faker->domainWord, 'id' => $faker->unique()->randomNumber(),
'long' => $faker->catchPhrase, 'short' => $faker->domainWord,
]; 'long' => $faker->catchPhrase,
];
}); });
$factory->define(Pterodactyl\Models\Node::class, function (Faker\Generator $faker) { $factory->define(Pterodactyl\Models\Node::class, function (Faker\Generator $faker) {
return [ return [
'id' => $faker->unique()->randomNumber(),
'public' => true, 'public' => true,
'name' => $faker->firstName, 'name' => $faker->firstName,
'fqdn' => $faker->ipv4, '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) { $factory->define(Pterodactyl\Models\ServiceVariable::class, function (Faker\Generator $faker) {
return [ return [
'id' => $faker->randomNumber(), 'id' => $faker->unique()->randomNumber(),
'name' => $faker->firstName, 'name' => $faker->firstName,
'description' => $faker->sentence(), 'description' => $faker->sentence(),
'env_variable' => strtoupper(str_replace(' ', '_', $faker->words(2, true))), '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', 'rules' => 'required|string',
'created_at' => \Carbon\Carbon::now(), 'created_at' => \Carbon\Carbon::now(),
'updated_at' => \Carbon\Carbon::now(), 'updated_at' => \Carbon\Carbon::now(),
]; ];
}); });
$factory->state(Pterodactyl\Models\ServiceVariable::class, 'viewable', function () { $factory->state(Pterodactyl\Models\ServiceVariable::class, 'viewable', function () {

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CascadeNullValuesForDatabaseHostWhenNodeIsDeleted extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('database_hosts', function (Blueprint $table) {
$table->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');
});
}
}

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AllowNegativeValuesForOverallocation extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('nodes', function (Blueprint $table) {
$table->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();
});
}
}

View file

@ -0,0 +1,31 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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 <strong>has been updated</strong>, 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.',
],
];

View file

@ -0,0 +1,36 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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. <strong>Before you can add any servers you must first allocate at least one IP address and port.</strong>',
'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.',
],
];

View file

@ -218,6 +218,7 @@
</div> </div>
</div> </div>
<div class="box-footer"> <div class="box-footer">
{!! method_field('PATCH') !!}
{!! csrf_field() !!} {!! csrf_field() !!}
<button type="submit" class="btn btn-primary pull-right">Save Changes</button> <button type="submit" class="btn btn-primary pull-right">Save Changes</button>
</div> </div>

View file

@ -137,21 +137,22 @@ Route::group(['prefix' => 'servers'], function () {
Route::group(['prefix' => 'nodes'], function () { Route::group(['prefix' => 'nodes'], function () {
Route::get('/', 'NodesController@index')->name('admin.nodes'); Route::get('/', 'NodesController@index')->name('admin.nodes');
Route::get('/new', 'NodesController@create')->name('admin.nodes.new'); Route::get('/new', 'NodesController@create')->name('admin.nodes.new');
Route::get('/view/{id}', 'NodesController@viewIndex')->name('admin.nodes.view'); Route::get('/view/{node}', 'NodesController@viewIndex')->name('admin.nodes.view');
Route::get('/view/{id}/settings', 'NodesController@viewSettings')->name('admin.nodes.view.settings'); Route::get('/view/{node}/settings', 'NodesController@viewSettings')->name('admin.nodes.view.settings');
Route::get('/view/{id}/configuration', 'NodesController@viewConfiguration')->name('admin.nodes.view.configuration'); Route::get('/view/{node}/configuration', 'NodesController@viewConfiguration')->name('admin.nodes.view.configuration');
Route::get('/view/{id}/allocation', 'NodesController@viewAllocation')->name('admin.nodes.view.allocation'); Route::get('/view/{node}/allocation', 'NodesController@viewAllocation')->name('admin.nodes.view.allocation');
Route::get('/view/{id}/servers', 'NodesController@viewServers')->name('admin.nodes.view.servers'); Route::get('/view/{node}/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}/settings/token', 'NodesController@setToken')->name('admin.nodes.view.configuration.token');
Route::post('/new', 'NodesController@store'); Route::post('/new', 'NodesController@store');
Route::post('/view/{id}/settings', 'NodesController@updateSettings'); Route::post('/view/{node}/allocation', 'NodesController@createAllocation');
Route::post('/view/{id}/allocation', 'NodesController@createAllocation'); Route::post('/view/{node}/allocation/remove', 'NodesController@allocationRemoveBlock')->name('admin.nodes.view.allocation.removeBlock');
Route::post('/view/{id}/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::post('/view/{id}/allocation/alias', 'NodesController@allocationSetAlias')->name('admin.nodes.view.allocation.setAlias');
Route::delete('/view/{id}/delete', 'NodesController@delete')->name('admin.nodes.view.delete'); Route::patch('/view/{node}/settings', 'NodesController@updateSettings');
Route::delete('/view/{id}/allocation/remove/{allocation}', 'NodesController@allocationRemoveSingle')->name('admin.nodes.view.allocation.removeSingle');
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');
}); });
/* /*

View file

@ -2,7 +2,6 @@
namespace Tests; namespace Tests;
use Mockery as m;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase; use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase abstract class TestCase extends BaseTestCase
@ -12,6 +11,5 @@ abstract class TestCase extends BaseTestCase
public function setUp() public function setUp()
{ {
parent::setUp(); parent::setUp();
m::close();
} }
} }

View file

@ -0,0 +1,74 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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']));
}
}

View file

@ -0,0 +1,121 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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.'
);
}
}

View file

@ -0,0 +1,182 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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']));
}
}

View file

@ -85,7 +85,7 @@ class DeletionServiceTest extends TestCase
$this->repository->shouldReceive('delete')->with($this->user->id)->once()->andReturn(true); $this->repository->shouldReceive('delete')->with($this->user->id)->once()->andReturn(true);
$this->assertTrue( $this->assertTrue(
$this->service->handle($this->user), $this->service->handle($this->user->id),
'Assert that service responds true.' '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->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->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. * 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->serverRepository->shouldReceive('findWhere')->with([['owner_id', '=', $this->user->id]])->once()->andReturn([]);
$this->repository->shouldReceive('delete')->with($this->user->id)->once()->andReturn(true); $this->repository->shouldReceive('delete')->with($this->user->id)->once()->andReturn(true);
$this->assertTrue( $this->assertTrue(
$this->service->handle($this->user->id), $this->service->handle($this->user),
'Assert that service responds true.' 'Assert that service responds true.'
); );
} }