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, GatewayError};
15use crate::plugins::{Plugin, PluginExecutionError, 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(
134 &self,
135 mut ctx: Context,
136 _named_inputs: &HashMap<String, serde_json::Value>,
137 ) -> PluginResult {
138 let key = match &self.key_source {
139 KeySource::RemoteAddr => ctx.request.remote_addr.clone(),
140 KeySource::Header(header) => ctx
141 .request
142 .headers
143 .get(header)
144 .and_then(|v| v.first())
145 .cloned()
146 .unwrap_or_else(|| ctx.request.remote_addr.clone()),
147 };
148
149 let allowed = {
150 let mut bucket = self.buckets.entry(key).or_insert_with(|| {
151 TokenBucket::new(self.burst as f64, self.requests_per_second as f64)
152 });
153 bucket.try_consume()
154 };
155
156 if allowed {
157 Ok(PluginOutput {
158 context: ctx,
159 named_outputs: HashMap::new(),
160 })
161 } else {
162 ctx.response.status_code = 429;
163 ctx.response.body =
164 Bytes::from(r#"{"error": "rate_limited", "message": "Too many requests"}"#);
165 ctx.response.headers.insert(
166 "content-type".to_string(),
167 vec!["application/json".to_string()],
168 );
169 ctx.response
170 .headers
171 .insert("retry-after".to_string(), vec!["1".to_string()]);
172
173 let error = GatewayError {
174 node_id: String::new(),
175 code: "RATE_LIMITED".to_string(),
176 message: "Too many requests".to_string(),
177 metadata: HashMap::new(),
178 };
179 Err(PluginExecutionError {
180 context: ctx,
181 error,
182 })
183 }
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
196 use crate::context::{GatewayRequest, GatewayResponse, Protocol};
197
198 fn ctx(remote_addr: &str, header: Option<(&str, &str)>) -> Context {
199 let mut headers = HashMap::new();
200 if let Some((k, v)) = header {
201 headers.insert(k.to_string(), vec![v.to_string()]);
202 }
203 Context {
204 request: GatewayRequest {
205 method: "GET".to_string(),
206 path: "/hello".to_string(),
207 host: "h".to_string(),
208 scheme: "http".to_string(),
209 headers,
210 query_params: HashMap::new(),
211 body: Bytes::new(),
212 remote_addr: remote_addr.to_string(),
213 protocol: Protocol::Http1,
214 },
215 response: GatewayResponse {
216 status_code: 0,
217 headers: HashMap::new(),
218 body: Bytes::new(),
219 },
220 message: HashMap::new(),
221 errors: Vec::new(),
222 }
223 }
224
225 fn plugin(config: serde_json::Value) -> RateLimitPlugin {
226 let map: HashMap<String, serde_json::Value> =
227 config.as_object().unwrap().clone().into_iter().collect();
228 RateLimitPlugin::from_config(&map).unwrap()
229 }
230
231 #[tokio::test]
233 async fn test_burst_then_throttle() {
234 let p = plugin(serde_json::json!({ "requests_per_second": 1, "burst": 2 }));
235 assert!(p
237 .execute(ctx("1.2.3.4:5", None), &HashMap::new())
238 .await
239 .is_ok());
240 assert!(p
241 .execute(ctx("1.2.3.4:5", None), &HashMap::new())
242 .await
243 .is_ok());
244 let err = p
246 .execute(ctx("1.2.3.4:5", None), &HashMap::new())
247 .await
248 .unwrap_err();
249 assert_eq!(err.error.code, "RATE_LIMITED");
250 assert_eq!(err.context.response.status_code, 429);
251 assert_eq!(
252 err.context
253 .response
254 .headers
255 .get("retry-after")
256 .and_then(|v| v.first())
257 .map(String::as_str),
258 Some("1")
259 );
260 }
261
262 #[tokio::test]
265 async fn test_buckets_are_per_key() {
266 let p = plugin(serde_json::json!({ "requests_per_second": 1, "burst": 1 }));
267 assert!(p
268 .execute(ctx("10.0.0.1:5", None), &HashMap::new())
269 .await
270 .is_ok());
271 assert!(p
273 .execute(ctx("10.0.0.1:5", None), &HashMap::new())
274 .await
275 .is_err());
276 assert!(p
278 .execute(ctx("10.0.0.2:5", None), &HashMap::new())
279 .await
280 .is_ok());
281 }
282
283 #[tokio::test]
285 async fn test_key_from_header() {
286 let p = plugin(serde_json::json!({
287 "requests_per_second": 1, "burst": 1, "key_from": "header:x-api-key"
288 }));
289 assert!(p
291 .execute(
292 ctx("10.0.0.1:5", Some(("x-api-key", "k1"))),
293 &HashMap::new()
294 )
295 .await
296 .is_ok());
297 assert!(p
298 .execute(
299 ctx("10.0.0.2:5", Some(("x-api-key", "k1"))),
300 &HashMap::new()
301 )
302 .await
303 .is_err());
304 assert!(p
306 .execute(
307 ctx("10.0.0.3:5", Some(("x-api-key", "k2"))),
308 &HashMap::new()
309 )
310 .await
311 .is_ok());
312 }
313
314 #[test]
315 fn test_burst_defaults_to_rps() {
316 let p = plugin(serde_json::json!({ "requests_per_second": 7 }));
317 assert_eq!(p.burst, 7);
318 }
319}