//! The transport seam: com.fedfork.exec..* pseudo-calls, request and subscribe, //! served by crates outside atlibs-host (the atlibs-iroh repo). A host registers the transports it was //! built with, each saying which NSIDs it provides (provides(), for load-time checks); this crate knows //! none of them. HostCall refuses a connect no transport took: unavailable(). //! javascript/src/run-module/host-call.js is the same seam (its `calls` map) with the same refusal text. //! A transport may also fetch URLs of its own schemes (webrtc:, iroh:) for com.fedfork.exec.http.fetch: //! its call's Call::schemes() and Call::fetch(), gathered by fetchers() into run::Http::schemes (JS: runModule's //! `fetchers`, a transport plugin's fetchers()). use std::collections::{HashMap, HashSet}; use std::sync::Arc; use serde_json::{Value, json}; use crate::BoxFut; use crate::host::{Fetch, Response}; use crate::run::{Call, Request, Sub}; pub const PSEUDO: &str = "com.fedfork.exec."; /// A transport: the NSIDs it provides (request and subscribe) and the call that serves them. #[derive(Clone)] pub struct Transport { pub provides: &'static [&'static str], pub call: Arc, } impl Transport { /// The URL schemes (no colon) whose com.fedfork.exec.http.fetch requests it serves: its call's. pub fn schemes(&self) -> &'static [&'static str] { self.call.schemes() } } /// One request to a URL of its schemes(): its call's Call::fetch. impl Fetch for Transport { fn fetch(&self, method: &str, url: &str, headers: Vec<(String, String)>, body: Option>) -> BoxFut<'_, Result> { self.call.fetch(method, url, headers, body) } } /// Every scheme the transports fetch, ":" -> that transport: run::Http::schemes takes it as is. pub fn fetchers(transports: &[Transport]) -> HashMap> { transports.iter().flat_map(|t| t.schemes().iter().map(move |s| (format!("{s}:"), Arc::new(t.clone()) as Arc))).collect() } /// The transports a host was built with. pub type Transports = Vec; /// Every NSID the transports provide: what module and composition loading may count on (a hook a /// host transport satisfies), known before any call. /// run::Options.provides takes it as is. pub fn provides(transports: &[Transport]) -> HashSet { transports.iter().flat_map(|t| t.provides.iter().map(|n| n.to_string())).collect() } /// The transport an NSID names, com.fedfork.exec..connect -> : "built without ". pub fn name(nsid: &str) -> Option<&str> { nsid.strip_prefix(PSEUDO)?.strip_suffix(".connect").filter(|n| !n.is_empty() && !n.contains('.')) } /// The call-time refusal of a transport connect this host was built without (the load-time check is /// the caller's, over provides()): ": not available on this host (built without )". pub fn unavailable(nsid: &str) -> Option { name(nsid).map(|name| format!("{nsid}: not available on this host (built without {name})")) } /// Routes the NSIDs a transport provides to it and everything else to inner. listed: the module record /// lists its calls; one that lists none may call anything, so it gets no transport (the host's keys /// stay out of it), as it gets no attest. pub struct TransportCall { pub transports: Transports, pub listed: bool, pub inner: Arc, } impl TransportCall { fn route(&self, nsid: &str) -> Result<&Arc, String> { match self.transports.iter().find(|t| t.provides.contains(&nsid)) { None => Ok(&self.inner), Some(_) if !self.listed => Err(format!("{nsid} needs a module record that lists its calls")), Some(t) => Ok(&t.call), } } } impl TransportCall { /// A did#id call with endpoints: the first endpoint of a scheme a transport fetches (not https: HostCall /// takes that), with a service-auth token aud did#id the inner session mints (the module never sees it). async fn peer(&self, nsid: &str, request: &Request) -> Option> { let (Some(endpoints), Some(aud)) = (request.endpoints.as_ref()?.as_array(), request.service.as_deref()) else { return None }; let (base, transport) = endpoints.iter().find_map(|e| { let (kind, address) = (e["kind"].as_str()?, e["address"].as_str()?); let t = self.transports.iter().find(|t| kind != "https" && t.schemes().contains(&kind))?; Some((format!("{kind}://{address}"), t)) })?; let params = json!({ "aud": aud, "lxm": nsid }).as_object().cloned(); let minted = self.inner.call("com.atproto.server.getServiceAuth".into(), Request { params, signal: request.signal.clone(), ..Default::default() }).await; Some(async { let token = minted?.and_then(|a| a["token"].as_str().map(String::from)).filter(|t| !t.is_empty()).ok_or("getServiceAuth answered no service-auth token")?; crate::host::bearer_call(transport, &base, nsid, request.params.as_ref(), crate::host::xrpc_body(request.input.as_ref(), request.encoding.as_deref())?, &token).await }.await) } } impl Call for TransportCall { fn call(&self, nsid: String, request: Request) -> BoxFut<'_, Result, String>> { Box::pin(async move { if !nsid.starts_with(PSEUDO) { if let Some(answer) = self.peer(&nsid, &request).await { return answer.map(Some); } } self.route(&nsid)?.call(nsid, request).await }) } fn subscribe(&self, nsid: String, request: Request) -> BoxFut<'_, Result> { Box::pin(async move { self.route(&nsid)?.subscribe(nsid, request).await }) } } #[cfg(test)] mod tests { use super::*; use serde_json::json; struct Named(&'static str); impl Call for Named { fn call(&self, nsid: String, _: Request) -> BoxFut<'_, Result, String>> { Box::pin(async move { Ok(Some(json!([self.0, nsid]))) }) } fn subscribe(&self, nsid: String, _: Request) -> BoxFut<'_, Result> { Box::pin(async move { Err(format!("{} {nsid}", self.0)) }) } } #[tokio::test] async fn transports_take_what_they_provide_and_refuse_unlisted_modules() { let call = |listed| TransportCall { transports: vec![Transport { provides: &["com.fedfork.exec.iroh.connect"], call: Arc::new(Named("iroh")) }], listed, inner: Arc::new(Named("inner")) }; let iroh = "com.fedfork.exec.iroh.connect"; assert_eq!(provides(&call(true).transports), HashSet::from([iroh.to_string()])); assert_eq!(call(true).call(iroh.into(), Request::default()).await.unwrap(), Some(json!(["iroh", iroh]))); assert_eq!(call(true).subscribe(iroh.into(), Request::default()).await.err().unwrap(), format!("iroh {iroh}")); for other in ["com.fedfork.exec.webrtc.connect", "com.fedfork.exec.iroh.other", "com.atproto.repo.getRecord"] { assert_eq!(call(true).call(other.into(), Request::default()).await.unwrap(), Some(json!(["inner", other]))); } assert_eq!(call(false).call(iroh.into(), Request::default()).await.unwrap_err(), format!("{iroh} needs a module record that lists its calls")); assert_eq!(unavailable(iroh).unwrap(), "com.fedfork.exec.iroh.connect: not available on this host (built without iroh)"); assert_eq!(unavailable("com.fedfork.exec.webrtc.connect").unwrap(), "com.fedfork.exec.webrtc.connect: not available on this host (built without webrtc)"); assert!(fetchers(&call(true).transports).is_empty()); for no in ["com.fedfork.exec.attest", "com.fedfork.exec.didkey.sign", "com.fedfork.exec.a.b.connect", "x.y.connect"] { assert_eq!(unavailable(no), None, "{no}"); } } // JS host-call.test.mjs "a did#id call with endpoints" struct Peer(std::sync::Mutex>); impl Call for Peer { fn call(&self, nsid: String, request: Request) -> BoxFut<'_, Result, String>> { Box::pin(async move { self.0.lock().unwrap().push(json!([nsid, request.params, request.endpoints])); Ok(Some(json!({ "token": "jwt1", "via": "inner" }))) }) } fn subscribe(&self, nsid: String, _: Request) -> BoxFut<'_, Result> { Box::pin(async move { Err(nsid) }) } fn schemes(&self) -> &'static [&'static str] { &["iroh"] } fn fetch(&self, method: &str, url: &str, headers: Vec<(String, String)>, body: Option>) -> BoxFut<'_, Result> { self.0.lock().unwrap().push(json!([method, url, headers, String::from_utf8(body.unwrap_or_default()).unwrap()])); Box::pin(async { Ok(Response { status: 200, headers: vec![("content-type".into(), "application/json".into())], body: br#"{"via":"iroh"}"#.to_vec() }) }) } } #[tokio::test] async fn a_did_service_call_with_endpoints_goes_to_a_transport_scheme_with_service_auth_else_inner() { let (iroh, inner) = (Arc::new(Peer(Default::default())), Arc::new(Peer(Default::default()))); let call = TransportCall { transports: vec![Transport { provides: &[], call: iroh.clone() }], listed: true, inner: inner.clone() }; let (nsid, aud) = ("com.publicdomainrelay.temp.market.submitRfp", "did:plc:bidder#pdr_temp_market"); let request = |endpoints: Value| Request { service: Some(aud.into()), endpoints: Some(endpoints), input: Some(json!({ "a": 1 })), ..Default::default() }; let both = json!([{ "kind": "https", "address": "https://bidder.example" }, { "kind": "iroh", "address": "endpointabc234" }]); assert_eq!(call.call(nsid.into(), request(both)).await.unwrap(), Some(json!({ "via": "iroh" }))); assert_eq!( *iroh.0.lock().unwrap(), vec![json!(["POST", format!("iroh://endpointabc234/xrpc/{nsid}"), [["authorization", "Bearer jwt1"], ["content-type", "application/json"]], r#"{"a":1}"#])] ); let https = json!([{ "kind": "https", "address": "https://bidder.example" }]); assert_eq!(call.call(nsid.into(), request(https.clone())).await.unwrap(), Some(json!({ "token": "jwt1", "via": "inner" }))); assert_eq!( *inner.0.lock().unwrap(), vec![json!(["com.atproto.server.getServiceAuth", { "aud": aud, "lxm": nsid }, null]), json!([nsid, null, https])] ); } }