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>
89 lines
2.8 KiB
C++
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
|