Skip to main content

datumesh_core/
mesh.rs

1//! Mesh documents (9.6) and cross-mesh resolution, `spec-0.4.1`. A verifier never needs them.
2
3use std::collections::BTreeMap;
4
5use crate::error::{err, member, ErrorCode, Result};
6use crate::json::{self, Value};
7use crate::keys::{key_doc, KeyDoc};
8use crate::lexical::limits;
9use crate::schema;
10
11/// The endpoints of 9.6: name, placeholders, whether required.
12const ENDPOINTS: [(&str, &[&str], bool); 11] = [
13    ("blob", &["sha256"], false),
14    ("cell", &["cell"], true),
15    ("cells", &[], false),
16    ("descriptor", &["hash"], true),
17    ("explain", &["cell"], false),
18    ("find", &[], false),
19    ("jobs", &["job"], true),
20    ("manifest", &["slice_id", "members_hash"], true),
21    ("receipt", &["hash"], true),
22    ("slice", &["cell"], false),
23    ("verify", &[], false),
24];
25
26/// What one HTTPS `GET` of a document gave, as the caller's HTTP client saw it.
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
28pub enum Retrieval<'a> {
29    /// A response: its status and body (`None` when there was none).
30    Response {
31        /// The integer HTTP status.
32        status: u16,
33        /// The body.
34        body: Option<&'a [u8]>,
35    },
36    /// No response: a network, DNS, TLS or timeout failure, a redirect, or a retrieval the
37    /// client abandoned.
38    Failed,
39}
40
41/// A checked mesh document (9.6): the host's bindings URL and its known endpoint templates.
42/// Unknown members are dropped.
43#[derive(Clone, Debug, PartialEq, Eq, Hash)]
44#[non_exhaustive]
45pub struct MeshDoc {
46    /// The host it was fetched from (and names).
47    pub host: String,
48    /// The URL of the bindings resource (9.7).
49    pub bindings: String,
50    /// The known endpoints present, by name (`cell`, `receipt`, `jobs`, ...): URL templates
51    /// with their placeholders, such as `https://<host>/cell/{cell}`.
52    pub endpoints: BTreeMap<String, String>,
53}
54
55impl MeshDoc {
56    /// Parses and checks a mesh document fetched from `host`, on its own (without the key
57    /// document): strict JSON, an object, `host` equal to `host`, `proto` `"datumesh/1"`, then
58    /// `bindings`, `endpoints` (an object holding every required endpoint) and each known
59    /// endpoint in ascending name order (9.6): on this host (beginning `https://<host>/`), with
60    /// its placeholders each once and no other brace. Unknown members are not checked. Every
61    /// failure is `E_MESH_DOC`. Cross-mesh resolution also needs the key document to agree:
62    /// see [`resolve_mesh`].
63    pub fn from_json(host: &str, body: &[u8]) -> Result<MeshDoc> {
64        let v = parse_mesh(body)?;
65        let m = v.as_object().unwrap_or(&[]);
66        if schema::get(m, "host").as_str() != Some(host) {
67            return err(ErrorCode::MeshDoc, "$.host", "the mesh document names another host");
68        }
69        if schema::get(m, "proto").as_str() != Some("datumesh/1") {
70            return err(ErrorCode::MeshDoc, "$.proto", "proto must be datumesh/1");
71        }
72        mesh_members(host, m)
73    }
74
75    /// The URL of endpoint `name` with its placeholders replaced: `values` must give each of
76    /// the template's placeholders exactly once and nothing else (by name, without braces).
77    /// Values are inserted as given: percent-encode them first where 5.1 says. `None` for an
78    /// endpoint the document does not have, or values that do not match its placeholders.
79    pub fn url(&self, name: &str, values: &[(&str, &str)]) -> Option<String> {
80        let template = self.endpoints.get(name)?;
81        let (_, placeholders, _) = ENDPOINTS.iter().find(|(n, _, _)| *n == name)?;
82        if values.len() != placeholders.len() || !placeholders.iter().all(|p| values.iter().any(|(k, _)| k == p)) {
83            return None;
84        }
85        let mut out = template.clone();
86        for (k, v) in values {
87            out = out.replacen(&format!("{{{k}}}"), v, 1);
88        }
89        Some(out)
90    }
91}
92
93/// A host accepted by cross-mesh resolution: its key document and its mesh document.
94#[derive(Clone, Debug, PartialEq, Eq, Hash)]
95#[non_exhaustive]
96pub struct ResolvedMesh {
97    /// The key document (9.2).
98    pub keys: KeyDoc,
99    /// The mesh document (9.6).
100    pub mesh: MeshDoc,
101}
102
103/// Cross-mesh resolution of `host` (9.6, `spec-0.4.1`), from what `GET
104/// https://<host>/.well-known/datumesh.json` and `GET
105/// https://<host>/.well-known/datumesh-mesh.json` gave. Separate from verification, which never
106/// needs it; it fetches nothing. In the order of 9.6, the first failure is reported:
107///
108/// 1. the key document retrieved as in 9.3 (`E_KEY_UNAVAILABLE`; a 200 without a body is
109///    `E_KEY_DOC`);
110/// 2. the mesh document retrieved: the integer status 200 with a body (`E_MESH_DOC`);
111/// 3. both parsed strictly: the key document (`E_KEY_DOC`), then the mesh document, an object
112///    (`E_MESH_DOC`);
113/// 4. the two describe the same mesh (`E_MESH_DOC`): the mesh document's `host` is `host` and its
114///    `proto` `"datumesh/1"`; the key document's `host` and `proto`, each where it is a string in
115///    an object, too (anything else about the key document is left to step 5);
116/// 5. the key document by 9.2 (`E_KEY_DOC`);
117/// 6. the mesh document's members (`E_MESH_DOC`, see [`MeshDoc::from_json`]).
118pub fn resolve_mesh(host: &str, key_doc_got: Retrieval<'_>, mesh_doc_got: Retrieval<'_>) -> Result<ResolvedMesh> {
119    // 1.
120    let key_body = match key_doc_got {
121        Retrieval::Response { status: 200, body: Some(b) } => b,
122        Retrieval::Response { status: 200, body: None } => {
123            return err(ErrorCode::KeyDoc, "$", "a 200 response without a key document");
124        }
125        _ => return err(ErrorCode::KeyUnavailable, "$", "the key document could not be retrieved"),
126    };
127    // 2.
128    let Retrieval::Response { status: 200, body: Some(mesh_body) } = mesh_doc_got else {
129        return err(ErrorCode::MeshDoc, "$", "the mesh document could not be retrieved");
130    };
131    // 3.
132    let kv = json::parse(key_body).map_err(|e| e.recode(ErrorCode::KeyDoc))?;
133    let mv = parse_mesh(mesh_body)?;
134    let mm = mv.as_object().unwrap_or(&[]);
135    // 4.
136    if schema::get(mm, "host").as_str() != Some(host) {
137        return err(ErrorCode::MeshDoc, "$.host", "the mesh document names another host than the one fetched");
138    }
139    if schema::get(mm, "proto").as_str() != Some("datumesh/1") {
140        return err(ErrorCode::MeshDoc, "$.proto", "the mesh document's proto is not datumesh/1");
141    }
142    if let Value::Object(km) = &kv {
143        if let Some(Value::String(h)) = schema::opt(km, "host") {
144            if h != host {
145                return err(ErrorCode::MeshDoc, "$.host", "the key document names another host");
146            }
147        }
148        // Compared only where it is a string (3f79313); otherwise step 5 gives E_KEY_DOC.
149        if let Some(Value::String(p)) = schema::opt(km, "proto") {
150            if p != "datumesh/1" {
151                return err(ErrorCode::MeshDoc, "$.proto", "the key document names another protocol");
152            }
153        }
154    }
155    // 5.
156    let keys = key_doc(host, &kv)?;
157    // 6.
158    let mesh = mesh_members(host, mm)?;
159    Ok(ResolvedMesh { keys, mesh })
160}
161
162fn parse_mesh(body: &[u8]) -> Result<Value> {
163    let v = json::parse(body).map_err(|e| e.recode(ErrorCode::MeshDoc))?;
164    if !matches!(v, Value::Object(_)) {
165        return err(ErrorCode::MeshDoc, "$", "a mesh document is a JSON object");
166    }
167    Ok(v)
168}
169
170/// A URL by the licence URL rule on `host`: `https://`, printable ASCII, at most 512
171/// characters, its authority exactly `host`, followed by a path.
172fn url_on_host(u: &str, host: &str) -> bool {
173    let Some(rest) = u.strip_prefix("https://") else { return false };
174    u.len() <= limits::LICENCE_URL
175        && rest.bytes().all(|c| (0x21..=0x7E).contains(&c))
176        && rest.strip_prefix(host).is_some_and(|path| path.starts_with('/'))
177}
178
179fn mesh_members(host: &str, m: &[(String, Value)]) -> Result<MeshDoc> {
180    let md = ErrorCode::MeshDoc;
181    let bindings = match schema::get(m, "bindings") {
182        Value::String(b) if url_on_host(b, host) => b.clone(),
183        _ => return err(md, "$.bindings", "bindings is an https URL on this host"),
184    };
185    let Value::Object(em) = schema::get(m, "endpoints") else {
186        return err(md, "$.endpoints", "endpoints is an object");
187    };
188    for (name, _, required) in ENDPOINTS {
189        if required && schema::opt(em, name).is_none() {
190            return err(md, "$.endpoints", format!("the required endpoint {name} is missing"));
191        }
192    }
193    let mut endpoints = BTreeMap::new();
194    for (name, placeholders, _) in ENDPOINTS {
195        let Some(v) = schema::opt(em, name) else { continue };
196        let p = member("$.endpoints", name);
197        let Value::String(t) = v else { return err(md, &p, "an endpoint is a URL template") };
198        if !url_on_host(t, host) {
199            return err(md, &p, "an endpoint is an https URL on this host");
200        }
201        let mut rest = t.clone();
202        for ph in placeholders {
203            let token = format!("{{{ph}}}");
204            if rest.matches(&token).count() != 1 {
205                return err(md, &p, format!("the template has {token} exactly once"));
206            }
207            rest = rest.replacen(&token, "", 1);
208        }
209        // Every brace belongs to a placeholder (3f79313).
210        if rest.contains('{') || rest.contains('}') {
211            return err(md, &p, "every brace of the template belongs to one of its placeholders");
212        }
213        endpoints.insert(name.to_string(), t.clone());
214    }
215    Ok(MeshDoc { host: host.into(), bindings, endpoints })
216}