this repo has no description
at main 3.8 kB view raw
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}