featherbit/plugins/native/
rate_limit.rs1use async_trait::async_trait;
8use bytes::Bytes;
9use dashmap::DashMap;
10use std::collections::HashMap;
11use std::sync::Arc;
12use std::time::Instant;
13
14use crate::context::Context;
15use crate::plugins::{Plugin, PluginOutput, PluginResult};
16
17pub struct RateLimitPlugin {
26 requests_per_second: u64,
28 burst: u64,
30 key_source: KeySource,
32 buckets: Arc<DashMap<String, TokenBucket>>,
34}
35
36#[derive(Debug, Clone)]
38enum KeySource {
39 RemoteAddr,
41 Header(String),
43}
44
45struct TokenBucket {
48 tokens: f64,
49 last_refill: Instant,
50 max_tokens: f64,
51 refill_rate: f64,
52}
53
54impl TokenBucket {
55 fn new(max_tokens: f64, refill_rate: f64) -> Self {
56 Self {
57 tokens: max_tokens,
58 last_refill: Instant::now(),
59 max_tokens,
60 refill_rate,
61 }
62 }
63
64 fn try_consume(&mut self) -> bool {
67 let now = Instant::now();
68 let elapsed = now.duration_since(self.last_refill).as_secs_f64();
69 self.tokens = (self.tokens + elapsed * self.refill_rate).min(self.max_tokens);
70 self.last_refill = now;
71
72 if self.tokens >= 1.0 {
73 self.tokens -= 1.0;
74 true
75 } else {
76 false
77 }
78 }
79}
80
81impl RateLimitPlugin {
82 pub fn from_config(config: &HashMap<String, serde_json::Value>) -> Result<Self, String> {
101 let requests_per_second = config
102 .get("requests_per_second")
103 .and_then(|v| v.as_u64())
104 .unwrap_or(100);
105
106 let burst = config
107 .get("burst")
108 .and_then(|v| v.as_u64())
109 .unwrap_or(requests_per_second);
110
111 let key_source = match config.get("key_from").and_then(|v| v.as_str()) {
112 Some(header) if header.starts_with("header:") => {
113 KeySource::Header(header[7..].to_string())
114 }
115 _ => KeySource::RemoteAddr,
116 };
117
118 Ok(Self {
119 requests_per_second,
120 burst,
121 key_source,
122 buckets: Arc::new(DashMap::new()),
123 })
124 }
125}
126
127#[async_trait]
128impl Plugin for RateLimitPlugin {
129 fn plugin_type(&self) -> &str {
130 "rate-limit"
131 }
132
133 async fn execute(&self, mut ctx: Context) -> PluginResult {
134 let key = match &self.key_source {
135 KeySource::RemoteAddr => ctx.request.remote_addr.clone(),
136 KeySource::Header(header) => ctx
137 .request
138 .headers
139 .get(header)
140 .and_then(|v| v.first())
141 .cloned()
142 .unwrap_or_else(|| ctx.request.remote_addr.clone()),
143 };
144
145 let allowed = {
146 let mut bucket = self.buckets.entry(key).or_insert_with(|| {
147 TokenBucket::new(self.burst as f64, self.requests_per_second as f64)
148 });
149 bucket.try_consume()
150 };
151
152 if allowed {
153 Ok(PluginOutput::success(ctx))
154 } else {
155 ctx.response.status_code = 429;
156 ctx.response.body =
157 Bytes::from(r#"{"error": "rate_limited", "message": "Too many requests"}"#);
158 ctx.response.headers.insert(
159 "content-type".to_string(),
160 vec!["application/json".to_string()],
161 );
162 ctx.response
163 .headers
164 .insert("retry-after".to_string(), vec!["1".to_string()]);
165
166 Ok(PluginOutput::on_port(ctx, "limited"))
167 }
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
180 use crate::context::{GatewayRequest, GatewayResponse, Protocol};
181
182 fn ctx(remote_addr: &str, header: Option<(&str, &str)>) -> Context {
183 let mut headers = HashMap::new();
184 if let Some((k, v)) = header {
185 headers.insert(k.to_string(), vec![v.to_string()]);
186 }
187 Context {
188 request: GatewayRequest {
189 method: "GET".to_string(),
190 path: "/hello".to_string(),
191 host: "h".to_string(),
192 scheme: "http".to_string(),
193 headers,
194 query_params: HashMap::new(),
195 body: Bytes::new(),
196 remote_addr: remote_addr.to_string(),
197 protocol: Protocol::Http1,
198 },
199 response: GatewayResponse {
200 status_code: 0,
201 headers: HashMap::new(),
202 body: Bytes::new(),
203 stream: None,
204 },
205 message: HashMap::new(),
206 errors: Vec::new(),
207 }
208 }
209
210 fn plugin(config: serde_json::Value) -> RateLimitPlugin {
211 let map: HashMap<String, serde_json::Value> =
212 config.as_object().unwrap().clone().into_iter().collect();
213 RateLimitPlugin::from_config(&map).unwrap()
214 }
215
216 #[tokio::test]
218 async fn test_burst_then_throttle() {
219 let p = plugin(serde_json::json!({ "requests_per_second": 1, "burst": 2 }));
220 assert!(p
222 .execute(ctx("1.2.3.4:5", None))
223 .await
224 .unwrap()
225 .port
226 .is_none());
227 assert!(p
228 .execute(ctx("1.2.3.4:5", None))
229 .await
230 .unwrap()
231 .port
232 .is_none());
233 let out = p.execute(ctx("1.2.3.4:5", None)).await.unwrap();
235 assert_eq!(out.port, Some("limited"));
236 assert_eq!(out.context.response.status_code, 429);
237 assert_eq!(
238 out.context
239 .response
240 .headers
241 .get("retry-after")
242 .and_then(|v| v.first())
243 .map(String::as_str),
244 Some("1")
245 );
246 }
247
248 #[tokio::test]
251 async fn test_buckets_are_per_key() {
252 let p = plugin(serde_json::json!({ "requests_per_second": 1, "burst": 1 }));
253 assert!(p
254 .execute(ctx("10.0.0.1:5", None))
255 .await
256 .unwrap()
257 .port
258 .is_none());
259 assert_eq!(
261 p.execute(ctx("10.0.0.1:5", None)).await.unwrap().port,
262 Some("limited")
263 );
264 assert!(p
266 .execute(ctx("10.0.0.2:5", None))
267 .await
268 .unwrap()
269 .port
270 .is_none());
271 }
272
273 #[tokio::test]
275 async fn test_key_from_header() {
276 let p = plugin(serde_json::json!({
277 "requests_per_second": 1, "burst": 1, "key_from": "header:x-api-key"
278 }));
279 assert!(p
281 .execute(ctx("10.0.0.1:5", Some(("x-api-key", "k1"))))
282 .await
283 .unwrap()
284 .port
285 .is_none());
286 assert_eq!(
287 p.execute(ctx("10.0.0.2:5", Some(("x-api-key", "k1"))))
288 .await
289 .unwrap()
290 .port,
291 Some("limited")
292 );
293 assert!(p
295 .execute(ctx("10.0.0.3:5", Some(("x-api-key", "k2"))))
296 .await
297 .unwrap()
298 .port
299 .is_none());
300 }
301
302 #[test]
303 fn test_burst_defaults_to_rps() {
304 let p = plugin(serde_json::json!({ "requests_per_second": 7 }));
305 assert_eq!(p.burst, 7);
306 }
307}