feat: add test environment deployment workflow
This commit is contained in:
@@ -6,7 +6,7 @@ use axum::{
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::SocketAddr,
|
||||
net::{IpAddr, SocketAddr},
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@@ -38,27 +38,79 @@ impl RateLimiter {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn client_key(req: &Request) -> String {
|
||||
let peer_ip = req
|
||||
.extensions()
|
||||
.get::<ConnectInfo<SocketAddr>>()
|
||||
.map(|value| value.0.ip());
|
||||
|
||||
// Only the host Nginx can reach the production backend through loopback.
|
||||
// It overwrites X-Real-IP with the actual remote address, so untrusted
|
||||
// direct clients cannot choose their own rate-limit key.
|
||||
if peer_ip.is_some_and(|ip| ip.is_loopback()) {
|
||||
if let Some(forwarded_ip) = req
|
||||
.headers()
|
||||
.get("x-real-ip")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| value.trim().parse::<IpAddr>().ok())
|
||||
{
|
||||
return forwarded_ip.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
peer_ip
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_else(|| "unknown".into())
|
||||
}
|
||||
|
||||
pub async fn rate_limit_middleware(
|
||||
State(limiter): State<RateLimiter>,
|
||||
req: Request,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
let key = req
|
||||
.extensions()
|
||||
.get::<ConnectInfo<SocketAddr>>()
|
||||
.map(|v| v.0.ip().to_string())
|
||||
.or_else(|| {
|
||||
req.headers()
|
||||
.get("x-forwarded-for")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.split(',').next())
|
||||
.map(str::trim)
|
||||
.map(str::to_string)
|
||||
})
|
||||
.unwrap_or_else(|| "unknown".into());
|
||||
let key = client_key(&req);
|
||||
if limiter.allow(key).await {
|
||||
next.run(req).await
|
||||
} else {
|
||||
(StatusCode::TOO_MANY_REQUESTS, "rate limit exceeded").into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use axum::body::Body;
|
||||
|
||||
fn request_with_peer(peer_ip: IpAddr, real_ip: Option<&str>) -> Request {
|
||||
let mut request = Request::builder().uri("/").body(Body::empty()).unwrap();
|
||||
request
|
||||
.extensions_mut()
|
||||
.insert(ConnectInfo(SocketAddr::new(peer_ip, 12345)));
|
||||
if let Some(real_ip) = real_ip {
|
||||
request.headers_mut().insert(
|
||||
"x-real-ip",
|
||||
real_ip.parse().expect("test IP header must be valid"),
|
||||
);
|
||||
}
|
||||
request
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loopback_proxy_uses_valid_real_ip() {
|
||||
let request = request_with_peer("127.0.0.1".parse().unwrap(), Some("203.0.113.8"));
|
||||
assert_eq!(client_key(&request), "203.0.113.8");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_peer_cannot_spoof_real_ip() {
|
||||
let request = request_with_peer("203.0.113.9".parse().unwrap(), Some("198.51.100.7"));
|
||||
assert_eq!(client_key(&request), "203.0.113.9");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loopback_proxy_with_invalid_real_ip_falls_back_to_peer() {
|
||||
let request = request_with_peer("::1".parse().unwrap(), Some("not-an-ip"));
|
||||
assert_eq!(client_key(&request), "::1");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user