config->get('APP_KEY'); if (strlen($key) < 32) { throw new \RuntimeException('APP_KEY must have at least 32 characters'); } $directory = $this->config->get('RATE_LIMIT_DIR') ?: $this->config->root . '/var/rate-limits'; if (!is_dir($directory) && !@mkdir($directory, 0700, true) && !is_dir($directory)) { throw new \RuntimeException('Rate limit storage unavailable'); } $file = $directory . '/' . hash_hmac('sha256', $ip, $key) . '.json'; $handle = @fopen($file, 'c+'); if (!$handle) { throw new \RuntimeException('Rate limit storage unavailable'); } try { if (!flock($handle, LOCK_EX)) { throw new \RuntimeException('Rate limit lock unavailable'); } $now = time(); $window = $this->config->number('INQUIRY_RATE_WINDOW', 900); $record = json_decode(stream_get_contents($handle), true); if (!is_array($record) || !isset($record['start'], $record['count']) || $record['start'] + $window <= $now) { $record = ['start' => $now, 'count' => 0]; } if ($record['count'] >= $this->config->number('INQUIRY_RATE_MAX', 5)) { return false; } $record['count']++; $payload = json_encode($record, JSON_THROW_ON_ERROR); rewind($handle); if (!ftruncate($handle, 0) || fwrite($handle, $payload) !== strlen($payload) || !fflush($handle)) { throw new \RuntimeException('Rate limit write unavailable'); } return true; } finally { flock($handle, LOCK_UN); fclose($handle); } } }