Skip to main content

didbot_serve/
foreign.rs

1//! The deployment's one reader for records in repositories it does not
2//! hold.
3//!
4//! `didbot-lookup` is the cache and the fetch; this is where a running
5//! server's own facts reach it — which repositories it already holds, which
6//! names it answers to, and whether this stack may fetch from its own
7//! network. One reader per process, not one per session:
8//! `plan/ai-preference.md`'s "one resolver, one cache, deployment-wide" is
9//! the difference between being a good guest on somebody else's server and
10//! being the traffic that gets a deployment defederated.
11
12use std::sync::Arc;
13
14use didbot_http::PrivateAddresses;
15use didbot_lookup::{ForeignRecords, Limits, LocalRepositories};
16use didbot_pds::Registry;
17
18/// The registry, answering "is this ours?" for the reader.
19struct Hosted(Arc<dyn Registry>);
20
21impl LocalRepositories for Hosted {
22    fn holds(&self, did: &str) -> bool {
23        self.0.account(did).is_some()
24    }
25
26    fn serves(&self, host: &str) -> bool {
27        serves(self.0.as_ref(), host)
28    }
29}
30
31/// Whether `host` is a name this deployment answers to: an account's
32/// document or handle, or anything in or at the apex of one of its zones.
33pub(crate) fn serves(registry: &dyn Registry, host: &str) -> bool {
34    if registry.did_document(host).is_some() || registry.handle_did(host).is_some() {
35        return true;
36    }
37    // `available_user_domains` spells each zone with a leading dot, so
38    // the suffix test is the zone itself and the apex is the same
39    // string without it.
40    registry
41        .available_user_domains()
42        .iter()
43        .any(|domain| host.ends_with(domain.as_str()) || host == domain.trim_start_matches('.'))
44}
45
46/// The reader this deployment gives its evaluators.
47pub(crate) fn reader(
48    registry: Arc<dyn Registry>,
49    private: PrivateAddresses,
50) -> Arc<ForeignRecords> {
51    Arc::new(ForeignRecords::over_network(
52        Arc::new(Hosted(registry)),
53        private,
54        Limits::default(),
55    ))
56}
57
58/// What a DID document fetch from elsewhere found.
59pub(crate) enum Document {
60    /// The document, whole, its `id` the DID asked for.
61    Found(serde_json::Value),
62    /// The host answered `404`: no document.
63    NotFound,
64    /// The host answered `410`, as the PLC directory does for a DID that
65    /// was deactivated.
66    Gone,
67}
68
69/// Why a DID document fetch from elsewhere found nothing.
70pub(crate) enum DocumentError {
71    /// The URL's host resolves where `private` does not reach, or did not
72    /// resolve.
73    Address(didbot_http::AddressError),
74    /// The fetch, the read or the parse failed, or the document is about
75    /// another DID.
76    Failed(String),
77}
78
79/// Fetches `did`'s document from `url`, which `didbot_identity::document_url`
80/// gave for it, through a client pinned to the addresses it checked.
81///
82/// The caller refuses a `url` on a name this deployment answers to first:
83/// that fetch would be this server asking itself.
84pub(crate) async fn fetch_document(
85    url: &url::Url,
86    did: &str,
87    private: PrivateAddresses,
88) -> Result<Document, DocumentError> {
89    let client = didbot_http::pinned_client(url, private)
90        .await
91        .map_err(DocumentError::Address)?;
92    let response = client
93        .get(url.clone())
94        .send()
95        .await
96        .map_err(|error| DocumentError::Failed(format!("fetching {url}: {error}")))?;
97    match response.status() {
98        reqwest::StatusCode::NOT_FOUND => return Ok(Document::NotFound),
99        reqwest::StatusCode::GONE => return Ok(Document::Gone),
100        status if !status.is_success() => {
101            return Err(DocumentError::Failed(format!(
102                "fetching {url}: it answered {status}"
103            )))
104        }
105        _ => {}
106    }
107    let body = didbot_http::read_bounded(response, didbot_http::MAX_JSON_BODY)
108        .await
109        .map_err(|error| DocumentError::Failed(format!("reading {url}: {error}")))?;
110    let document: serde_json::Value = serde_json::from_slice(&body)
111        .map_err(|error| DocumentError::Failed(format!("parsing {url}: {error}")))?;
112    // The check `didbot_identity::resolve` makes: a document claiming
113    // another subject is somebody else's.
114    if document.get("id").and_then(serde_json::Value::as_str) != Some(did) {
115        return Err(DocumentError::Failed(format!(
116            "the document at {url} is about {:?}",
117            document.get("id")
118        )));
119    }
120    Ok(Document::Found(document))
121}