this repo has no description
1pub mod reserved;
2
3use hickory_resolver::TokioAsyncResolver;
4use hickory_resolver::config::{ResolverConfig, ResolverOpts};
5use thiserror::Error;
6
7#[derive(Error, Debug)]
8pub enum HandleResolutionError {
9 #[error("DNS lookup failed: {0}")]
10 DnsError(String),
11 #[error("HTTP request failed: {0}")]
12 HttpError(String),
13 #[error("No DID found for handle")]
14 NotFound,
15 #[error("Invalid DID format in record")]
16 InvalidDid,
17 #[error("DID mismatch: expected {expected}, got {actual}")]
18 DidMismatch { expected: String, actual: String },
19}
20
21pub async fn resolve_handle_dns(handle: &str) -> Result<String, HandleResolutionError> {
22 let resolver = TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default());
23 let query_name = format!("_atproto.{}", handle);
24 let txt_lookup = resolver
25 .txt_lookup(&query_name)
26 .await
27 .map_err(|e| HandleResolutionError::DnsError(e.to_string()))?;
28 txt_lookup
29 .iter()
30 .flat_map(|record| record.txt_data())
31 .find_map(|txt| {
32 let txt_str = String::from_utf8_lossy(txt);
33 txt_str.strip_prefix("did=").and_then(|did| {
34 let did = did.trim();
35 did.starts_with("did:").then(|| did.to_string())
36 })
37 })
38 .ok_or(HandleResolutionError::NotFound)
39}
40
41pub async fn resolve_handle_http(handle: &str) -> Result<String, HandleResolutionError> {
42 let url = format!("https://{}/.well-known/atproto-did", handle);
43 let client = crate::api::proxy_client::handle_resolution_client();
44 let response = client
45 .get(&url)
46 .header("Accept", "text/plain")
47 .send()
48 .await
49 .map_err(|e| HandleResolutionError::HttpError(e.to_string()))?;
50 if !response.status().is_success() {
51 return Err(HandleResolutionError::NotFound);
52 }
53 let body = response
54 .text()
55 .await
56 .map_err(|e| HandleResolutionError::HttpError(e.to_string()))?;
57 let did = body.trim();
58 if did.starts_with("did:") {
59 Ok(did.to_string())
60 } else {
61 Err(HandleResolutionError::InvalidDid)
62 }
63}
64
65pub async fn resolve_handle(handle: &str) -> Result<String, HandleResolutionError> {
66 match resolve_handle_dns(handle).await {
67 Ok(did) => return Ok(did),
68 Err(e) => {
69 tracing::debug!("DNS resolution failed for {}: {}, trying HTTP", handle, e);
70 }
71 }
72 resolve_handle_http(handle).await
73}
74
75pub async fn verify_handle_ownership(
76 handle: &str,
77 expected_did: &str,
78) -> Result<(), HandleResolutionError> {
79 let resolved_did = resolve_handle(handle).await?;
80 if resolved_did == expected_did {
81 Ok(())
82 } else {
83 Err(HandleResolutionError::DidMismatch {
84 expected: expected_did.to_string(),
85 actual: resolved_did,
86 })
87 }
88}
89
90pub fn is_service_domain_handle(handle: &str, hostname: &str) -> bool {
91 if !handle.contains('.') {
92 return true;
93 }
94 let service_domains: Vec<String> = std::env::var("PDS_SERVICE_HANDLE_DOMAINS")
95 .map(|s| s.split(',').map(|d| d.trim().to_string()).collect())
96 .unwrap_or_else(|_| vec![hostname.to_string()]);
97 service_domains
98 .iter()
99 .any(|domain| handle.ends_with(&format!(".{}", domain)) || handle == domain)
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn test_is_service_domain_handle() {
108 assert!(is_service_domain_handle("user.example.com", "example.com"));
109 assert!(is_service_domain_handle("example.com", "example.com"));
110 assert!(is_service_domain_handle("myhandle", "example.com"));
111 assert!(!is_service_domain_handle("user.other.com", "example.com"));
112 assert!(!is_service_domain_handle("myhandle.xyz", "example.com"));
113 }
114}