atproto_identity/
resolve.rs1use anyhow::Result;
16#[cfg(feature = "hickory-dns")]
17use hickory_resolver::{
18 Resolver, TokioResolver,
19 config::{NameServerConfigGroup, ResolverConfig},
20 name_server::TokioConnectionProvider,
21};
22use reqwest::Client;
23use std::collections::HashSet;
24use std::ops::Deref;
25use std::sync::Arc;
26use std::time::Duration;
27use tracing::{Instrument, instrument};
28
29use crate::errors::ResolveError;
30use crate::model::Document;
31use crate::plc::query as plc_query;
32use crate::validation::{is_valid_did_method_plc, is_valid_handle};
33use crate::web::query as web_query;
34
35#[async_trait::async_trait]
41pub trait IdentityResolver: Send + Sync {
42 async fn resolve(&self, subject: &str) -> Result<Document>;
54}
55
56#[async_trait::async_trait]
59pub trait DnsResolver: Send + Sync {
60 async fn resolve_txt(&self, domain: &str) -> Result<Vec<String>, ResolveError>;
63}
64
65#[cfg(feature = "hickory-dns")]
68#[derive(Clone)]
69pub struct HickoryDnsResolver {
70 resolver: TokioResolver,
71}
72
73#[cfg(feature = "hickory-dns")]
74impl HickoryDnsResolver {
75 pub fn new(resolver: TokioResolver) -> Self {
77 Self { resolver }
78 }
79
80 pub fn create_resolver(nameservers: &[std::net::IpAddr]) -> Self {
83 let tokio_resolver = if !nameservers.is_empty() {
85 tracing::debug!("Using custom DNS nameservers: {:?}", nameservers);
86 let nameserver_group = NameServerConfigGroup::from_ips_clear(nameservers, 53, true);
87 let resolver_config = ResolverConfig::from_parts(None, vec![], nameserver_group);
88 Resolver::builder_with_config(resolver_config, TokioConnectionProvider::default())
89 .build()
90 } else {
91 tracing::debug!("Using system default DNS nameservers");
92 Resolver::builder_tokio().unwrap().build()
93 };
94 Self::new(tokio_resolver)
95 }
96}
97
98#[cfg(feature = "hickory-dns")]
99#[async_trait::async_trait]
100impl DnsResolver for HickoryDnsResolver {
101 async fn resolve_txt(&self, domain: &str) -> Result<Vec<String>, ResolveError> {
102 let lookup = self
103 .resolver
104 .txt_lookup(domain)
105 .instrument(tracing::info_span!("txt_lookup"))
106 .await
107 .map_err(|error| ResolveError::DNSResolutionFailed { error })?;
108
109 Ok(lookup.iter().map(|record| record.to_string()).collect())
110 }
111}
112
113pub enum InputType {
116 Handle(String),
118 Plc(String),
120 Web(String),
122}
123
124#[instrument(skip(dns_resolver), err)]
127pub async fn resolve_handle_dns<R: DnsResolver + ?Sized>(
128 dns_resolver: &R,
129 lookup_dns: &str,
130) -> Result<String, ResolveError> {
131 let txt_records = dns_resolver
132 .resolve_txt(&format!("_atproto.{}", lookup_dns))
133 .await?;
134
135 let dids = txt_records
136 .iter()
137 .filter_map(|record| record.strip_prefix("did=").map(|did| did.to_string()))
138 .collect::<HashSet<String>>();
139
140 if dids.len() > 1 {
141 return Err(ResolveError::MultipleDIDsFound);
142 }
143
144 dids.iter().next().cloned().ok_or(ResolveError::NoDIDsFound)
145}
146
147#[instrument(skip(http_client), err)]
150pub async fn resolve_handle_http(
151 http_client: &reqwest::Client,
152 handle: &str,
153) -> Result<String, ResolveError> {
154 let lookup_url = format!("https://{}/.well-known/atproto-did", handle);
155
156 http_client
157 .get(lookup_url.clone())
158 .timeout(Duration::from_secs(10))
159 .send()
160 .instrument(tracing::info_span!("http_client_get"))
161 .await
162 .map_err(|error| ResolveError::HTTPResolutionFailed { error })?
163 .text()
164 .instrument(tracing::info_span!("response_text"))
165 .await
166 .map_err(|error| ResolveError::HTTPResolutionFailed { error })
167 .and_then(|body| {
168 if body.starts_with("did:") {
169 Ok(body.trim().to_string())
170 } else {
171 Err(ResolveError::InvalidHTTPResolutionResponse)
172 }
173 })
174}
175
176pub fn parse_input(input: &str) -> Result<InputType, ResolveError> {
179 let trimmed = {
180 if let Some(value) = input.trim().strip_prefix("at://") {
181 value.trim()
182 } else if let Some(value) = input.trim().strip_prefix('@') {
183 value.trim()
184 } else {
185 input.trim()
186 }
187 };
188 if trimmed.is_empty() {
189 return Err(ResolveError::InvalidInput);
190 }
191 if trimmed.starts_with("did:web:") {
192 Ok(InputType::Web(trimmed.to_string()))
193 } else if trimmed.starts_with("did:plc:") && is_valid_did_method_plc(trimmed) {
194 Ok(InputType::Plc(trimmed.to_string()))
195 } else {
196 is_valid_handle(trimmed)
197 .map(InputType::Handle)
198 .ok_or(ResolveError::InvalidInput)
199 }
200}
201
202#[instrument(skip(http_client, dns_resolver), err)]
205pub async fn resolve_handle<R: DnsResolver + ?Sized>(
206 http_client: &reqwest::Client,
207 dns_resolver: &R,
208 handle: &str,
209) -> Result<String, ResolveError> {
210 let trimmed = {
211 if let Some(value) = handle.trim().strip_prefix("at://") {
212 value
213 } else if let Some(value) = handle.trim().strip_prefix('@') {
214 value
215 } else {
216 handle.trim()
217 }
218 };
219
220 let (dns_lookup, http_lookup) = tokio::join!(
221 resolve_handle_dns(dns_resolver, trimmed),
222 resolve_handle_http(http_client, trimmed),
223 );
224
225 let results = vec![dns_lookup, http_lookup]
226 .into_iter()
227 .filter_map(|result| result.ok())
228 .collect::<Vec<String>>();
229 if results.is_empty() {
230 return Err(ResolveError::NoDIDsFound);
231 }
232
233 let first = results[0].clone();
234 if results.iter().all(|result| result == &first) {
235 return Ok(first);
236 }
237 Err(ResolveError::ConflictingDIDsFound)
238}
239
240#[instrument(skip(http_client, dns_resolver), err)]
243pub async fn resolve_subject<R: DnsResolver + ?Sized>(
244 http_client: &reqwest::Client,
245 dns_resolver: &R,
246 subject: &str,
247) -> Result<String, ResolveError> {
248 match parse_input(subject)? {
249 InputType::Handle(handle) => resolve_handle(http_client, dns_resolver, &handle).await,
250 InputType::Plc(did) | InputType::Web(did) => Ok(did),
251 }
252}
253
254pub struct InnerIdentityResolver {
259 pub dns_resolver: Arc<dyn DnsResolver>,
261 pub http_client: Client,
263 pub plc_hostname: String,
265}
266
267#[derive(Clone)]
272pub struct SharedIdentityResolver(pub Arc<InnerIdentityResolver>);
273
274impl Deref for SharedIdentityResolver {
275 type Target = InnerIdentityResolver;
276
277 fn deref(&self) -> &Self::Target {
278 &self.0
279 }
280}
281
282#[async_trait::async_trait]
283impl IdentityResolver for SharedIdentityResolver {
284 async fn resolve(&self, subject: &str) -> Result<Document> {
285 self.0.resolve(subject).await
286 }
287}
288
289#[async_trait::async_trait]
290impl IdentityResolver for InnerIdentityResolver {
291 async fn resolve(&self, subject: &str) -> Result<Document> {
292 let resolved_did = resolve_subject(&self.http_client, &*self.dns_resolver, subject).await?;
293
294 match parse_input(&resolved_did) {
295 Ok(InputType::Plc(did)) => plc_query(&self.http_client, &self.plc_hostname, &did)
296 .await
297 .map_err(Into::into),
298 Ok(InputType::Web(did)) => web_query(&self.http_client, &did).await.map_err(Into::into),
299 Ok(InputType::Handle(_)) => Err(ResolveError::SubjectResolvedToHandle.into()),
300 Err(err) => Err(err.into()),
301 }
302 }
303}
304
305impl InnerIdentityResolver {
306 pub async fn resolve(&self, subject: &str) -> Result<Document> {
311 <Self as IdentityResolver>::resolve(self, subject).await
312 }
313}