this repo has no description
1use reqwest::Client; 2use serde::{Deserialize, Serialize}; 3use std::collections::HashMap; 4use std::sync::Arc; 5use std::time::{Duration, Instant}; 6use tokio::sync::RwLock; 7use tracing::{debug, error, info, warn}; 8 9#[derive(Debug, Clone, Serialize, Deserialize)] 10pub struct DidDocument { 11 pub id: String, 12 #[serde(default)] 13 pub service: Vec<DidService>, 14} 15 16#[derive(Debug, Clone, Serialize, Deserialize)] 17#[serde(rename_all = "camelCase")] 18pub struct DidService { 19 pub id: String, 20 #[serde(rename = "type")] 21 pub service_type: String, 22 pub service_endpoint: String, 23} 24 25#[derive(Clone)] 26struct CachedDid { 27 url: String, 28 did: String, 29 resolved_at: Instant, 30} 31 32#[derive(Clone)] 33struct CachedDidDocument { 34 document: serde_json::Value, 35 resolved_at: Instant, 36} 37 38#[derive(Debug, Clone)] 39pub struct ResolvedService { 40 pub url: String, 41 pub did: String, 42} 43 44pub struct DidResolver { 45 did_cache: RwLock<HashMap<String, CachedDid>>, 46 did_doc_cache: RwLock<HashMap<String, CachedDidDocument>>, 47 client: Client, 48 cache_ttl: Duration, 49 plc_directory_url: String, 50} 51 52impl Clone for DidResolver { 53 fn clone(&self) -> Self { 54 Self { 55 did_cache: RwLock::new(HashMap::new()), 56 did_doc_cache: RwLock::new(HashMap::new()), 57 client: self.client.clone(), 58 cache_ttl: self.cache_ttl, 59 plc_directory_url: self.plc_directory_url.clone(), 60 } 61 } 62} 63 64impl DidResolver { 65 pub fn new() -> Self { 66 let cache_ttl_secs: u64 = std::env::var("DID_CACHE_TTL_SECS") 67 .ok() 68 .and_then(|v| v.parse().ok()) 69 .unwrap_or(300); 70 71 let plc_directory_url = std::env::var("PLC_DIRECTORY_URL") 72 .unwrap_or_else(|_| "https://plc.directory".to_string()); 73 74 let client = Client::builder() 75 .timeout(Duration::from_secs(10)) 76 .connect_timeout(Duration::from_secs(5)) 77 .pool_max_idle_per_host(10) 78 .build() 79 .unwrap_or_else(|_| Client::new()); 80 81 info!("DID resolver initialized"); 82 83 Self { 84 did_cache: RwLock::new(HashMap::new()), 85 did_doc_cache: RwLock::new(HashMap::new()), 86 client, 87 cache_ttl: Duration::from_secs(cache_ttl_secs), 88 plc_directory_url, 89 } 90 } 91 92 fn build_did_web_url(did: &str) -> Result<String, String> { 93 let host = did 94 .strip_prefix("did:web:") 95 .ok_or("Invalid did:web format")?; 96 97 let (host, path) = if host.contains(':') { 98 let decoded = host.replace("%3A", ":"); 99 let parts: Vec<&str> = decoded.splitn(2, '/').collect(); 100 if parts.len() > 1 { 101 (parts[0].to_string(), format!("/{}", parts[1])) 102 } else { 103 (decoded, String::new()) 104 } 105 } else { 106 let parts: Vec<&str> = host.splitn(2, ':').collect(); 107 if parts.len() > 1 && parts[1].contains('/') { 108 let path_parts: Vec<&str> = parts[1].splitn(2, '/').collect(); 109 if path_parts.len() > 1 { 110 ( 111 format!("{}:{}", parts[0], path_parts[0]), 112 format!("/{}", path_parts[1]), 113 ) 114 } else { 115 (host.to_string(), String::new()) 116 } 117 } else { 118 (host.to_string(), String::new()) 119 } 120 }; 121 122 let scheme = 123 if host.starts_with("localhost") || host.starts_with("127.0.0.1") || host.contains(':') 124 { 125 "http" 126 } else { 127 "https" 128 }; 129 130 let url = if path.is_empty() { 131 format!("{}://{}/.well-known/did.json", scheme, host) 132 } else { 133 format!("{}://{}{}/did.json", scheme, host, path) 134 }; 135 136 Ok(url) 137 } 138 139 pub async fn resolve_did(&self, did: &str) -> Option<ResolvedService> { 140 { 141 let cache = self.did_cache.read().await; 142 if let Some(cached) = cache.get(did) 143 && cached.resolved_at.elapsed() < self.cache_ttl 144 { 145 return Some(ResolvedService { 146 url: cached.url.clone(), 147 did: cached.did.clone(), 148 }); 149 } 150 } 151 152 let resolved = self.resolve_did_internal(did).await?; 153 154 { 155 let mut cache = self.did_cache.write().await; 156 cache.insert( 157 did.to_string(), 158 CachedDid { 159 url: resolved.url.clone(), 160 did: resolved.did.clone(), 161 resolved_at: Instant::now(), 162 }, 163 ); 164 } 165 166 Some(resolved) 167 } 168 169 pub async fn refresh_did(&self, did: &str) -> Option<ResolvedService> { 170 { 171 let mut cache = self.did_cache.write().await; 172 cache.remove(did); 173 } 174 self.resolve_did(did).await 175 } 176 177 async fn resolve_did_internal(&self, did: &str) -> Option<ResolvedService> { 178 let did_doc = if did.starts_with("did:web:") { 179 self.resolve_did_web(did).await 180 } else if did.starts_with("did:plc:") { 181 self.resolve_did_plc(did).await 182 } else { 183 warn!("Unsupported DID method: {}", did); 184 return None; 185 }; 186 187 let doc = match did_doc { 188 Ok(doc) => doc, 189 Err(e) => { 190 error!("Failed to resolve DID {}: {}", did, e); 191 return None; 192 } 193 }; 194 195 self.extract_service_endpoint(&doc) 196 } 197 198 async fn resolve_did_web(&self, did: &str) -> Result<DidDocument, String> { 199 let url = Self::build_did_web_url(did)?; 200 201 debug!("Resolving did:web {} via {}", did, url); 202 203 let resp = self 204 .client 205 .get(&url) 206 .send() 207 .await 208 .map_err(|e| format!("HTTP request failed: {}", e))?; 209 210 if !resp.status().is_success() { 211 return Err(format!("HTTP {}", resp.status())); 212 } 213 214 resp.json::<DidDocument>() 215 .await 216 .map_err(|e| format!("Failed to parse DID document: {}", e)) 217 } 218 219 async fn resolve_did_plc(&self, did: &str) -> Result<DidDocument, String> { 220 let url = format!("{}/{}", self.plc_directory_url, urlencoding::encode(did)); 221 222 debug!("Resolving did:plc {} via {}", did, url); 223 224 let resp = self 225 .client 226 .get(&url) 227 .send() 228 .await 229 .map_err(|e| format!("HTTP request failed: {}", e))?; 230 231 if resp.status() == reqwest::StatusCode::NOT_FOUND { 232 return Err("DID not found".to_string()); 233 } 234 235 if !resp.status().is_success() { 236 return Err(format!("HTTP {}", resp.status())); 237 } 238 239 resp.json::<DidDocument>() 240 .await 241 .map_err(|e| format!("Failed to parse DID document: {}", e)) 242 } 243 244 fn extract_service_endpoint(&self, doc: &DidDocument) -> Option<ResolvedService> { 245 for service in &doc.service { 246 if service.service_type == "AtprotoAppView" 247 || service.id.contains("atproto_appview") 248 || service.id.ends_with("#bsky_appview") 249 { 250 return Some(ResolvedService { 251 url: service.service_endpoint.clone(), 252 did: doc.id.clone(), 253 }); 254 } 255 } 256 257 for service in &doc.service { 258 if service.service_type.contains("AppView") || service.id.contains("appview") { 259 return Some(ResolvedService { 260 url: service.service_endpoint.clone(), 261 did: doc.id.clone(), 262 }); 263 } 264 } 265 266 if let Some(service) = doc.service.first() 267 && service.service_endpoint.starts_with("http") 268 { 269 warn!( 270 "No explicit AppView service found for {}, using first service: {}", 271 doc.id, service.service_endpoint 272 ); 273 return Some(ResolvedService { 274 url: service.service_endpoint.clone(), 275 did: doc.id.clone(), 276 }); 277 } 278 279 if doc.id.starts_with("did:web:") { 280 let host = doc.id.strip_prefix("did:web:")?; 281 let decoded_host = host.replace("%3A", ":"); 282 let base_host = decoded_host.split('/').next()?; 283 let scheme = if base_host.starts_with("localhost") 284 || base_host.starts_with("127.0.0.1") 285 || base_host.contains(':') 286 { 287 "http" 288 } else { 289 "https" 290 }; 291 warn!( 292 "No service found for {}, deriving URL from DID: {}://{}", 293 doc.id, scheme, base_host 294 ); 295 return Some(ResolvedService { 296 url: format!("{}://{}", scheme, base_host), 297 did: doc.id.clone(), 298 }); 299 } 300 301 None 302 } 303 304 pub async fn resolve_did_document(&self, did: &str) -> Option<serde_json::Value> { 305 { 306 let cache = self.did_doc_cache.read().await; 307 if let Some(cached) = cache.get(did) 308 && cached.resolved_at.elapsed() < self.cache_ttl 309 { 310 return Some(cached.document.clone()); 311 } 312 } 313 314 let result = if did.starts_with("did:web:") { 315 self.fetch_did_document_web(did).await 316 } else if did.starts_with("did:plc:") { 317 self.fetch_did_document_plc(did).await 318 } else { 319 warn!("Unsupported DID method for document resolution: {}", did); 320 return None; 321 }; 322 323 match result { 324 Ok(doc) => { 325 let mut cache = self.did_doc_cache.write().await; 326 cache.insert( 327 did.to_string(), 328 CachedDidDocument { 329 document: doc.clone(), 330 resolved_at: Instant::now(), 331 }, 332 ); 333 Some(doc) 334 } 335 Err(e) => { 336 warn!("Failed to resolve DID document for {}: {}", did, e); 337 None 338 } 339 } 340 } 341 342 async fn fetch_did_document_web(&self, did: &str) -> Result<serde_json::Value, String> { 343 let url = Self::build_did_web_url(did)?; 344 345 let resp = self 346 .client 347 .get(&url) 348 .send() 349 .await 350 .map_err(|e| format!("HTTP request failed: {}", e))?; 351 352 if !resp.status().is_success() { 353 return Err(format!("HTTP {}", resp.status())); 354 } 355 356 resp.json::<serde_json::Value>() 357 .await 358 .map_err(|e| format!("Failed to parse DID document: {}", e)) 359 } 360 361 async fn fetch_did_document_plc(&self, did: &str) -> Result<serde_json::Value, String> { 362 let url = format!("{}/{}", self.plc_directory_url, urlencoding::encode(did)); 363 364 let resp = self 365 .client 366 .get(&url) 367 .send() 368 .await 369 .map_err(|e| format!("HTTP request failed: {}", e))?; 370 371 if resp.status() == reqwest::StatusCode::NOT_FOUND { 372 return Err("DID not found".to_string()); 373 } 374 375 if !resp.status().is_success() { 376 return Err(format!("HTTP {}", resp.status())); 377 } 378 379 resp.json::<serde_json::Value>() 380 .await 381 .map_err(|e| format!("Failed to parse DID document: {}", e)) 382 } 383 384 pub async fn invalidate_cache(&self, did: &str) { 385 let mut cache = self.did_cache.write().await; 386 cache.remove(did); 387 drop(cache); 388 let mut doc_cache = self.did_doc_cache.write().await; 389 doc_cache.remove(did); 390 } 391} 392 393impl Default for DidResolver { 394 fn default() -> Self { 395 Self::new() 396 } 397} 398 399pub fn create_did_resolver() -> Arc<DidResolver> { 400 Arc::new(DidResolver::new()) 401}