diff --git a/app/Exceptions/Service/Allocation/AutoAllocationNotEnabledException.php b/app/Exceptions/Service/Allocation/AutoAllocationNotEnabledException.php new file mode 100644 index 000000000..a593347e6 --- /dev/null +++ b/app/Exceptions/Service/Allocation/AutoAllocationNotEnabledException.php @@ -0,0 +1,18 @@ +repository = $repository; $this->serverRepository = $serverRepository; - $this->assignmentService = $assignmentService; - $this->config = $config; + $this->assignableAllocationService = $assignableAllocationService; } /** @@ -125,52 +113,16 @@ class NetworkAllocationController extends ClientApiController /** * Set the notes for the allocation for a server. *s + * * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Network\NewAllocationRequest $request * @param \Pterodactyl\Models\Server $server - * @param \Pterodactyl\Models\Allocation $allocation * @return array * * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function addNew(NewAllocationRequest $request, Server $server): array + public function store(NewAllocationRequest $request, Server $server): array { - $topRange = config('pterodactyl.allocation.stop',0); - $bottomRange = config('pterodactyl.allocation.start',0); - - if($server->allocation_limit <= $server->allocations->count()) { - throw new DisplayException( - 'You have created the maximum number of allocations!' - ); - } - - $allocation = $server->node->allocations()->where('ip',$server->allocation->ip)->whereNull('server_id')->first(); - - if(!$allocation) { - if($server->node->allocations()->where('ip',$server->allocation->ip)->where([['port', '>=', $bottomRange ], ['port', '<=', $topRange],])->count() >= $topRange-$bottomRange+1 || !config('allocation.enabled', false)) { - throw new DisplayException( - 'No more allocations available!' - ); - } - - $allPorts = $server->node->allocations()->select(['port'])->where('ip',$server->allocation->ip)->get()->pluck('port')->toArray(); - - do { - $port = rand($bottomRange, $topRange); - } while(array_search($port, $allPorts)); - - $this->assignmentService->handle($server->node,[ - 'allocation_ip'=>$server->allocation->ip, - 'allocation_ports'=>[$port], - 'server_id'=>$server->id - ]); - - $allocation = $server->node->allocations()->where('ip',$server->allocation->ip)->where('port', $port)->first(); - - } - - $allocation->update(['server_id' => $server->id]); + $allocation = $this->assignableAllocationService->handle($server); return $this->fractal->item($allocation) ->transformWith($this->getTransformer(AllocationTransformer::class)) diff --git a/app/Http/Requests/Admin/Settings/AdvancedSettingsFormRequest.php b/app/Http/Requests/Admin/Settings/AdvancedSettingsFormRequest.php index 496268293..76f786cf9 100644 --- a/app/Http/Requests/Admin/Settings/AdvancedSettingsFormRequest.php +++ b/app/Http/Requests/Admin/Settings/AdvancedSettingsFormRequest.php @@ -21,9 +21,9 @@ class AdvancedSettingsFormRequest extends AdminFormRequest 'pterodactyl:guzzle:connect_timeout' => 'required|integer|between:1,60', 'pterodactyl:console:count' => 'required|integer|min:1', 'pterodactyl:console:frequency' => 'required|integer|min:10', - 'allocation:enabled' => 'required|in:true,false', - 'pterodactyl:allocation:start' => 'required|integer|between:2000,65535', - 'pterodactyl:allocation:stop' => 'required|integer|between:2000,65535', + 'pterodactyl:client_features:allocations:enabled' => 'required|in:true,false', + 'pterodactyl:client_features:allocations:range_start' => 'required|integer|between:1024,65535', + 'pterodactyl:client_features:allocations:range_end' => 'required|integer|between:1024,65535', ]; } @@ -40,9 +40,9 @@ class AdvancedSettingsFormRequest extends AdminFormRequest 'pterodactyl:guzzle:connect_timeout' => 'HTTP Connection Timeout', 'pterodactyl:console:count' => 'Console Message Count', 'pterodactyl:console:frequency' => 'Console Frequency Tick', - 'allocation:enabled' => 'Auto Create Allocations Enabled', - 'pterodactyl:allocation:start' => 'Starting Port', - 'pterodactyl:allocation:stop' => 'Ending Port', + 'pterodactyl:client_features:allocations:enabled' => 'Auto Create Allocations Enabled', + 'pterodactyl:client_features:allocations:range_start' => 'Starting Port', + 'pterodactyl:client_features:allocations:range_end' => 'Ending Port', ]; } } diff --git a/app/Providers/SettingsServiceProvider.php b/app/Providers/SettingsServiceProvider.php index 042c12bf9..5cbcec105 100644 --- a/app/Providers/SettingsServiceProvider.php +++ b/app/Providers/SettingsServiceProvider.php @@ -30,9 +30,9 @@ class SettingsServiceProvider extends ServiceProvider 'pterodactyl:console:count', 'pterodactyl:console:frequency', 'pterodactyl:auth:2fa_required', - 'allocation:enabled', - 'pterodactyl:allocation:stop', - 'pterodactyl:allocation:start', + 'pterodactyl:client_features:allocations:enabled', + 'pterodactyl:client_features:allocations:range_start', + 'pterodactyl:client_features:allocations:range_end', ]; /** diff --git a/app/Services/Allocations/FindAssignableAllocationService.php b/app/Services/Allocations/FindAssignableAllocationService.php new file mode 100644 index 000000000..43b4c9d9d --- /dev/null +++ b/app/Services/Allocations/FindAssignableAllocationService.php @@ -0,0 +1,130 @@ +service = $service; + } + + /** + * Finds an existing unassigned allocation and attempts to assign it to the given server. If + * no allocation can be found, a new one will be created with a random port between the defined + * range from the configuration. + * + * @param \Pterodactyl\Models\Server $server + * @return \Pterodactyl\Models\Allocation + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Service\Allocation\CidrOutOfRangeException + * @throws \Pterodactyl\Exceptions\Service\Allocation\InvalidPortMappingException + * @throws \Pterodactyl\Exceptions\Service\Allocation\PortOutOfRangeException + * @throws \Pterodactyl\Exceptions\Service\Allocation\TooManyPortsInRangeException + */ + public function handle(Server $server) + { + if (!config('pterodactyl.client_features.allocations.enabled')) { + throw new AutoAllocationNotEnabledException; + } + + if ($server->allocations()->count() >= $server->allocation_limit) { + throw new DisplayException( + 'Cannot assign additional allocations to this server: limit has been reached.' + ); + } + + // Attempt to find a given available allocation for a server. If one cannot be found + // we will fall back to attempting to create a new allocation that can be used for the + // server. + /** @var \Pterodactyl\Models\Allocation|null $allocation */ + $allocation = $server->node->allocations() + ->where('ip', $server->allocation->ip) + ->whereNull('server_id') + ->inRandomOrder() + ->first(); + + $allocation = $allocation ?? $this->createNewAllocation($server); + + $allocation->update(['server_id' => $server->id]); + + return $allocation->refresh(); + } + + /** + * @param \Pterodactyl\Models\Server $server + * @return \Pterodactyl\Models\Allocation + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Service\Allocation\CidrOutOfRangeException + * @throws \Pterodactyl\Exceptions\Service\Allocation\InvalidPortMappingException + * @throws \Pterodactyl\Exceptions\Service\Allocation\PortOutOfRangeException + * @throws \Pterodactyl\Exceptions\Service\Allocation\TooManyPortsInRangeException + */ + protected function createNewAllocation(Server $server): Allocation + { + $start = config('pterodactyl.client_features.allocations.range_start', null); + $end = config('pterodactyl.client_features.allocations.range_end', null); + + if (!$start || !$end) { + throw new NoAutoAllocationSpaceAvailableException; + } + + Assert::integerish($start); + Assert::integerish($end); + + // Get all of the currently allocated ports for the node so that we can figure out + // which port might be available. + $ports = $server->node->allocations() + ->where('ip', $server->allocation->ip) + ->whereBetween('port', [$start, $end]) + ->pluck('port'); + + // Compute the difference of the range and the currently created ports, finding + // any port that does not already exist in the database. We will then use this + // array of ports to create a new allocation to assign to the server. + $available = array_diff(range($start, $end), $ports->toArray()); + + // If we've already allocated all of the ports, just abort. + if (empty($available)) { + throw new NoAutoAllocationSpaceAvailableException; + } + + // dd($available, array_rand($available)); + // Pick a random port out of the remaining available ports. + /** @var int $port */ + $port = $available[array_rand($available)]; + + $this->service->handle($server->node, [ + 'server_id' => $server->id, + 'allocation_ip' => $server->allocation->ip, + 'allocation_ports' => [$port], + ]); + + /** @var \Pterodactyl\Models\Allocation $allocation */ + $allocation = $server->node->allocations() + ->where('ip', $server->allocation->ip) + ->where('port', $port) + ->firstOrFail(); + + return $allocation; + } +} diff --git a/config/pterodactyl.php b/config/pterodactyl.php index aba0d729a..404f7aa34 100644 --- a/config/pterodactyl.php +++ b/config/pterodactyl.php @@ -143,6 +143,12 @@ return [ // The total number of tasks that can exist for any given schedule at once. 'per_schedule_task_limit' => 10, ], + + 'allocations' => [ + 'enabled' => env('PTERODACTYL_CLIENT_ALLOCATIONS_ENABLED', false), + 'range_start' => env('PTERODACTYL_CLIENT_ALLOCATIONS_RANGE_START'), + 'range_end' => env('PTERODACTYL_CLIENT_ALLOCATIONS_RANGE_END'), + ], ], /* diff --git a/resources/scripts/api/server/network/createServerAllocation.ts b/resources/scripts/api/server/network/createServerAllocation.ts index 39fc56afe..3e94cf7f6 100644 --- a/resources/scripts/api/server/network/createServerAllocation.ts +++ b/resources/scripts/api/server/network/createServerAllocation.ts @@ -2,10 +2,8 @@ import { Allocation } from '@/api/server/getServer'; import http from '@/api/http'; import { rawDataToServerAllocation } from '@/api/transformers'; -export default (uuid: string): Promise => { - return new Promise((resolve, reject) => { - http.get(`/api/client/servers/${uuid}/network/allocations/new`) - .then(({ data }) => resolve(rawDataToServerAllocation(data))) - .catch(reject); - }); +export default async (uuid: string): Promise => { + const { data } = await http.post(`/api/client/servers/${uuid}/network/allocations`); + + return rawDataToServerAllocation(data); }; diff --git a/resources/views/admin/settings/advanced.blade.php b/resources/views/admin/settings/advanced.blade.php index f749511f3..b903b2435 100644 --- a/resources/views/admin/settings/advanced.blade.php +++ b/resources/views/admin/settings/advanced.blade.php @@ -114,24 +114,24 @@
- - + -

If enabled, the panel will attempt to auto create a new allocation in the range specified if there are no more allocations already created on the node.

+

If enabled users will have the option to automatically create new allocations for their server via the frontend.

- +

The starting port in the range that can be automatically allocated.

- +

The ending port in the range that can be automatically allocated.

diff --git a/routes/api-client.php b/routes/api-client.php index 9f6a3f8d4..170828be1 100644 --- a/routes/api-client.php +++ b/routes/api-client.php @@ -82,9 +82,9 @@ Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateServ Route::group(['prefix' => '/network', 'middleware' => [AllocationBelongsToServer::class]], function () { Route::get('/allocations', 'Servers\NetworkAllocationController@index'); + Route::post('/allocations', 'Servers\NetworkAllocationController@store'); Route::post('/allocations/{allocation}', 'Servers\NetworkAllocationController@update'); Route::post('/allocations/{allocation}/primary', 'Servers\NetworkAllocationController@setPrimary'); - Route::get('/allocations/new', 'Servers\NetworkAllocationController@addNew'); Route::delete('/allocations/{allocation}', 'Servers\NetworkAllocationController@delete'); });