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 for record in txt_lookup.iter() {
29 for txt in record.txt_data() {
30 let txt_str = String::from_utf8_lossy(txt);
31 if let Some(did) = txt_str.strip_prefix("did=") {
32 let did = did.trim();
33 if did.starts_with("did:") {
34 return Ok(did.to_string());
35 }
36 }
37 }
38 }
39 Err(HandleResolutionError::NotFound)
40}
41
42pub async fn resolve_handle_http(handle: &str) -> Result<String, HandleResolutionError> {
43 let url = format!("https://{}/.well-known/atproto-did", handle);
44 let client = crate::api::proxy_client::handle_resolution_client();
45 let response = client
46 .get(&url)
47 .header("Accept", "text/plain")
48 .send()
49 .await
50 .map_err(|e| HandleResolutionError::HttpError(e.to_string()))?;
51 if !response.status().is_success() {
52 return Err(HandleResolutionError::NotFound);
53 }
54 let body = response
55 .text()
56 .await
57 .map_err(|e| HandleResolutionError::HttpError(e.to_string()))?;
58 let did = body.trim();
59 if did.starts_with("did:") {
60 Ok(did.to_string())
61 } else {
62 Err(HandleResolutionError::InvalidDid)
63 }
64}
65
66pub async fn resolve_handle(handle: &str) -> Result<String, HandleResolutionError> {
67 match resolve_handle_dns(handle).await {
68 Ok(did) => return Ok(did),
69 Err(e) => {
70 tracing::debug!("DNS resolution failed for {}: {}, trying HTTP", handle, e);
71 }
72 }
73 resolve_handle_http(handle).await
74}
75
76pub async fn verify_handle_ownership(
77 handle: &str,
78 expected_did: &str,
79) -> Result<(), HandleResolutionError> {
80 let resolved_did = resolve_handle(handle).await?;
81 if resolved_did == expected_did {
82 Ok(())
83 } else {
84 Err(HandleResolutionError::DidMismatch {
85 expected: expected_did.to_string(),
86 actual: resolved_did,
87 })
88 }
89}
90
91pub fn is_service_domain_handle(handle: &str, hostname: &str) -> bool {
92 if !handle.contains('.') {
93 return true;
94 }
95 let service_domains: Vec<String> = std::env::var("PDS_SERVICE_HANDLE_DOMAINS")
96 .map(|s| s.split(',').map(|d| d.trim().to_string()).collect())
97 .unwrap_or_else(|_| vec![hostname.to_string()]);
98 for domain in service_domains {
99 if handle.ends_with(&format!(".{}", domain)) {
100 return true;
101 }
102 if handle == domain {
103 return true;
104 }
105 }
106 false
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn test_is_service_domain_handle() {
115 assert!(is_service_domain_handle("user.example.com", "example.com"));
116 assert!(is_service_domain_handle("example.com", "example.com"));
117 assert!(is_service_domain_handle("myhandle", "example.com"));
118 assert!(!is_service_domain_handle("user.other.com", "example.com"));
119 assert!(!is_service_domain_handle("myhandle.xyz", "example.com"));
120 }
121}