oatpp-authkit/include/oatpp-authkit/util/RateLimiter.hpp
Uwe Schuster 081e0b36dc v0.2.1: wrap clean-lift headers in namespace oatpp_authkit
The four clean-lift headers (SecurityHeadersInterceptor,
BodySizeLimitInterceptor, JsonErrorHandler, RateLimiter) were copied
verbatim in v0.1.0 and left in the global namespace — consumers that
adopt the library alongside existing same-named classes (e.g. fewo-webapp
during the #417 swap) would hit ODR clashes.

Wrap them in the same namespace the v0.2 auth seams use. Patch bump; no
API surface change beyond the qualifier.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 21:53:21 +02:00

89 lines
2.8 KiB
C++

#ifndef UTIL_RATE_LIMITER_HPP
#define UTIL_RATE_LIMITER_HPP
#include <chrono>
#include <mutex>
#include <string>
#include <unordered_map>
namespace oatpp_authkit {
/**
* @brief Per-key token bucket rate limiter.
*
* Each unique key (typically a client IP) gets its own bucket that refills
* at a steady rate up to a maximum capacity. Thread-safe.
*
* Lazy eviction (#391): when the bucket map exceeds 10,000 entries, expired
* buckets (tokens fully refilled = idle for capacity/refillRate seconds)
* are removed to prevent unbounded memory growth.
*
* Usage:
* RateLimiter limiter(30.0, 0.5); // 30 burst, 0.5 tokens/sec
* if (!limiter.allow("192.168.1.1")) { ... // reject }
*/
class RateLimiter {
public:
/**
* @param capacity Maximum burst size (tokens).
* @param refillRate Tokens added per second.
*/
RateLimiter(double capacity, double refillRate)
: m_capacity(capacity), m_refillRate(refillRate) {}
/** @brief Try to consume one token for the given key. Returns true if allowed. */
bool allow(const std::string& key) {
std::lock_guard<std::mutex> lk(m_mutex);
// Lazy eviction: sweep expired entries when map grows too large (#391)
if (m_buckets.size() > 10000) {
evictExpired();
}
auto& bucket = m_buckets[key];
if (!bucket.initialized) {
bucket.tokens = m_capacity;
bucket.lastRefill = std::chrono::steady_clock::now();
bucket.initialized = true;
}
auto now = std::chrono::steady_clock::now();
double secs = std::chrono::duration<double>(now - bucket.lastRefill).count();
bucket.tokens = std::min(m_capacity, bucket.tokens + secs * m_refillRate);
bucket.lastRefill = now;
if (bucket.tokens >= 1.0) {
bucket.tokens -= 1.0;
return true;
}
return false;
}
private:
struct Bucket {
double tokens = 0.0;
std::chrono::steady_clock::time_point lastRefill;
bool initialized = false;
};
/** @brief Remove entries that have been idle long enough to fully refill (called under lock). */
void evictExpired() {
auto now = std::chrono::steady_clock::now();
double fullRefillSecs = m_refillRate > 0 ? m_capacity / m_refillRate : 60.0;
for (auto it = m_buckets.begin(); it != m_buckets.end(); ) {
double secs = std::chrono::duration<double>(now - it->second.lastRefill).count();
if (secs >= fullRefillSecs) {
it = m_buckets.erase(it);
} else {
++it;
}
}
}
double m_capacity;
double m_refillRate;
std::mutex m_mutex;
std::unordered_map<std::string, Bucket> m_buckets;
};
} // namespace oatpp_authkit
#endif // UTIL_RATE_LIMITER_HPP