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}