feat: complete Epic 1 production foundation

This commit is contained in:
2026-08-25 01:24:11 +03:00
parent 767428436d
commit 182bde8ac0
298 changed files with 35719 additions and 5299 deletions
+39 -15
View File
@@ -10,20 +10,23 @@ use crank_runtime::{RateLimitCheckError, RateLimitRejection};
use crate::{error::ApiError, state::AppState};
#[derive(Clone, Debug)]
pub struct ClientIdentityBucket(pub String);
pub async fn apply_api_rate_limit(
State(state): State<AppState>,
request: Request,
mut request: Request,
next: Next,
) -> Result<Response, ApiError> {
let peer_ip = request
.extensions()
.get::<ConnectInfo<SocketAddr>>()
.map(|ConnectInfo(address)| address.ip());
let key = rate_limit_key(
let key = client_rate_limit_key(
request.headers(),
request.uri().path(),
peer_ip,
state.trust_forwarded_headers,
&state.trusted_proxy_ips,
);
if let Err(error) = state.api_rate_limiter.check(&key).await {
return match error {
@@ -37,6 +40,8 @@ pub async fn apply_api_rate_limit(
};
}
request.extensions_mut().insert(ClientIdentityBucket(key));
Ok(next.run(request).await)
}
@@ -46,13 +51,15 @@ fn rejection_context(rejection: RateLimitRejection) -> serde_json::Value {
})
}
fn rate_limit_key(
pub fn client_rate_limit_key(
headers: &HeaderMap,
path: &str,
peer_ip: Option<IpAddr>,
trust_forwarded_headers: bool,
trusted_proxy_ips: &[IpAddr],
) -> String {
if trust_forwarded_headers && let Some(client_ip) = forwarded_client_ip(headers) {
if peer_ip.is_some_and(|peer_ip| trusted_proxy_ips.contains(&peer_ip))
&& let Some(client_ip) = forwarded_client_ip(headers)
{
return format!("ip:{client_ip}");
}
@@ -63,7 +70,8 @@ fn rate_limit_key(
format!("anonymous:{path}")
}
/// Resolves the client IP from proxy headers, assuming a single trusted proxy.
/// Resolves the client IP from proxy headers after the immediate peer was
/// matched against the trusted-proxy allowlist.
///
/// `X-Real-IP` is preferred because a trusted proxy (e.g. nginx) sets it to the
/// real peer address. For `X-Forwarded-For` the proxy *appends* the observed
@@ -96,7 +104,7 @@ mod tests {
use axum::http::{HeaderMap, HeaderValue, header::COOKIE};
use super::rate_limit_key;
use super::client_rate_limit_key;
fn peer() -> Option<IpAddr> {
Some(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 7)))
@@ -112,7 +120,7 @@ mod tests {
headers.insert("x-forwarded-for", HeaderValue::from_static("10.0.0.5"));
assert_eq!(
rate_limit_key(&headers, "/api/auth/login", peer(), true),
client_rate_limit_key(&headers, "/api/auth/login", peer(), &[peer().unwrap()]),
"ip:10.0.0.5"
);
}
@@ -124,7 +132,23 @@ mod tests {
headers.insert("x-real-ip", HeaderValue::from_static("10.0.0.9"));
assert_eq!(
rate_limit_key(&headers, "/api/auth/login", peer(), false),
client_rate_limit_key(&headers, "/api/auth/login", peer(), &[]),
"ip:203.0.113.7"
);
}
#[test]
fn ignores_forwarded_headers_from_unlisted_peer() {
let mut headers = HeaderMap::new();
headers.insert("x-real-ip", HeaderValue::from_static("10.0.0.9"));
assert_eq!(
client_rate_limit_key(
&headers,
"/api/auth/login",
peer(),
&[IpAddr::V4(Ipv4Addr::new(198, 51, 100, 10))]
),
"ip:203.0.113.7"
);
}
@@ -139,7 +163,7 @@ mod tests {
headers.insert("x-real-ip", HeaderValue::from_static("10.0.0.9"));
assert_eq!(
rate_limit_key(&headers, "/api/auth/login", peer(), true),
client_rate_limit_key(&headers, "/api/auth/login", peer(), &[peer().unwrap()]),
"ip:10.0.0.9"
);
}
@@ -155,7 +179,7 @@ mod tests {
);
assert_eq!(
rate_limit_key(&headers, "/api/auth/login", peer(), true),
client_rate_limit_key(&headers, "/api/auth/login", peer(), &[peer().unwrap()]),
"ip:10.0.0.6"
);
}
@@ -165,7 +189,7 @@ mod tests {
let headers = HeaderMap::new();
assert_eq!(
rate_limit_key(&headers, "/api/auth/login", peer(), true),
client_rate_limit_key(&headers, "/api/auth/login", peer(), &[peer().unwrap()]),
"ip:203.0.113.7"
);
}
@@ -180,7 +204,7 @@ mod tests {
);
assert_eq!(
rate_limit_key(&headers, "/api/auth/login", peer(), true),
client_rate_limit_key(&headers, "/api/auth/login", peer(), &[peer().unwrap()]),
"ip:203.0.113.7"
);
}
@@ -190,7 +214,7 @@ mod tests {
let headers = HeaderMap::new();
assert_eq!(
rate_limit_key(&headers, "/api/auth/login", None, false),
client_rate_limit_key(&headers, "/api/auth/login", None, &[]),
"anonymous:/api/auth/login"
);
}