mirror of
https://github.com/AnimeThemes/animethemes-server.git
synced 2026-07-11 01:24:46 +02:00
feat: video throttling (#637)
This commit is contained in:
@@ -291,6 +291,7 @@ VIDEO_DISKS=videos_local
|
||||
VIDEO_DISK_ROOT=
|
||||
VIDEO_PATH=/video
|
||||
VIDEO_URL=
|
||||
VIDEO_RATE_LIMITER=90
|
||||
VIDEO_STREAMING_METHOD=response
|
||||
VIDEO_NGINX_REDIRECT=
|
||||
|
||||
|
||||
@@ -289,6 +289,7 @@ VIDEO_DISKS=videos_local
|
||||
VIDEO_DISK_ROOT=
|
||||
VIDEO_PATH=/video
|
||||
VIDEO_URL=
|
||||
VIDEO_RATE_LIMITER=90
|
||||
VIDEO_STREAMING_METHOD=response
|
||||
VIDEO_NGINX_REDIRECT=
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ class VideoConstants
|
||||
|
||||
final public const PATH_QUALIFIED = 'video.path';
|
||||
|
||||
final public const RATE_LIMITER_QUALIFIED = 'video.rate_limiter';
|
||||
|
||||
final public const STREAMING_METHOD_QUALIFIED = 'video.streaming_method';
|
||||
|
||||
final public const URL_QUALIFIED = 'video.url';
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Events\Wiki\Video;
|
||||
|
||||
use App\Constants\Config\ServiceConstants;
|
||||
use App\Contracts\Events\DiscordMessageEvent;
|
||||
use App\Enums\Discord\EmbedColor;
|
||||
use App\Models\Wiki\Video;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use NotificationChannels\Discord\DiscordMessage;
|
||||
|
||||
/**
|
||||
* Class VideoThrottled.
|
||||
*/
|
||||
class VideoThrottled implements DiscordMessageEvent
|
||||
{
|
||||
use Dispatchable;
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create new event instance.
|
||||
*
|
||||
* @param Video $video
|
||||
* @param string $user
|
||||
*/
|
||||
public function __construct(protected Video $video, protected string $user)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Discord message payload.
|
||||
*
|
||||
* @return DiscordMessage
|
||||
*/
|
||||
public function getDiscordMessage(): DiscordMessage
|
||||
{
|
||||
return DiscordMessage::create('', [
|
||||
'description' => "Video '**{$this->video->getName()}**' throttled for user '**$this->user**'",
|
||||
'color' => EmbedColor::YELLOW->value,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Discord channel the message will be sent to.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDiscordChannel(): string
|
||||
{
|
||||
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the message should be sent.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldSendDiscordMessage(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,15 @@ use App\Constants\Config\AudioConstants;
|
||||
use App\Constants\Config\DumpConstants;
|
||||
use App\Constants\Config\VideoConstants;
|
||||
use App\Enums\Auth\SpecialPermission;
|
||||
use App\Events\Wiki\Video\VideoThrottled;
|
||||
use App\Models\Auth\User;
|
||||
use App\Models\Wiki\Video;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
@@ -70,6 +73,21 @@ class RouteServiceProvider extends ServiceProvider
|
||||
return Limit::perMinute(90)->by(Auth::check() ? Auth::id() : $ip);
|
||||
});
|
||||
|
||||
RateLimiter::for('video', function (Request $request) {
|
||||
$limit = Config::get(VideoConstants::RATE_LIMITER_QUALIFIED);
|
||||
|
||||
if ($limit <= 0) {
|
||||
return Limit::none();
|
||||
}
|
||||
|
||||
return Limit::perMinute($limit)->by($request->ip())->response(function (Request $request) {
|
||||
/** @var Video $video */
|
||||
$video = $request->route('video');
|
||||
|
||||
VideoThrottled::dispatch($video, Crypt::encryptString($request->ip()));
|
||||
});
|
||||
});
|
||||
|
||||
$this->routes(function () {
|
||||
Route::middleware('web')
|
||||
->domain(Config::get('web.url'))
|
||||
|
||||
@@ -38,6 +38,19 @@ return [
|
||||
|
||||
'path' => env('VIDEO_PATH'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Video Rate Limiter
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value represents the number of requests permitted to stream video per minute.
|
||||
| If set to a value less than or equal to zero, the limiter shall be unlimited.
|
||||
| If set to a value greater than 0, the limiter shall restrict by that value.
|
||||
|
|
||||
*/
|
||||
|
||||
'rate_limiter' => (int) env('VIDEO_RATE_LIMITER', -1),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Video Streaming
|
||||
|
||||
+1
-1
@@ -15,4 +15,4 @@ $isVideoStreamingAllowed = Str::of(EnsureFeaturesAreActive::class)
|
||||
|
||||
Route::get('/{video}', [VideoController::class, 'show'])
|
||||
->name('video.show')
|
||||
->middleware([$isVideoStreamingAllowed, 'record_view:video']);
|
||||
->middleware([$isVideoStreamingAllowed, 'record_view:video', 'throttle:video']);
|
||||
|
||||
@@ -8,13 +8,17 @@ use App\Constants\Config\VideoConstants;
|
||||
use App\Constants\FeatureConstants;
|
||||
use App\Enums\Auth\SpecialPermission;
|
||||
use App\Enums\Http\StreamingMethod;
|
||||
use App\Events\Wiki\Video\VideoThrottled;
|
||||
use App\Features\AllowVideoStreams;
|
||||
use App\Jobs\SendDiscordNotificationJob;
|
||||
use App\Models\Auth\User;
|
||||
use App\Models\Wiki\Video;
|
||||
use Illuminate\Foundation\Testing\WithFaker;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Laravel\Pennant\Feature;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
@@ -207,4 +211,100 @@ class VideoTest extends TestCase
|
||||
|
||||
$response->assertHeader('X-Accel-Redirect');
|
||||
}
|
||||
|
||||
/**
|
||||
* If the video rate limit is less than or equal to zero, videos shall not be throttled.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testNotThrottled(): void
|
||||
{
|
||||
Storage::fake(Config::get(VideoConstants::DEFAULT_DISK_QUALIFIED));
|
||||
|
||||
Feature::activate(AllowVideoStreams::class);
|
||||
Config::set(VideoConstants::STREAMING_METHOD_QUALIFIED, Arr::random(StreamingMethod::cases())->value);
|
||||
Config::set(VideoConstants::RATE_LIMITER_QUALIFIED, -1);
|
||||
|
||||
$video = Video::factory()->createOne();
|
||||
|
||||
$response = $this->get(route('video.show', ['video' => $video]));
|
||||
|
||||
$response->assertHeaderMissing('X-RateLimit-Limit');
|
||||
$response->assertHeaderMissing('X-RateLimit-Remaining');
|
||||
}
|
||||
|
||||
/**
|
||||
* If the video rate limit is greater than or equal to zero, videos shall be throttled.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testRateLimited(): void
|
||||
{
|
||||
Storage::fake(Config::get(VideoConstants::DEFAULT_DISK_QUALIFIED));
|
||||
|
||||
Feature::activate(AllowVideoStreams::class);
|
||||
Config::set(VideoConstants::STREAMING_METHOD_QUALIFIED, Arr::random(StreamingMethod::cases())->value);
|
||||
Config::set(VideoConstants::RATE_LIMITER_QUALIFIED, $this->faker->randomDigitNotNull());
|
||||
|
||||
$video = Video::factory()->createOne();
|
||||
|
||||
$response = $this->get(route('video.show', ['video' => $video]));
|
||||
|
||||
$response->assertHeader('X-RateLimit-Limit');
|
||||
$response->assertHeader('X-RateLimit-Remaining');
|
||||
}
|
||||
|
||||
/**
|
||||
* If the video rate limit attempt is exceeded, a VideoThrottled event shall be dispatched.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testThrottledEvent(): void
|
||||
{
|
||||
$limit = $this->faker->randomDigitNotNull();
|
||||
|
||||
Event::fake();
|
||||
|
||||
Storage::fake(Config::get(VideoConstants::DEFAULT_DISK_QUALIFIED));
|
||||
|
||||
Feature::activate(AllowVideoStreams::class);
|
||||
Config::set(VideoConstants::STREAMING_METHOD_QUALIFIED, Arr::random(StreamingMethod::cases())->value);
|
||||
Config::set(VideoConstants::RATE_LIMITER_QUALIFIED, $limit);
|
||||
|
||||
$video = Video::factory()->createOne();
|
||||
|
||||
Collection::times($limit + 1, function () use ($video) {
|
||||
$this->get(route('video.show', ['video' => $video]));
|
||||
});
|
||||
|
||||
Event::assertDispatched(VideoThrottled::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the video rate limit attempt is exceeded, a SendDiscordNotification job shall be dispatched.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testThrottledNotification(): void
|
||||
{
|
||||
$limit = $this->faker->randomDigitNotNull();
|
||||
|
||||
Storage::fake(Config::get(VideoConstants::DEFAULT_DISK_QUALIFIED));
|
||||
|
||||
Bus::fake(SendDiscordNotificationJob::class);
|
||||
|
||||
Feature::activate(FeatureConstants::ALLOW_DISCORD_NOTIFICATIONS);
|
||||
Feature::activate(AllowVideoStreams::class);
|
||||
Config::set(VideoConstants::STREAMING_METHOD_QUALIFIED, Arr::random(StreamingMethod::cases())->value);
|
||||
Config::set(VideoConstants::RATE_LIMITER_QUALIFIED, $limit);
|
||||
Event::fakeExcept(VideoThrottled::class);
|
||||
|
||||
$video = Video::factory()->createOne();
|
||||
|
||||
Collection::times($limit + 1, function () use ($video) {
|
||||
$this->get(route('video.show', ['video' => $video]));
|
||||
});
|
||||
|
||||
Bus::assertDispatched(SendDiscordNotificationJob::class);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user