| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174 |
- <?php
- declare(strict_types=1);
- // File-based per-IP rate limiting for hosting without Redis. Ported from the
- // PSA order system (includes/functions.php rateLimitEvaluate). State lives
- // under storage/ratelimit/ and is therefore not web-readable.
- function manageRateLimitClientIp(): string
- {
- $ip = trim((string) ($_SERVER["REMOTE_ADDR"] ?? ""));
- return $ip !== "" ? $ip : "unknown";
- }
- function manageRateLimitStatePath(string $name): string
- {
- $name = preg_replace("/[^a-z0-9_-]/", "", strtolower($name)) ?? "";
- if ($name === "") {
- $name = "default";
- }
- return manageStorageDir() . "ratelimit" . DIRECTORY_SEPARATOR . $name . ".json";
- }
- /**
- * @return bool true while under the limit; increments the counter when $consume
- */
- function manageRateLimitEvaluate(
- string $name,
- int $maxAttempts,
- int $windowSeconds,
- bool $consume,
- ): bool {
- if ($maxAttempts < 1 || $windowSeconds < 1) {
- return true;
- }
- $path = manageRateLimitStatePath($name);
- $dir = dirname($path);
- if (!is_dir($dir) && !@mkdir($dir, 02775, true) && !is_dir($dir)) {
- // Without usable state the limiter must not lock anybody out.
- return true;
- }
- @chmod($dir, 02775);
- $handle = fopen($path, "c+");
- if ($handle === false) {
- return true;
- }
- if (!flock($handle, LOCK_EX)) {
- fclose($handle);
- return true;
- }
- rewind($handle);
- $raw = stream_get_contents($handle);
- $data = [];
- if (is_string($raw) && trim($raw) !== "") {
- $decoded = json_decode($raw, true);
- if (is_array($decoded)) {
- $data = $decoded;
- }
- }
- $now = time();
- $ip = manageRateLimitClientIp();
- foreach ($data as $key => $entry) {
- if (!is_array($entry)) {
- unset($data[$key]);
- continue;
- }
- if ($now - (int) ($entry["w"] ?? 0) > $windowSeconds * 2) {
- unset($data[$key]);
- }
- }
- $entry = isset($data[$ip]) && is_array($data[$ip]) ? $data[$ip] : null;
- $windowStart = (int) ($entry["w"] ?? $now);
- $count = (int) ($entry["c"] ?? 0);
- if ($entry === null || $now - $windowStart > $windowSeconds) {
- $windowStart = $now;
- $count = 0;
- }
- if ($count >= $maxAttempts) {
- flock($handle, LOCK_UN);
- fclose($handle);
- return false;
- }
- if ($consume) {
- $data[$ip] = ["w" => $windowStart, "c" => $count + 1];
- $payload = json_encode($data, JSON_UNESCAPED_UNICODE);
- if ($payload !== false) {
- ftruncate($handle, 0);
- rewind($handle);
- fwrite($handle, $payload);
- fflush($handle);
- @chmod($path, 0660);
- }
- }
- flock($handle, LOCK_UN);
- fclose($handle);
- return true;
- }
- function manageRateLimitTryConsume(string $name, int $maxAttempts, int $windowSeconds): bool
- {
- return manageRateLimitEvaluate($name, $maxAttempts, $windowSeconds, true);
- }
- function manageRateLimitClearIp(string $name): void
- {
- $path = manageRateLimitStatePath($name);
- if (!is_file($path)) {
- return;
- }
- $handle = fopen($path, "c+");
- if ($handle === false) {
- return;
- }
- if (!flock($handle, LOCK_EX)) {
- fclose($handle);
- return;
- }
- rewind($handle);
- $raw = stream_get_contents($handle);
- $data = is_string($raw) && trim($raw) !== "" ? json_decode($raw, true) : [];
- if (is_array($data)) {
- unset($data[manageRateLimitClientIp()]);
- $payload = json_encode($data, JSON_UNESCAPED_UNICODE);
- if ($payload !== false) {
- ftruncate($handle, 0);
- rewind($handle);
- fwrite($handle, $payload);
- fflush($handle);
- }
- }
- flock($handle, LOCK_UN);
- fclose($handle);
- }
- function manageLoginIsRateLimited(): bool
- {
- return !manageRateLimitEvaluate(
- "admin-login",
- (int) MANAGE_LOGIN_RATE_LIMIT_MAX,
- (int) MANAGE_LOGIN_RATE_LIMIT_WINDOW,
- false,
- );
- }
- function manageLoginRecordFailure(): void
- {
- manageRateLimitTryConsume(
- "admin-login",
- (int) MANAGE_LOGIN_RATE_LIMIT_MAX,
- (int) MANAGE_LOGIN_RATE_LIMIT_WINDOW,
- );
- }
- function manageLoginClearRateLimit(): void
- {
- manageRateLimitClearIp("admin-login");
- }
|