~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atlibs

0
0
main
196 lines · 10.3 KB Raw History
//! The transport seam: com.fedfork.exec.<name>.* 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<dyn Call>,
}

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<Vec<u8>>) -> BoxFut<'_, Result<Response, String>> {
        self.call.fetch(method, url, headers, body)
    }
}

/// Every scheme the transports fetch, "<scheme>:" -> that transport: run::Http::schemes takes it as is.
pub fn fetchers(transports: &[Transport]) -> HashMap<String, Arc<dyn Fetch>> {
    transports.iter().flat_map(|t| t.schemes().iter().map(move |s| (format!("{s}:"), Arc::new(t.clone()) as Arc<dyn Fetch>))).collect()
}

/// The transports a host was built with.
pub type Transports = Vec<Transport>;

/// 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<String> {
    transports.iter().flat_map(|t| t.provides.iter().map(|n| n.to_string())).collect()
}

/// The transport an NSID names, com.fedfork.exec.<name>.connect -> <name>: "built without <name>".
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()): "<nsid>: not available on this host (built without <name>)".
pub fn unavailable(nsid: &str) -> Option<String> {
    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<dyn Call>,
}

impl TransportCall {
    fn route(&self, nsid: &str) -> Result<&Arc<dyn Call>, 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<Result<Value, String>> {
        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<Option<Value>, 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<Sub, String>> {
        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<Option<Value>, String>> {
            Box::pin(async move { Ok(Some(json!([self.0, nsid]))) })
        }
        fn subscribe(&self, nsid: String, _: Request) -> BoxFut<'_, Result<Sub, String>> {
            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<Vec<Value>>);
    impl Call for Peer {
        fn call(&self, nsid: String, request: Request) -> BoxFut<'_, Result<Option<Value>, 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<Sub, String>> {
            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<Vec<u8>>) -> BoxFut<'_, Result<Response, String>> {
            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])]
        );
    }
}