1use std::collections::HashMap;
16use std::num::{NonZeroU32, NonZeroUsize};
17use std::sync::{Arc, Mutex};
18use std::time::{Duration, Instant};
19
20use didbot_http::PrivateAddresses;
21use governor::clock::Clock;
22use governor::{DefaultKeyedRateLimiter, Quota};
23use url::Url;
24
25const MAX_CACHED_TARGET: usize = didbot_identity::MAX_DID_LENGTH + 64;
30
31const SWEEP_EVERY: Duration = Duration::from_secs(60);
34
35#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct ProxySettings {
39 pub calls_per_minute: u32,
46 pub burst_calls: u32,
52 pub in_flight_per_account: usize,
59 pub in_flight_per_tree: usize,
68 pub in_flight_total: usize,
75 pub target_cache_ttl: Duration,
82 pub target_cache_entries: usize,
89 pub app_view: Option<String>,
97}
98
99impl Default for ProxySettings {
100 fn default() -> Self {
101 Self {
102 calls_per_minute: 60,
103 burst_calls: 60,
104 in_flight_per_account: 8,
105 in_flight_per_tree: 64,
106 in_flight_total: 128,
107 target_cache_ttl: Duration::from_secs(300),
108 target_cache_entries: 1024,
109 app_view: None,
110 }
111 }
112}
113
114impl ProxySettings {
115 pub fn check(&self) -> Result<(), String> {
118 if let Some(app_view) = &self.app_view {
119 let target = super::Target::parse(app_view)
120 .map_err(|error| format!("[proxy] app_view {app_view:?}: {}", error.message()))?;
121 didbot_identity::document_url(target.did)
122 .map_err(|error| format!("[proxy] app_view {app_view:?}: {error}"))?;
123 }
124 let ttl = self.target_cache_ttl.as_secs();
125 let out_of_range = [
126 (
127 "calls_per_minute",
128 u64::from(self.calls_per_minute),
129 1,
130 6000,
131 ),
132 ("burst_calls", u64::from(self.burst_calls), 1, 1000),
133 (
134 "in_flight_per_account",
135 self.in_flight_per_account as u64,
136 1,
137 64,
138 ),
139 ("in_flight_per_tree", self.in_flight_per_tree as u64, 1, 512),
140 ("in_flight_total", self.in_flight_total as u64, 1, 512),
141 ("target_cache_secs", ttl, 10, 3600),
142 (
143 "target_cache_entries",
144 self.target_cache_entries as u64,
145 16,
146 100_000,
147 ),
148 ]
149 .into_iter()
150 .find(|(_, value, low, high)| !(low..=high).contains(&value));
151 match out_of_range {
152 Some((key, value, low, high)) => Err(format!(
153 "[proxy] {key} must be {low} to {high}, not {value}"
154 )),
155 None => Ok(()),
156 }
157 }
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162pub(crate) enum Refusal {
163 Rate(Duration),
166 InFlight(usize),
168 Tree(usize),
170 Full(usize),
172}
173
174#[derive(Default)]
176struct Open {
177 per_account: HashMap<String, usize>,
178 per_tree: HashMap<String, usize>,
179 total: usize,
180}
181
182pub(crate) struct InFlight {
184 open: Arc<Mutex<Open>>,
185 did: String,
186 tree: String,
187}
188
189impl Drop for InFlight {
190 fn drop(&mut self) {
191 let mut open = lock(&self.open);
192 open.total -= 1;
193 let Open {
194 per_account,
195 per_tree,
196 ..
197 } = &mut *open;
198 release(per_account, &self.did);
199 release(per_tree, &self.tree);
200 }
201}
202
203fn release(counts: &mut HashMap<String, usize>, key: &str) {
205 if let Some(count) = counts.get_mut(key) {
206 *count -= 1;
207 if *count == 0 {
208 counts.remove(key);
209 }
210 }
211}
212
213struct Endpoint {
215 url: Url,
216 private: PrivateAddresses,
219 fetched: Instant,
220}
221
222pub struct ProxyLimits {
225 settings: ProxySettings,
226 rate: DefaultKeyedRateLimiter<String>,
227 last_swept: Mutex<Instant>,
228 open: Arc<Mutex<Open>>,
229 targets: Mutex<lru::LruCache<String, Endpoint>>,
230}
231
232impl ProxyLimits {
233 pub fn new(settings: ProxySettings) -> Self {
235 let rate = NonZeroU32::new(settings.calls_per_minute).unwrap_or(NonZeroU32::MIN);
236 let burst = NonZeroU32::new(settings.burst_calls).unwrap_or(NonZeroU32::MIN);
237 let entries = NonZeroUsize::new(settings.target_cache_entries).unwrap_or(NonZeroUsize::MIN);
238 Self {
239 settings,
240 rate: DefaultKeyedRateLimiter::keyed(Quota::per_minute(rate).allow_burst(burst)),
241 last_swept: Mutex::new(Instant::now()),
242 open: Arc::new(Mutex::new(Open::default())),
243 targets: Mutex::new(lru::LruCache::new(entries)),
244 }
245 }
246
247 pub fn settings(&self) -> &ProxySettings {
249 &self.settings
250 }
251
252 pub(crate) fn admit(&self, did: &str, tree: &str) -> Result<InFlight, Refusal> {
260 self.sweep();
261 let key = did.to_owned();
262 if let Err(not_until) = self.rate.check_key(&key) {
263 return Err(Refusal::Rate(
264 not_until.wait_time_from(self.rate.clock().now()),
265 ));
266 }
267 let mut open = lock(&self.open);
268 let account = open.per_account.get(&key).copied().unwrap_or(0);
269 if account >= self.settings.in_flight_per_account {
270 return Err(Refusal::InFlight(account));
271 }
272 let held = open.per_tree.get(tree).copied().unwrap_or(0);
273 if held >= self.settings.in_flight_per_tree {
274 return Err(Refusal::Tree(held));
275 }
276 if open.total >= self.settings.in_flight_total {
277 return Err(Refusal::Full(open.total));
278 }
279 open.total += 1;
280 *open.per_account.entry(key.clone()).or_insert(0) += 1;
281 *open.per_tree.entry(tree.to_owned()).or_insert(0) += 1;
282 Ok(InFlight {
283 open: self.open.clone(),
284 did: key,
285 tree: tree.to_owned(),
286 })
287 }
288
289 pub(crate) fn cached_target(&self, target: &str, private: PrivateAddresses) -> Option<Url> {
292 let mut targets = lock(&self.targets);
293 let entry = targets.get(target)?;
294 if entry.fetched.elapsed() >= self.settings.target_cache_ttl || entry.private != private {
295 targets.pop(target);
296 return None;
297 }
298 Some(entry.url.clone())
299 }
300
301 pub(crate) fn cache_target(&self, target: &str, private: PrivateAddresses, url: &Url) {
303 if target.len() > MAX_CACHED_TARGET {
304 return;
305 }
306 lock(&self.targets).put(
307 target.to_owned(),
308 Endpoint {
309 url: url.clone(),
310 private,
311 fetched: Instant::now(),
312 },
313 );
314 }
315
316 fn sweep(&self) {
320 let mut last = lock(&self.last_swept);
321 if last.elapsed() < SWEEP_EVERY {
322 return;
323 }
324 *last = Instant::now();
325 self.rate.retain_recent();
326 self.rate.shrink_to_fit();
327 }
328}
329
330impl Default for ProxyLimits {
331 fn default() -> Self {
332 Self::new(ProxySettings::default())
333 }
334}
335
336fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
337 mutex
338 .lock()
339 .unwrap_or_else(|poisoned| poisoned.into_inner())
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345
346 fn endpoint() -> Url {
347 Url::parse("https://service.example").unwrap()
348 }
349
350 #[test]
353 fn a_cached_target_expires_and_does_not_cross_the_private_setting() {
354 let limits = ProxyLimits::new(ProxySettings {
355 target_cache_ttl: Duration::from_millis(50),
356 ..ProxySettings::default()
357 });
358 let target = "did:web:service.example#svc";
359 limits.cache_target(target, PrivateAddresses::Allowed, &endpoint());
360 assert_eq!(
361 limits.cached_target(target, PrivateAddresses::Allowed),
362 Some(endpoint())
363 );
364 assert_eq!(
365 limits.cached_target(target, PrivateAddresses::Refused),
366 None
367 );
368
369 limits.cache_target(target, PrivateAddresses::Refused, &endpoint());
370 std::thread::sleep(Duration::from_millis(60));
371 assert_eq!(
372 limits.cached_target(target, PrivateAddresses::Refused),
373 None
374 );
375 }
376
377 #[test]
380 fn a_released_call_frees_its_place() {
381 let limits = ProxyLimits::new(ProxySettings {
382 in_flight_per_account: 1,
383 ..ProxySettings::default()
384 });
385 let held = limits
386 .admit("did:web:one.example", "did:web:one.example")
387 .unwrap();
388 assert_eq!(
389 limits
390 .admit("did:web:one.example", "did:web:one.example")
391 .err(),
392 Some(Refusal::InFlight(1))
393 );
394 drop(held);
395 assert!(lock(&limits.open).per_account.is_empty());
396 assert!(lock(&limits.open).per_tree.is_empty());
397 assert!(limits
398 .admit("did:web:one.example", "did:web:one.example")
399 .is_ok());
400 }
401
402 #[test]
405 fn one_trees_accounts_hold_no_more_than_the_trees_share() {
406 let limits = ProxyLimits::new(ProxySettings {
407 in_flight_per_account: 2,
408 in_flight_per_tree: 3,
409 ..ProxySettings::default()
410 });
411 let creator = "did:web:creator.example";
412 let held: Vec<_> = [
413 "did:web:a.example",
414 "did:web:b.example",
415 "did:web:b.example",
416 ]
417 .into_iter()
418 .map(|did| limits.admit(did, creator).unwrap())
419 .collect();
420 assert_eq!(
421 limits.admit("did:web:c.example", creator).err(),
422 Some(Refusal::Tree(3))
423 );
424 assert!(limits
425 .admit("did:web:other.example", "did:web:other.example")
426 .is_ok());
427 drop(held);
428 assert!(limits.admit("did:web:c.example", creator).is_ok());
429 }
430
431 #[test]
434 fn every_account_together_holds_no_more_than_the_total() {
435 let limits = ProxyLimits::new(ProxySettings {
436 in_flight_per_account: 2,
437 in_flight_total: 3,
438 ..ProxySettings::default()
439 });
440 let first: Vec<_> = (0..2)
441 .map(|_| {
442 limits
443 .admit("did:web:one.example", "did:web:one.example")
444 .unwrap()
445 })
446 .collect();
447 let second = limits
448 .admit("did:web:two.example", "did:web:two.example")
449 .unwrap();
450 assert_eq!(
451 limits
452 .admit("did:web:three.example", "did:web:three.example")
453 .err(),
454 Some(Refusal::Full(3))
455 );
456 drop(first);
457 drop(second);
458 assert_eq!(lock(&limits.open).total, 0);
459 assert!(limits
460 .admit("did:web:three.example", "did:web:three.example")
461 .is_ok());
462 }
463}