pub mod cards;
pub mod disclosure;
pub mod external;
pub mod fields;
pub mod images;
pub mod model;
pub mod notes;
pub mod present;
pub mod project;
pub mod read;
pub mod sweep;
pub mod values;
pub mod write_policy;
use std::collections::{HashMap, VecDeque};
use std::sync::Mutex;
use std::time::{Duration, Instant};
use vti_common::store::KeyspaceHandle;
use model::Settings;
pub(crate) static WRITE_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
pub struct Directory {
ks: KeyspaceHandle,
settings: Settings,
recent: Mutex<HashMap<String, VecDeque<Instant>>>,
uploads: Mutex<HashMap<String, VecDeque<Instant>>>,
image_reads: Mutex<HashMap<String, VecDeque<Instant>>>,
}
fn admit(
map: &Mutex<HashMap<String, VecDeque<Instant>>>,
key: &str,
window: Duration,
limit: usize,
) -> bool {
let now = Instant::now();
let mut recent = map.lock().unwrap_or_else(|p| p.into_inner());
if recent.len() > 10_000 {
recent.retain(|_, q| q.back().is_some_and(|t| now.duration_since(*t) < window));
}
let q = recent.entry(key.to_string()).or_default();
while q.front().is_some_and(|t| now.duration_since(*t) >= window) {
q.pop_front();
}
if q.len() >= limit {
return false;
}
q.push_back(now);
true
}
pub(crate) fn changed() {
crate::admin_events::notify(crate::admin_events::Topic::Directory);
}
impl Directory {
pub fn new(ks: KeyspaceHandle, settings: Settings) -> Self {
Self {
ks,
settings,
recent: Mutex::new(HashMap::new()),
uploads: Mutex::new(HashMap::new()),
image_reads: Mutex::new(HashMap::new()),
}
}
pub fn ks(&self) -> &KeyspaceHandle {
&self.ks
}
pub fn settings(&self) -> Settings {
self.settings
}
pub fn admit_list_request(&self, did: &str) -> bool {
admit(
&self.recent,
did,
Duration::from_secs(60),
self.settings.list_rate_per_minute as usize,
)
}
pub fn admit_upload(&self, did: &str) -> bool {
admit(
&self.uploads,
did,
Duration::from_secs(3600),
self.settings.image_uploads_per_hour as usize,
)
}
pub fn admit_image_read(&self, did: &str) -> bool {
admit(
&self.image_reads,
did,
Duration::from_secs(60),
(self.settings.list_rate_per_minute as usize).saturating_mul(10),
)
}
}
#[cfg(test)]
pub(crate) mod tests {
use vti_common::store::KeyspaceHandle;
pub(crate) async fn scratch() -> KeyspaceHandle {
let dir = tempfile::tempdir().unwrap();
let store = vti_common::store::test_fixture::open_with_keyspaces(
&dir.path().join("db"),
&["directory"],
)
.unwrap();
let ks = store.keyspace("directory").unwrap();
std::mem::forget(dir);
std::mem::forget(store);
ks
}
#[test]
fn vti_dir_114_reads_are_rate_limited_per_caller() {
let rt = tokio::runtime::Builder::new_current_thread()
.build()
.unwrap();
let ks = rt.block_on(scratch());
let d = super::Directory::new(
ks,
super::Settings {
list_rate_per_minute: 2,
..super::Settings::default()
},
);
assert!(d.admit_list_request("did:example:a"));
assert!(d.admit_list_request("did:example:a"));
assert!(!d.admit_list_request("did:example:a"));
assert!(d.admit_list_request("did:example:b"), "counted per caller");
}
}