Skip to content

Commit

Permalink
#2664 Changes to API limiting - now has more fine-grained control.
Browse files Browse the repository at this point in the history
Also, the user is now loaded before rate limiting is applied.
  • Loading branch information
Wotuu committed Jan 19, 2025
1 parent 4e6d22a commit 22d9697
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 39 deletions.
6 changes: 6 additions & 0 deletions app/Exceptions/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\Exceptions\ThrottleRequestsException;
use Illuminate\Http\Request;
use Illuminate\Session\TokenMismatchException;
use Illuminate\Validation\ValidationException;
Expand All @@ -16,6 +17,7 @@
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
use Teapot\StatusCode;
use Teapot\StatusCode\RFC\RFC6585;
use Throwable;

class Handler extends ExceptionHandler
Expand Down Expand Up @@ -82,8 +84,12 @@ public function render($request, Throwable $e)
], StatusCode::NOT_FOUND);
} else if ($e instanceof NotFoundHttpException) {
return response()->json(['message' => __('exceptions.handler.api_route_not_found')], StatusCode::NOT_FOUND);
} else if ($e instanceof ThrottleRequestsException) {
return response()->json(['message' => __('exceptions.handler.too_many_requests')], RFC6585::TOO_MANY_REQUESTS);
} else if (!config('app.debug')) {
return response()->json(['message' => __('exceptions.handler.internal_server_error')], StatusCode::INTERNAL_SERVER_ERROR);
} else {
return response()->json(['message' => $e->getMessage()], $e->getCode());
}
}

Expand Down
7 changes: 3 additions & 4 deletions app/Http/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,20 +56,19 @@ class Kernel extends HttpKernel
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
TrustProxies::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
TrustProxies::class,
],

'api' => [
ThrottleRequests::class . ':60,1',
'authentication' => ApiAuthentication::class,
TrustProxies::class,
'bindings',
'debug_info_context_logger' => DebugInfoContextLogger::class,
'read_only_mode' => ReadOnlyMode::class,
'authentication' => ApiAuthentication::class,
TrustProxies::class,
],
];

Expand Down
36 changes: 18 additions & 18 deletions app/Http/Requests/Api/V1/CombatLog/Route/CombatLogRouteRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ public function rules(): array
'metadata.wowInstanceId' => ['nullable', 'int'],
'settings.temporary' => ['nullable', 'bool'],
'settings.debugIcons' => ['nullable', 'bool'],
'roster.numMembers' => ['nullable', 'int'], // @TODO make required after raider.io supports it
'roster.averageItemLevel' => ['nullable', 'numeric'], // @TODO make required after raider.io supports it
'roster.characterIds' => ['nullable', 'array'], // @TODO make required after raider.io supports it
'roster.characterIds.*' => ['nullable', 'int'], // @TODO make required after raider.io supports it
'roster.specIds' => ['nullable', 'array'], // @TODO make required after raider.io supports it
'roster.specIds.*' => ['nullable', 'int'], // @TODO make required after raider.io supports it
'roster.classIds' => ['nullable', 'array'], // @TODO make required after raider.io supports it
'roster.classIds.*' => ['nullable', 'int'], // @TODO make required after raider.io supports it
'roster.numMembers' => ['nullable', 'int'], // @TODO make required after raider.io supports it
'roster.averageItemLevel' => ['nullable', 'numeric'], // @TODO make required after raider.io supports it
'roster.characterIds' => ['nullable', 'array'], // @TODO make required after raider.io supports it
'roster.characterIds.*' => ['nullable', 'int'], // @TODO make required after raider.io supports it
'roster.specIds' => ['nullable', 'array'], // @TODO make required after raider.io supports it
'roster.specIds.*' => ['nullable', 'int'], // @TODO make required after raider.io supports it
'roster.classIds' => ['nullable', 'array'], // @TODO make required after raider.io supports it
'roster.classIds.*' => ['nullable', 'int'], // @TODO make required after raider.io supports it
'challengeMode.start' => ['required', $dateFormat],
'challengeMode.end' => ['required', $dateFormat],
'challengeMode.durationMs' => ['required', 'int'],
Expand All @@ -65,7 +65,7 @@ public function rules(): array
'challengeMode.level' => ['required', 'int'],
'challengeMode.numDeaths' => ['nullable', 'int'], // @TODO make required after raider.io supports it
'challengeMode.affixes' => ['required', 'array'],
'challengeMode.affixes.*' => ['required', Rule::exists(Affix::class, 'affix_id')],
'challengeMode.affixes.*' => ['required', 'integer'], // #1818 Rule::exists(Affix::class, 'affix_id')],
'npcs' => ['required', 'array', new CombatLogRouteNpcChronologicalRule()],
'npcs.*.npcId' => ['required', 'integer'], // #1818 Rule::exists('npcs', 'id')
'npcs.*.spawnUid' => ['required', 'string', 'max:10'],
Expand All @@ -81,15 +81,15 @@ public function rules(): array
'spells.*.coord.x' => 'numeric',
'spells.*.coord.y' => 'numeric',
'spells.*.coord.uiMapId' => Rule::exists(Floor::class, 'ui_map_id'),
'playerDeaths' => 'nullable|array', // @TODO make required after raider.io supports it
'playerDeaths.*.characterId' => 'integer', // @TODO make required after raider.io supports it
'playerDeaths.*.classId' => 'integer', // @TODO make required after raider.io supports it
'playerDeaths.*.specId' => 'integer', // @TODO make required after raider.io supports it
'playerDeaths.*.itemLevel' => 'numeric', // @TODO make required after raider.io supports it
'playerDeaths.*.diedAt' => $dateFormat, // @TODO make required after raider.io supports it
'playerDeaths.*.coord.x' => 'numeric', // @TODO make required after raider.io supports it
'playerDeaths.*.coord.y' => 'numeric', // @TODO make required after raider.io supports it
'playerDeaths.*.coord.uiMapId' => Rule::exists(Floor::class, 'ui_map_id'), // @TODO make required after raider.io supports it
'playerDeaths' => 'nullable|array', // @TODO make required after raider.io supports it
'playerDeaths.*.characterId' => 'integer', // @TODO make required after raider.io supports it
'playerDeaths.*.classId' => 'integer', // @TODO make required after raider.io supports it
'playerDeaths.*.specId' => 'integer', // @TODO make required after raider.io supports it
'playerDeaths.*.itemLevel' => 'numeric', // @TODO make required after raider.io supports it
'playerDeaths.*.diedAt' => $dateFormat, // @TODO make required after raider.io supports it
'playerDeaths.*.coord.x' => 'numeric', // @TODO make required after raider.io supports it
'playerDeaths.*.coord.y' => 'numeric', // @TODO make required after raider.io supports it
'playerDeaths.*.coord.uiMapId' => Rule::exists(Floor::class, 'ui_map_id'), // @TODO make required after raider.io supports it
];
}
}
55 changes: 43 additions & 12 deletions app/Providers/RouteServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,19 @@

class RouteServiceProvider extends ServiceProvider
{
private const RATE_LIMIT_OVERRIDE = null;
private const RATE_LIMIT_OVERRIDE_HTTP = null;
private const RATE_LIMIT_OVERRIDE_PER_MINUTE_API = null;

/**
* Define your route model bindings, pattern filters, etc.
*/
public function boot(): void
{
parent::boot();

$this->configureRateLimiting();

parent::boot();
$this->configureApiRateLimiting();
}

/**
Expand Down Expand Up @@ -54,7 +57,7 @@ protected function mapWebRoutes(): void
protected function mapApiRoutes(): void
{
Route::prefix('api')
->middleware('api')
->middleware(['api', 'throttle:api-general'])
->group(base_path('routes/api.php'));
}

Expand All @@ -64,35 +67,51 @@ protected function mapApiRoutes(): void
protected function configureRateLimiting(): void
{
RateLimiter::for('create-dungeonroute', function (Request $request) {
return $this->noLimitForExemptions($request) ?? Limit::perHour(self::RATE_LIMIT_OVERRIDE ?? 20)->by($this->userKey($request));
return $this->noLimitForExemptions($request) ?? Limit::perHour(self::RATE_LIMIT_OVERRIDE_HTTP ?? 20)->by($this->userKey($request));
});
RateLimiter::for('create-tag', function (Request $request) {
return $this->noLimitForExemptions($request) ?? Limit::perHour(self::RATE_LIMIT_OVERRIDE ?? 60)->by($this->userKey($request));
return $this->noLimitForExemptions($request) ?? Limit::perHour(self::RATE_LIMIT_OVERRIDE_HTTP ?? 60)->by($this->userKey($request));
});
RateLimiter::for('create-team', function (Request $request) {
return $this->noLimitForExemptions($request) ?? Limit::perHour(self::RATE_LIMIT_OVERRIDE ?? 5)->by($this->userKey($request));
return $this->noLimitForExemptions($request) ?? Limit::perHour(self::RATE_LIMIT_OVERRIDE_HTTP ?? 5)->by($this->userKey($request));
});
RateLimiter::for('create-reports', function (Request $request) {
return $this->noLimitForExemptions($request) ?? Limit::perHour(self::RATE_LIMIT_OVERRIDE ?? 60)->by($this->userKey($request));
return $this->noLimitForExemptions($request) ?? Limit::perHour(self::RATE_LIMIT_OVERRIDE_HTTP ?? 60)->by($this->userKey($request));
});
RateLimiter::for('create-user', function (Request $request) {
return $this->noLimitForExemptions($request) ?? Limit::perHour(self::RATE_LIMIT_OVERRIDE ?? 10)->by($this->userKey($request));
return $this->noLimitForExemptions($request) ?? Limit::perHour(self::RATE_LIMIT_OVERRIDE_HTTP ?? 10)->by($this->userKey($request));
});

// Heavy GET requests
RateLimiter::for('search-dungeonroute', function (Request $request) {
return $this->noLimitForExemptions($request) ?? Limit::perHour(self::RATE_LIMIT_OVERRIDE ?? 300)->by($this->userKey($request));
return $this->noLimitForExemptions($request) ?? Limit::perHour(self::RATE_LIMIT_OVERRIDE_HTTP ?? 300)->by($this->userKey($request));
});

// This consumes the same resources as creating a route - so we limit it
RateLimiter::for('mdt-details', function (Request $request) {
return $this->noLimitForExemptions($request) ?? Limit::perHour(self::RATE_LIMIT_OVERRIDE ?? 60)->by($this->userKey($request));
return $this->noLimitForExemptions($request) ?? Limit::perHour(self::RATE_LIMIT_OVERRIDE_HTTP ?? 60)->by($this->userKey($request));
});
RateLimiter::for('mdt-export', function (Request $request) {
return $this->noLimitForExemptions($request) ?? Limit::perHour(self::RATE_LIMIT_OVERRIDE ?? 60)->by($this->userKey($request));
return $this->noLimitForExemptions($request) ?? Limit::perHour(self::RATE_LIMIT_OVERRIDE_HTTP ?? 60)->by($this->userKey($request));
});
RateLimiter::for('simulate', function (Request $request) {
return $this->noLimitForExemptions($request) ?? Limit::perHour(self::RATE_LIMIT_OVERRIDE ?? 120)->by($this->userKey($request));
return $this->noLimitForExemptions($request) ?? Limit::perHour(self::RATE_LIMIT_OVERRIDE_HTTP ?? 120)->by($this->userKey($request));
});
}

private function configureApiRateLimiting(): void
{
RateLimiter::for('api-general', function (Request $request) {
return $this->noLimitForExemptionsApi($request) ?? Limit::perMinute(self::RATE_LIMIT_OVERRIDE_PER_MINUTE_API ?? 600)->by($this->userKey($request));
});
RateLimiter::for('api-combatlog-create-dungeonroute', function (Request $request) {
return $this->noLimitForExemptionsApi($request) ?? Limit::perMinute(self::RATE_LIMIT_OVERRIDE_PER_MINUTE_API ?? 120)->by($this->userKey($request));
});
RateLimiter::for('api-combatlog-correct-event', function (Request $request) {
return $this->noLimitForExemptionsApi($request) ?? Limit::perMinute(self::RATE_LIMIT_OVERRIDE_PER_MINUTE_API ?? 5)->by($this->userKey($request));
});
RateLimiter::for('api-create-dungeonroute-thumbnail', function (Request $request) {
return $this->noLimitForExemptionsApi($request) ?? Limit::perMinute(self::RATE_LIMIT_OVERRIDE_PER_MINUTE_API ?? 30)->by($this->userKey($request));
});
}

Expand All @@ -108,6 +127,18 @@ private function noLimitForExemptions(Request $request): ?Limit
return null;
}

private function noLimitForExemptionsApi(Request $request): ?Limit
{
/** @var User|null $user */
$user = $request->user();

if ($user?->hasRole(Role::ROLE_ADMIN)) {
return Limit::none();
}

return null;
}

private function userKey(Request $request): string
{
/** @var User|null $user */
Expand Down
4 changes: 2 additions & 2 deletions docker-compose/app/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ RUN /root/.cargo/bin/cargo install --git https://github.com/Zireael-N/cli-weakau
# Install actual LUA language
RUN apt-get update && apt-get install -y lua5.3 liblua5.3 && \
ln -s /usr/include/lua5.3/ /usr/include/lua && \
cp /usr/lib/x86_64-linux-gnu/liblua5.3.a /usr/lib/liblua.a && \
cp /usr/lib/x86_64-linux-gnu/liblua5.3.so.0.0.0 /usr/lib/liblua.so && \
cp /usr/lib/*-linux-gnu/liblua5.3.a /usr/lib/liblua.a && \
cp /usr/lib/*-linux-gnu/liblua5.3.so.0.0.0 /usr/lib/liblua.so && \
ln /usr/include/lua5.3/lua.h /usr/include/lauxlib.h && \
ln /usr/include/lua5.3/lua.h /usr/include/lua.h && \
ln /usr/include/lua5.3/lua.h /usr/include/luaconf.h
Expand Down
1 change: 1 addition & 0 deletions lang/en_US/exceptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
'api_route_not_found' => 'API route not found',
'internal_server_error' => 'Internal server error',
'unauthenticated' => 'Unauthenticated',
'too_many_requests' => 'Too many requests',
],

];
8 changes: 5 additions & 3 deletions routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,19 @@
*/
Route::prefix('v1')->group(static function () {
Route::prefix('combatlog')->group(static function () {
Route::prefix('route')->group(static function () {
Route::middleware('throttle:api-combatlog-create-dungeonroute')->prefix('route')->group(static function () {
Route::post('/', (new APICombatLogController())->createRoute(...))->name('api.v1.combatlog.route.create');
});
Route::prefix('event')->group(static function () {
Route::middleware('throttle:api-combatlog-correct-event')->prefix('event')->group(static function () {
Route::post('correct', (new APICombatLogController())->correctEvents(...))->name('api.v1.combatlog.event.correct');
});
});

Route::prefix('route')->group(static function () {
Route::get('/', (new APIDungeonRouteController())->get(...))->name('api.v1.route.list');
Route::post('/{dungeonRoute}/thumbnail', (new APIDungeonRouteController())->createThumbnails(...))->name('api.v1.route.thumbnail.create');
Route::middleware('throttle:api-create-dungeonroute-thumbnail')->group(static function () {
Route::post('/{dungeonRoute}/thumbnail', (new APIDungeonRouteController())->createThumbnails(...))->name('api.v1.route.thumbnail.create');
});
Route::get('/thumbnailJob/{dungeonRouteThumbnailJob}', (new APIDungeonRouteThumbnailJobController())->get(...))->name('api.v1.thumbnailjob.get');
});

Expand Down

0 comments on commit 22d9697

Please sign in to comment.