Skip to content

Commit 123da18

Browse files
committed
feat: re-implement rate limiting
1 parent 39d10da commit 123da18

File tree

3 files changed

+122
-0
lines changed

3 files changed

+122
-0
lines changed

config/image-transform-url.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,23 @@
6161
'lifetime' => env('IMAGE_TRANSFORM_CACHE_LIFETIME', 60 * 24 * 7), // 7 days
6262
],
6363

64+
/*
65+
|--------------------------------------------------------------------------
66+
| Rate Limit
67+
|--------------------------------------------------------------------------
68+
|
69+
| Below you may configure the rate limit which is applied for each image
70+
| new transformation by the path and IP address. It is recommended to
71+
| set this to a low value, e.g. 2 requests per minute, to prevent
72+
| abuse.
73+
*/
74+
75+
'rate_limit' => [
76+
'enabled' => env('IMAGE_TRANSFORM_RATE_LIMIT_ENABLED', app()->isProduction()),
77+
'max_attempts' => env('IMAGE_TRANSFORM_RATE_LIMIT_MAX_REQUESTS', 2),
78+
'decay_seconds' => env('IMAGE_TRANSFORM_RATE_LIMIT_DECAY_SECONDS', 60),
79+
],
80+
6481
/*
6582
|--------------------------------------------------------------------------
6683
| Response Headers

src/Http/Controllers/ImageTransformerController.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Illuminate\Support\Arr;
1111
use Illuminate\Support\Facades\Cache;
1212
use Illuminate\Support\Facades\File;
13+
use Illuminate\Support\Facades\RateLimiter;
1314
use Illuminate\Support\Facades\Storage;
1415
use Intervention\Image\Drivers\Gd\Encoders\WebpEncoder;
1516
use Intervention\Image\Encoders\AutoEncoder;
@@ -49,6 +50,10 @@ public function __invoke(Request $request, string $options, string $path)
4950
}
5051
}
5152

53+
if (config()->boolean('image-transform-url.rate_limit.enabled')) {
54+
$this->rateLimit($request, $path);
55+
}
56+
5257
$image = Image::read($publicPath);
5358

5459
if (Arr::hasAny($options, ['width', 'height'])) {
@@ -120,6 +125,23 @@ public function __invoke(Request $request, string $options, string $path)
120125

121126
}
122127

128+
/**
129+
* Rate limit the request.
130+
*/
131+
protected function rateLimit(Request $request, string $path): void
132+
{
133+
$key = 'image-transform-url:'.$request->ip().':'.$path;
134+
135+
$passed = RateLimiter::attempt(
136+
key: $key,
137+
maxAttempts: config()->integer('image-transform-url.rate_limit.max_attempts'),
138+
callback: fn () => true,
139+
decaySeconds: config()->integer('image-transform-url.rate_limit.decay_seconds'),
140+
);
141+
142+
abort_unless($passed, 429, 'Too many requests. Please try again later.');
143+
}
144+
123145
/**
124146
* Parse the given options.
125147
*

tests/Feature/RateLimitTest.php

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use AceOfAces\LaravelImageTransformUrl\Tests\TestCase;
6+
7+
beforeEach(function () {
8+
Cache::flush();
9+
Storage::fake('local');
10+
});
11+
12+
it('can apply rate limiting to image transformation requests with distinct options', function () {
13+
// Set up the rate limit configuration
14+
config()->set('image-transform-url.rate_limit.enabled', true);
15+
config()->set('image-transform-url.rate_limit.max_attempts', 2);
16+
config()->set('image-transform-url.rate_limit.decay_seconds', 60);
17+
18+
/** @var TestCase $this */
19+
$firstResponse = $this->get(route('image.transform', [
20+
'options' => 'width=100',
21+
'path' => 'cat.jpg',
22+
]));
23+
24+
expect($firstResponse)->toBeImage([
25+
'width' => 100,
26+
'mime' => 'image/jpeg',
27+
]);
28+
29+
$secondResponse = $this->get(route('image.transform', [
30+
'options' => 'width=200',
31+
'path' => 'cat.jpg',
32+
]));
33+
34+
expect($secondResponse)->toBeImage([
35+
'width' => 200,
36+
'mime' => 'image/jpeg',
37+
]);
38+
39+
$thirdResponse = $this->get(route('image.transform', [
40+
'options' => 'width=300',
41+
'path' => 'cat.jpg',
42+
]));
43+
44+
$thirdResponse->assertTooManyRequests();
45+
});
46+
47+
it('does not apply rate limiting to image transformations with identical options', function () {
48+
// Set up the rate limit configuration
49+
config()->set('image-transform-url.rate_limit.enabled', true);
50+
config()->set('image-transform-url.rate_limit.max_attempts', 2);
51+
config()->set('image-transform-url.rate_limit.decay_seconds', 60);
52+
53+
/** @var TestCase $this */
54+
$firstResponse = $this->get(route('image.transform', [
55+
'options' => 'width=100',
56+
'path' => 'cat.jpg',
57+
]));
58+
59+
expect($firstResponse)->toBeImage([
60+
'width' => 100,
61+
'mime' => 'image/jpeg',
62+
]);
63+
64+
$secondResponse = $this->get(route('image.transform', [
65+
'options' => 'width=100',
66+
'path' => 'cat.jpg',
67+
]));
68+
69+
expect($secondResponse)->toBeImage([
70+
'width' => 100,
71+
'mime' => 'image/jpeg',
72+
]);
73+
74+
$thirdResponse = $this->get(route('image.transform', [
75+
'options' => 'width=100',
76+
'path' => 'cat.jpg',
77+
]));
78+
79+
expect($thirdResponse)->toBeImage([
80+
'width' => 100,
81+
'mime' => 'image/jpeg',
82+
]);
83+
});

0 commit comments

Comments
 (0)