use anyhow::{bail, Context, Result}; use atrium_api::did_doc::DidDocument; use bytes::Buf; use http_body_util::BodyExt; use hyper::{client::conn::http1, Request, StatusCode, Uri}; use hyper_util::rt::TokioIo; use rustls::pki_types::ServerName; use serde::{Deserialize, Serialize}; use tokio::net::TcpStream; use crate::{ http::{body_empty, HttpBody}, tls::open_tls_stream, RelayServer, }; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct User { pub did: String, pub pds: Option, #[serde(default)] pub handle: Option, } pub async fn fetch_user(server: &RelayServer, did: &str) -> Result { tracing::debug!(%did, "fetching user"); if did.starts_with("did:plc:") { // TODO: configurable plc resolver location let domain = "plc.directory"; let tcp_stream = TcpStream::connect((domain, 443)).await?; let domain_tls: ServerName<'_> = ServerName::try_from(domain.to_string())?; let tls_stream = open_tls_stream(tcp_stream, domain_tls).await?; let io = TokioIo::new(tls_stream); let req = Request::builder() .method("GET") .uri(format!("https://{domain}/{did}")) .header("Host", domain.to_string()) .body(body_empty())?; let (mut sender, conn) = http1::handshake::<_, HttpBody>(io).await?; tokio::task::spawn(async move { if let Err(err) = conn.await { println!("Connection failed: {:?}", err); } }); let res = sender .send_request(req) .await .context("Failed to send plc request")?; if res.status() != StatusCode::OK { bail!("plc directory returned non-200 status"); } let body = res.collect().await?.aggregate(); let did_doc = serde_json::from_reader::<_, DidDocument>(body.reader()) .context("Failed to parse plc DID doc as JSON")?; let pds_endpoint = did_doc.get_pds_endpoint(); let pds_uri: Option = pds_endpoint.as_deref().unwrap_or_default().parse().ok(); let pds = pds_uri .as_ref() .and_then(|u| u.authority()) .map(|a| a.host()) .map(|s| s.to_string()); let handle = did_doc .also_known_as .and_then(|v| v.into_iter().next()) .and_then(|s| s.strip_prefix("at://").map(str::to_string)); let did = did_doc.id; // TODO: check if handle resolves to did and fill none otherwise let user = User { pds, did, handle }; store_user(server, &user)?; Ok(user) } else if did.starts_with("did:web:") { todo!("resolve did web") } else { bail!("unknown did type {did}"); } } pub async fn lookup_user(server: &RelayServer, did: &str) -> Result { if let Some(cached_user) = server.db_users.get(did)? { let cached_user = serde_ipld_dagcbor::from_slice::(&cached_user)?; return Ok(cached_user); } return fetch_user(server, did).await; } pub fn store_user(server: &RelayServer, user: &User) -> Result<()> { let data = serde_ipld_dagcbor::to_vec(&user)?; server.db_users.insert(&user.did, data)?; Ok(()) }