i18n+filtering fork - fluent-templates v2
at main 6.2 kB view raw
1use anyhow::Result; 2use errors::ResolveError; 3use futures_util::future::join3; 4use hickory_resolver::{ 5 config::{NameServerConfigGroup, ResolverConfig, ResolverOpts}, 6 TokioAsyncResolver, 7}; 8use std::collections::HashSet; 9use std::time::Duration; 10 11use crate::config::DnsNameservers; 12use crate::did::web::query_hostname; 13 14pub enum InputType { 15 Handle(String), 16 Plc(String), 17 Web(String), 18} 19 20pub async fn resolve_handle_dns( 21 dns_resolver: &TokioAsyncResolver, 22 lookup_dns: &str, 23) -> Result<String, ResolveError> { 24 let lookup = dns_resolver 25 .txt_lookup(&format!("_atproto.{}", lookup_dns)) 26 .await 27 .map_err(ResolveError::DNSResolutionFailed)?; 28 29 let dids = lookup 30 .iter() 31 .filter_map(|record| { 32 record 33 .to_string() 34 .strip_prefix("did=") 35 .map(|did| did.to_string()) 36 }) 37 .collect::<HashSet<String>>(); 38 39 if dids.len() > 1 { 40 return Err(ResolveError::MultipleDIDsFound); 41 } 42 43 dids.iter().next().cloned().ok_or(ResolveError::NoDIDsFound) 44} 45 46pub async fn resolve_handle_http( 47 http_client: &reqwest::Client, 48 handle: &str, 49) -> Result<String, ResolveError> { 50 let lookup_url = format!("https://{}/.well-known/atproto-did", handle); 51 52 http_client 53 .get(lookup_url.clone()) 54 .timeout(Duration::from_secs(10)) 55 .send() 56 .await 57 .map_err(ResolveError::HTTPResolutionFailed)? 58 .text() 59 .await 60 .map_err(ResolveError::HTTPResolutionFailed) 61 .and_then(|body| { 62 if body.starts_with("did:") { 63 Ok(body.trim().to_string()) 64 } else { 65 Err(ResolveError::InvalidHTTPResolutionResponse) 66 } 67 }) 68} 69 70pub fn parse_input(input: &str) -> Result<InputType, ResolveError> { 71 let trimmed = { 72 if let Some(value) = input.trim().strip_prefix("at://") { 73 value.trim() 74 } else if let Some(value) = input.trim().strip_prefix('@') { 75 value.trim() 76 } else { 77 input.trim() 78 } 79 }; 80 if trimmed.is_empty() { 81 return Err(ResolveError::InvalidInput); 82 } 83 if trimmed.starts_with("did:web:") { 84 Ok(InputType::Web(trimmed.to_string())) 85 } else if trimmed.starts_with("did:plc:") { 86 Ok(InputType::Plc(trimmed.to_string())) 87 } else { 88 Ok(InputType::Handle(trimmed.to_string())) 89 } 90} 91 92pub async fn resolve_handle( 93 http_client: &reqwest::Client, 94 dns_resolver: &TokioAsyncResolver, 95 handle: &str, 96) -> Result<String, ResolveError> { 97 let trimmed = { 98 if let Some(value) = handle.trim().strip_prefix("at://") { 99 value 100 } else if let Some(value) = handle.trim().strip_prefix('@') { 101 value 102 } else { 103 handle.trim() 104 } 105 }; 106 107 let (dns_lookup, http_lookup, did_web_lookup) = join3( 108 resolve_handle_dns(dns_resolver, trimmed), 109 resolve_handle_http(http_client, trimmed), 110 query_hostname(http_client, trimmed), 111 ) 112 .await; 113 114 tracing::debug!( 115 ?handle, 116 ?dns_lookup, 117 ?http_lookup, 118 ?did_web_lookup, 119 "raw query results" 120 ); 121 122 let did_web_lookup_did = did_web_lookup 123 .map(|document| document.id) 124 .map_err(ResolveError::DIDWebResolutionFailed); 125 126 let results = vec![dns_lookup, http_lookup, did_web_lookup_did] 127 .into_iter() 128 .filter_map(|result| result.ok()) 129 .collect::<Vec<String>>(); 130 if results.is_empty() { 131 return Err(ResolveError::NoDIDsFound); 132 } 133 134 tracing::debug!(?handle, ?results, "query results"); 135 136 let first = results[0].clone(); 137 if results.iter().all(|result| result == &first) { 138 return Ok(first); 139 } 140 Err(ResolveError::ConflictingDIDsFound) 141} 142 143pub async fn resolve_subject( 144 http_client: &reqwest::Client, 145 dns_resolver: &TokioAsyncResolver, 146 subject: &str, 147) -> Result<String, ResolveError> { 148 match parse_input(subject)? { 149 InputType::Handle(handle) => resolve_handle(http_client, dns_resolver, &handle).await, 150 InputType::Plc(did) | InputType::Web(did) => Ok(did), 151 } 152} 153 154/// Creates a new DNS resolver with configuration based on app config. 155/// 156/// If custom nameservers are configured in app config, they will be used. 157/// Otherwise, the system default resolver configuration will be used. 158pub fn create_resolver(nameservers: DnsNameservers) -> TokioAsyncResolver { 159 // Initialize the DNS resolver with custom nameservers if configured 160 let nameservers = nameservers.as_ref(); 161 let resolver_config = if !nameservers.is_empty() { 162 // Use custom nameservers 163 tracing::info!("Using custom DNS nameservers: {:?}", nameservers); 164 let nameserver_group = NameServerConfigGroup::from_ips_clear(nameservers, 53, true); 165 ResolverConfig::from_parts(None, vec![], nameserver_group) 166 } else { 167 // Use system default 168 tracing::info!("Using system default DNS nameservers"); 169 ResolverConfig::default() 170 }; 171 172 // TokioAsyncResolver::tokio returns an AsyncResolver directly, not a Result 173 TokioAsyncResolver::tokio(resolver_config, ResolverOpts::default()) 174} 175 176pub mod errors { 177 use thiserror::Error; 178 179 #[derive(Debug, Error)] 180 pub enum ResolveError { 181 #[error("error-resolve-1 Multiple DIDs resolved for method")] 182 MultipleDIDsFound, 183 184 #[error("error-resolve-2 No DIDs resolved for method")] 185 NoDIDsFound, 186 187 #[error("error-resolve-3 No DIDs resolved for method")] 188 ConflictingDIDsFound, 189 190 #[error("error-resolve-4 DNS resolution failed: {0:?}")] 191 DNSResolutionFailed(hickory_resolver::error::ResolveError), 192 193 #[error("error-resolve-5 HTTP resolution failed: {0:?}")] 194 HTTPResolutionFailed(reqwest::Error), 195 196 #[error("error-resolve-6 HTTP resolution failed")] 197 InvalidHTTPResolutionResponse, 198 199 #[error("error-resolve-7 HTTP resolution failed: {0:?}")] 200 DIDWebResolutionFailed(anyhow::Error), 201 202 #[error("error-resolve-8 Invalid input")] 203 InvalidInput, 204 } 205}