53 lines
1.6 KiB
PHP
53 lines
1.6 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
final class RateLimiter
|
|
{
|
|
public function __construct(private string $dir)
|
|
{
|
|
}
|
|
|
|
public function assertAllowed(string $bucket, int $limit, int $windowSeconds): void
|
|
{
|
|
if ($limit < 1) {
|
|
throw new RuntimeException('Rate limit is invalid');
|
|
}
|
|
if (!is_dir($this->dir)) {
|
|
mkdir($this->dir, 0700, true);
|
|
}
|
|
$file = $this->dir . '/' . preg_replace('/[^A-Za-z0-9._-]/', '_', $bucket) . '.json';
|
|
$handle = fopen($file, 'c+');
|
|
if ($handle === false) {
|
|
throw new RuntimeException('Cannot open rate limit bucket');
|
|
}
|
|
if (!flock($handle, LOCK_EX)) {
|
|
fclose($handle);
|
|
throw new RuntimeException('Cannot lock rate limit bucket');
|
|
}
|
|
$now = time();
|
|
$events = [];
|
|
$contents = stream_get_contents($handle);
|
|
$decoded = json_decode(is_string($contents) ? $contents : '', true);
|
|
if (is_array($decoded)) {
|
|
foreach ($decoded as $event) {
|
|
if (is_int($event) && $event > $now - $windowSeconds) {
|
|
$events[] = $event;
|
|
}
|
|
}
|
|
}
|
|
if (count($events) >= $limit) {
|
|
flock($handle, LOCK_UN);
|
|
fclose($handle);
|
|
throw new RuntimeException('Rate limit exceeded');
|
|
}
|
|
$events[] = $now;
|
|
ftruncate($handle, 0);
|
|
rewind($handle);
|
|
fwrite($handle, json_encode($events) ?: '[]');
|
|
fflush($handle);
|
|
flock($handle, LOCK_UN);
|
|
fclose($handle);
|
|
@chmod($file, 0600);
|
|
}
|
|
}
|