1use std::future::Future;
15use std::pin::Pin;
16use std::time::Duration;
17
18use didbot_http::PrivateAddresses;
19
20pub type Forward<'a> = Pin<Box<dyn Future<Output = Result<Option<String>, String>> + Send + 'a>>;
23
24pub trait HandleDid: Send + Sync {
28 fn did_for<'a>(&'a self, handle: &'a str, private: PrivateAddresses) -> Forward<'a>;
30}
31
32pub struct Network {
34 doh: String,
35}
36
37impl Network {
38 pub fn new(doh: impl Into<String>) -> Self {
42 Self { doh: doh.into() }
43 }
44
45 async fn txt(&self, handle: &str) -> Result<Option<String>, String> {
46 let name = format!("_atproto.{handle}");
47 let records = didbot_http::doh::lookup(
48 &self.doh,
49 &name,
50 didbot_http::doh::TXT,
51 PrivateAddresses::Allowed,
52 )
53 .await?;
54 let dids: Vec<&str> = records
55 .iter()
56 .filter_map(|record| record.strip_prefix("did="))
57 .collect();
58 Ok(match dids.as_slice() {
61 [did] => Some((*did).to_owned()),
62 _ => None,
63 })
64 }
65
66 async fn well_known(
67 &self,
68 handle: &str,
69 private: PrivateAddresses,
70 ) -> Result<Option<String>, String> {
71 let url = didbot_identity::atproto_did_url(handle, None).map_err(|e| e.to_string())?;
72 let parsed = url::Url::parse(&url).map_err(|error| error.to_string())?;
73 let client = didbot_http::pinned_client(&parsed, private)
74 .await
75 .map_err(|error| error.to_string())?;
76 let response = client
77 .get(parsed)
78 .send()
79 .await
80 .map_err(|error| format!("fetching {url}: {error}"))?;
81 if !response.status().is_success() {
82 return Ok(None);
83 }
84 let body = didbot_http::read_bounded(response, MAX_WELL_KNOWN_BYTES)
85 .await
86 .map_err(|error| format!("reading {url}: {error}"))?;
87 let body = didbot_http::body_text(body);
88 Ok(didbot_identity::parse_atproto_did(&body)
89 .ok()
90 .map(str::to_owned))
91 }
92}
93
94const MAX_WELL_KNOWN_BYTES: usize = 4 * 1024;
97
98impl HandleDid for Network {
99 fn did_for<'a>(&'a self, handle: &'a str, private: PrivateAddresses) -> Forward<'a> {
100 Box::pin(async move {
101 let txt = self.txt(handle).await;
102 if let Ok(Some(did)) = txt {
103 return Ok(Some(did));
104 }
105 let https = self.well_known(handle, private).await;
106 match (txt, https) {
107 (_, Ok(Some(did))) => Ok(Some(did)),
108 (Err(dns), Err(https)) => Err(format!("{dns}; {https}")),
109 _ => Ok(None),
110 }
111 })
112 }
113}
114
115impl Default for Network {
116 fn default() -> Self {
118 Self::new(didbot_http::doh::CLOUDFLARE)
119 }
120}
121
122pub const LOOKUP_TIMEOUT: Duration = Duration::from_secs(5);