1use std::collections::BTreeMap;
9use std::sync::Mutex;
10
11use base64::prelude::*;
12use didbot_pds::AccountKind;
13use sha2::{Digest, Sha256};
14use time::{Duration, OffsetDateTime};
15
16pub const DEFAULT_PARK_TTL: Duration = Duration::minutes(10);
24
25pub const PARK_RATE_LIMIT: u32 = 6;
33
34pub const PARK_RATE_WINDOW: std::time::Duration = std::time::Duration::from_secs(10 * 60);
40
41#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct ParkedKey {
45 pub name: String,
47 pub kind: AccountKind,
49 pub key: String,
51 pub fingerprint: String,
53 pub parked_at: OffsetDateTime,
55 pub expires_at: OffsetDateTime,
57}
58
59pub fn fingerprint(multibase: &str) -> String {
62 format!(
63 "SHA256:{}",
64 BASE64_URL_SAFE_NO_PAD.encode(Sha256::digest(multibase.as_bytes()))
65 )
66}
67
68#[derive(Debug)]
78pub struct ParkedKeys {
79 ttl: Duration,
80 held: Mutex<BTreeMap<(String, String), ParkedKey>>,
81}
82
83impl ParkedKeys {
84 pub fn new(ttl: Duration) -> Self {
86 Self {
87 ttl,
88 held: Mutex::new(BTreeMap::new()),
89 }
90 }
91
92 pub fn park(
98 &self,
99 name: &str,
100 kind: AccountKind,
101 key: &str,
102 now: OffsetDateTime,
103 ) -> Result<ParkedKey, OffsetDateTime> {
104 let mut held = self.held.lock().unwrap_or_else(|p| p.into_inner());
105 held.retain(|_, parked| parked.expires_at > now);
106 let fingerprint = fingerprint(key);
107 let at = (name.to_owned(), fingerprint.clone());
108 if let Some(existing) = held.get(&at) {
109 return Err(existing.expires_at);
110 }
111 let parked = ParkedKey {
112 name: name.to_owned(),
113 kind,
114 key: key.to_owned(),
115 fingerprint,
116 parked_at: now,
117 expires_at: now + self.ttl,
118 };
119 held.insert(at, parked.clone());
120 Ok(parked)
121 }
122
123 pub fn describe(
130 &self,
131 name: &str,
132 fingerprint: Option<&str>,
133 now: OffsetDateTime,
134 ) -> Option<ParkedKey> {
135 let held = self.held.lock().unwrap_or_else(|p| p.into_inner());
136 let live = |parked: &&ParkedKey| parked.expires_at > now;
137 match fingerprint {
138 Some(fingerprint) => held
139 .get(&(name.to_owned(), fingerprint.to_owned()))
140 .filter(live)
141 .cloned(),
142 None => held
143 .values()
144 .filter(|parked| parked.name == name)
145 .filter(live)
146 .max_by_key(|parked| parked.parked_at)
147 .cloned(),
148 }
149 }
150
151 pub fn take(&self, name: &str, fingerprint: &str, now: OffsetDateTime) -> Option<ParkedKey> {
154 let mut held = self.held.lock().unwrap_or_else(|p| p.into_inner());
155 let at = (name.to_owned(), fingerprint.to_owned());
156 match held.get(&at) {
157 Some(parked) if parked.expires_at > now => held.remove(&at),
158 _ => None,
159 }
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn one_key_is_parked_once_per_name_until_it_lapses() {
169 let parked = ParkedKeys::new(Duration::minutes(10));
170 let now = OffsetDateTime::from_unix_timestamp(1_700_000_000).expect("a clock");
171 let first = parked
172 .park("kestrel.pds.example", AccountKind::Host, "zQ3shKey", now)
173 .expect("parks");
174 assert_eq!(first.fingerprint, fingerprint("zQ3shKey"));
175 assert_eq!(
176 parked.park("kestrel.pds.example", AccountKind::Host, "zQ3shKey", now),
177 Err(now + Duration::minutes(10)),
178 "the same key parked twice waits for its own park to lapse"
179 );
180 assert!(parked
181 .describe("kestrel.pds.example", None, now + Duration::minutes(11))
182 .is_none());
183 assert!(parked
184 .park(
185 "kestrel.pds.example",
186 AccountKind::Host,
187 "zQ3shKey",
188 now + Duration::minutes(11)
189 )
190 .is_ok());
191 }
192
193 #[test]
196 fn two_keys_park_under_one_name_and_each_is_taken_by_its_fingerprint() {
197 let parked = ParkedKeys::new(Duration::minutes(10));
198 let now = OffsetDateTime::from_unix_timestamp(1_700_000_000).expect("a clock");
199 let squatter = parked
200 .park("kestrel.pds.example", AccountKind::Host, "zQ3shSquat", now)
201 .expect("parks");
202 let machine = parked
203 .park(
204 "kestrel.pds.example",
205 AccountKind::Host,
206 "zQ3shMachine",
207 now + Duration::seconds(1),
208 )
209 .expect("a second key parks under the same name");
210
211 let later = now + Duration::seconds(2);
212 let described = |fingerprint: Option<&str>| {
213 parked
214 .describe("kestrel.pds.example", fingerprint, later)
215 .map(|found| found.fingerprint)
216 };
217 assert_eq!(
218 described(None),
219 Some(machine.fingerprint.clone()),
220 "with no fingerprint, the newest park is the one described"
221 );
222 assert_eq!(
223 described(Some(&squatter.fingerprint)),
224 Some(squatter.fingerprint.clone()),
225 "each fingerprint describes its own park"
226 );
227 assert_eq!(
228 described(Some(&machine.fingerprint)),
229 Some(machine.fingerprint.clone())
230 );
231 assert_eq!(described(Some("SHA256:unknown")), None);
232 assert_eq!(
233 parked
234 .describe(
235 "kestrel.pds.example",
236 Some(&squatter.fingerprint),
237 now + Duration::minutes(11)
238 )
239 .map(|found| found.fingerprint),
240 None,
241 "a lapsed park is not described by its fingerprint"
242 );
243 assert_eq!(
244 parked.take(
245 "kestrel.pds.example",
246 &machine.fingerprint,
247 now + Duration::seconds(2)
248 ),
249 Some(machine)
250 );
251 assert_eq!(
252 parked
253 .take(
254 "kestrel.pds.example",
255 &squatter.fingerprint,
256 now + Duration::seconds(2)
257 )
258 .map(|found| found.fingerprint),
259 Some(squatter.fingerprint),
260 "the other park is still there to admit"
261 );
262 }
263
264 #[test]
265 fn a_park_is_taken_once_and_only_by_its_fingerprint() {
266 let parked = ParkedKeys::new(Duration::minutes(10));
267 let now = OffsetDateTime::from_unix_timestamp(1_700_000_000).expect("a clock");
268 let first = parked
269 .park("kestrel.pds.example", AccountKind::Host, "zQ3shKey", now)
270 .expect("parks");
271 assert!(parked
272 .take("kestrel.pds.example", "SHA256:wrong", now)
273 .is_none());
274 assert_eq!(
275 parked.take("kestrel.pds.example", &first.fingerprint, now),
276 Some(first.clone())
277 );
278 assert!(parked
279 .take("kestrel.pds.example", &first.fingerprint, now)
280 .is_none());
281 }
282}