diff --git a/app/Contracts/Repository/RepositoryInterface.php b/app/Contracts/Repository/RepositoryInterface.php
index ad600817b..f48755d6c 100644
--- a/app/Contracts/Repository/RepositoryInterface.php
+++ b/app/Contracts/Repository/RepositoryInterface.php
@@ -186,4 +186,12 @@ interface RepositoryInterface
* @return bool
*/
public function insert(array $data);
+
+ /**
+ * Insert multiple records into the database and ignore duplicates.
+ *
+ * @param array $values
+ * @return bool
+ */
+ public function insertIgnore(array $values);
}
diff --git a/app/Http/Controllers/Admin/LocationController.php b/app/Http/Controllers/Admin/LocationController.php
index a37d4c616..11d67ac0f 100644
--- a/app/Http/Controllers/Admin/LocationController.php
+++ b/app/Http/Controllers/Admin/LocationController.php
@@ -29,7 +29,7 @@ use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Services\LocationService;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller;
-use Pterodactyl\Http\Requests\Admin\LocationRequest;
+use Pterodactyl\Http\Requests\Admin\LocationFormRequest;
use Pterodactyl\Contracts\Repository\LocationRepositoryInterface;
class LocationController extends Controller
@@ -94,13 +94,13 @@ class LocationController extends Controller
/**
* Handle request to create new location.
*
- * @param \Pterodactyl\Http\Requests\Admin\LocationRequest $request
+ * @param \Pterodactyl\Http\Requests\Admin\LocationFormRequest $request
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Throwable
* @throws \Watson\Validating\ValidationException
*/
- public function create(LocationRequest $request)
+ public function create(LocationFormRequest $request)
{
$location = $this->service->create($request->normalize());
$this->alert->success('Location was created successfully.')->flash();
@@ -111,14 +111,14 @@ class LocationController extends Controller
/**
* Handle request to update or delete location.
*
- * @param \Pterodactyl\Http\Requests\Admin\LocationRequest $request
- * @param \Pterodactyl\Models\Location $location
+ * @param \Pterodactyl\Http\Requests\Admin\LocationFormRequest $request
+ * @param \Pterodactyl\Models\Location $location
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Throwable
* @throws \Watson\Validating\ValidationException
*/
- public function update(LocationRequest $request, Location $location)
+ public function update(LocationFormRequest $request, Location $location)
{
if ($request->input('action') === 'delete') {
return $this->delete($location);
diff --git a/app/Http/Controllers/Admin/NodesController.php b/app/Http/Controllers/Admin/NodesController.php
index 74e6cfa0e..97b85abc8 100644
--- a/app/Http/Controllers/Admin/NodesController.php
+++ b/app/Http/Controllers/Admin/NodesController.php
@@ -24,25 +24,25 @@
namespace Pterodactyl\Http\Controllers\Admin;
-use Log;
use Alert;
use Javascript;
use Illuminate\Http\Request;
use Pterodactyl\Models\Node;
use Pterodactyl\Models\Allocation;
use Prologue\Alerts\AlertsMessageBag;
-use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller;
-use Pterodactyl\Repositories\NodeRepository;
use Pterodactyl\Services\Nodes\UpdateService;
use Pterodactyl\Services\Nodes\CreationService;
use Pterodactyl\Services\Nodes\DeletionService;
use Illuminate\Contracts\Translation\Translator;
use Illuminate\Cache\Repository as CacheRepository;
-use Pterodactyl\Http\Requests\Admin\NodeFormRequest;
-use Pterodactyl\Exceptions\DisplayValidationException;
+use Pterodactyl\Services\Allocations\AssignmentService;
+use Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
+use Pterodactyl\Http\Requests\Admin\Node\AllocationFormRequest;
use Pterodactyl\Contracts\Repository\LocationRepositoryInterface;
+use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
+use Pterodactyl\Http\Requests\Admin\Node\AllocationAliasFormRequest;
class NodesController extends Controller
{
@@ -51,6 +51,16 @@ class NodesController extends Controller
*/
protected $alert;
+ /**
+ * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface
+ */
+ protected $allocationRepository;
+
+ /**
+ * @var \Pterodactyl\Services\Allocations\AssignmentService
+ */
+ protected $assignmentService;
+
/**
* @var \Illuminate\Cache\Repository
*/
@@ -86,8 +96,24 @@ class NodesController extends Controller
*/
protected $updateService;
+ /**
+ * NodesController constructor.
+ *
+ * @param \Prologue\Alerts\AlertsMessageBag $alert
+ * @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $allocationRepository
+ * @param \Pterodactyl\Services\Allocations\AssignmentService $assignmentService
+ * @param \Illuminate\Cache\Repository $cache
+ * @param \Pterodactyl\Services\Nodes\CreationService $creationService
+ * @param \Pterodactyl\Services\Nodes\DeletionService $deletionService
+ * @param \Pterodactyl\Contracts\Repository\LocationRepositoryInterface $locationRepository
+ * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository
+ * @param \Illuminate\Contracts\Translation\Translator $translator
+ * @param \Pterodactyl\Services\Nodes\UpdateService $updateService
+ */
public function __construct(
AlertsMessageBag $alert,
+ AllocationRepositoryInterface $allocationRepository,
+ AssignmentService $assignmentService,
CacheRepository $cache,
CreationService $creationService,
DeletionService $deletionService,
@@ -97,6 +123,8 @@ class NodesController extends Controller
UpdateService $updateService
) {
$this->alert = $alert;
+ $this->allocationRepository = $allocationRepository;
+ $this->assignmentService = $assignmentService;
$this->cache = $cache;
$this->creationService = $creationService;
$this->deletionService = $deletionService;
@@ -139,7 +167,7 @@ class NodesController extends Controller
/**
* Post controller to create a new node on the system.
*
- * @param \Pterodactyl\Http\Requests\Admin\NodeFormRequest $request
+ * @param \Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest $request
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
@@ -224,8 +252,8 @@ class NodesController extends Controller
/**
* Updates settings for a node.
*
- * @param \Pterodactyl\Http\Requests\Admin\NodeFormRequest $request
- * @param \Pterodactyl\Models\Node $node
+ * @param \Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest $request
+ * @param \Pterodactyl\Models\Node $node
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Pterodactyl\Exceptions\DisplayException
@@ -242,12 +270,11 @@ class NodesController extends Controller
/**
* Removes a single allocation from a node.
*
- * @param \Illuminate\Http\Request $request
- * @param int $node
- * @param int $allocation
+ * @param int $node
+ * @param int $allocation
* @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
*/
- public function allocationRemoveSingle(Request $request, $node, $allocation)
+ public function allocationRemoveSingle($node, $allocation)
{
$query = Allocation::where('node_id', $node)->whereNull('server_id')->where('id', $allocation)->delete();
if ($query < 1) {
@@ -284,55 +311,35 @@ class NodesController extends Controller
/**
* Sets an alias for a specific allocation on a node.
*
- * @param \Illuminate\Http\Request $request
- * @param int $node
- * @return \Illuminate\Http\Response
+ * @param \Pterodactyl\Http\Requests\Admin\Node\AllocationAliasFormRequest $request
+ * @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
+ *
+ * @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/
- public function allocationSetAlias(Request $request, $node)
+ public function allocationSetAlias(AllocationAliasFormRequest $request)
{
- if (! $request->input('allocation_id')) {
- return response('Missing required parameters.', 422);
- }
+ $this->allocationRepository->update($request->input('allocation_id'), [
+ 'ip_alias' => (empty($request->input('alias'))) ? null : $request->input('alias'),
+ ]);
- try {
- $update = Allocation::findOrFail($request->input('allocation_id'));
- $update->ip_alias = (empty($request->input('alias'))) ? null : $request->input('alias');
- $update->save();
-
- return response('', 204);
- } catch (\Exception $ex) {
- throw $ex;
- }
+ return response('', 204);
}
/**
* Creates new allocations on a node.
*
- * @param \Illuminate\Http\Request $request
- * @param int $node
+ * @param \Pterodactyl\Http\Requests\Admin\Node\AllocationFormRequest $request
+ * @param int|\Pterodactyl\Models\Node $node
* @return \Illuminate\Http\RedirectResponse
+ *
+ * @throws \Pterodactyl\Exceptions\DisplayException
*/
- public function createAllocation(Request $request, $node)
+ public function createAllocation(AllocationFormRequest $request, Node $node)
{
- $repo = new NodeRepository;
+ $this->assignmentService->handle($node, $request->normalize());
+ $this->alert->success($this->translator->trans('admin/node.notices.allocations_added'))->flash();
- try {
- $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();
- } 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();
- }
-
- return redirect()->route('admin.nodes.view.allocation', $node);
+ return redirect()->route('admin.nodes.view.allocation', $node->id);
}
/**
diff --git a/app/Http/Requests/Admin/LocationRequest.php b/app/Http/Requests/Admin/LocationFormRequest.php
similarity index 91%
rename from app/Http/Requests/Admin/LocationRequest.php
rename to app/Http/Requests/Admin/LocationFormRequest.php
index 48c618287..a4c8ca8d3 100644
--- a/app/Http/Requests/Admin/LocationRequest.php
+++ b/app/Http/Requests/Admin/LocationFormRequest.php
@@ -26,7 +26,7 @@ namespace Pterodactyl\Http\Requests\Admin;
use Pterodactyl\Models\Location;
-class LocationRequest extends AdminFormRequest
+class LocationFormRequest extends AdminFormRequest
{
/**
* Setup the validation rules to use for these requests.
@@ -36,7 +36,7 @@ class LocationRequest extends AdminFormRequest
public function rules()
{
if ($this->method() === 'PATCH') {
- return Location::getUpdateRulesForId($this->location->id);
+ return Location::getUpdateRulesForId($this->route()->parameter('location')->id);
}
return Location::getCreateRules();
diff --git a/app/Http/Requests/Admin/Node/AllocationAliasFormRequest.php b/app/Http/Requests/Admin/Node/AllocationAliasFormRequest.php
new file mode 100644
index 000000000..a2832567f
--- /dev/null
+++ b/app/Http/Requests/Admin/Node/AllocationAliasFormRequest.php
@@ -0,0 +1,41 @@
+.
+ *
+ * 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\Node;
+
+use Pterodactyl\Http\Requests\Admin\AdminFormRequest;
+
+class AllocationAliasFormRequest extends AdminFormRequest
+{
+ /**
+ * @return array
+ */
+ public function rules()
+ {
+ return [
+ 'alias' => 'required|nullable|string',
+ 'allocation_id' => 'required|numeric|exists:allocations,id',
+ ];
+ }
+}
diff --git a/app/Http/Requests/Admin/Node/AllocationFormRequest.php b/app/Http/Requests/Admin/Node/AllocationFormRequest.php
new file mode 100644
index 000000000..162896b5e
--- /dev/null
+++ b/app/Http/Requests/Admin/Node/AllocationFormRequest.php
@@ -0,0 +1,42 @@
+.
+ *
+ * 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\Node;
+
+use Pterodactyl\Http\Requests\Admin\AdminFormRequest;
+
+class AllocationFormRequest extends AdminFormRequest
+{
+ /**
+ * @return array
+ */
+ public function rules()
+ {
+ return [
+ 'allocation_ip' => 'required|string',
+ 'allocation_alias' => 'sometimes|string|max:255',
+ 'allocation_ports' => 'required|array',
+ ];
+ }
+}
diff --git a/app/Http/Requests/Admin/NodeFormRequest.php b/app/Http/Requests/Admin/Node/NodeFormRequest.php
similarity index 95%
rename from app/Http/Requests/Admin/NodeFormRequest.php
rename to app/Http/Requests/Admin/Node/NodeFormRequest.php
index 97080c3bf..4ab23aad2 100644
--- a/app/Http/Requests/Admin/NodeFormRequest.php
+++ b/app/Http/Requests/Admin/Node/NodeFormRequest.php
@@ -22,9 +22,10 @@
* SOFTWARE.
*/
-namespace Pterodactyl\Http\Requests\Admin;
+namespace Pterodactyl\Http\Requests\Admin\Node;
use Pterodactyl\Models\Node;
+use Pterodactyl\Http\Requests\Admin\AdminFormRequest;
class NodeFormRequest extends AdminFormRequest
{
diff --git a/app/Http/Requests/Admin/UserFormRequest.php b/app/Http/Requests/Admin/UserFormRequest.php
index 71e1a29d6..c067e14d0 100644
--- a/app/Http/Requests/Admin/UserFormRequest.php
+++ b/app/Http/Requests/Admin/UserFormRequest.php
@@ -34,7 +34,7 @@ class UserFormRequest extends AdminFormRequest
public function rules()
{
if ($this->method() === 'PATCH') {
- return User::getUpdateRulesForId($this->user->id);
+ return User::getUpdateRulesForId($this->route()->parameter('user')->id);
}
return User::getCreateRules();
diff --git a/app/Models/Allocation.php b/app/Models/Allocation.php
index d37e52a0e..65458be18 100644
--- a/app/Models/Allocation.php
+++ b/app/Models/Allocation.php
@@ -24,10 +24,15 @@
namespace Pterodactyl\Models;
+use Sofa\Eloquence\Eloquence;
+use Sofa\Eloquence\Validable;
use Illuminate\Database\Eloquent\Model;
+use Sofa\Eloquence\Contracts\Validable as ValidableContract;
-class Allocation extends Model
+class Allocation extends Model implements ValidableContract
{
+ use Eloquence, Validable;
+
/**
* The table associated with the model.
*
@@ -42,46 +47,66 @@ class Allocation extends Model
*/
protected $guarded = ['id', 'created_at', 'updated_at'];
- /**
- * Cast values to correct type.
- *
- * @var array
- */
- protected $casts = [
- 'node_id' => 'integer',
- 'port' => 'integer',
- 'server_id' => 'integer',
- ];
+ /**
+ * Cast values to correct type.
+ *
+ * @var array
+ */
+ protected $casts = [
+ 'node_id' => 'integer',
+ 'port' => 'integer',
+ 'server_id' => 'integer',
+ ];
- /**
- * Accessor to automatically provide the IP alias if defined.
- *
- * @param null|string $value
- * @return string
- */
- public function getAliasAttribute($value)
- {
- return (is_null($this->ip_alias)) ? $this->ip : $this->ip_alias;
- }
+ /**
+ * @var array
+ */
+ protected static $applicationRules = [
+ 'node_id' => 'required',
+ 'ip' => 'required',
+ 'port' => 'required',
+ ];
- /**
- * Accessor to quickly determine if this allocation has an alias.
- *
- * @param null|string $value
- * @return bool
- */
- public function getHasAliasAttribute($value)
- {
- return ! is_null($this->ip_alias);
- }
+ /**
+ * @var array
+ */
+ protected static $dataIntegrityRules = [
+ 'node_id' => 'exists:nodes,id',
+ 'ip' => 'ip',
+ 'port' => 'numeric|between:1024,65553',
+ 'alias' => 'string',
+ 'server_id' => 'nullable|exists:servers,id',
+ ];
- /**
- * Gets information for the server associated with this allocation.
- *
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
- */
- public function server()
- {
- return $this->belongsTo(Server::class);
- }
+ /**
+ * Accessor to automatically provide the IP alias if defined.
+ *
+ * @param null|string $value
+ * @return string
+ */
+ public function getAliasAttribute($value)
+ {
+ return (is_null($this->ip_alias)) ? $this->ip : $this->ip_alias;
+ }
+
+ /**
+ * Accessor to quickly determine if this allocation has an alias.
+ *
+ * @param null|string $value
+ * @return bool
+ */
+ public function getHasAliasAttribute($value)
+ {
+ return ! is_null($this->ip_alias);
+ }
+
+ /**
+ * Gets information for the server associated with this allocation.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
+ public function server()
+ {
+ return $this->belongsTo(Server::class);
+ }
}
diff --git a/app/Repositories/Eloquent/EloquentRepository.php b/app/Repositories/Eloquent/EloquentRepository.php
index c73f0935c..98c546221 100644
--- a/app/Repositories/Eloquent/EloquentRepository.php
+++ b/app/Repositories/Eloquent/EloquentRepository.php
@@ -24,6 +24,7 @@
namespace Pterodactyl\Repositories\Eloquent;
+use Illuminate\Database\Query\Expression;
use Pterodactyl\Repository\Repository;
use Pterodactyl\Contracts\Repository\RepositoryInterface;
use Pterodactyl\Exceptions\Model\DataValidationException;
@@ -185,6 +186,44 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf
return $this->getBuilder()->insert($data);
}
+ /**
+ * Insert multiple records into the database and ignore duplicates.
+ *
+ * @param array $values
+ * @return bool
+ */
+ public function insertIgnore(array $values)
+ {
+ if (empty($values)) {
+ return true;
+ }
+
+ if (! is_array(reset($values))) {
+ $values = [$values];
+ } else {
+ foreach ($values as $key => $value) {
+ ksort($value);
+ $values[$key] = $value;
+ }
+ }
+
+ $bindings = array_values(array_filter(array_flatten($values, 1), function ($binding) {
+ return ! $binding instanceof Expression;
+ }));
+
+ $grammar = $this->getBuilder()->toBase()->getGrammar();
+ $table = $grammar->wrapTable($this->getModel()->getTable());
+ $columns = $grammar->columnize(array_keys(reset($values)));
+
+ $parameters = collect($values)->map(function ($record) use ($grammar) {
+ return sprintf('(%s)', $grammar->parameterize($record));
+ })->implode(', ');
+
+ $statement = "insert ignore into $table ($columns) values $parameters";
+
+ return $this->getBuilder()->getConnection()->statement($statement, $bindings);
+ }
+
/**
* {@inheritdoc}
* @return bool|\Illuminate\Database\Eloquent\Model
diff --git a/app/Repositories/Eloquent/NodeRepository.php b/app/Repositories/Eloquent/NodeRepository.php
index 7375570e5..7dd0cb40f 100644
--- a/app/Repositories/Eloquent/NodeRepository.php
+++ b/app/Repositories/Eloquent/NodeRepository.php
@@ -113,10 +113,8 @@ class NodeRepository extends SearchableRepository implements NodeRepositoryInter
$instance->setRelation(
'allocations',
- $this->getModel()->allocations()->orderBy('ip', 'asc')
- ->orderBy('port', 'asc')
- ->with('server')
- ->paginate(50)
+ $instance->allocations()->orderBy('ip', 'asc')->orderBy('port', 'asc')
+ ->with('server')->paginate(50)
);
return $instance;
diff --git a/app/Services/Allocations/AssignmentService.php b/app/Services/Allocations/AssignmentService.php
new file mode 100644
index 000000000..bc7ea35c8
--- /dev/null
+++ b/app/Services/Allocations/AssignmentService.php
@@ -0,0 +1,125 @@
+.
+ *
+ * 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\Allocations;
+
+use IPTools\Network;
+use Pterodactyl\Models\Node;
+use Illuminate\Database\ConnectionInterface;
+use Pterodactyl\Exceptions\DisplayException;
+use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
+
+class AssignmentService
+{
+ const CIDR_MAX_BITS = 27;
+ const CIDR_MIN_BITS = 32;
+ const PORT_RANGE_LIMIT = 1000;
+ const PORT_RANGE_REGEX = '/^(\d{1,5})-(\d{1,5})$/';
+
+ /**
+ * @var \Illuminate\Database\ConnectionInterface
+ */
+ protected $connection;
+
+ /**
+ * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface
+ */
+ protected $repository;
+
+ /**
+ * AssignmentService constructor.
+ *
+ * @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $repository
+ * @param \Illuminate\Database\ConnectionInterface $connection
+ */
+ public function __construct(
+ AllocationRepositoryInterface $repository,
+ ConnectionInterface $connection
+ ) {
+ $this->connection = $connection;
+ $this->repository = $repository;
+ }
+
+ /**
+ * Insert allocations into the database and link them to a specific node.
+ *
+ * @param int|\Pterodactyl\Models\Node $node
+ * @param array $data
+ *
+ * @throws \Pterodactyl\Exceptions\DisplayException
+ */
+ public function handle($node, array $data)
+ {
+ if ($node instanceof Node) {
+ $node = $node->id;
+ }
+
+ $explode = explode('/', $data['allocation_ip']);
+ if (count($explode) !== 1) {
+ if (! ctype_digit($explode[1]) || ($explode[1] > self::CIDR_MIN_BITS || $explode[1] < self::CIDR_MAX_BITS)) {
+ throw new DisplayException(trans('admin/exceptions.allocations.cidr_out_of_range'));
+ }
+ }
+
+ $this->connection->beginTransaction();
+ foreach (Network::parse(gethostbyname($data['allocation_ip'])) as $ip) {
+ foreach ($data['allocation_ports'] as $port) {
+ if (! ctype_digit($port) && ! preg_match(self::PORT_RANGE_REGEX, $port)) {
+ throw new DisplayException(trans('admin/exceptions.allocations.invalid_mapping', ['port' => $port]));
+ }
+
+ $insertData = [];
+ if (preg_match(self::PORT_RANGE_REGEX, $port, $matches)) {
+ $block = range($matches[1], $matches[2]);
+
+ if (count($block) > self::PORT_RANGE_LIMIT) {
+ throw new DisplayException(trans('admin/exceptions.allocations.too_many_ports'));
+ }
+
+ foreach ($block as $unit) {
+ $insertData[] = [
+ 'node_id' => $node,
+ 'ip' => $ip->__toString(),
+ 'port' => (int) $unit,
+ 'ip_alias' => array_get($data, 'allocation_alias'),
+ 'server_id' => null,
+ ];
+ }
+ } else {
+ $insertData[] = [
+ 'node_id' => $node,
+ 'ip' => $ip->__toString(),
+ 'port' => (int) $port,
+ 'ip_alias' => array_get($data, 'allocation_alias'),
+ 'server_id' => null,
+ ];
+ }
+
+ $this->repository->insertIgnore($insertData);
+ }
+ }
+
+ $this->connection->commit();
+ }
+}
diff --git a/coverage.xml b/coverage.xml
deleted file mode 100644
index e392d5294..000000000
--- a/coverage.xml
+++ /dev/null
@@ -1,5812 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/database/migrations/2017_08_05_174811_SetAllocationUnqiueUsingMultipleFields.php b/database/migrations/2017_08_05_174811_SetAllocationUnqiueUsingMultipleFields.php
new file mode 100644
index 000000000..56e149d4c
--- /dev/null
+++ b/database/migrations/2017_08_05_174811_SetAllocationUnqiueUsingMultipleFields.php
@@ -0,0 +1,32 @@
+unique(['node_id', 'ip', 'port']);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table('allocations', function (Blueprint $table) {
+ $table->dropUnique(['node_id', 'ip', 'port']);
+ });
+ }
+}
diff --git a/resources/lang/en/admin/exceptions.php b/resources/lang/en/admin/exceptions.php
index 1a5bcaa37..f2d9e33f5 100644
--- a/resources/lang/en/admin/exceptions.php
+++ b/resources/lang/en/admin/exceptions.php
@@ -28,4 +28,9 @@ return [
'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.',
],
+ 'allocations' => [
+ 'too_many_ports' => 'Adding more than 1000 ports at a single time is not supported. Please use a smaller range.',
+ 'invalid_mapping' => 'The mapping provided for :port was invalid and could not be processed.',
+ 'cidr_out_of_range' => 'CIDR notation only allows masks between /25 and /32.',
+ ],
];
diff --git a/resources/lang/en/admin/node.php b/resources/lang/en/admin/node.php
index fc5b0b1ca..2b3a4bf51 100644
--- a/resources/lang/en/admin/node.php
+++ b/resources/lang/en/admin/node.php
@@ -28,6 +28,7 @@ return [
'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' => [
+ 'allocations_added' => 'Allocations have successfully been added to this node.',
'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.',
diff --git a/tests/Unit/Services/Allocations/AssignmentServiceTest.php b/tests/Unit/Services/Allocations/AssignmentServiceTest.php
new file mode 100644
index 000000000..2c222859c
--- /dev/null
+++ b/tests/Unit/Services/Allocations/AssignmentServiceTest.php
@@ -0,0 +1,327 @@
+.
+ *
+ * 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\Allocations;
+
+use Exception;
+use Mockery as m;
+use Tests\TestCase;
+use phpmock\phpunit\PHPMock;
+use Pterodactyl\Models\Node;
+use Illuminate\Database\ConnectionInterface;
+use Pterodactyl\Exceptions\DisplayException;
+use Pterodactyl\Services\Allocations\AssignmentService;
+use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
+
+class AssignmentServiceTest extends TestCase
+{
+ use PHPMock;
+
+ /**
+ * @var \Illuminate\Database\ConnectionInterface
+ */
+ protected $connection;
+
+ /**
+ * @var \Pterodactyl\Models\Node
+ */
+ protected $node;
+
+ /**
+ * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface
+ */
+ protected $repository;
+
+ /**
+ * @var \Pterodactyl\Services\Allocations\AssignmentService
+ */
+ protected $service;
+
+ /**
+ * Setup tests.
+ */
+ public function setUp()
+ {
+ parent::setUp();
+
+ // Due to a bug in PHP, this is necessary since we only have a single test
+ // that relies on this mock. If this does not exist the test will fail to register
+ // correctly.
+ //
+ // This can also be avoided if tests were run in isolated processes, or if that test
+ // came first, but neither of those are good solutions, so this is the next best option.
+ PHPMock::defineFunctionMock('\\Pterodactyl\\Services\\Allocations', 'gethostbyname');
+
+ $this->node = factory(Node::class)->make();
+ $this->connection = m::mock(ConnectionInterface::class);
+ $this->repository = m::mock(AllocationRepositoryInterface::class);
+
+ $this->service = new AssignmentService($this->repository, $this->connection);
+ }
+
+ /**
+ * Test a non-CIDR notated IP address without a port range.
+ */
+ public function testIndividualIpAddressWithoutRange()
+ {
+ $data = [
+ 'allocation_ip' => '192.168.1.1',
+ 'allocation_ports' => ['1024']
+ ];
+
+ $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
+ $this->repository->shouldReceive('insertIgnore')->with([
+ [
+ 'node_id' => $this->node->id,
+ 'ip' => '192.168.1.1',
+ 'port' => 1024,
+ 'ip_alias' => null,
+ 'server_id' => null,
+ ],
+ ])->once()->andReturnNull();
+ $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
+
+ $this->service->handle($this->node->id, $data);
+ }
+
+ /**
+ * Test a non-CIDR IP address with a port range provided.
+ */
+ public function testIndividualIpAddressWithRange()
+ {
+ $data = [
+ 'allocation_ip' => '192.168.1.1',
+ 'allocation_ports' => ['1024-1026']
+ ];
+
+ $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
+ $this->repository->shouldReceive('insertIgnore')->with([
+ [
+ 'node_id' => $this->node->id,
+ 'ip' => '192.168.1.1',
+ 'port' => 1024,
+ 'ip_alias' => null,
+ 'server_id' => null,
+ ],
+ [
+ 'node_id' => $this->node->id,
+ 'ip' => '192.168.1.1',
+ 'port' => 1025,
+ 'ip_alias' => null,
+ 'server_id' => null,
+ ],
+ [
+ 'node_id' => $this->node->id,
+ 'ip' => '192.168.1.1',
+ 'port' => 1026,
+ 'ip_alias' => null,
+ 'server_id' => null,
+ ],
+ ])->once()->andReturnNull();
+ $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
+
+ $this->service->handle($this->node->id, $data);
+
+ }
+
+ /**
+ * Test a non-CIRD IP address with a single port and an alias.
+ */
+ public function testIndividualIPAddressWithAlias()
+ {
+ $data = [
+ 'allocation_ip' => '192.168.1.1',
+ 'allocation_ports' => ['1024'],
+ 'allocation_alias' => 'my.alias.net',
+ ];
+
+ $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
+ $this->repository->shouldReceive('insertIgnore')->with([
+ [
+ 'node_id' => $this->node->id,
+ 'ip' => '192.168.1.1',
+ 'port' => 1024,
+ 'ip_alias' => 'my.alias.net',
+ 'server_id' => null,
+ ],
+ ])->once()->andReturnNull();
+ $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
+
+ $this->service->handle($this->node->id, $data);
+ }
+
+ /**
+ * Test that a domain name can be passed in place of an IP address.
+ */
+ public function testDomainNamePassedInPlaceOfIPAddress()
+ {
+ $data = [
+ 'allocation_ip' => 'test-domain.com',
+ 'allocation_ports' => ['1024'],
+ ];
+
+ $this->getFunctionMock('\\Pterodactyl\\Services\\Allocations', 'gethostbyname')
+ ->expects($this->once())->willReturn('192.168.1.1');
+
+ $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
+ $this->repository->shouldReceive('insertIgnore')->with([
+ [
+ 'node_id' => $this->node->id,
+ 'ip' => '192.168.1.1',
+ 'port' => 1024,
+ 'ip_alias' => null,
+ 'server_id' => null,
+ ],
+ ])->once()->andReturnNull();
+ $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
+
+ $this->service->handle($this->node->id, $data);
+ }
+
+ /**
+ * Test that a CIDR IP address without a range works properly.
+ */
+ public function testCIDRNotatedIPAddressWithoutRange()
+ {
+ $data = [
+ 'allocation_ip' => '192.168.1.100/31',
+ 'allocation_ports' => ['1024'],
+ ];
+
+ $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
+ $this->repository->shouldReceive('insertIgnore')->with([
+ [
+ 'node_id' => $this->node->id,
+ 'ip' => '192.168.1.100',
+ 'port' => 1024,
+ 'ip_alias' => null,
+ 'server_id' => null,
+ ],
+ ])->once()->andReturnNull();
+
+ $this->repository->shouldReceive('insertIgnore')->with([
+ [
+ 'node_id' => $this->node->id,
+ 'ip' => '192.168.1.101',
+ 'port' => 1024,
+ 'ip_alias' => null,
+ 'server_id' => null,
+ ],
+ ])->once()->andReturnNull();
+ $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
+
+ $this->service->handle($this->node->id, $data);
+ }
+
+ /**
+ * Test that a CIDR IP address with a range works properly.
+ */
+ public function testCIDRNotatedIPAddressOutsideRangeLimit()
+ {
+ $data = [
+ 'allocation_ip' => '192.168.1.100/20',
+ 'allocation_ports' => ['1024'],
+ ];
+
+ try {
+ $this->service->handle($this->node->id, $data);
+ } catch (Exception $exception) {
+ $this->assertInstanceOf(DisplayException::class, $exception);
+ $this->assertEquals(trans('admin/exceptions.allocations.cidr_out_of_range'), $exception->getMessage());
+ }
+ }
+
+ /**
+ * Test that an exception is thrown if there are too many ports.
+ */
+ public function testAllocationWithPortsExceedingLimit()
+ {
+ $data = [
+ 'allocation_ip' => '192.168.1.1',
+ 'allocation_ports' => ['5000-7000'],
+ ];
+
+ $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
+
+ try {
+ $this->service->handle($this->node->id, $data);
+ } catch (Exception $exception) {
+ if (! $exception instanceof DisplayException) {
+ throw $exception;
+ }
+
+ $this->assertInstanceOf(DisplayException::class, $exception);
+ $this->assertEquals(trans('admin/exceptions.allocations.too_many_ports'), $exception->getMessage());
+ }
+ }
+
+ /**
+ * Test that an exception is thrown if an invalid port is provided.
+ */
+ public function testInvalidPortProvided()
+ {
+ $data = [
+ 'allocation_ip' => '192.168.1.1',
+ 'allocation_ports' => ['test123'],
+ ];
+
+ $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
+
+ try {
+ $this->service->handle($this->node->id, $data);
+ } catch (Exception $exception) {
+ if (! $exception instanceof DisplayException) {
+ throw $exception;
+ }
+
+ $this->assertInstanceOf(DisplayException::class, $exception);
+ $this->assertEquals(trans('admin/exceptions.allocations.invalid_mapping', ['port' => 'test123']), $exception->getMessage());
+ }
+ }
+
+ /**
+ * Test that a model can be passed in place of an ID.
+ */
+ public function testModelCanBePassedInPlaceOfNodeModel()
+ {
+ $data = [
+ 'allocation_ip' => '192.168.1.1',
+ 'allocation_ports' => ['1024']
+ ];
+
+ $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
+ $this->repository->shouldReceive('insertIgnore')->with([
+ [
+ 'node_id' => $this->node->id,
+ 'ip' => '192.168.1.1',
+ 'port' => 1024,
+ 'ip_alias' => null,
+ 'server_id' => null,
+ ],
+ ])->once()->andReturnNull();
+ $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
+
+ $this->service->handle($this->node, $data);
+ }
+}