#![deny(missing_docs)]
use async_trait::async_trait;
use churust_core::{AppBuilder, Call, Error, Middleware, Next, Phase, Plugin, Response};
use http::header::RETRY_AFTER;
use http::{HeaderValue, StatusCode};
use std::collections::hash_map::RandomState;
use std::collections::HashMap;
use std::hash::BuildHasher;
use std::net::{IpAddr, Ipv6Addr};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
const DEFAULT_MAX_KEYS: usize = 100_000;
type KeyFn = Arc<dyn Fn(&Call) -> Option<String> + Send + Sync>;
#[derive(Debug, Default)]
struct Table {
tat: HashMap<u64, Instant>,
seed: RandomState,
}
impl Table {
fn digest(&self, key: &str) -> u64 {
self.seed.hash_one(key)
}
fn prune(&mut self, now: Instant, max_keys: usize) {
self.tat.retain(|_, tat| *tat > now);
if self.tat.len() < max_keys {
return;
}
let target = max_keys * 9 / 10;
let mut by_expiry: Vec<(u64, Instant)> = self.tat.iter().map(|(k, v)| (*k, *v)).collect();
by_expiry.sort_by_key(|(_, tat)| *tat);
for (key, _) in by_expiry.into_iter().take(self.tat.len() - target) {
self.tat.remove(&key);
}
}
}
#[derive(Clone)]
pub struct RateLimit {
emission: Duration,
tolerance: Duration,
limit: u32,
period: Duration,
max_keys: usize,
key: KeyFn,
table: Arc<Mutex<Table>>,
}
impl std::fmt::Debug for RateLimit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RateLimit")
.field("limit", &self.limit)
.field("period", &self.period)
.field("emission", &self.emission)
.field("tolerance", &self.tolerance)
.field("max_keys", &self.max_keys)
.finish_non_exhaustive()
}
}
impl RateLimit {
pub fn per(limit: u32, period: Duration) -> Self {
assert!(limit > 0, "rate limit must admit at least one request");
assert!(
!period.is_zero(),
"rate limit period must be greater than zero"
);
let emission = period / limit;
Self {
emission,
tolerance: emission * (limit - 1),
limit,
period,
max_keys: DEFAULT_MAX_KEYS,
key: Arc::new(default_key),
table: Arc::new(Mutex::new(Table::default())),
}
}
pub fn per_second(limit: u32) -> Self {
Self::per(limit, Duration::from_secs(1))
}
pub fn per_minute(limit: u32) -> Self {
Self::per(limit, Duration::from_secs(60))
}
pub fn per_hour(limit: u32) -> Self {
Self::per(limit, Duration::from_secs(3600))
}
pub fn burst(mut self, burst: u32) -> Self {
assert!(burst > 0, "burst must be at least one request");
self.tolerance = self.emission * (burst - 1);
self
}
pub fn by<F>(mut self, f: F) -> Self
where
F: Fn(&Call) -> Option<String> + Send + Sync + 'static,
{
self.key = Arc::new(f);
self
}
pub fn max_keys(mut self, n: usize) -> Self {
assert!(n > 0, "max_keys must be at least one");
self.max_keys = n;
self
}
fn check(&self, key: &str) -> Result<(), Duration> {
let now = Instant::now();
let mut table = self.table.lock().unwrap_or_else(|p| p.into_inner());
if table.tat.len() >= self.max_keys {
table.prune(now, self.max_keys);
}
let digest = table.digest(key);
let tat = table.tat.get(&digest).copied().unwrap_or(now);
let earliest = tat.checked_sub(self.tolerance).unwrap_or(now);
if now < earliest {
return Err(earliest - now);
}
let base = if tat > now { tat } else { now };
table.tat.insert(digest, base + self.emission);
Ok(())
}
fn too_many(&self, delay: Duration) -> Response {
let secs = delay.as_secs_f64().ceil().max(1.0) as u64;
let message = format!(
"rate limit exceeded: {} requests per {} seconds",
self.limit,
self.period.as_secs_f64()
);
let mut error = Error::new(StatusCode::TOO_MANY_REQUESTS, message);
if let Ok(value) = HeaderValue::from_str(&secs.to_string()) {
error = error.with_response_header(RETRY_AFTER, value);
}
churust_core::IntoResponse::into_response(error)
}
}
fn default_key(call: &Call) -> Option<String> {
Some(match call.peer_addr() {
Some(addr) => address_bucket(addr.ip()),
None => "unknown".to_string(),
})
}
fn address_bucket(ip: IpAddr) -> String {
let v6 = match ip {
IpAddr::V4(v4) => return v4.to_string(),
IpAddr::V6(v6) => v6,
};
if let Some(v4) = v6.to_ipv4_mapped() {
return v4.to_string();
}
let mut octets = v6.octets();
octets[8..].fill(0);
Ipv6Addr::from(octets).to_string()
}
#[async_trait]
impl Middleware for RateLimit {
async fn handle(&self, call: Call, next: Next) -> Response {
let Some(key) = (self.key)(&call) else {
return next.run(call).await;
};
match self.check(&key) {
Ok(()) => next.run(call).await,
Err(delay) => self.too_many(delay),
}
}
}
impl Plugin for RateLimit {
fn install(self: Box<Self>, app: &mut AppBuilder) {
app.add_middleware_in(Phase::Plugins, Arc::new(*self));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_fresh_key_may_spend_the_whole_allowance_at_once() {
let limiter = RateLimit::per(3, Duration::from_secs(10));
assert!(limiter.check("a").is_ok());
assert!(limiter.check("a").is_ok());
assert!(limiter.check("a").is_ok());
assert!(limiter.check("a").is_err(), "the fourth exceeds the burst");
}
#[test]
fn keys_are_independent() {
let limiter = RateLimit::per(1, Duration::from_secs(10));
assert!(limiter.check("a").is_ok());
assert!(limiter.check("a").is_err());
assert!(limiter.check("b").is_ok(), "b has its own budget");
}
#[test]
fn burst_one_admits_a_single_request() {
let limiter = RateLimit::per(100, Duration::from_secs(1)).burst(1);
assert!(limiter.check("a").is_ok());
assert!(limiter.check("a").is_err());
}
#[test]
fn the_reported_delay_never_exceeds_the_period() {
let limiter = RateLimit::per(2, Duration::from_secs(4));
assert!(limiter.check("a").is_ok());
assert!(limiter.check("a").is_ok());
let delay = limiter.check("a").expect_err("should be limited");
assert!(
delay <= Duration::from_secs(4),
"waiting longer than the period would be wrong: {delay:?}"
);
}
#[test]
fn clones_share_one_budget() {
let limiter = RateLimit::per(1, Duration::from_secs(10));
let clone = limiter.clone();
assert!(limiter.check("a").is_ok());
assert!(
clone.check("a").is_err(),
"a clone must not reset the table"
);
}
#[test]
fn pruning_bounds_the_table() {
let limiter = RateLimit::per(1, Duration::from_millis(1)).max_keys(8);
for i in 0..64 {
let _ = limiter.check(&format!("key-{i}"));
}
let len = limiter.table.lock().unwrap().tat.len();
assert!(len <= 8, "table grew past its cap: {len}");
}
#[test]
fn a_long_key_is_stored_at_the_same_width_as_a_short_one() {
let limiter = RateLimit::per(2, Duration::from_secs(30));
let long = "k".repeat(1 << 20);
assert!(limiter.check("k").is_ok());
assert!(limiter.check(&long).is_ok());
let table = limiter.table.lock().unwrap();
assert_eq!(table.tat.len(), 2, "the two keys are distinct");
let rendered = format!("{:?}", table.tat);
assert!(
rendered.len() < 256,
"the table retained the key's {} bytes instead of a fixed-width \
digest, so a caller picks the cost of its own bucket",
long.len()
);
}
#[test]
fn a_client_rotating_through_one_ipv6_subnet_gets_no_extra_budget() {
let limiter = RateLimit::per(2, Duration::from_secs(30));
let rotate = |suffix: &str| {
let ip: IpAddr = format!("2001:db8:1:2::{suffix}").parse().unwrap();
limiter.check(&address_bucket(ip))
};
assert!(rotate("1").is_ok());
assert!(rotate("2").is_ok());
assert!(
rotate("3").is_err(),
"a new address out of the same /64 is the same client and must not \
reset the budget"
);
}
#[test]
fn separate_ipv6_subnets_keep_separate_budgets() {
let one: IpAddr = "2001:db8:1:2::1".parse().unwrap();
let other: IpAddr = "2001:db8:1:3::1".parse().unwrap();
assert_ne!(
address_bucket(one),
address_bucket(other),
"different /64s are different networks"
);
}
#[test]
fn an_ipv4_peer_keeps_every_octet() {
let ip: IpAddr = "203.0.113.7".parse().unwrap();
assert_eq!(address_bucket(ip), "203.0.113.7");
}
#[test]
fn an_ipv4_mapped_peer_is_bucketed_as_the_address_it_carries() {
let mapped: IpAddr = "::ffff:203.0.113.7".parse().unwrap();
let neighbour: IpAddr = "::ffff:203.0.113.8".parse().unwrap();
assert_eq!(address_bucket(mapped), "203.0.113.7");
assert_ne!(
address_bucket(mapped),
address_bucket(neighbour),
"a dual-stack socket reports IPv4 peers inside one /64, so masking \
them would put the whole IPv4 internet in a single bucket"
);
}
#[test]
#[should_panic(expected = "at least one request")]
fn a_zero_limit_is_a_configuration_error() {
let _ = RateLimit::per(0, Duration::from_secs(1));
}
#[test]
#[should_panic(expected = "greater than zero")]
fn a_zero_period_is_a_configuration_error() {
let _ = RateLimit::per(1, Duration::ZERO);
}
}