didbot_serve/
operator_handle.rs1use std::sync::atomic::{AtomicBool, Ordering};
22use std::sync::{Arc, Mutex};
23
24use didbot_http::PrivateAddresses;
25use didbot_identity::DidDocument;
26use tracing::{debug, info};
27
28use crate::handles::{HandleDid, Network, LOOKUP_TIMEOUT};
29
30pub struct OperatorHandle {
32 forward: Box<dyn HandleDid>,
33 verified: Mutex<Option<String>>,
34 checking: AtomicBool,
36}
37
38impl OperatorHandle {
39 pub fn new(forward: impl HandleDid + 'static) -> Self {
42 Self {
43 forward: Box::new(forward),
44 verified: Mutex::new(None),
45 checking: AtomicBool::new(false),
46 }
47 }
48
49 pub fn over_network(doh: impl Into<String>) -> Self {
51 Self::new(Network::new(doh))
52 }
53
54 pub fn read(&self) -> Option<String> {
56 self.verified
57 .lock()
58 .unwrap_or_else(|poisoned| poisoned.into_inner())
59 .clone()
60 }
61
62 pub(crate) fn refresh_in_background(
65 self: &Arc<Self>,
66 document: DidDocument,
67 private: PrivateAddresses,
68 ) {
69 if self.checking.swap(true, Ordering::AcqRel) {
70 debug!("the last check of the operator's handle is still running");
71 return;
72 }
73 let this = self.clone();
74 tokio::spawn(async move {
75 this.refresh(&document, private).await;
76 this.checking.store(false, Ordering::Release);
77 });
78 }
79
80 pub(crate) async fn refresh(&self, document: &DidDocument, private: PrivateAddresses) {
87 let held = self.read();
88 let verified = match document.handle() {
89 None => None,
90 Some(handle) => {
91 match tokio::time::timeout(LOOKUP_TIMEOUT, self.forward.did_for(handle, private))
92 .await
93 .unwrap_or_else(|_| Err(format!("no answer within {LOOKUP_TIMEOUT:?}")))
94 {
95 Ok(points_at) => points_at
96 .filter(|did| *did == document.id)
97 .map(|_| handle.to_owned()),
98 Err(error) => {
99 debug!(handle, %error, "could not resolve the operator's handle");
100 held.clone().filter(|held| held == handle)
101 }
102 }
103 }
104 };
105 if verified != held {
106 info!(
107 operator = document.id,
108 claimed = document.handle(),
109 handle = verified.as_deref(),
110 "the operator's verified handle changed"
111 );
112 }
113 *self
114 .verified
115 .lock()
116 .unwrap_or_else(|poisoned| poisoned.into_inner()) = verified;
117 }
118}
119
120impl Default for OperatorHandle {
121 fn default() -> Self {
123 Self::new(Network::default())
124 }
125}
126
127#[cfg(test)]
128pub(crate) mod tests {
129 use super::*;
130 use crate::handles::Forward;
131
132 const DID: &str = "did:plc:operatorexampleaaaaaaaaa";
133
134 fn claiming(handle: &str) -> DidDocument {
136 serde_json::from_value(serde_json::json!({
137 "@context": ["https://www.w3.org/ns/did/v1"],
138 "id": DID,
139 "alsoKnownAs": [format!("at://{handle}")],
140 }))
141 .expect("a document")
142 }
143
144 struct PointsAt(Result<Option<&'static str>, &'static str>);
146
147 impl HandleDid for PointsAt {
148 fn did_for<'a>(&'a self, _: &'a str, _: PrivateAddresses) -> Forward<'a> {
149 let answer = self
150 .0
151 .map(|did| did.map(str::to_owned))
152 .map_err(str::to_owned);
153 Box::pin(async move { answer })
154 }
155 }
156
157 pub(crate) struct Stalls;
159
160 impl HandleDid for Stalls {
161 fn did_for<'a>(&'a self, _: &'a str, _: PrivateAddresses) -> Forward<'a> {
162 Box::pin(std::future::pending())
163 }
164 }
165
166 #[tokio::test(start_paused = true)]
169 async fn a_lookup_that_never_answers_ends_at_the_timeout() {
170 let mut handle = OperatorHandle::new(PointsAt(Ok(Some(DID))));
171 handle
172 .refresh(&claiming("operator.example"), PrivateAddresses::Refused)
173 .await;
174 handle.forward = Box::new(Stalls);
175 let started = tokio::time::Instant::now();
176 handle
177 .refresh(&claiming("operator.example"), PrivateAddresses::Refused)
178 .await;
179 assert_eq!(started.elapsed(), LOOKUP_TIMEOUT);
180 assert_eq!(handle.read().as_deref(), Some("operator.example"));
181 }
182
183 #[tokio::test]
186 async fn a_handle_counts_only_when_it_points_back() {
187 let held = OperatorHandle::new(PointsAt(Ok(Some(DID))));
188 held.refresh(&claiming("operator.example"), PrivateAddresses::Refused)
189 .await;
190 assert_eq!(held.read().as_deref(), Some("operator.example"));
191
192 let claimed = OperatorHandle::new(PointsAt(Ok(Some("did:plc:someoneelse"))));
193 claimed
194 .refresh(&claiming("operator.example"), PrivateAddresses::Refused)
195 .await;
196 assert_eq!(claimed.read(), None);
197 }
198
199 #[tokio::test]
202 async fn an_outage_keeps_the_handle_already_verified() {
203 let mut handle = OperatorHandle::new(PointsAt(Ok(Some(DID))));
204 handle
205 .refresh(&claiming("operator.example"), PrivateAddresses::Refused)
206 .await;
207
208 handle.forward = Box::new(PointsAt(Err("unreachable")));
209 handle
210 .refresh(&claiming("operator.example"), PrivateAddresses::Refused)
211 .await;
212 assert_eq!(handle.read().as_deref(), Some("operator.example"));
213 handle
214 .refresh(&claiming("renamed.example"), PrivateAddresses::Refused)
215 .await;
216 assert_eq!(handle.read(), None);
217
218 handle.forward = Box::new(PointsAt(Ok(Some(DID))));
219 handle
220 .refresh(&claiming("operator.example"), PrivateAddresses::Refused)
221 .await;
222 handle.forward = Box::new(PointsAt(Ok(None)));
223 handle
224 .refresh(&claiming("operator.example"), PrivateAddresses::Refused)
225 .await;
226 assert_eq!(handle.read(), None);
227 }
228}