Skip to content

Commit 09caa0d

Browse files
Merge commit from fork
* Add throttling to resource creation endpoints * Fix middleware registration for the throttlers * Lock the server's resource models when adding new ones * Throttle subusers even more --------- Co-authored-by: DaneEveritt <dane@daneeveritt.com>
1 parent 82f22cd commit 09caa0d

File tree

9 files changed

+128
-32
lines changed

9 files changed

+128
-32
lines changed

app/Enum/ResourceLimit.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
namespace Pterodactyl\Enum;
4+
5+
use Illuminate\Http\Request;
6+
use Webmozart\Assert\Assert;
7+
use Pterodactyl\Models\Server;
8+
use Illuminate\Cache\RateLimiting\Limit;
9+
use Illuminate\Support\Facades\RateLimiter;
10+
use Illuminate\Routing\Middleware\ThrottleRequests;
11+
12+
/**
13+
* A basic resource throttler for individual servers. This is applied in addition
14+
* to existing rate limits and allows the code to slow down speedy users that might
15+
* be creating resources a little too quickly for comfort. This throttle generally
16+
* only applies to creation flows, and not general view/edit/delete flows.
17+
*/
18+
enum ResourceLimit
19+
{
20+
case Allocation;
21+
case Backup;
22+
case Database;
23+
case Schedule;
24+
case Subuser;
25+
case Websocket;
26+
case FilePull;
27+
28+
public function throttleKey(): string
29+
{
30+
return mb_strtolower("api.client:server-resource:{$this->name}");
31+
}
32+
33+
/**
34+
* Returns a middleware that will throttle the specific resource by server. This
35+
* throttle applies to any user making changes to that resource on the specific
36+
* server, it is NOT per-user.
37+
*/
38+
public function middleware(): string
39+
{
40+
return ThrottleRequests::using($this->throttleKey());
41+
}
42+
43+
public function limit(): Limit
44+
{
45+
return match($this) {
46+
self::Backup => Limit::perMinutes(15, 3),
47+
self::Database => Limit::perMinute(2),
48+
self::FilePull => Limit::perMinutes(10, 5),
49+
self::Subuser => Limit::perMinutes(15, 10),
50+
self::Websocket => Limit::perMinute(5),
51+
default => Limit::perMinute(2),
52+
};
53+
}
54+
55+
public static function boot(): void
56+
{
57+
foreach (self::cases() as $case) {
58+
RateLimiter::for($case->throttleKey(), function (Request $request) use ($case) {
59+
Assert::isInstanceOf($server = $request->route()->parameter('server'), Server::class);
60+
61+
return $case->limit()->by($server->uuid);
62+
});
63+
}
64+
}
65+
}

app/Http/Controllers/Api/Client/Servers/BackupController.php

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,21 @@ public function store(StoreBackupRequest $request, Server $server): array
7474
// how best to allow a user to create a backup that is locked without also preventing
7575
// them from just filling up a server with backups that can never be deleted?
7676
if ($request->user()->can(Permission::ACTION_BACKUP_DELETE, $server)) {
77-
$action->setIsLocked((bool) $request->input('is_locked'));
77+
$action->setIsLocked($request->boolean('is_locked'));
7878
}
7979

80-
$backup = $action->handle($server, $request->input('name'));
80+
$backup = Activity::event('server:backup.start')->transaction(function ($log) use ($action, $server, $request) {
81+
$server->backups()->lockForUpdate();
8182

82-
Activity::event('server:backup.start')
83-
->subject($backup)
84-
->property(['name' => $backup->name, 'locked' => (bool) $request->input('is_locked')])
85-
->log();
83+
$backup = $action->handle($server, $request->input('name'));
84+
85+
$log->subject($backup)->property([
86+
'name' => $backup->name,
87+
'locked' => $request->boolean('is_locked'),
88+
]);
89+
90+
return $backup;
91+
});
8692

8793
return $this->fractal->item($backup)
8894
->transformWith($this->getTransformer(BackupTransformer::class))

app/Http/Controllers/Api/Client/Servers/DatabaseController.php

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,15 @@ public function index(GetDatabasesRequest $request, Server $server): array
4848
*/
4949
public function store(StoreDatabaseRequest $request, Server $server): array
5050
{
51-
$database = $this->deployDatabaseService->handle($server, $request->validated());
51+
$database = Activity::event('server:database.create')->transaction(function ($log) use ($request, $server) {
52+
$server->databases()->lockForUpdate();
5253

53-
Activity::event('server:database.create')
54-
->subject($database)
55-
->property('name', $database->database)
56-
->log();
54+
$database = $this->deployDatabaseService->handle($server, $request->validated());
55+
56+
$log->subject($database)->property('name', $database->database);
57+
58+
return $database;
59+
});
5760

5861
return $this->fractal->item($database)
5962
->parseIncludes(['password'])
@@ -69,15 +72,16 @@ public function store(StoreDatabaseRequest $request, Server $server): array
6972
*/
7073
public function rotatePassword(RotatePasswordRequest $request, Server $server, Database $database): array
7174
{
72-
$this->passwordService->handle($database);
73-
$database->refresh();
74-
7575
Activity::event('server:database.rotate-password')
7676
->subject($database)
7777
->property('name', $database->database)
78-
->log();
78+
->transaction(function () use ($database) {
79+
$database->lockForUpdate();
7980

80-
return $this->fractal->item($database)
81+
$this->passwordService->handle($database);
82+
});
83+
84+
return $this->fractal->item($database->refresh())
8185
->parseIncludes(['password'])
8286
->transformWith($this->getTransformer(DatabaseTransformer::class))
8387
->toArray();

app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Illuminate\Http\JsonResponse;
77
use Pterodactyl\Facades\Activity;
88
use Pterodactyl\Models\Allocation;
9+
use Illuminate\Database\ConnectionInterface;
910
use Pterodactyl\Exceptions\DisplayException;
1011
use Pterodactyl\Repositories\Eloquent\ServerRepository;
1112
use Pterodactyl\Transformers\Api\Client\AllocationTransformer;
@@ -23,6 +24,7 @@ class NetworkAllocationController extends ClientApiController
2324
* NetworkAllocationController constructor.
2425
*/
2526
public function __construct(
27+
protected readonly ConnectionInterface $connection,
2628
private FindAssignableAllocationService $assignableAllocationService,
2729
private ServerRepository $serverRepository,
2830
) {
@@ -92,16 +94,17 @@ public function setPrimary(SetPrimaryAllocationRequest $request, Server $server,
9294
*/
9395
public function store(NewAllocationRequest $request, Server $server): array
9496
{
95-
if ($server->allocations()->count() >= $server->allocation_limit) {
96-
throw new DisplayException('Cannot assign additional allocations to this server: limit has been reached.');
97-
}
97+
$allocation = Activity::event('server:allocation.create')->transaction(function ($log) use ($server) {
98+
if ($server->allocations()->lockForUpdate()->count() >= $server->allocation_limit) {
99+
throw new DisplayException('Cannot assign additional allocations to this server: limit has been reached.');
100+
}
98101

99-
$allocation = $this->assignableAllocationService->handle($server);
102+
$allocation = $this->assignableAllocationService->handle($server);
100103

101-
Activity::event('server:allocation.create')
102-
->subject($allocation)
103-
->property('allocation', $allocation->toString())
104-
->log();
104+
$log->subject($allocation)->property('allocation', $allocation->toString());
105+
106+
return $allocation;
107+
});
105108

106109
return $this->fractal->item($allocation)
107110
->transformWith($this->getTransformer(AllocationTransformer::class))

app/Http/Kernel.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ class Kernel extends HttpKernel
4848
ConvertEmptyStringsToNull::class,
4949
];
5050

51+
protected $middlewarePriority = [
52+
SubstituteClientBindings::class,
53+
];
54+
5155
/**
5256
* The application's route middleware groups.
5357
*/

app/Providers/RouteServiceProvider.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Illuminate\Http\Request;
66
use Pterodactyl\Models\Database;
7+
use Pterodactyl\Enum\ResourceLimit;
78
use Illuminate\Support\Facades\Route;
89
use Illuminate\Cache\RateLimiting\Limit;
910
use Illuminate\Support\Facades\RateLimiter;
@@ -106,5 +107,7 @@ protected function configureRateLimiting(): void
106107
config('http.rate_limit.application')
107108
)->by($key);
108109
});
110+
111+
ResourceLimit::boot();
109112
}
110113
}

app/Services/Activity/ActivityLogService.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ public function clone(): self
166166
* and will only save the activity log entry if everything else successfully
167167
* settles.
168168
*
169+
* @param \Closure($this): mixed $callback
170+
*
169171
* @throws \Throwable
170172
*/
171173
public function transaction(\Closure $callback)

config/http.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
*/
1414
'rate_limit' => [
1515
'client_period' => 1,
16-
'client' => env('APP_API_CLIENT_RATELIMIT', 720),
16+
'client' => env('APP_API_CLIENT_RATELIMIT', 128),
1717

1818
'application_period' => 1,
1919
'application' => env('APP_API_APPLICATION_RATELIMIT', 240),

routes/api-client.php

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?php
22

3+
use Pterodactyl\Enum\ResourceLimit;
34
use Illuminate\Support\Facades\Route;
45
use Pterodactyl\Http\Controllers\Api\Client;
56
use Pterodactyl\Http\Middleware\Activity\ServerSubject;
@@ -60,7 +61,9 @@
6061
],
6162
], function () {
6263
Route::get('/', [Client\Servers\ServerController::class, 'index'])->name('api:client:server.view');
63-
Route::get('/websocket', Client\Servers\WebsocketController::class)->name('api:client:server.ws');
64+
Route::middleware([ResourceLimit::Websocket->middleware()])
65+
->get('/websocket', Client\Servers\WebsocketController::class)
66+
->name('api:client:server.ws');
6467
Route::get('/resources', Client\Servers\ResourceUtilizationController::class)->name('api:client:server.resources');
6568
Route::get('/activity', Client\Servers\ActivityLogController::class)->name('api:client:server.activity');
6669

@@ -69,7 +72,8 @@
6972

7073
Route::group(['prefix' => '/databases'], function () {
7174
Route::get('/', [Client\Servers\DatabaseController::class, 'index']);
72-
Route::post('/', [Client\Servers\DatabaseController::class, 'store']);
75+
Route::middleware([ResourceLimit::Database->middleware()])
76+
->post('/', [Client\Servers\DatabaseController::class, 'store']);
7377
Route::post('/{database}/rotate-password', [Client\Servers\DatabaseController::class, 'rotatePassword']);
7478
Route::delete('/{database}', [Client\Servers\DatabaseController::class, 'delete']);
7579
});
@@ -86,13 +90,15 @@
8690
Route::post('/delete', [Client\Servers\FileController::class, 'delete']);
8791
Route::post('/create-folder', [Client\Servers\FileController::class, 'create']);
8892
Route::post('/chmod', [Client\Servers\FileController::class, 'chmod']);
89-
Route::post('/pull', [Client\Servers\FileController::class, 'pull'])->middleware(['throttle:10,5']);
93+
Route::middleware([ResourceLimit::FilePull->middleware()])
94+
->post('/pull', [Client\Servers\FileController::class, 'pull']);
9095
Route::get('/upload', Client\Servers\FileUploadController::class);
9196
});
9297

9398
Route::group(['prefix' => '/schedules'], function () {
9499
Route::get('/', [Client\Servers\ScheduleController::class, 'index']);
95-
Route::post('/', [Client\Servers\ScheduleController::class, 'store']);
100+
Route::middleware([ResourceLimit::Schedule->middleware()])
101+
->post('/', [Client\Servers\ScheduleController::class, 'store']);
96102
Route::get('/{schedule}', [Client\Servers\ScheduleController::class, 'view']);
97103
Route::post('/{schedule}', [Client\Servers\ScheduleController::class, 'update']);
98104
Route::post('/{schedule}/execute', [Client\Servers\ScheduleController::class, 'execute']);
@@ -105,15 +111,17 @@
105111

106112
Route::group(['prefix' => '/network'], function () {
107113
Route::get('/allocations', [Client\Servers\NetworkAllocationController::class, 'index']);
108-
Route::post('/allocations', [Client\Servers\NetworkAllocationController::class, 'store']);
114+
Route::middleware([ResourceLimit::Allocation->middleware()])
115+
->post('/allocations', [Client\Servers\NetworkAllocationController::class, 'store']);
109116
Route::post('/allocations/{allocation}', [Client\Servers\NetworkAllocationController::class, 'update']);
110117
Route::post('/allocations/{allocation}/primary', [Client\Servers\NetworkAllocationController::class, 'setPrimary']);
111118
Route::delete('/allocations/{allocation}', [Client\Servers\NetworkAllocationController::class, 'delete']);
112119
});
113120

114121
Route::group(['prefix' => '/users'], function () {
115122
Route::get('/', [Client\Servers\SubuserController::class, 'index']);
116-
Route::post('/', [Client\Servers\SubuserController::class, 'store']);
123+
Route::middleware([ResourceLimit::Subuser->middleware()])
124+
->post('/', [Client\Servers\SubuserController::class, 'store']);
117125
Route::get('/{user}', [Client\Servers\SubuserController::class, 'view']);
118126
Route::post('/{user}', [Client\Servers\SubuserController::class, 'update']);
119127
Route::delete('/{user}', [Client\Servers\SubuserController::class, 'delete']);
@@ -125,7 +133,8 @@
125133
Route::get('/{backup}', [Client\Servers\BackupController::class, 'view']);
126134
Route::get('/{backup}/download', [Client\Servers\BackupController::class, 'download']);
127135
Route::post('/{backup}/lock', [Client\Servers\BackupController::class, 'toggleLock']);
128-
Route::post('/{backup}/restore', [Client\Servers\BackupController::class, 'restore']);
136+
Route::middleware([ResourceLimit::Backup->middleware()])
137+
->post('/{backup}/restore', [Client\Servers\BackupController::class, 'restore']);
129138
Route::delete('/{backup}', [Client\Servers\BackupController::class, 'delete']);
130139
});
131140

0 commit comments

Comments
 (0)