ratelimiter.php 1.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Security;
  4. use App\App\Bootstrap;
  5. final class RateLimiter
  6. {
  7. private string $storageDir;
  8. private bool $enabled;
  9. public function __construct()
  10. {
  11. $app = Bootstrap::config('app');
  12. $this->enabled = (bool) ($app['rate_limit']['enabled'] ?? true);
  13. $this->storageDir = (string) ($app['storage']['rate_limit'] ?? Bootstrap::rootPath() . '/storage/rate_limit');
  14. if ($this->enabled && !is_dir($this->storageDir)) {
  15. mkdir($this->storageDir, 0775, true);
  16. }
  17. }
  18. public function allow(string $key, int $limit, int $windowSeconds): bool
  19. {
  20. if (!$this->enabled) {
  21. return true;
  22. }
  23. $hash = hash('sha256', $key);
  24. $path = $this->storageDir . '/' . $hash . '.json';
  25. $now = time();
  26. $handle = fopen($path, 'c+');
  27. if ($handle === false) {
  28. return true;
  29. }
  30. try {
  31. if (!flock($handle, LOCK_EX)) {
  32. return true;
  33. }
  34. $contents = stream_get_contents($handle);
  35. $timestamps = [];
  36. if (is_string($contents) && $contents !== '') {
  37. $decoded = json_decode($contents, true);
  38. if (is_array($decoded)) {
  39. $timestamps = array_values(array_filter($decoded, static fn ($ts): bool => is_int($ts)));
  40. }
  41. }
  42. $threshold = $now - $windowSeconds;
  43. $timestamps = array_values(array_filter($timestamps, static fn (int $ts): bool => $ts >= $threshold));
  44. if (count($timestamps) >= $limit) {
  45. return false;
  46. }
  47. $timestamps[] = $now;
  48. ftruncate($handle, 0);
  49. rewind($handle);
  50. fwrite($handle, json_encode($timestamps));
  51. return true;
  52. } finally {
  53. flock($handle, LOCK_UN);
  54. fclose($handle);
  55. }
  56. }
  57. }