use std::collections::HashMap;
use std::net::IpAddr;
use std::time::{Duration, Instant};
use crate::protocol;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RateLimitConfig {
pub max_requests_per_window: u32,
pub window_duration: Duration,
pub min_interval: Duration,
}
impl Default for RateLimitConfig {
fn default() -> Self {
RateLimitConfig {
max_requests_per_window: 20,
window_duration: Duration::from_secs(60),
min_interval: Duration::from_secs(2),
}
}
}
pub(crate) enum RateLimitResult {
Allow,
RateExceeded,
}
pub struct ClientState {
last_request_time: Instant,
request_count: u32,
window_start: Instant,
pub(crate) last_t2: protocol::TimestampFormat,
pub(crate) last_t3: protocol::TimestampFormat,
pub(crate) last_client_xmt: protocol::TimestampFormat,
}
impl ClientState {
pub fn new(now: Instant) -> Self {
ClientState {
last_request_time: now,
request_count: 0,
window_start: now,
last_t2: protocol::TimestampFormat::default(),
last_t3: protocol::TimestampFormat::default(),
last_client_xmt: protocol::TimestampFormat::default(),
}
}
}
pub struct ClientTable {
entries: HashMap<IpAddr, ClientState>,
max_entries: usize,
stale_threshold: Duration,
}
impl ClientTable {
pub fn new(max_entries: usize) -> Self {
ClientTable {
entries: HashMap::new(),
max_entries,
stale_threshold: Duration::from_secs(24 * 3600),
}
}
pub(crate) fn get_or_insert(&mut self, ip: IpAddr, now: Instant) -> &mut ClientState {
if !self.entries.contains_key(&ip) && self.entries.len() >= self.max_entries {
self.evict_stale(now);
}
self.entries
.entry(ip)
.or_insert_with(|| ClientState::new(now))
}
pub(crate) fn get(&self, ip: &IpAddr) -> Option<&ClientState> {
self.entries.get(ip)
}
pub(crate) fn len(&self) -> usize {
self.entries.len()
}
fn evict_stale(&mut self, now: Instant) {
let threshold = self.stale_threshold;
self.entries
.retain(|_, state| now.duration_since(state.last_request_time) < threshold);
if self.entries.len() >= self.max_entries
&& let Some(oldest_ip) = self
.entries
.iter()
.min_by_key(|(_, state)| state.last_request_time)
.map(|(ip, _)| *ip)
{
self.entries.remove(&oldest_ip);
}
}
}
pub(crate) fn check_rate_limit(
client: &mut ClientState,
now: Instant,
config: &RateLimitConfig,
) -> RateLimitResult {
if now.duration_since(client.last_request_time) < config.min_interval {
return RateLimitResult::RateExceeded;
}
if now.duration_since(client.window_start) > config.window_duration {
client.window_start = now;
client.request_count = 0;
}
client.request_count = client.request_count.saturating_add(1);
if client.request_count > config.max_requests_per_window {
return RateLimitResult::RateExceeded;
}
client.last_request_time = now;
RateLimitResult::Allow
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rate_limit_allows_first_request() {
let now = Instant::now();
let mut client = ClientState::new(now - Duration::from_secs(10)); let config = RateLimitConfig::default();
assert!(matches!(
check_rate_limit(&mut client, now, &config),
RateLimitResult::Allow
));
}
#[test]
fn test_rate_limit_min_interval() {
let now = Instant::now();
let mut client = ClientState::new(now);
client.last_request_time = now; let config = RateLimitConfig {
min_interval: Duration::from_secs(2),
..Default::default()
};
let result = check_rate_limit(&mut client, now + Duration::from_secs(1), &config);
assert!(matches!(result, RateLimitResult::RateExceeded));
}
#[test]
fn test_rate_limit_window_exceeded() {
let now = Instant::now();
let mut client = ClientState::new(now - Duration::from_secs(10));
let config = RateLimitConfig {
max_requests_per_window: 2,
window_duration: Duration::from_secs(60),
min_interval: Duration::from_millis(1),
};
let t1 = now;
let t2 = now + Duration::from_millis(100);
let t3 = now + Duration::from_millis(200);
assert!(matches!(
check_rate_limit(&mut client, t1, &config),
RateLimitResult::Allow
));
assert!(matches!(
check_rate_limit(&mut client, t2, &config),
RateLimitResult::Allow
));
assert!(matches!(
check_rate_limit(&mut client, t3, &config),
RateLimitResult::RateExceeded
));
}
#[test]
fn test_rate_limit_window_reset() {
let now = Instant::now();
let mut client = ClientState::new(now - Duration::from_secs(10));
let config = RateLimitConfig {
max_requests_per_window: 1,
window_duration: Duration::from_secs(1),
min_interval: Duration::from_millis(1),
};
let t1 = now;
let t2 = now + Duration::from_millis(100);
let t3 = now + Duration::from_secs(2);
assert!(matches!(
check_rate_limit(&mut client, t1, &config),
RateLimitResult::Allow
));
assert!(matches!(
check_rate_limit(&mut client, t2, &config),
RateLimitResult::RateExceeded
));
assert!(matches!(
check_rate_limit(&mut client, t3, &config),
RateLimitResult::Allow
));
}
#[test]
fn test_client_table_get_or_insert() {
let mut table = ClientTable::new(100);
let now = Instant::now();
let ip: IpAddr = "1.2.3.4".parse().unwrap();
let _client = table.get_or_insert(ip, now);
assert!(table.get(&ip).is_some());
}
#[test]
fn test_client_table_eviction() {
let mut table = ClientTable::new(2);
let now = Instant::now();
let ip1: IpAddr = "1.0.0.1".parse().unwrap();
let ip2: IpAddr = "1.0.0.2".parse().unwrap();
let ip3: IpAddr = "1.0.0.3".parse().unwrap();
table.get_or_insert(ip1, now);
table.get_or_insert(ip2, now + Duration::from_secs(1));
table.get_or_insert(ip3, now + Duration::from_secs(2));
assert_eq!(table.entries.len(), 2);
assert!(table.get(&ip1).is_none());
assert!(table.get(&ip3).is_some());
}
}