aster_forge_middleware/axum/
rate_limit.rs1pub use crate::shared::rate_limit::{NormalizedStringRateLimiter, RateLimitRejection};
3use axum::{
4 extract::{Request, State},
5 middleware::Next,
6 response::Response,
7};
8use governor::{RateLimiter, clock::DefaultClock, state::keyed::DefaultKeyedStateStore};
9use ipnet::IpNet;
10use std::{
11 net::{IpAddr, Ipv4Addr},
12 num::{NonZeroU32, NonZeroU64},
13 sync::Arc,
14};
15
16type IpLimiter = RateLimiter<IpAddr, DefaultKeyedStateStore<IpAddr>, DefaultClock>;
17type PeerResolver = dyn Fn(&Request) -> Option<IpAddr> + Send + Sync;
18type RejectionMapper = dyn Fn(RateLimitRejection) -> Response + Send + Sync;
19type KeyResolver = dyn Fn(&Request) -> Result<String, Box<Response>> + Send + Sync;
20
21#[derive(Clone)]
23pub struct IpRateLimitConfig {
24 enabled: bool,
25 trusted: Vec<IpNet>,
26 limiter: Arc<IpLimiter>,
27 peer: Arc<PeerResolver>,
28 rejection: Arc<RejectionMapper>,
29}
30
31impl IpRateLimitConfig {
32 pub fn new<F>(
34 enabled: bool,
35 seconds_per_request: NonZeroU64,
36 burst_size: NonZeroU32,
37 trusted_proxies: &[String],
38 rejection: F,
39 ) -> Self
40 where
41 F: Fn(RateLimitRejection) -> Response + Send + Sync + 'static,
42 {
43 Self {
44 enabled,
45 trusted: aster_forge_utils::net::parse_trusted_proxies(trusted_proxies),
46 limiter: Arc::new(RateLimiter::keyed(
47 crate::shared::rate_limit::rate_limit_quota(seconds_per_request, burst_size),
48 )),
49 peer: Arc::new(super::client_ip::direct_peer),
50 rejection: Arc::new(rejection),
51 }
52 }
53 #[must_use]
56 pub fn peer_resolver<P>(mut self, peer: P) -> Self
57 where
58 P: Fn(&Request) -> Option<IpAddr> + Send + Sync + 'static,
59 {
60 self.peer = Arc::new(peer);
61 self
62 }
63 pub fn retain_recent(&self) {
65 self.limiter.retain_recent();
66 }
67}
68
69pub async fn ip_rate_limit(
72 State(config): State<IpRateLimitConfig>,
73 request: Request,
74 next: Next,
75) -> Response {
76 if config.enabled {
77 let key = (config.peer)(&request).map_or(IpAddr::V4(Ipv4Addr::LOCALHOST), |peer| {
78 super::client_ip::real_ip_from_trusted_headers(request.headers(), peer, &config.trusted)
79 });
80 if let Err(negative) = config.limiter.check_key(&key) {
81 return (config.rejection)(RateLimitRejection::from_not_until(&negative));
82 }
83 }
84 next.run(request).await
85}
86
87#[derive(Clone)]
89pub struct KeyedRateLimitConfig {
90 limiter: NormalizedStringRateLimiter,
91 key: Arc<KeyResolver>,
92 rejection: Arc<RejectionMapper>,
93}
94impl KeyedRateLimitConfig {
95 pub fn new<K, F>(limiter: NormalizedStringRateLimiter, key: K, rejection: F) -> Self
97 where
98 K: Fn(&Request) -> Result<String, Box<Response>> + Send + Sync + 'static,
99 F: Fn(RateLimitRejection) -> Response + Send + Sync + 'static,
100 {
101 Self {
102 limiter,
103 key: Arc::new(key),
104 rejection: Arc::new(rejection),
105 }
106 }
107}
108pub async fn keyed_rate_limit(
110 State(config): State<KeyedRateLimitConfig>,
111 request: Request,
112 next: Next,
113) -> Response {
114 if config.limiter.enabled() {
115 let key = match (config.key)(&request) {
116 Ok(key) => key,
117 Err(response) => return *response,
118 };
119 if let Some(rejection) = config.limiter.check(&key) {
120 return (config.rejection)(rejection);
121 }
122 }
123 next.run(request).await
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use axum::{
130 Json, Router,
131 body::{Body, to_bytes},
132 extract::ConnectInfo,
133 middleware,
134 response::IntoResponse,
135 routing::get,
136 };
137 use http::{Request as HttpRequest, StatusCode};
138 use std::net::SocketAddr;
139 use tower::ServiceExt;
140
141 fn reject(rejection: RateLimitRejection) -> Response {
142 let wait = rejection.retry_after_seconds();
143 let mut response = (
144 StatusCode::TOO_MANY_REQUESTS,
145 Json(serde_json::json!({"code": "product_limit", "retry_after": wait})),
146 )
147 .into_response();
148 response
149 .headers_mut()
150 .insert("retry-after", wait.to_string().parse().unwrap());
151 response
152 }
153 fn ip_app(enabled: bool, burst: u32, trusted: &[String]) -> Router {
154 let config = IpRateLimitConfig::new(
155 enabled,
156 NonZeroU64::new(60).unwrap(),
157 NonZeroU32::new(burst).unwrap(),
158 trusted,
159 reject,
160 );
161 Router::new()
162 .route("/", get(|| async { "called" }))
163 .layer(middleware::from_fn_with_state(config, ip_rate_limit))
164 }
165 fn request(peer: Option<&str>, forwarded: &str) -> HttpRequest<Body> {
166 let mut request = HttpRequest::builder()
167 .header("x-forwarded-for", forwarded)
168 .body(Body::empty())
169 .unwrap();
170 if let Some(peer) = peer {
171 request
172 .extensions_mut()
173 .insert(ConnectInfo(peer.parse::<SocketAddr>().unwrap()));
174 }
175 request
176 }
177
178 #[tokio::test]
179 async fn ip_limits_isolate_clients_and_preserve_custom_429_retry_after() {
180 let router = ip_app(true, 1, &["10.0.0.0/8".into()]);
181 assert_eq!(
182 router
183 .clone()
184 .oneshot(request(Some("10.0.0.5:1234"), "203.0.113.1"))
185 .await
186 .unwrap()
187 .status(),
188 StatusCode::OK
189 );
190 assert_eq!(
191 router
192 .clone()
193 .oneshot(request(Some("10.0.0.5:1234"), "203.0.113.2"))
194 .await
195 .unwrap()
196 .status(),
197 StatusCode::OK
198 );
199 let response = router
200 .oneshot(request(Some("10.0.0.5:1234"), "203.0.113.1"))
201 .await
202 .unwrap();
203 assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
204 let delay: u64 = response.headers()["retry-after"]
205 .to_str()
206 .unwrap()
207 .parse()
208 .unwrap();
209 assert!((1..=60).contains(&delay));
210 let value: serde_json::Value =
211 serde_json::from_slice(&to_bytes(response.into_body(), 4096).await.unwrap()).unwrap();
212 assert_eq!(value["code"], "product_limit");
213 assert_eq!(value["retry_after"], delay);
214 }
215
216 #[tokio::test]
217 async fn untrusted_and_missing_peers_cannot_rotate_forwarded_headers_to_bypass_limits() {
218 for peer in [Some("198.51.100.1:1234"), None] {
219 let router = ip_app(true, 1, &["127.0.0.1".into()]);
220 assert_eq!(
221 router
222 .clone()
223 .oneshot(request(peer, "203.0.113.1"))
224 .await
225 .unwrap()
226 .status(),
227 StatusCode::OK
228 );
229 assert_eq!(
230 router
231 .oneshot(request(peer, "203.0.113.2"))
232 .await
233 .unwrap()
234 .status(),
235 StatusCode::TOO_MANY_REQUESTS
236 );
237 }
238 }
239
240 #[tokio::test]
241 async fn disabled_ip_limit_never_extracts_the_peer() {
242 let config = IpRateLimitConfig::new(
243 false,
244 NonZeroU64::new(60).unwrap(),
245 NonZeroU32::new(1).unwrap(),
246 &[],
247 reject,
248 )
249 .peer_resolver(|_| panic!("disabled limiter must not invoke peer resolver"));
250 let router = Router::new()
251 .route("/", get(|| async { "called" }))
252 .layer(middleware::from_fn_with_state(config, ip_rate_limit));
253 for _ in 0..3 {
254 assert_eq!(
255 router
256 .clone()
257 .oneshot(request(None, "anything"))
258 .await
259 .unwrap()
260 .status(),
261 StatusCode::OK
262 );
263 }
264 }
265
266 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
267 async fn concurrent_requests_share_exactly_one_burst_across_router_clones() {
268 let router = ip_app(true, 4, &[]);
269 let mut jobs = Vec::new();
270 for _ in 0..32 {
271 let router = router.clone();
272 jobs.push(tokio::spawn(async move {
273 router
274 .oneshot(request(Some("203.0.113.1:1234"), ""))
275 .await
276 .unwrap()
277 .status()
278 }));
279 }
280 let mut allowed = 0;
281 for job in jobs {
282 match job.await.unwrap() {
283 StatusCode::OK => allowed += 1,
284 StatusCode::TOO_MANY_REQUESTS => (),
285 status => panic!("unexpected status {status}"),
286 }
287 }
288 assert_eq!(allowed, 4);
289 }
290
291 fn keyed_app(enabled: bool) -> Router {
292 let limiter = NormalizedStringRateLimiter::new(
293 enabled,
294 NonZeroU64::new(60).unwrap(),
295 NonZeroU32::new(1).unwrap(),
296 );
297 let config = KeyedRateLimitConfig::new(
298 limiter,
299 |request| {
300 request
301 .headers()
302 .get("x-key")
303 .and_then(|value| value.to_str().ok())
304 .map(str::to_string)
305 .ok_or_else(|| {
306 Box::new(
307 (StatusCode::UNPROCESSABLE_ENTITY, "missing product key")
308 .into_response(),
309 )
310 })
311 },
312 reject,
313 );
314 Router::new()
315 .route("/", get(|| async { "called" }))
316 .layer(middleware::from_fn_with_state(config, keyed_rate_limit))
317 }
318 #[tokio::test]
319 async fn string_keys_are_normalized_isolated_and_product_extraction_errors_are_retained() {
320 let router = keyed_app(true);
321 for (key, expected) in [
322 ("User@Example.com", StatusCode::OK),
323 ("other@example.com", StatusCode::OK),
324 (" user@example.com ", StatusCode::TOO_MANY_REQUESTS),
325 ] {
326 let request = HttpRequest::builder()
327 .header("x-key", key)
328 .body(Body::empty())
329 .unwrap();
330 assert_eq!(
331 router.clone().oneshot(request).await.unwrap().status(),
332 expected
333 );
334 }
335 let response = router
336 .oneshot(HttpRequest::new(Body::empty()))
337 .await
338 .unwrap();
339 assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
340 assert_eq!(
341 to_bytes(response.into_body(), 4096).await.unwrap().as_ref(),
342 b"missing product key"
343 );
344 }
345 #[tokio::test]
346 async fn disabled_keyed_limit_bypasses_product_key_extraction() {
347 let router = keyed_app(false);
348 for _ in 0..3 {
349 assert_eq!(
350 router
351 .clone()
352 .oneshot(HttpRequest::new(Body::empty()))
353 .await
354 .unwrap()
355 .status(),
356 StatusCode::OK
357 );
358 }
359 }
360}