From be7b1ae3f70530d00eab729cf685549f62b11fb7 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sat, 9 Nov 2024 09:52:32 +0100 Subject: [PATCH 01/43] `core`: Create `nodeinfo` table --- .../2024-11-09-084403_Add nodeinfo table/down.sql | 1 + .../2024-11-09-084403_Add nodeinfo table/up.sql | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 acrate-core/migrations/2024-11-09-084403_Add nodeinfo table/down.sql create mode 100644 acrate-core/migrations/2024-11-09-084403_Add nodeinfo table/up.sql diff --git a/acrate-core/migrations/2024-11-09-084403_Add nodeinfo table/down.sql b/acrate-core/migrations/2024-11-09-084403_Add nodeinfo table/down.sql new file mode 100644 index 0000000..2111f46 --- /dev/null +++ b/acrate-core/migrations/2024-11-09-084403_Add nodeinfo table/down.sql @@ -0,0 +1 @@ +DROP TABLE nodeinfo; \ No newline at end of file diff --git a/acrate-core/migrations/2024-11-09-084403_Add nodeinfo table/up.sql b/acrate-core/migrations/2024-11-09-084403_Add nodeinfo table/up.sql new file mode 100644 index 0000000..c21c18f --- /dev/null +++ b/acrate-core/migrations/2024-11-09-084403_Add nodeinfo table/up.sql @@ -0,0 +1,9 @@ +CREATE TABLE nodeinfo ( + nodeinfo_schema VARCHAR NOT NULL, + nodeinfo_href VARCHAR NOT NULL, + nodeinfo_data JSON NOT NULL, + + last_updated TIMESTAMP NOT NULL, + + PRIMARY KEY(nodeinfo_href) +); From 949a84922bff2b890462fbab2577979deca3a248 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sat, 9 Nov 2024 09:58:12 +0100 Subject: [PATCH 02/43] `nodeinfo`: Add `reqwest` dep --- acrate-nodeinfo/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/acrate-nodeinfo/Cargo.toml b/acrate-nodeinfo/Cargo.toml index 6832979..a9f89a2 100644 --- a/acrate-nodeinfo/Cargo.toml +++ b/acrate-nodeinfo/Cargo.toml @@ -4,3 +4,4 @@ version = "0.1.0" edition = "2021" [dependencies] +reqwest = "0.12.9" From bab93b3f5568b349fcb760ab2aafb49912684d77 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sat, 9 Nov 2024 09:59:27 +0100 Subject: [PATCH 03/43] `nodeinfo`: Add `serde` and `serde_json` deps --- acrate-nodeinfo/Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/acrate-nodeinfo/Cargo.toml b/acrate-nodeinfo/Cargo.toml index a9f89a2..8c3b461 100644 --- a/acrate-nodeinfo/Cargo.toml +++ b/acrate-nodeinfo/Cargo.toml @@ -5,3 +5,5 @@ edition = "2021" [dependencies] reqwest = "0.12.9" +serde = "1.0.214" +serde_json = "1.0.132" From e0019c877291cd292b8244fe93dc04a1c815d727 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sat, 9 Nov 2024 10:08:54 +0100 Subject: [PATCH 04/43] `nodeinfo`: Add `json` feature to `reqwest` --- acrate-nodeinfo/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acrate-nodeinfo/Cargo.toml b/acrate-nodeinfo/Cargo.toml index 8c3b461..805891d 100644 --- a/acrate-nodeinfo/Cargo.toml +++ b/acrate-nodeinfo/Cargo.toml @@ -4,6 +4,6 @@ version = "0.1.0" edition = "2021" [dependencies] -reqwest = "0.12.9" +reqwest = { version = "0.12.9", features = ["json"] } serde = "1.0.214" serde_json = "1.0.132" From 27a9b2fa0891d68e60d677756d0a5906342803dc Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sat, 9 Nov 2024 10:25:05 +0100 Subject: [PATCH 05/43] `nodeinfo`: Add `log` dep --- acrate-nodeinfo/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/acrate-nodeinfo/Cargo.toml b/acrate-nodeinfo/Cargo.toml index 805891d..6c24d7b 100644 --- a/acrate-nodeinfo/Cargo.toml +++ b/acrate-nodeinfo/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +log = "0.4.22" reqwest = { version = "0.12.9", features = ["json"] } serde = "1.0.214" serde_json = "1.0.132" From e795f1ad67ed9aee4ed537064dca22471e8af197 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sat, 9 Nov 2024 10:27:59 +0100 Subject: [PATCH 06/43] Add `.editorconfig` --- .editorconfig | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d9a86a3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = tab +insert_final_newline = true +tab_width = 4 + +[*.yml] +indent_size = 2 +indent_style = space + +[*.md] +indent_size = 3 +indent_style = space + +[*.rst] +indent_size = 3 +indent_style = space From 1a374d3d98f95751b6249c0ee60bef9cec50ebf4 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sat, 9 Nov 2024 11:01:06 +0100 Subject: [PATCH 07/43] `nodeinfo`: Add `derive` feature to `serde` --- acrate-nodeinfo/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acrate-nodeinfo/Cargo.toml b/acrate-nodeinfo/Cargo.toml index 6c24d7b..010a239 100644 --- a/acrate-nodeinfo/Cargo.toml +++ b/acrate-nodeinfo/Cargo.toml @@ -6,5 +6,5 @@ edition = "2021" [dependencies] log = "0.4.22" reqwest = { version = "0.12.9", features = ["json"] } -serde = "1.0.214" +serde = { version = "1.0.214", features = ["derive"] } serde_json = "1.0.132" From c5e3fcfc99f7a1810bc1fddcafa1af97500eea96 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sat, 9 Nov 2024 11:01:26 +0100 Subject: [PATCH 08/43] Create crate --- acrate-nodeinfo/src/lib.rs | 146 ++++++++++++++++++++++++++++++++++--- 1 file changed, 135 insertions(+), 11 deletions(-) diff --git a/acrate-nodeinfo/src/lib.rs b/acrate-nodeinfo/src/lib.rs index b93cf3f..c43fef7 100644 --- a/acrate-nodeinfo/src/lib.rs +++ b/acrate-nodeinfo/src/lib.rs @@ -1,14 +1,138 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right +use serde::Deserialize; + + +#[derive(Debug, Clone, Deserialize)] +pub struct Discovery { + pub links: Vec, } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } +#[derive(Debug, Clone, Deserialize)] +pub struct DiscoveryDocument { + pub rel: String, + pub href: String, +} + +impl Discovery { + pub async fn get(url: reqwest::Url) -> Result { + use DiscoveryGetError::*; + + log::debug!("Getting nodeinfo discovery at: {url}"); + + log::trace!("Sending GET request to: {url}"); + let response = reqwest::get(url) + .await + .map_err(Request)?; + + log::trace!("Checking headers of the response..."); + response + .headers() + .get("Content-Type") + .ok_or(ContentTypeMissing)? + .eq("application/json") + .then_some(()) + .ok_or(ContentTypeInvalid)?; + + log::trace!("Attempting to parse nodeinfo discovery as JSON..."); + let data = response.json::() + .await + .map_err(Parse)?; + + Ok(data) + } + + const WELLKNOWN_DISCOVERY_PATH: &str = "/.well-known/nodeinfo"; + + pub async fn discover(base: &reqwest::Url) -> Result { + use DiscoveryDiscoverError::*; + + log::debug!("Discovering nodeinfo at base: {base}"); + + let mut url = base.clone(); + + let path = Self::WELLKNOWN_DISCOVERY_PATH; + log::trace!("Setting URL path to `{path}`..."); + url.set_path(path); + + log::trace!("Unsetting URL query..."); + url.set_query(None); + + log::trace!("Unsetting URL fragment..."); + url.set_fragment(None); + + log::trace!("Setting URL scheme to HTTPS..."); + url.set_scheme("https") + .map_err(UrlManipulation)?; + + log::trace!("Attempting to retrieve nodeinfo via HTTPS..."); + let https = Self::get(url.clone()) + .await; + + let https = match https { + Ok(data) => { + log::trace!("HTTPS retrieval was successful, returning..."); + return Ok(data) + } + Err(err) => { + log::warn!("HTTPS retrieval failed."); + err + } + }; + + log::trace!("Setting URL scheme to HTTP..."); + url.set_scheme("http") + .map_err(UrlManipulation)?; + + log::trace!("Attempting to retrieve nodeinfo via HTTP..."); + let http = Self::get(url.clone()) + .await; + + let http = match http { + Ok(data) => { + log::trace!("HTTP retrieval was successful, returning..."); + return Ok(data) + } + Err(err) => { + log::warn!("HTTP retrieval failed."); + err + } + }; + + Err( + DiscoveryDiscoverError::Fetch( + DiscoveryDiscoverAttemptsErrors { + https, + http, + } + ) + ) + } +} + +pub enum DiscoveryGetError { + /// The HTTP request failed. + Request(reqwest::Error), + /// The `Content-Type` header of the response is missing. + ContentTypeMissing, + /// The `Content-Type` header of the response is invalid. + ContentTypeInvalid, + /// The JSON document failed to be parsed. + Parse(reqwest::Error), +} + +pub enum DiscoveryDiscoverError { + /// Manipulation of the URL scheme of the given base failed. + /// + /// See [reqwest::Url::set_scheme] for possible causes. + UrlManipulation(()), + + /// All attempts of fetching the discovery metadata failed. + Fetch(DiscoveryDiscoverAttemptsErrors), +} + +pub struct DiscoveryDiscoverAttemptsErrors { + /// The error occurred during the HTTPS request. + pub https: DiscoveryGetError, + + /// The error occurred during the HTTP request. + pub http: DiscoveryGetError, } From d57811cfb791c190673293d85b53192cda05473d Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sat, 9 Nov 2024 12:44:56 +0100 Subject: [PATCH 09/43] `nodeinfo`: Add `quick-xml` dep --- acrate-nodeinfo/Cargo.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/acrate-nodeinfo/Cargo.toml b/acrate-nodeinfo/Cargo.toml index 010a239..4936bb9 100644 --- a/acrate-nodeinfo/Cargo.toml +++ b/acrate-nodeinfo/Cargo.toml @@ -5,6 +5,8 @@ edition = "2021" [dependencies] log = "0.4.22" -reqwest = { version = "0.12.9", features = ["json"] } +quick-xml = "0.37.0" +reqwest = { version = "0.12.9", features = ["json", "stream"] } serde = { version = "1.0.214", features = ["derive"] } serde_json = "1.0.132" + From 370895e7be170b3d73b0d61848c88a12d6ea36f1 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sat, 9 Nov 2024 12:45:21 +0100 Subject: [PATCH 10/43] `nodeinfo`: Add `serialize` feature to `quick-xml` --- acrate-nodeinfo/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acrate-nodeinfo/Cargo.toml b/acrate-nodeinfo/Cargo.toml index 4936bb9..015de24 100644 --- a/acrate-nodeinfo/Cargo.toml +++ b/acrate-nodeinfo/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] log = "0.4.22" -quick-xml = "0.37.0" +quick-xml = { version = "0.37.0", features = ["serialize"] } reqwest = { version = "0.12.9", features = ["json", "stream"] } serde = { version = "1.0.214", features = ["derive"] } serde_json = "1.0.132" From a6672315fff1ae5dee29c3ee4a1d2bba0591bb71 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sat, 9 Nov 2024 12:58:43 +0100 Subject: [PATCH 11/43] `nodeinfo`: Add `overlapped-lists` feature to `quick-xml` --- acrate-nodeinfo/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acrate-nodeinfo/Cargo.toml b/acrate-nodeinfo/Cargo.toml index 015de24..9fa954b 100644 --- a/acrate-nodeinfo/Cargo.toml +++ b/acrate-nodeinfo/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] log = "0.4.22" -quick-xml = { version = "0.37.0", features = ["serialize"] } +quick-xml = { version = "0.37.0", features = ["overlapped-lists", "serialize"] } reqwest = { version = "0.12.9", features = ["json", "stream"] } serde = { version = "1.0.214", features = ["derive"] } serde_json = "1.0.132" From 585eb1476e59f0815d392dd54800da7ec157f3a1 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sat, 9 Nov 2024 13:14:11 +0100 Subject: [PATCH 12/43] `nodeinfo`: this is actually host-meta --- acrate-nodeinfo/src/lib.rs | 282 +++++++++++++++++++++++++++++-------- 1 file changed, 222 insertions(+), 60 deletions(-) diff --git a/acrate-nodeinfo/src/lib.rs b/acrate-nodeinfo/src/lib.rs index c43fef7..6a5afc4 100644 --- a/acrate-nodeinfo/src/lib.rs +++ b/acrate-nodeinfo/src/lib.rs @@ -1,25 +1,62 @@ -use serde::Deserialize; +//! Serde-based [RFC 6415] `host-meta` parser. +//! +//! [RFC 6415]: https://datatracker.ietf.org/doc/html/rfc6415 +use serde::{Serialize, Deserialize}; -#[derive(Debug, Clone, Deserialize)] -pub struct Discovery { - pub links: Vec, +/// A [host-meta document]. +/// +/// [host-meta document]: https://datatracker.ietf.org/doc/html/rfc6415 +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct HostMetaDocument { + /// The resource this document refers to. + pub subject: Option, + + /// Links established between the [`Self::subject`] and other resources. + pub links: Vec, } -#[derive(Debug, Clone, Deserialize)] -pub struct DiscoveryDocument { +/// A [host-meta Link Element], which puts the subject resource in relation with another. +/// +/// [host-meta Link Element]: https://datatracker.ietf.org/doc/html/rfc6415#section-3.1.1 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HostMetaLink { + /// The kind of relation established by the subject with the attached resource. pub rel: String, + + /// The resource put in relation with the subject resource. pub href: String, } -impl Discovery { - pub async fn get(url: reqwest::Url) -> Result { - use DiscoveryGetError::*; +impl HostMetaDocument { + /// Get an [host-meta document] in the [JRD format]. + /// + /// This follows redirects until the redirect chain is 10 hops; see [`reqwest::redirect`] for more info. + /// + /// [JRD format]: https://datatracker.ietf.org/doc/html/rfc6415#appendix-A + /// [host-meta document]: https://datatracker.ietf.org/doc/html/rfc6415 + pub async fn get_jrd(client: &reqwest::Client, url: reqwest::Url) -> Result { + use HostMetaGetJRDError::*; - log::debug!("Getting nodeinfo discovery at: {url}"); + log::debug!("Getting host-meta JRD document at: {url}"); - log::trace!("Sending GET request to: {url}"); - let response = reqwest::get(url) + log::trace!("Building request..."); + let request = { + log::trace!("Creating new request..."); + let mut request = reqwest::Request::new(reqwest::Method::GET, url); + + log::trace!("Setting request headers..."); + let headers = request.headers_mut(); + + log::trace!("Setting `Accept: application/json`..."); + let _ = headers.insert(reqwest::header::ACCEPT, "application/json".parse().unwrap()); + + request + }; + + log::trace!("Sending request..."); + let response = client.execute(request) .await .map_err(Request)?; @@ -32,7 +69,7 @@ impl Discovery { .then_some(()) .ok_or(ContentTypeInvalid)?; - log::trace!("Attempting to parse nodeinfo discovery as JSON..."); + log::trace!("Attempting to parse response as JSON..."); let data = response.json::() .await .map_err(Parse)?; @@ -40,18 +77,75 @@ impl Discovery { Ok(data) } - const WELLKNOWN_DISCOVERY_PATH: &str = "/.well-known/nodeinfo"; + /// Get an [host-meta document] in the [XRD format]. + /// + /// This follows redirects until the redirect chain is 10 hops; see [`reqwest::redirect`] for more info. + /// + /// [XRD format]: https://datatracker.ietf.org/doc/html/rfc6415#section-3.1 + /// [host-meta document]: https://datatracker.ietf.org/doc/html/rfc6415 + pub async fn get_xrd(client: &reqwest::Client, url: reqwest::Url) -> Result { + use HostMetaGetXRDError::*; - pub async fn discover(base: &reqwest::Url) -> Result { - use DiscoveryDiscoverError::*; + log::debug!("Getting host-meta XRD document at: {url}"); - log::debug!("Discovering nodeinfo at base: {base}"); + log::trace!("Building request..."); + let request = { + log::trace!("Creating new request..."); + let mut request = reqwest::Request::new(reqwest::Method::GET, url); - let mut url = base.clone(); + log::trace!("Setting request headers..."); + let headers = request.headers_mut(); - let path = Self::WELLKNOWN_DISCOVERY_PATH; - log::trace!("Setting URL path to `{path}`..."); - url.set_path(path); + log::trace!("Setting `Accept: application/xrd+xml`..."); + let _ = headers.insert(reqwest::header::ACCEPT, "application/xrd+xml".parse().unwrap()); + + request + }; + + log::trace!("Sending request..."); + let response = client.execute(request) + .await + .map_err(Request)?; + + log::trace!("Checking headers of the response..."); + response + .headers() + .get("Content-Type") + .ok_or(ContentTypeMissing)? + .eq("application/xrd+json") + .then_some(()) + .ok_or(ContentTypeInvalid)?; + + log::trace!("Attempting to parse response as text..."); + let data = response.text() + .await + .map_err(Decode)?; + + log::trace!("Parsing response as XML..."); + let data = quick_xml::de::from_str::(&data) + .map_err(Parse)?; + + Ok(data) + } + + /// Attempt to discover the [host-meta document] at the given URL in various ways. + /// + /// In order, this method attempts: + /// 1. [HTTPS] [XRD](Self::get_xrd) + /// 2. [HTTPS] [JRD](Self::get_jrd) + /// 3. [HTTP] [XRD](Self::get_xrd) + /// 4. [HTTP] [JRD](Self::get_jrd) + /// + /// This follows redirects until the redirect chain is 10 hops; see [`reqwest::redirect`] for more info. + /// + /// [host-meta document]: https://datatracker.ietf.org/doc/html/rfc6415 + /// [HTTPS]: https://datatracker.ietf.org/doc/html/rfc2818 + /// [HTTP]: https://datatracker.ietf.org/doc/html/rfc2616 + /// [JRD]: https://datatracker.ietf.org/doc/html/rfc6415#appendix-A + pub async fn discover(client: &reqwest::Client, mut url: reqwest::Url) -> Result { + use HostMetaDiscoverError::*; + + log::debug!("Discovering host-meta document at base: {url}"); log::trace!("Unsetting URL query..."); url.set_query(None); @@ -63,17 +157,26 @@ impl Discovery { url.set_scheme("https") .map_err(UrlManipulation)?; - log::trace!("Attempting to retrieve nodeinfo via HTTPS..."); - let https = Self::get(url.clone()) - .await; - - let https = match https { + log::trace!("Attempting to retrieve XRD host-meta via HTTPS..."); + let https_xrd = match Self::get_xrd(client, url.clone()).await { Ok(data) => { - log::trace!("HTTPS retrieval was successful, returning..."); + log::trace!("HTTPS XRD retrieval was successful, returning..."); return Ok(data) } Err(err) => { - log::warn!("HTTPS retrieval failed."); + log::trace!("HTTPS XRD retrieval failed."); + err + } + }; + + log::trace!("Attempting to retrieve JRD host-meta via HTTPS..."); + let https_jrd = match Self::get_jrd(client, url.clone()).await { + Ok(data) => { + log::trace!("HTTPS JRD retrieval was successful, returning..."); + return Ok(data) + } + Err(err) => { + log::trace!("HTTPS JRD retrieval failed."); err } }; @@ -82,57 +185,116 @@ impl Discovery { url.set_scheme("http") .map_err(UrlManipulation)?; - log::trace!("Attempting to retrieve nodeinfo via HTTP..."); - let http = Self::get(url.clone()) - .await; - - let http = match http { + log::trace!("Attempting to retrieve XRD host-meta via HTTP..."); + let http_xrd = match Self::get_xrd(client, url.clone()).await { Ok(data) => { - log::trace!("HTTP retrieval was successful, returning..."); + log::trace!("HTTP XRD retrieval was successful, returning..."); return Ok(data) } Err(err) => { - log::warn!("HTTP retrieval failed."); + log::trace!("HTTP XRD retrieval failed."); + err + } + }; + + log::trace!("Attempting to retrieve JRD host-meta via HTTP..."); + let http_jrd = match Self::get_jrd(client, url.clone()).await { + Ok(data) => { + log::trace!("HTTP JRD retrieval was successful, returning..."); + return Ok(data) + } + Err(err) => { + log::trace!("HTTP JRD retrieval failed."); err } }; Err( - DiscoveryDiscoverError::Fetch( - DiscoveryDiscoverAttemptsErrors { - https, - http, + HostMetaDiscoverError::Fetch( + HostMetaDiscoveryAttemptsErrors { + https_xrd, + https_jrd, + http_xrd, + http_jrd, } ) ) } + + const WELLKNOWN_NODEINFO_PATH: &str = "/.well-known/nodeinfo"; + + pub async fn discover_nodeinfo(client: &reqwest::Client, mut base: reqwest::Url) -> Result { + base.set_path(Self::WELLKNOWN_NODEINFO_PATH); + + Self::discover(client, base) + .await + } + + const WELLKNOWN_HOSTMETA_PATH: &str = "/.well-known/host-meta"; + + pub async fn discover_hostmeta(client: &reqwest::Client, mut base: reqwest::Url) -> Result { + base.set_path(Self::WELLKNOWN_HOSTMETA_PATH); + + Self::discover(client, base) + .await + } } -pub enum DiscoveryGetError { +pub enum HostMetaDiscoverError { + /// Manipulation of the URL scheme of the given base failed. + /// + /// See [reqwest::Url::set_scheme] for possible causes. + UrlManipulation(()), + + /// All attempts of fetching the host-meta document failed. + Fetch(HostMetaDiscoveryAttemptsErrors), +} +pub struct HostMetaDiscoveryAttemptsErrors { + /// The error that occoured when trying to fetch the host-meta document in [XRD format] via [HTTPS]. + /// + /// [HTTPS]: https://datatracker.ietf.org/doc/html/rfc2818 + /// [XRD format]: https://datatracker.ietf.org/doc/html/rfc6415#section-3.1 + pub https_xrd: HostMetaGetXRDError, + + /// The error that occoured when trying to fetch the host-meta document in [JRD format] via [HTTPS]. + /// + /// [HTTPS]: https://datatracker.ietf.org/doc/html/rfc2818 + /// [JRD format]: https://datatracker.ietf.org/doc/html/rfc6415#appendix-A + pub https_jrd: HostMetaGetJRDError, + + /// The error that occoured when trying to fetch the host-meta document in [XRD format] via [HTTP]. + /// + /// [HTTP]: https://datatracker.ietf.org/doc/html/rfc2616 + /// [XRD format]: https://datatracker.ietf.org/doc/html/rfc6415#section-3.1 + pub http_xrd: HostMetaGetXRDError, + + /// The error that occoured when trying to fetch the host-meta document in [JRD format] via [HTTP]. + /// + /// [HTTP]: https://datatracker.ietf.org/doc/html/rfc2616 + /// [JRD format]: https://datatracker.ietf.org/doc/html/rfc6415#appendix-A + pub http_jrd: HostMetaGetJRDError, +} + +pub enum HostMetaGetXRDError { /// The HTTP request failed. Request(reqwest::Error), /// The `Content-Type` header of the response is missing. ContentTypeMissing, /// The `Content-Type` header of the response is invalid. ContentTypeInvalid, - /// The JSON document failed to be parsed. + /// The document failed to be read as text. + Decode(reqwest::Error), + /// The document failed to be parsed as XML by [`quick_xml`]. + Parse(quick_xml::DeError), +} + +pub enum HostMetaGetJRDError { + /// The HTTP request failed. + Request(reqwest::Error), + /// The `Content-Type` header of the response is missing. + ContentTypeMissing, + /// The `Content-Type` header of the response is invalid. + ContentTypeInvalid, + /// The document failed to be parsed as JSON by [`reqwest`]. Parse(reqwest::Error), -} - -pub enum DiscoveryDiscoverError { - /// Manipulation of the URL scheme of the given base failed. - /// - /// See [reqwest::Url::set_scheme] for possible causes. - UrlManipulation(()), - - /// All attempts of fetching the discovery metadata failed. - Fetch(DiscoveryDiscoverAttemptsErrors), -} - -pub struct DiscoveryDiscoverAttemptsErrors { - /// The error occurred during the HTTPS request. - pub https: DiscoveryGetError, - - /// The error occurred during the HTTP request. - pub http: DiscoveryGetError, -} +} \ No newline at end of file From 2f8e231b0457445105cba6160dbe52adcbdc6aca Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sat, 9 Nov 2024 13:23:31 +0100 Subject: [PATCH 13/43] `nodeinfo`: Add `tokio` and `pretty_env_logger` deps --- acrate-nodeinfo/Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/acrate-nodeinfo/Cargo.toml b/acrate-nodeinfo/Cargo.toml index 9fa954b..31a20c8 100644 --- a/acrate-nodeinfo/Cargo.toml +++ b/acrate-nodeinfo/Cargo.toml @@ -10,3 +10,7 @@ reqwest = { version = "0.12.9", features = ["json", "stream"] } serde = { version = "1.0.214", features = ["derive"] } serde_json = "1.0.132" +[dev-dependencies] +pretty_env_logger = "0.5.0" +tokio = "1.41.1" + From f9da27784546434192e5c0bdeabb62c0a91b760b Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sat, 9 Nov 2024 13:25:47 +0100 Subject: [PATCH 14/43] `nodeinfo`: Add `rt-multi-thread` and `macros` features to `tokio` --- acrate-nodeinfo/Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/acrate-nodeinfo/Cargo.toml b/acrate-nodeinfo/Cargo.toml index 31a20c8..986b27e 100644 --- a/acrate-nodeinfo/Cargo.toml +++ b/acrate-nodeinfo/Cargo.toml @@ -12,5 +12,4 @@ serde_json = "1.0.132" [dev-dependencies] pretty_env_logger = "0.5.0" -tokio = "1.41.1" - +tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread"] } From 00befc432dccdbebf7fde981cc6be9348fc4064a Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sat, 9 Nov 2024 13:27:26 +0100 Subject: [PATCH 15/43] `nodeinfo`: Derive `Debug` on all `struct`s --- acrate-nodeinfo/src/lib.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/acrate-nodeinfo/src/lib.rs b/acrate-nodeinfo/src/lib.rs index 6a5afc4..625707e 100644 --- a/acrate-nodeinfo/src/lib.rs +++ b/acrate-nodeinfo/src/lib.rs @@ -240,6 +240,7 @@ impl HostMetaDocument { } } +#[derive(Debug)] pub enum HostMetaDiscoverError { /// Manipulation of the URL scheme of the given base failed. /// @@ -249,6 +250,8 @@ pub enum HostMetaDiscoverError { /// All attempts of fetching the host-meta document failed. Fetch(HostMetaDiscoveryAttemptsErrors), } + +#[derive(Debug)] pub struct HostMetaDiscoveryAttemptsErrors { /// The error that occoured when trying to fetch the host-meta document in [XRD format] via [HTTPS]. /// @@ -275,6 +278,7 @@ pub struct HostMetaDiscoveryAttemptsErrors { pub http_jrd: HostMetaGetJRDError, } +#[derive(Debug)] pub enum HostMetaGetXRDError { /// The HTTP request failed. Request(reqwest::Error), @@ -288,6 +292,7 @@ pub enum HostMetaGetXRDError { Parse(quick_xml::DeError), } +#[derive(Debug)] pub enum HostMetaGetJRDError { /// The HTTP request failed. Request(reqwest::Error), From 707d6d3d543d9e51a7b03a52f5193681b366d062 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sat, 9 Nov 2024 13:33:08 +0100 Subject: [PATCH 16/43] `nodeinfo`: Add two basic (failing) tests --- acrate-nodeinfo/tests/discover_test.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 acrate-nodeinfo/tests/discover_test.rs diff --git a/acrate-nodeinfo/tests/discover_test.rs b/acrate-nodeinfo/tests/discover_test.rs new file mode 100644 index 0000000..716e014 --- /dev/null +++ b/acrate-nodeinfo/tests/discover_test.rs @@ -0,0 +1,26 @@ +#![allow(non_snake_case)] + +use acrate_nodeinfo::*; + + +#[tokio::test] +async fn discover_hostmeta__junimo_party() { + let client = reqwest::Client::new(); + let base: reqwest::Url = "https://junimo.party".parse() + .expect("a valid URL"); + + HostMetaDocument::discover_hostmeta(&client, base) + .await + .expect("host-meta discovery to succeed"); +} + +#[tokio::test] +async fn discover_nodeinfo__junimo_party() { + let client = reqwest::Client::new(); + let base: reqwest::Url = "https://junimo.party".parse() + .expect("a valid URL"); + + HostMetaDocument::discover_nodeinfo(&client, base) + .await + .expect("nodeinfo discovery to succeed"); +} From de5fdf26f359af35e29d2567786b6582c2276414 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sat, 9 Nov 2024 14:17:51 +0100 Subject: [PATCH 17/43] `nodeinfo`: Use header names from `reqwest::header` --- acrate-nodeinfo/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acrate-nodeinfo/src/lib.rs b/acrate-nodeinfo/src/lib.rs index 625707e..48579dd 100644 --- a/acrate-nodeinfo/src/lib.rs +++ b/acrate-nodeinfo/src/lib.rs @@ -63,7 +63,7 @@ impl HostMetaDocument { log::trace!("Checking headers of the response..."); response .headers() - .get("Content-Type") + .get(reqwest::header::CONTENT_TYPE) .ok_or(ContentTypeMissing)? .eq("application/json") .then_some(()) @@ -110,7 +110,7 @@ impl HostMetaDocument { log::trace!("Checking headers of the response..."); response .headers() - .get("Content-Type") + .get(reqwest::header::CONTENT_TYPE) .ok_or(ContentTypeMissing)? .eq("application/xrd+json") .then_some(()) From d964f1befc86c5271f1f941f6ca75075743ba9c6 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sat, 9 Nov 2024 14:57:55 +0100 Subject: [PATCH 18/43] `nodeinfo`: Split off `Content-Type` parameters before checking the MIME type --- acrate-nodeinfo/src/lib.rs | 44 ++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/acrate-nodeinfo/src/lib.rs b/acrate-nodeinfo/src/lib.rs index 48579dd..728bd38 100644 --- a/acrate-nodeinfo/src/lib.rs +++ b/acrate-nodeinfo/src/lib.rs @@ -2,6 +2,7 @@ //! //! [RFC 6415]: https://datatracker.ietf.org/doc/html/rfc6415 +use reqwest::header::HeaderValue; use serde::{Serialize, Deserialize}; /// A [host-meta document]. @@ -60,15 +61,21 @@ impl HostMetaDocument { .await .map_err(Request)?; - log::trace!("Checking headers of the response..."); - response + log::trace!("Checking `Content-Type` of the response..."); + let content_type = response .headers() .get(reqwest::header::CONTENT_TYPE) - .ok_or(ContentTypeMissing)? - .eq("application/json") - .then_some(()) + .ok_or(ContentTypeMissing)?; + + log::trace!("Extracting MIME type from the `Content-Type` header..."); + let mime_type = extract_mime_from_content_type(content_type) .ok_or(ContentTypeInvalid)?; + log::trace!("Ensuring MIME type of `{mime_type}` is acceptable for JRD parsing..."); + if mime_type != "application/json" { + return Err(ContentTypeInvalid) + } + log::trace!("Attempting to parse response as JSON..."); let data = response.json::() .await @@ -107,15 +114,21 @@ impl HostMetaDocument { .await .map_err(Request)?; - log::trace!("Checking headers of the response..."); - response + log::trace!("Checking `Content-Type` of the response..."); + let content_type = response .headers() .get(reqwest::header::CONTENT_TYPE) - .ok_or(ContentTypeMissing)? - .eq("application/xrd+json") - .then_some(()) + .ok_or(ContentTypeMissing)?; + + log::trace!("Extracting MIME type from the `Content-Type` header..."); + let mime_type = extract_mime_from_content_type(content_type) .ok_or(ContentTypeInvalid)?; + log::trace!("Ensuring MIME type of `{mime_type}` is acceptable for JRD parsing..."); + if mime_type != "application/xrd+xml" { + return Err(ContentTypeInvalid) + } + log::trace!("Attempting to parse response as text..."); let data = response.text() .await @@ -302,4 +315,13 @@ pub enum HostMetaGetJRDError { ContentTypeInvalid, /// The document failed to be parsed as JSON by [`reqwest`]. Parse(reqwest::Error), -} \ No newline at end of file +} + +/// Extract the MIME type from the value of the `Content-Type` header. +fn extract_mime_from_content_type(value: &HeaderValue) -> Option { + let value = value.to_str().ok()?; + match value.split_once("; ") { + None => Some(value.to_string()), + Some((mime, _)) => Some(mime.to_string()), + } +} From e5f52a760abc27489f4c2741de888e880c0413d7 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sat, 9 Nov 2024 15:02:44 +0100 Subject: [PATCH 19/43] `nodeinfo`: Don't `Serialize`, just `Deserialize` for now --- acrate-nodeinfo/src/lib.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/acrate-nodeinfo/src/lib.rs b/acrate-nodeinfo/src/lib.rs index 728bd38..6dbf77d 100644 --- a/acrate-nodeinfo/src/lib.rs +++ b/acrate-nodeinfo/src/lib.rs @@ -3,13 +3,12 @@ //! [RFC 6415]: https://datatracker.ietf.org/doc/html/rfc6415 use reqwest::header::HeaderValue; -use serde::{Serialize, Deserialize}; +use serde::Deserialize; /// A [host-meta document]. /// /// [host-meta document]: https://datatracker.ietf.org/doc/html/rfc6415 -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "PascalCase")] +#[derive(Debug, Clone, Deserialize)] pub struct HostMetaDocument { /// The resource this document refers to. pub subject: Option, @@ -21,7 +20,7 @@ pub struct HostMetaDocument { /// A [host-meta Link Element], which puts the subject resource in relation with another. /// /// [host-meta Link Element]: https://datatracker.ietf.org/doc/html/rfc6415#section-3.1.1 -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct HostMetaLink { /// The kind of relation established by the subject with the attached resource. pub rel: String, From ee2f386cf064174db9040fc00a36802559fbd6e7 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sat, 9 Nov 2024 15:03:54 +0100 Subject: [PATCH 20/43] `nodeinfo`: Alias PascalCase tags to snake_case ones --- acrate-nodeinfo/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/acrate-nodeinfo/src/lib.rs b/acrate-nodeinfo/src/lib.rs index 6dbf77d..c7167b1 100644 --- a/acrate-nodeinfo/src/lib.rs +++ b/acrate-nodeinfo/src/lib.rs @@ -11,9 +11,11 @@ use serde::Deserialize; #[derive(Debug, Clone, Deserialize)] pub struct HostMetaDocument { /// The resource this document refers to. + #[serde(alias = "Subject")] pub subject: Option, /// Links established between the [`Self::subject`] and other resources. + #[serde(alias = "Links")] pub links: Vec, } From 6ca0b5368a53e9edd6d9a99fa53b15ef96bff327 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sat, 9 Nov 2024 16:18:46 +0100 Subject: [PATCH 21/43] `nodeinfo`: Add more tests --- acrate-nodeinfo/tests/discover_test.rs | 85 +++++++++++++++++++------- 1 file changed, 63 insertions(+), 22 deletions(-) diff --git a/acrate-nodeinfo/tests/discover_test.rs b/acrate-nodeinfo/tests/discover_test.rs index 716e014..08df112 100644 --- a/acrate-nodeinfo/tests/discover_test.rs +++ b/acrate-nodeinfo/tests/discover_test.rs @@ -1,26 +1,67 @@ -#![allow(non_snake_case)] - -use acrate_nodeinfo::*; - - -#[tokio::test] -async fn discover_hostmeta__junimo_party() { - let client = reqwest::Client::new(); - let base: reqwest::Url = "https://junimo.party".parse() - .expect("a valid URL"); - - HostMetaDocument::discover_hostmeta(&client, base) - .await - .expect("host-meta discovery to succeed"); +fn make_client() -> reqwest::Client { + let crate_name = env!("CARGO_PKG_NAME"); + let crate_version = env!("CARGO_PKG_VERSION"); + let crate_repository = env!("CARGO_PKG_REPOSITORY"); + let user_agent = format!("{crate_name}/{crate_version} ({crate_repository})"); + + reqwest::Client::builder() + .user_agent(user_agent) + .build() + .expect("reqwest client to build") } -#[tokio::test] -async fn discover_nodeinfo__junimo_party() { - let client = reqwest::Client::new(); - let base: reqwest::Url = "https://junimo.party".parse() - .expect("a valid URL"); +macro_rules! test { + ($id:ident, $url:literal) => { + test!($id, $url,); + }; + ($id:ident, $url:literal, $($tag:meta),*) => { + mod $id { + use acrate_nodeinfo::*; + use super::*; - HostMetaDocument::discover_nodeinfo(&client, base) - .await - .expect("nodeinfo discovery to succeed"); + #[tokio::test] + $(#[$tag])* + async fn test_hostmeta() { + let client = make_client(); + + let base: reqwest::Url = $url.parse() + .expect("a valid URL"); + + let doc = HostMetaDocument::discover_hostmeta(&client, base) + .await + .expect("host-meta discovery to succeed"); + + println!("{doc:#?}"); + } + + #[tokio::test] + $(#[$tag])* + async fn test_nodeinfo() { + let client = make_client(); + + let base: reqwest::Url = $url.parse() + .expect("a valid URL"); + + let doc = HostMetaDocument::discover_nodeinfo(&client, base) + .await + .expect("host-meta discovery to succeed"); + + println!("{doc:#?}"); + } + } + }; } + +test!(akkoma, "https://junimo.party"); + +test!(mastodon, "https://mastodon.social"); + +test!(misskey, "https://misskey.io"); + +test!(iceshrimpnet, "https://ice.frieren.quest"); + +test!(gotosocial, "https://alpha.polymaths.social"); + +test!(bridgyfed, "https://fed.brid.gy", ignore = "Returns application/jrd+json"); + +test!(threads, "https://threads.net", ignore = "Not implemented on their end"); From 2aeed9773fe2737528366447c5db30883ed103b0 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sat, 9 Nov 2024 16:20:36 +0100 Subject: [PATCH 22/43] `nodeinfo`: Improve XRD compatibility --- acrate-nodeinfo/src/lib.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/acrate-nodeinfo/src/lib.rs b/acrate-nodeinfo/src/lib.rs index c7167b1..a6a40b4 100644 --- a/acrate-nodeinfo/src/lib.rs +++ b/acrate-nodeinfo/src/lib.rs @@ -5,7 +5,7 @@ use reqwest::header::HeaderValue; use serde::Deserialize; -/// A [host-meta document]. +/// A [host-meta document] in JRD representation. /// /// [host-meta document]: https://datatracker.ietf.org/doc/html/rfc6415 #[derive(Debug, Clone, Deserialize)] @@ -15,7 +15,7 @@ pub struct HostMetaDocument { pub subject: Option, /// Links established between the [`Self::subject`] and other resources. - #[serde(alias = "Links")] + #[serde(alias = "Link")] pub links: Vec, } @@ -25,10 +25,20 @@ pub struct HostMetaDocument { #[derive(Debug, Clone, Deserialize)] pub struct HostMetaLink { /// The kind of relation established by the subject with the attached resource. + #[serde(alias = "@rel")] pub rel: String, /// The resource put in relation with the subject resource. - pub href: String, + #[serde(alias = "@href")] + pub href: Option, + + /// Template to fill to get the resource put in relation with the subject resource. + #[serde(alias = "@template")] + pub template: Option, + + /// The `Content-Type` of the resource put in relation. + #[serde(alias = "@type")] + pub r#type: Option, } impl HostMetaDocument { From 0001c43f793c43050d38eba77fef252a0be288ea Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 11 Nov 2024 02:12:36 +0100 Subject: [PATCH 23/43] `nodeinfo`: Add ignored test for snac --- acrate-nodeinfo/tests/discover_test.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/acrate-nodeinfo/tests/discover_test.rs b/acrate-nodeinfo/tests/discover_test.rs index 08df112..780b1f1 100644 --- a/acrate-nodeinfo/tests/discover_test.rs +++ b/acrate-nodeinfo/tests/discover_test.rs @@ -65,3 +65,5 @@ test!(gotosocial, "https://alpha.polymaths.social"); test!(bridgyfed, "https://fed.brid.gy", ignore = "Returns application/jrd+json"); test!(threads, "https://threads.net", ignore = "Not implemented on their end"); + +test!(snac, "https://ngoa.giao.loan", ignore = "Does not support host-meta"); From a74ee45f6e4547aa5c312196c7db18716e128953 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 11 Nov 2024 02:13:58 +0100 Subject: [PATCH 24/43] `nodeinfo`: Add ignored test for hollo --- acrate-nodeinfo/tests/discover_test.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/acrate-nodeinfo/tests/discover_test.rs b/acrate-nodeinfo/tests/discover_test.rs index 780b1f1..a8775a5 100644 --- a/acrate-nodeinfo/tests/discover_test.rs +++ b/acrate-nodeinfo/tests/discover_test.rs @@ -67,3 +67,5 @@ test!(bridgyfed, "https://fed.brid.gy", ignore = "Returns application/jrd+json") test!(threads, "https://threads.net", ignore = "Not implemented on their end"); test!(snac, "https://ngoa.giao.loan", ignore = "Does not support host-meta"); + +test!(hollo, "https://hollo.social", ignore = "Returns application/jrd+json, does not support host-meta"); From 76783eabe4f4cd6c2720d8d987d6d18e5bdb01f0 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 11 Nov 2024 02:18:57 +0100 Subject: [PATCH 25/43] `nodeinfo`: Allow MIME type of `application/jrd+json` --- acrate-nodeinfo/src/lib.rs | 8 ++++++-- acrate-nodeinfo/tests/discover_test.rs | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/acrate-nodeinfo/src/lib.rs b/acrate-nodeinfo/src/lib.rs index a6a40b4..51e7b88 100644 --- a/acrate-nodeinfo/src/lib.rs +++ b/acrate-nodeinfo/src/lib.rs @@ -82,8 +82,12 @@ impl HostMetaDocument { let mime_type = extract_mime_from_content_type(content_type) .ok_or(ContentTypeInvalid)?; - log::trace!("Ensuring MIME type of `{mime_type}` is acceptable for JRD parsing..."); - if mime_type != "application/json" { + log::trace!("Ensuring MIME type is acceptable for JRD parsing..."); + if mime_type == "application/jrd+json" { + log::warn!("MIME type `{mime_type}` would not be acceptable for JRD parsing, but is temporarily allowed anyways due to widespread use.") + } + else if mime_type != "application/json" { + log::error!("MIME type `{mime_type}` is not acceptable for JRD parsing."); return Err(ContentTypeInvalid) } diff --git a/acrate-nodeinfo/tests/discover_test.rs b/acrate-nodeinfo/tests/discover_test.rs index a8775a5..553d722 100644 --- a/acrate-nodeinfo/tests/discover_test.rs +++ b/acrate-nodeinfo/tests/discover_test.rs @@ -62,10 +62,10 @@ test!(iceshrimpnet, "https://ice.frieren.quest"); test!(gotosocial, "https://alpha.polymaths.social"); -test!(bridgyfed, "https://fed.brid.gy", ignore = "Returns application/jrd+json"); +test!(bridgyfed, "https://fed.brid.gy"); test!(threads, "https://threads.net", ignore = "Not implemented on their end"); test!(snac, "https://ngoa.giao.loan", ignore = "Does not support host-meta"); -test!(hollo, "https://hollo.social", ignore = "Returns application/jrd+json, does not support host-meta"); +test!(hollo, "https://hollo.social", ignore = "Does not support host-meta"); From dba9a93e2e7e7040783f05997c5c11eff8151727 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 11 Nov 2024 02:33:24 +0100 Subject: [PATCH 26/43] `nodeinfo`: Fix typo in `test_nodeinfo` --- acrate-nodeinfo/tests/discover_test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acrate-nodeinfo/tests/discover_test.rs b/acrate-nodeinfo/tests/discover_test.rs index 553d722..b1a1c85 100644 --- a/acrate-nodeinfo/tests/discover_test.rs +++ b/acrate-nodeinfo/tests/discover_test.rs @@ -44,7 +44,7 @@ macro_rules! test { let doc = HostMetaDocument::discover_nodeinfo(&client, base) .await - .expect("host-meta discovery to succeed"); + .expect("nodeinfo discovery to succeed"); println!("{doc:#?}"); } From 3cfc86a8fd60892da6dda03ac6b09bfa3f8c34ca Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 11 Nov 2024 03:01:46 +0100 Subject: [PATCH 27/43] `nodeinfo`: Setup test logging, sort of --- acrate-nodeinfo/tests/discover_test.rs | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/acrate-nodeinfo/tests/discover_test.rs b/acrate-nodeinfo/tests/discover_test.rs index b1a1c85..71ac96b 100644 --- a/acrate-nodeinfo/tests/discover_test.rs +++ b/acrate-nodeinfo/tests/discover_test.rs @@ -1,8 +1,20 @@ +const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME"); +const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); +const CARGO_PKG_REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY"); + + +fn init_log() { + let mut builder = pretty_env_logger::formatted_builder(); + builder.filter_level(log::LevelFilter::max()); + builder.is_test(true); + + if builder.try_init().is_ok() { + log::debug!("Initialized logging!"); + } +} + fn make_client() -> reqwest::Client { - let crate_name = env!("CARGO_PKG_NAME"); - let crate_version = env!("CARGO_PKG_VERSION"); - let crate_repository = env!("CARGO_PKG_REPOSITORY"); - let user_agent = format!("{crate_name}/{crate_version} ({crate_repository})"); + let user_agent = format!("{CARGO_PKG_NAME}/{CARGO_PKG_VERSION} ({CARGO_PKG_REPOSITORY})"); reqwest::Client::builder() .user_agent(user_agent) @@ -22,6 +34,7 @@ macro_rules! test { #[tokio::test] $(#[$tag])* async fn test_hostmeta() { + init_log(); let client = make_client(); let base: reqwest::Url = $url.parse() @@ -31,12 +44,13 @@ macro_rules! test { .await .expect("host-meta discovery to succeed"); - println!("{doc:#?}"); + log::info!("Parsed host-meta document: {doc:#?}"); } #[tokio::test] $(#[$tag])* async fn test_nodeinfo() { + init_log(); let client = make_client(); let base: reqwest::Url = $url.parse() @@ -46,7 +60,7 @@ macro_rules! test { .await .expect("nodeinfo discovery to succeed"); - println!("{doc:#?}"); + log::info!("Parsed nodeinfo document: {doc:#?}"); } } }; From c12b0a7869eddc3a86c80b3486bf4f5adea7993f Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 11 Nov 2024 03:04:20 +0100 Subject: [PATCH 28/43] `nodeinfo`: Report error if MIME type check fails --- acrate-nodeinfo/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/acrate-nodeinfo/src/lib.rs b/acrate-nodeinfo/src/lib.rs index 51e7b88..e11d61f 100644 --- a/acrate-nodeinfo/src/lib.rs +++ b/acrate-nodeinfo/src/lib.rs @@ -139,8 +139,9 @@ impl HostMetaDocument { let mime_type = extract_mime_from_content_type(content_type) .ok_or(ContentTypeInvalid)?; - log::trace!("Ensuring MIME type of `{mime_type}` is acceptable for JRD parsing..."); + log::trace!("Ensuring MIME type is acceptable for XRD parsing..."); if mime_type != "application/xrd+xml" { + log::error!("MIME type `{mime_type}` is not acceptable for XRD parsing."); return Err(ContentTypeInvalid) } From a9369861986e542dee095c7e555820adbb5184c4 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 11 Nov 2024 03:06:34 +0100 Subject: [PATCH 29/43] `nodeinfo`: Rename to `hostmeta` --- Cargo.toml | 2 +- {acrate-nodeinfo => acrate-hostmeta}/Cargo.toml | 2 +- {acrate-nodeinfo => acrate-hostmeta}/src/lib.rs | 0 .../discover_test.rs => acrate-hostmeta/tests/hostmeta_tests.rs | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename {acrate-nodeinfo => acrate-hostmeta}/Cargo.toml (94%) rename {acrate-nodeinfo => acrate-hostmeta}/src/lib.rs (100%) rename acrate-nodeinfo/tests/discover_test.rs => acrate-hostmeta/tests/hostmeta_tests.rs (98%) diff --git a/Cargo.toml b/Cargo.toml index defdbd3..053f9fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "2" -members = ["acrate-core", "acrate-nodeinfo"] +members = ["acrate-core", "acrate-hostmeta"] diff --git a/acrate-nodeinfo/Cargo.toml b/acrate-hostmeta/Cargo.toml similarity index 94% rename from acrate-nodeinfo/Cargo.toml rename to acrate-hostmeta/Cargo.toml index 986b27e..67a5122 100644 --- a/acrate-nodeinfo/Cargo.toml +++ b/acrate-hostmeta/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "acrate-nodeinfo" +name = "acrate-hostmeta" version = "0.1.0" edition = "2021" diff --git a/acrate-nodeinfo/src/lib.rs b/acrate-hostmeta/src/lib.rs similarity index 100% rename from acrate-nodeinfo/src/lib.rs rename to acrate-hostmeta/src/lib.rs diff --git a/acrate-nodeinfo/tests/discover_test.rs b/acrate-hostmeta/tests/hostmeta_tests.rs similarity index 98% rename from acrate-nodeinfo/tests/discover_test.rs rename to acrate-hostmeta/tests/hostmeta_tests.rs index 71ac96b..5e2185e 100644 --- a/acrate-nodeinfo/tests/discover_test.rs +++ b/acrate-hostmeta/tests/hostmeta_tests.rs @@ -28,7 +28,7 @@ macro_rules! test { }; ($id:ident, $url:literal, $($tag:meta),*) => { mod $id { - use acrate_nodeinfo::*; + use acrate_hostmeta::*; use super::*; #[tokio::test] From 7bc09aaed4a14ae9963499e3812a3004be8d84ae Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 11 Nov 2024 03:11:39 +0100 Subject: [PATCH 30/43] `nodeinfo`: Create crate --- Cargo.toml | 2 +- acrate-nodeinfo/Cargo.toml | 11 +++++++++++ acrate-nodeinfo/src/lib.rs | 14 ++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 acrate-nodeinfo/Cargo.toml create mode 100644 acrate-nodeinfo/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 053f9fe..86f0e07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "2" -members = ["acrate-core", "acrate-hostmeta"] +members = ["acrate-core", "acrate-hostmeta", "acrate-nodeinfo"] diff --git a/acrate-nodeinfo/Cargo.toml b/acrate-nodeinfo/Cargo.toml new file mode 100644 index 0000000..9caaee0 --- /dev/null +++ b/acrate-nodeinfo/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "acrate-nodeinfo" +version = "0.1.0" +edition = "2021" + +[dependencies] +log = "0.4.22" + +[dev-dependencies] +pretty_env_logger = "0.5.0" +tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread"] } diff --git a/acrate-nodeinfo/src/lib.rs b/acrate-nodeinfo/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/acrate-nodeinfo/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} From 1d4f19063052b28c39cc9e1f32b2f07039f20be8 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 11 Nov 2024 03:20:48 +0100 Subject: [PATCH 31/43] `nodeinfo`: Add basics --- .gitmodules | 3 +++ acrate-nodeinfo/Cargo.toml | 3 +++ acrate-nodeinfo/spec | 1 + acrate-nodeinfo/src/lib.rs | 17 +++-------------- 4 files changed, 10 insertions(+), 14 deletions(-) create mode 100644 .gitmodules create mode 160000 acrate-nodeinfo/spec diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..2877be6 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "acrate-nodeinfo/nodeinfo"] + path = acrate-nodeinfo/spec + url = https://github.com/jhass/nodeinfo.git diff --git a/acrate-nodeinfo/Cargo.toml b/acrate-nodeinfo/Cargo.toml index 9caaee0..d74de93 100644 --- a/acrate-nodeinfo/Cargo.toml +++ b/acrate-nodeinfo/Cargo.toml @@ -4,7 +4,10 @@ version = "0.1.0" edition = "2021" [dependencies] +acrate-hostmeta = { version = "0.1.0", path = "../acrate-hostmeta" } log = "0.4.22" +reqwest = { version = "0.12.9", features = ["json", "stream"] } +serde = { version = "1.0.214", features = ["derive"] } [dev-dependencies] pretty_env_logger = "0.5.0" diff --git a/acrate-nodeinfo/spec b/acrate-nodeinfo/spec new file mode 160000 index 0000000..1ad645d --- /dev/null +++ b/acrate-nodeinfo/spec @@ -0,0 +1 @@ +Subproject commit 1ad645d23cce892dbb6fbaf349760dfb0f1aeaa1 diff --git a/acrate-nodeinfo/src/lib.rs b/acrate-nodeinfo/src/lib.rs index b93cf3f..90cfed6 100644 --- a/acrate-nodeinfo/src/lib.rs +++ b/acrate-nodeinfo/src/lib.rs @@ -1,14 +1,3 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +//! Serde-based [NodeInfo] parser. +//! +//! [NodeInfo]: https://github.com/jhass/nodeinfo/blob/main/PROTOCOL.md From 2be3d76f4ad1d44d0d63f9b4ce1ec6677204d1b0 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 11 Nov 2024 03:24:22 +0100 Subject: [PATCH 32/43] `nodeinfo`: Add `jsonschema` dep --- acrate-nodeinfo/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/acrate-nodeinfo/Cargo.toml b/acrate-nodeinfo/Cargo.toml index d74de93..de30d95 100644 --- a/acrate-nodeinfo/Cargo.toml +++ b/acrate-nodeinfo/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] acrate-hostmeta = { version = "0.1.0", path = "../acrate-hostmeta" } +jsonschema = { version = "0.26.1", default-features = false } log = "0.4.22" reqwest = { version = "0.12.9", features = ["json", "stream"] } serde = { version = "1.0.214", features = ["derive"] } From 83f61395fa015b9493c9a830c29b398af3db1906 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 11 Nov 2024 03:28:13 +0100 Subject: [PATCH 33/43] `nodeinfo`: Add `serde_json` dep --- acrate-nodeinfo/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/acrate-nodeinfo/Cargo.toml b/acrate-nodeinfo/Cargo.toml index de30d95..90f87fd 100644 --- a/acrate-nodeinfo/Cargo.toml +++ b/acrate-nodeinfo/Cargo.toml @@ -9,6 +9,7 @@ jsonschema = { version = "0.26.1", default-features = false } log = "0.4.22" reqwest = { version = "0.12.9", features = ["json", "stream"] } serde = { version = "1.0.214", features = ["derive"] } +serde_json = "1.0.132" [dev-dependencies] pretty_env_logger = "0.5.0" From 61d8bbea9527f79bdc2d6ee31c1ee27496fc8a5f Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 11 Nov 2024 03:34:31 +0100 Subject: [PATCH 34/43] `nodeinfo`: Move `spec` submodule to `src` --- .gitmodules | 2 +- acrate-nodeinfo/{ => src}/spec | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename acrate-nodeinfo/{ => src}/spec (100%) diff --git a/.gitmodules b/.gitmodules index 2877be6..9ebfed1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "acrate-nodeinfo/nodeinfo"] - path = acrate-nodeinfo/spec + path = acrate-nodeinfo/src/spec url = https://github.com/jhass/nodeinfo.git diff --git a/acrate-nodeinfo/spec b/acrate-nodeinfo/src/spec similarity index 100% rename from acrate-nodeinfo/spec rename to acrate-nodeinfo/src/spec From 5ecb0bb61510e88ca8dcfaa3ae33e5fc6c6c1419 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 11 Nov 2024 04:11:53 +0100 Subject: [PATCH 35/43] `nodeinfo`: Give up on following the spec and implement things loosely --- .gitmodules | 3 -- acrate-nodeinfo/Cargo.toml | 1 - acrate-nodeinfo/src/lib.rs | 75 +++++++++++++++++++++++++++++++++++++- acrate-nodeinfo/src/spec | 1 - 4 files changed, 74 insertions(+), 6 deletions(-) delete mode 160000 acrate-nodeinfo/src/spec diff --git a/.gitmodules b/.gitmodules index 9ebfed1..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "acrate-nodeinfo/nodeinfo"] - path = acrate-nodeinfo/src/spec - url = https://github.com/jhass/nodeinfo.git diff --git a/acrate-nodeinfo/Cargo.toml b/acrate-nodeinfo/Cargo.toml index 90f87fd..4345304 100644 --- a/acrate-nodeinfo/Cargo.toml +++ b/acrate-nodeinfo/Cargo.toml @@ -5,7 +5,6 @@ edition = "2021" [dependencies] acrate-hostmeta = { version = "0.1.0", path = "../acrate-hostmeta" } -jsonschema = { version = "0.26.1", default-features = false } log = "0.4.22" reqwest = { version = "0.12.9", features = ["json", "stream"] } serde = { version = "1.0.214", features = ["derive"] } diff --git a/acrate-nodeinfo/src/lib.rs b/acrate-nodeinfo/src/lib.rs index 90cfed6..eb34904 100644 --- a/acrate-nodeinfo/src/lib.rs +++ b/acrate-nodeinfo/src/lib.rs @@ -1,3 +1,76 @@ -//! Serde-based [NodeInfo] parser. +//! Serde-based loose [NodeInfo] fetcher and parser. //! //! [NodeInfo]: https://github.com/jhass/nodeinfo/blob/main/PROTOCOL.md + +use serde::Deserialize; + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NodeInfo1 { + pub version: String, + pub software: NodeInfo1Software, + pub protocols: NodeInfo1Protocols, + pub services: NodeInfo1Services, + pub open_registrations: bool, + pub usage: NodeInfo1Usage, + pub metadata: serde_json::Value, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NodeInfo2 { + pub version: String, + pub instance: Option, + pub software: NodeInfo1Software, + pub protocols: NodeInfo1Protocols, + pub services: NodeInfo1Services, + pub open_registrations: bool, + pub usage: NodeInfo1Usage, + pub metadata: serde_json::Value, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NodeInfo1Software { + pub name: String, + pub version: String, + pub repository: Option, + pub homepage: Option, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NodeInfo1Protocols { + pub inbound: Vec, + pub outbound: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NodeInfo1Services { + pub inbound: Vec, + pub outbound: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NodeInfo1Usage { + pub users: NodeInfo1UsageUsers, + pub local_posts: i32, + pub local_comments: i32, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NodeInfo1UsageUsers { + pub total: i32, + pub active_halfyear: i32, + pub active_month: i32, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NodeInfo2Instance { + pub name: String, + pub description: String, +} diff --git a/acrate-nodeinfo/src/spec b/acrate-nodeinfo/src/spec deleted file mode 160000 index 1ad645d..0000000 --- a/acrate-nodeinfo/src/spec +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1ad645d23cce892dbb6fbaf349760dfb0f1aeaa1 From 1ba354a1a5f727f9ca23872c43247d668c45da6e Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 11 Nov 2024 04:25:06 +0100 Subject: [PATCH 36/43] `hostmeta`: Fully qualify `reqwest::header::HeaderValue` --- acrate-hostmeta/src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/acrate-hostmeta/src/lib.rs b/acrate-hostmeta/src/lib.rs index e11d61f..5e2cc6f 100644 --- a/acrate-hostmeta/src/lib.rs +++ b/acrate-hostmeta/src/lib.rs @@ -2,7 +2,6 @@ //! //! [RFC 6415]: https://datatracker.ietf.org/doc/html/rfc6415 -use reqwest::header::HeaderValue; use serde::Deserialize; /// A [host-meta document] in JRD representation. @@ -334,7 +333,7 @@ pub enum HostMetaGetJRDError { } /// Extract the MIME type from the value of the `Content-Type` header. -fn extract_mime_from_content_type(value: &HeaderValue) -> Option { +fn extract_mime_from_content_type(value: &reqwest::header::HeaderValue) -> Option { let value = value.to_str().ok()?; match value.split_once("; ") { None => Some(value.to_string()), From 13f6a4e4c7a3f61671a791f6960ac38be7e21b7a Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 11 Nov 2024 06:06:41 +0100 Subject: [PATCH 37/43] Allow `tabs-in-doc-comments` (No reason not to.) --- acrate-core/Cargo.toml | 3 +++ acrate-hostmeta/Cargo.toml | 2 ++ acrate-nodeinfo/Cargo.toml | 3 +++ 3 files changed, 8 insertions(+) diff --git a/acrate-core/Cargo.toml b/acrate-core/Cargo.toml index 2688ecd..bef29b4 100644 --- a/acrate-core/Cargo.toml +++ b/acrate-core/Cargo.toml @@ -6,3 +6,6 @@ edition = "2021" [dependencies] diesel = "2.2.4" diesel_migrations = "2.2.0" + +[lints.clippy] +tabs-in-doc-comments = "allow" diff --git a/acrate-hostmeta/Cargo.toml b/acrate-hostmeta/Cargo.toml index 67a5122..8dec94a 100644 --- a/acrate-hostmeta/Cargo.toml +++ b/acrate-hostmeta/Cargo.toml @@ -13,3 +13,5 @@ serde_json = "1.0.132" [dev-dependencies] pretty_env_logger = "0.5.0" tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread"] } +[lints.clippy] +tabs-in-doc-comments = "allow" diff --git a/acrate-nodeinfo/Cargo.toml b/acrate-nodeinfo/Cargo.toml index 4345304..d97b378 100644 --- a/acrate-nodeinfo/Cargo.toml +++ b/acrate-nodeinfo/Cargo.toml @@ -13,3 +13,6 @@ serde_json = "1.0.132" [dev-dependencies] pretty_env_logger = "0.5.0" tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread"] } + +[lints.clippy] +tabs-in-doc-comments = "allow" From 3d8f34a84cff9a016aad2e72e3c41bea63d5e1a0 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 11 Nov 2024 06:07:24 +0100 Subject: [PATCH 38/43] `hostmeta`: Integrate webfinger specification --- acrate-hostmeta/Cargo.toml | 2 + acrate-hostmeta/src/lib.rs | 385 +++++++++++++++++++----- acrate-hostmeta/tests/hostmeta_tests.rs | 4 +- 3 files changed, 307 insertions(+), 84 deletions(-) diff --git a/acrate-hostmeta/Cargo.toml b/acrate-hostmeta/Cargo.toml index 8dec94a..bda02e5 100644 --- a/acrate-hostmeta/Cargo.toml +++ b/acrate-hostmeta/Cargo.toml @@ -13,5 +13,7 @@ serde_json = "1.0.132" [dev-dependencies] pretty_env_logger = "0.5.0" tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread"] } +tokio-test = "0.4.4" + [lints.clippy] tabs-in-doc-comments = "allow" diff --git a/acrate-hostmeta/src/lib.rs b/acrate-hostmeta/src/lib.rs index 5e2cc6f..fff705f 100644 --- a/acrate-hostmeta/src/lib.rs +++ b/acrate-hostmeta/src/lib.rs @@ -1,56 +1,150 @@ -//! Serde-based [RFC 6415] `host-meta` parser. +//! Resource descriptior deserializer. //! //! [RFC 6415]: https://datatracker.ietf.org/doc/html/rfc6415 +//! [RFC 7033]: https://datatracker.ietf.org/doc/html/rfc7033#section-4.4 + +use std::collections::HashMap; use serde::Deserialize; -/// A [host-meta document] in JRD representation. +/// A resource descriptor object. /// -/// [host-meta document]: https://datatracker.ietf.org/doc/html/rfc6415 +/// ## Specification +/// +/// - https://datatracker.ietf.org/doc/html/rfc6415#section-3 +/// - https://datatracker.ietf.org/doc/html/rfc7033#section-4.4 #[derive(Debug, Clone, Deserialize)] -pub struct HostMetaDocument { +pub struct ResourceDescriptor { /// The resource this document refers to. + /// + /// ## Specification + /// + /// - https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.1 #[serde(alias = "Subject")] pub subject: Option, + /// Other names the resource described by this document can be referred to. + /// + /// ## Specification + /// + /// - https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.2 + #[serde(alias = "Alias")] + pub aliases: Option>, + + /// Additional information about the resource described by this document. + /// + /// ## Specification + /// + /// - https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.3 + #[serde(alias = "Property")] + pub properties: Option>, + /// Links established between the [`Self::subject`] and other resources. + /// + /// ## Specification + /// + /// - https://datatracker.ietf.org/doc/html/rfc6415#section-3.1.1 + /// - https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4 #[serde(alias = "Link")] - pub links: Vec, + pub links: Option>, } -/// A [host-meta Link Element], which puts the subject resource in relation with another. +/// A link element, which puts the subject resource in relation with another. /// -/// [host-meta Link Element]: https://datatracker.ietf.org/doc/html/rfc6415#section-3.1.1 +/// ## Specification +/// +/// - https://datatracker.ietf.org/doc/html/rfc6415#section-3.1.1 #[derive(Debug, Clone, Deserialize)] -pub struct HostMetaLink { +pub struct ResourceDescriptorLink { /// The kind of relation established by the subject with the attached resource. + /// + /// ## Specification + /// + /// https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.1 #[serde(alias = "@rel")] pub rel: String, - /// The resource put in relation with the subject resource. + /// The media type of the resource put in relation. + /// + /// ## Specification + /// + /// - https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.2 + #[serde(alias = "@type")] + pub r#type: Option, + + /// URI to the resource put in relation. + /// + /// ## Specification + /// + /// - https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.3 #[serde(alias = "@href")] pub href: Option, - /// Template to fill to get the resource put in relation with the subject resource. + /// Titles of the resource put in relation in various languages. + /// + /// ## Specification + /// + /// - https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.4 + pub titles: Option>>, + + /// Additional information about the resource put in relation. + /// + /// ## Specification + /// + /// - https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.5 + pub properties: Option>, + + /// Template to fill to get the URL to resource-specific information. + /// + /// ## Specification + /// + /// - https://datatracker.ietf.org/doc/html/rfc6415#section-4.2 #[serde(alias = "@template")] pub template: Option, - - /// The `Content-Type` of the resource put in relation. - #[serde(alias = "@type")] - pub r#type: Option, } -impl HostMetaDocument { - /// Get an [host-meta document] in the [JRD format]. +/// A property element, which describes a certain aspect of the subject resource. +/// +/// ## Specification +/// +/// - https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.3 +#[derive(Debug, Clone, Deserialize)] +pub struct ResourceDescriptorProperty { + /// The property identifier, or type. + #[serde(alias = "@type")] + pub r#type: String, + + /// The property value. + pub value: Option, +} + +impl ResourceDescriptor { + /// Get a JRD (JSON [`ResourceDescriptor`]). + /// + /// ## Notes /// /// This follows redirects until the redirect chain is 10 hops; see [`reqwest::redirect`] for more info. /// - /// [JRD format]: https://datatracker.ietf.org/doc/html/rfc6415#appendix-A - /// [host-meta document]: https://datatracker.ietf.org/doc/html/rfc6415 + /// ## Examples + /// + /// ``` + /// # tokio_test::block_on(async { + /// use acrate_hostmeta::ResourceDescriptor; + /// + /// let client = reqwest::Client::new(); + /// let url: reqwest::Url = "https://junimo.party/.well-known/nodeinfo".parse() + /// .expect("URL to be valid"); + /// + /// let rd = ResourceDescriptor::get_jrd(&client, url) + /// .await + /// .expect("JRD to be processed correctly"); + /// # }) + /// ``` + /// pub async fn get_jrd(client: &reqwest::Client, url: reqwest::Url) -> Result { use HostMetaGetJRDError::*; - log::debug!("Getting host-meta JRD document at: {url}"); + log::debug!("Getting JRD document at: {url}"); log::trace!("Building request..."); let request = { @@ -82,10 +176,7 @@ impl HostMetaDocument { .ok_or(ContentTypeInvalid)?; log::trace!("Ensuring MIME type is acceptable for JRD parsing..."); - if mime_type == "application/jrd+json" { - log::warn!("MIME type `{mime_type}` would not be acceptable for JRD parsing, but is temporarily allowed anyways due to widespread use.") - } - else if mime_type != "application/json" { + if !(mime_type == "application/json" || mime_type == "application/jrd+json") { log::error!("MIME type `{mime_type}` is not acceptable for JRD parsing."); return Err(ContentTypeInvalid) } @@ -98,12 +189,27 @@ impl HostMetaDocument { Ok(data) } - /// Get an [host-meta document] in the [XRD format]. + /// Get a XRD (Extensible [`ResourceDescriptor`]). + /// + /// ## Notes /// /// This follows redirects until the redirect chain is 10 hops; see [`reqwest::redirect`] for more info. /// - /// [XRD format]: https://datatracker.ietf.org/doc/html/rfc6415#section-3.1 - /// [host-meta document]: https://datatracker.ietf.org/doc/html/rfc6415 + /// ## Examples + /// + /// ``` + /// # tokio_test::block_on(async { + /// use acrate_hostmeta::ResourceDescriptor; + /// + /// let client = reqwest::Client::new(); + /// let url: reqwest::Url = "https://junimo.party/.well-known/host-meta".parse() + /// .expect("URL to be valid"); + /// + /// let rd = ResourceDescriptor::get_xrd(&client, url) + /// .await + /// .expect("XRD to be processed correctly"); + /// # }) + /// ``` pub async fn get_xrd(client: &reqwest::Client, url: reqwest::Url) -> Result { use HostMetaGetXRDError::*; @@ -156,24 +262,41 @@ impl HostMetaDocument { Ok(data) } - /// Attempt to discover the [host-meta document] at the given URL in various ways. + /// Attempt to discover a [`ResourceDescriptor`] at the given URL in various ways. /// /// In order, this method attempts: + /// /// 1. [HTTPS] [XRD](Self::get_xrd) /// 2. [HTTPS] [JRD](Self::get_jrd) - /// 3. [HTTP] [XRD](Self::get_xrd) - /// 4. [HTTP] [JRD](Self::get_jrd) + /// 3. [HTTPS] [JRD](Self::get_jrd) with .json path extension + /// 4. [HTTP] [XRD](Self::get_xrd) + /// 5. [HTTP] [JRD](Self::get_jrd) + /// 6. [HTTP] [JRD](Self::get_jrd) with .json path extension + /// + /// ## Notes /// /// This follows redirects until the redirect chain is 10 hops; see [`reqwest::redirect`] for more info. /// - /// [host-meta document]: https://datatracker.ietf.org/doc/html/rfc6415 - /// [HTTPS]: https://datatracker.ietf.org/doc/html/rfc2818 - /// [HTTP]: https://datatracker.ietf.org/doc/html/rfc2616 - /// [JRD]: https://datatracker.ietf.org/doc/html/rfc6415#appendix-A - pub async fn discover(client: &reqwest::Client, mut url: reqwest::Url) -> Result { - use HostMetaDiscoverError::*; + /// ## Examples + /// + /// ``` + /// # tokio_test::block_on(async { + /// use acrate_hostmeta::ResourceDescriptor; + /// + /// let client = reqwest::Client::new(); + /// let url: reqwest::Url = "https://junimo.party/.well-known/host-meta".parse() + /// .expect("URL to be valid"); + /// + /// let rd = ResourceDescriptor::discover(&client, url) + /// .await + /// .expect("resource descriptor to be discovered correctly"); + /// # }) + /// ``` + /// + pub async fn discover(client: &reqwest::Client, mut url: reqwest::Url) -> Result { + use ResourceDescriptorDiscoveryError::*; - log::debug!("Discovering host-meta document at base: {url}"); + log::debug!("Discovering resource descriptor document at: {url}"); log::trace!("Unsetting URL query..."); url.set_query(None); @@ -185,26 +308,52 @@ impl HostMetaDocument { url.set_scheme("https") .map_err(UrlManipulation)?; - log::trace!("Attempting to retrieve XRD host-meta via HTTPS..."); - let https_xrd = match Self::get_xrd(client, url.clone()).await { + log::trace!("Cloning URL for HTTPS XRD retrieval..."); + let https_xrd_url = url.clone(); + + log::trace!("Attempting HTTPS XRD retrieval..."); + let https_xrd = match Self::get_xrd(client, https_xrd_url).await { Ok(data) => { log::trace!("HTTPS XRD retrieval was successful, returning..."); return Ok(data) } Err(err) => { - log::trace!("HTTPS XRD retrieval failed."); + log::warn!("HTTPS XRD retrieval failed."); err } }; - log::trace!("Attempting to retrieve JRD host-meta via HTTPS..."); - let https_jrd = match Self::get_jrd(client, url.clone()).await { + log::trace!("Cloning URL for HTTPS JRD retrieval..."); + let https_jrd_url = url.clone(); + + log::trace!("Attempting HTTPS JRD retrieval..."); + let https_jrd = match Self::get_jrd(client, https_jrd_url).await { Ok(data) => { log::trace!("HTTPS JRD retrieval was successful, returning..."); return Ok(data) } Err(err) => { - log::trace!("HTTPS JRD retrieval failed."); + log::warn!("HTTPS JRD retrieval failed."); + err + } + }; + + log::trace!("Cloning URL for HTTPS JRD .json retrieval..."); + let mut https_jrdj_url = url.clone(); + + log::trace!("Altering URL path for HTTPS JRD .json retrieval..."); + https_jrdj_url.set_path( + &format!("{}.json", https_jrdj_url.path()) + ); + + log::trace!("Attempting HTTPS JRD .json retrieval..."); + let https_jrdj = match Self::get_jrd(client, https_jrdj_url).await { + Ok(data) => { + log::trace!("HTTPS JRD .json retrieval was successful, returning..."); + return Ok(data) + } + Err(err) => { + log::warn!("HTTPS JRD .json retrieval failed."); err } }; @@ -213,99 +362,170 @@ impl HostMetaDocument { url.set_scheme("http") .map_err(UrlManipulation)?; - log::trace!("Attempting to retrieve XRD host-meta via HTTP..."); - let http_xrd = match Self::get_xrd(client, url.clone()).await { + log::trace!("Cloning URL for HTTP XRD retrieval..."); + let http_xrd_url = url.clone(); + + log::trace!("Attempting HTTP XRD retrieval..."); + let http_xrd = match Self::get_xrd(client, http_xrd_url).await { Ok(data) => { log::trace!("HTTP XRD retrieval was successful, returning..."); return Ok(data) } Err(err) => { - log::trace!("HTTP XRD retrieval failed."); + log::warn!("HTTP XRD retrieval failed."); err } }; - log::trace!("Attempting to retrieve JRD host-meta via HTTP..."); - let http_jrd = match Self::get_jrd(client, url.clone()).await { + log::trace!("Cloning URL for HTTP JRD retrieval..."); + let http_jrd_url = url.clone(); + + log::trace!("Attempting HTTP JRD retrieval..."); + let http_jrd = match Self::get_jrd(client, http_jrd_url).await { Ok(data) => { log::trace!("HTTP JRD retrieval was successful, returning..."); return Ok(data) } Err(err) => { - log::trace!("HTTP JRD retrieval failed."); + log::warn!("HTTP JRD retrieval failed."); + err + } + }; + + log::trace!("Cloning URL for HTTP JRD .json retrieval..."); + let mut http_jrdj_url = url.clone(); + + log::trace!("Altering URL path for HTTPS JRD .json retrieval..."); + http_jrdj_url.set_path( + &format!("{}.json", http_jrdj_url.path()) + ); + + log::trace!("Attempting HTTP JRD .json retrieval..."); + let http_jrdj = match Self::get_jrd(client, http_jrdj_url).await { + Ok(data) => { + log::trace!("HTTP JRD .json retrieval was successful, returning..."); + return Ok(data) + } + Err(err) => { + log::warn!("HTTP JRD .json retrieval failed."); err } }; Err( - HostMetaDiscoverError::Fetch( - HostMetaDiscoveryAttemptsErrors { + ResourceDescriptorDiscoveryError::Fetch( + ResourceDescriptorDiscoveryFailures { https_xrd, https_jrd, + https_jrdj, http_xrd, http_jrd, + http_jrdj, } ) ) } - const WELLKNOWN_NODEINFO_PATH: &str = "/.well-known/nodeinfo"; + /// Well-known path for host-meta documents. + /// + /// ## Specification + /// + /// - https://datatracker.ietf.org/doc/html/rfc6415#section-2 + pub const WELLKNOWN_HOSTMETA_PATH: &str = "/.well-known/host-meta"; - pub async fn discover_nodeinfo(client: &reqwest::Client, mut base: reqwest::Url) -> Result { - base.set_path(Self::WELLKNOWN_NODEINFO_PATH); - - Self::discover(client, base) - .await - } - - const WELLKNOWN_HOSTMETA_PATH: &str = "/.well-known/host-meta"; - - pub async fn discover_hostmeta(client: &reqwest::Client, mut base: reqwest::Url) -> Result { + /// Attempt to discover a host-meta document at the given base URL. + /// + /// ## Examples + /// + /// ``` + /// # tokio_test::block_on(async { + /// use acrate_hostmeta::ResourceDescriptor; + /// + /// let client = reqwest::Client::new(); + /// let base: reqwest::Url = "https://junimo.party".parse() + /// .expect("URL to be valid"); + /// + /// let rd = ResourceDescriptor::discover_hostmeta(&client, base) + /// .await + /// .expect("host-meta to be discovered correctly"); + /// }) + /// ``` + /// + pub async fn discover_hostmeta(client: &reqwest::Client, mut base: reqwest::Url) -> Result { base.set_path(Self::WELLKNOWN_HOSTMETA_PATH); Self::discover(client, base) .await } + + /// Well-known path for NodeInfo documents. + /// + /// ## Specification + /// + /// - https://github.com/jhass/nodeinfo/blob/main/PROTOCOL.md#discovery + pub const WELLKNOWN_NODEINFO_PATH: &str = "/.well-known/nodeinfo"; + + /// Attempt to discover a NodeInfo document at the given base URL. + /// + /// ## Examples + /// + /// ``` + /// # tokio_test::block_on(async { + /// use acrate_hostmeta::ResourceDescriptor; + /// + /// let client = reqwest::Client::new(); + /// let base: reqwest::Url = "https://junimo.party".parse() + /// .expect("URL to be valid"); + /// + /// let rd = ResourceDescriptor::discover_nodeinfo(&client, base) + /// .await + /// .expect("NodeInfo to be discovered correctly"); + /// # }) + /// ``` + /// + pub async fn discover_nodeinfo(client: &reqwest::Client, mut base: reqwest::Url) -> Result { + base.set_path(Self::WELLKNOWN_NODEINFO_PATH); + + Self::discover(client, base) + .await + } } +/// Error occurred during [`ResourceDescriptor::discover`]. #[derive(Debug)] -pub enum HostMetaDiscoverError { - /// Manipulation of the URL scheme of the given base failed. +pub enum ResourceDescriptorDiscoveryError { + /// Manipulation of the provided base [`reqwest::Url`] failed. /// /// See [reqwest::Url::set_scheme] for possible causes. UrlManipulation(()), - /// All attempts of fetching the host-meta document failed. - Fetch(HostMetaDiscoveryAttemptsErrors), + /// All attempts of fetching a resource descriptor document failed. + Fetch(ResourceDescriptorDiscoveryFailures), } +/// Request errors occurred during [`ResourceDescriptor::discover`]. #[derive(Debug)] -pub struct HostMetaDiscoveryAttemptsErrors { - /// The error that occoured when trying to fetch the host-meta document in [XRD format] via [HTTPS]. - /// - /// [HTTPS]: https://datatracker.ietf.org/doc/html/rfc2818 - /// [XRD format]: https://datatracker.ietf.org/doc/html/rfc6415#section-3.1 +pub struct ResourceDescriptorDiscoveryFailures { + /// HTTPS XRD retrieval. pub https_xrd: HostMetaGetXRDError, - /// The error that occoured when trying to fetch the host-meta document in [JRD format] via [HTTPS]. - /// - /// [HTTPS]: https://datatracker.ietf.org/doc/html/rfc2818 - /// [JRD format]: https://datatracker.ietf.org/doc/html/rfc6415#appendix-A + /// HTTPS JRD retrieval. pub https_jrd: HostMetaGetJRDError, - /// The error that occoured when trying to fetch the host-meta document in [XRD format] via [HTTP]. - /// - /// [HTTP]: https://datatracker.ietf.org/doc/html/rfc2616 - /// [XRD format]: https://datatracker.ietf.org/doc/html/rfc6415#section-3.1 + /// HTTPS JRD with .json extension retrieval. + pub https_jrdj: HostMetaGetJRDError, + + /// HTTPS XRD retrieval. pub http_xrd: HostMetaGetXRDError, - /// The error that occoured when trying to fetch the host-meta document in [JRD format] via [HTTP]. - /// - /// [HTTP]: https://datatracker.ietf.org/doc/html/rfc2616 - /// [JRD format]: https://datatracker.ietf.org/doc/html/rfc6415#appendix-A + /// HTTP JRD retrieval. pub http_jrd: HostMetaGetJRDError, + + /// HTTP JRD with .json extension retrieval. + pub http_jrdj: HostMetaGetJRDError, } +/// Error occurred during [`ResourceDescriptor::get_xrd`]. #[derive(Debug)] pub enum HostMetaGetXRDError { /// The HTTP request failed. @@ -320,6 +540,7 @@ pub enum HostMetaGetXRDError { Parse(quick_xml::DeError), } +/// Error occurred during [`ResourceDescriptor::get_jrd`]. #[derive(Debug)] pub enum HostMetaGetJRDError { /// The HTTP request failed. diff --git a/acrate-hostmeta/tests/hostmeta_tests.rs b/acrate-hostmeta/tests/hostmeta_tests.rs index 5e2185e..51c4e20 100644 --- a/acrate-hostmeta/tests/hostmeta_tests.rs +++ b/acrate-hostmeta/tests/hostmeta_tests.rs @@ -40,7 +40,7 @@ macro_rules! test { let base: reqwest::Url = $url.parse() .expect("a valid URL"); - let doc = HostMetaDocument::discover_hostmeta(&client, base) + let doc = ResourceDescriptor::discover_hostmeta(&client, base) .await .expect("host-meta discovery to succeed"); @@ -56,7 +56,7 @@ macro_rules! test { let base: reqwest::Url = $url.parse() .expect("a valid URL"); - let doc = HostMetaDocument::discover_nodeinfo(&client, base) + let doc = ResourceDescriptor::discover_nodeinfo(&client, base) .await .expect("nodeinfo discovery to succeed"); From 9fb2fd3b0af5f0ba32dd700562e7a8c7011fbb5d Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 11 Nov 2024 08:20:25 +0100 Subject: [PATCH 39/43] Final touches, will reorganize when not asleep --- acrate-hostmeta/src/lib.rs | 56 +-- acrate-hostmeta/tests/hostmeta_tests.rs | 49 +-- acrate-nodeinfo/Cargo.toml | 1 + acrate-nodeinfo/src/lib.rs | 437 +++++++++++++++++++++++- acrate-nodeinfo/tests/nodeinfo_tests.rs | 70 ++++ 5 files changed, 521 insertions(+), 92 deletions(-) create mode 100644 acrate-nodeinfo/tests/nodeinfo_tests.rs diff --git a/acrate-hostmeta/src/lib.rs b/acrate-hostmeta/src/lib.rs index fff705f..81dcb55 100644 --- a/acrate-hostmeta/src/lib.rs +++ b/acrate-hostmeta/src/lib.rs @@ -141,8 +141,8 @@ impl ResourceDescriptor { /// # }) /// ``` /// - pub async fn get_jrd(client: &reqwest::Client, url: reqwest::Url) -> Result { - use HostMetaGetJRDError::*; + pub async fn get_jrd(client: &reqwest::Client, url: reqwest::Url) -> Result { + use GetJRDError::*; log::debug!("Getting JRD document at: {url}"); @@ -210,8 +210,8 @@ impl ResourceDescriptor { /// .expect("XRD to be processed correctly"); /// # }) /// ``` - pub async fn get_xrd(client: &reqwest::Client, url: reqwest::Url) -> Result { - use HostMetaGetXRDError::*; + pub async fn get_xrd(client: &reqwest::Client, url: reqwest::Url) -> Result { + use GetXRDError::*; log::debug!("Getting host-meta XRD document at: {url}"); @@ -457,38 +457,6 @@ impl ResourceDescriptor { Self::discover(client, base) .await } - - /// Well-known path for NodeInfo documents. - /// - /// ## Specification - /// - /// - https://github.com/jhass/nodeinfo/blob/main/PROTOCOL.md#discovery - pub const WELLKNOWN_NODEINFO_PATH: &str = "/.well-known/nodeinfo"; - - /// Attempt to discover a NodeInfo document at the given base URL. - /// - /// ## Examples - /// - /// ``` - /// # tokio_test::block_on(async { - /// use acrate_hostmeta::ResourceDescriptor; - /// - /// let client = reqwest::Client::new(); - /// let base: reqwest::Url = "https://junimo.party".parse() - /// .expect("URL to be valid"); - /// - /// let rd = ResourceDescriptor::discover_nodeinfo(&client, base) - /// .await - /// .expect("NodeInfo to be discovered correctly"); - /// # }) - /// ``` - /// - pub async fn discover_nodeinfo(client: &reqwest::Client, mut base: reqwest::Url) -> Result { - base.set_path(Self::WELLKNOWN_NODEINFO_PATH); - - Self::discover(client, base) - .await - } } /// Error occurred during [`ResourceDescriptor::discover`]. @@ -507,27 +475,27 @@ pub enum ResourceDescriptorDiscoveryError { #[derive(Debug)] pub struct ResourceDescriptorDiscoveryFailures { /// HTTPS XRD retrieval. - pub https_xrd: HostMetaGetXRDError, + pub https_xrd: GetXRDError, /// HTTPS JRD retrieval. - pub https_jrd: HostMetaGetJRDError, + pub https_jrd: GetJRDError, /// HTTPS JRD with .json extension retrieval. - pub https_jrdj: HostMetaGetJRDError, + pub https_jrdj: GetJRDError, /// HTTPS XRD retrieval. - pub http_xrd: HostMetaGetXRDError, + pub http_xrd: GetXRDError, /// HTTP JRD retrieval. - pub http_jrd: HostMetaGetJRDError, + pub http_jrd: GetJRDError, /// HTTP JRD with .json extension retrieval. - pub http_jrdj: HostMetaGetJRDError, + pub http_jrdj: GetJRDError, } /// Error occurred during [`ResourceDescriptor::get_xrd`]. #[derive(Debug)] -pub enum HostMetaGetXRDError { +pub enum GetXRDError { /// The HTTP request failed. Request(reqwest::Error), /// The `Content-Type` header of the response is missing. @@ -542,7 +510,7 @@ pub enum HostMetaGetXRDError { /// Error occurred during [`ResourceDescriptor::get_jrd`]. #[derive(Debug)] -pub enum HostMetaGetJRDError { +pub enum GetJRDError { /// The HTTP request failed. Request(reqwest::Error), /// The `Content-Type` header of the response is missing. diff --git a/acrate-hostmeta/tests/hostmeta_tests.rs b/acrate-hostmeta/tests/hostmeta_tests.rs index 51c4e20..1bdd795 100644 --- a/acrate-hostmeta/tests/hostmeta_tests.rs +++ b/acrate-hostmeta/tests/hostmeta_tests.rs @@ -5,6 +5,7 @@ const CARGO_PKG_REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY"); fn init_log() { let mut builder = pretty_env_logger::formatted_builder(); + builder.target(pretty_env_logger::env_logger::Target::Stdout); builder.filter_level(log::LevelFilter::max()); builder.is_test(true); @@ -22,9 +23,9 @@ fn make_client() -> reqwest::Client { .expect("reqwest client to build") } -macro_rules! test { +macro_rules! test_discover_hostmeta { ($id:ident, $url:literal) => { - test!($id, $url,); + test_discover_hostmeta!($id, $url,); }; ($id:ident, $url:literal, $($tag:meta),*) => { mod $id { @@ -33,7 +34,7 @@ macro_rules! test { #[tokio::test] $(#[$tag])* - async fn test_hostmeta() { + async fn test() { init_log(); let client = make_client(); @@ -46,40 +47,16 @@ macro_rules! test { log::info!("Parsed host-meta document: {doc:#?}"); } - - #[tokio::test] - $(#[$tag])* - async fn test_nodeinfo() { - init_log(); - let client = make_client(); - - let base: reqwest::Url = $url.parse() - .expect("a valid URL"); - - let doc = ResourceDescriptor::discover_nodeinfo(&client, base) - .await - .expect("nodeinfo discovery to succeed"); - - log::info!("Parsed nodeinfo document: {doc:#?}"); - } } }; } -test!(akkoma, "https://junimo.party"); - -test!(mastodon, "https://mastodon.social"); - -test!(misskey, "https://misskey.io"); - -test!(iceshrimpnet, "https://ice.frieren.quest"); - -test!(gotosocial, "https://alpha.polymaths.social"); - -test!(bridgyfed, "https://fed.brid.gy"); - -test!(threads, "https://threads.net", ignore = "Not implemented on their end"); - -test!(snac, "https://ngoa.giao.loan", ignore = "Does not support host-meta"); - -test!(hollo, "https://hollo.social", ignore = "Does not support host-meta"); +test_discover_hostmeta!(akkoma, "https://junimo.party"); +test_discover_hostmeta!(mastodon, "https://mastodon.social"); +test_discover_hostmeta!(misskey, "https://misskey.io"); +test_discover_hostmeta!(iceshrimpnet, "https://ice.frieren.quest"); +test_discover_hostmeta!(gotosocial, "https://alpha.polymaths.social"); +test_discover_hostmeta!(bridgyfed, "https://fed.brid.gy"); +test_discover_hostmeta!(threads, "https://threads.net", ignore = "does not support host-meta"); +test_discover_hostmeta!(snac, "https://ngoa.giao.loan", ignore = "does not support host-meta"); +test_discover_hostmeta!(hollo, "https://hollo.social", ignore = "does not support host-meta"); diff --git a/acrate-nodeinfo/Cargo.toml b/acrate-nodeinfo/Cargo.toml index d97b378..d8ca425 100644 --- a/acrate-nodeinfo/Cargo.toml +++ b/acrate-nodeinfo/Cargo.toml @@ -13,6 +13,7 @@ serde_json = "1.0.132" [dev-dependencies] pretty_env_logger = "0.5.0" tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread"] } +tokio-test = "0.4.4" [lints.clippy] tabs-in-doc-comments = "allow" diff --git a/acrate-nodeinfo/src/lib.rs b/acrate-nodeinfo/src/lib.rs index eb34904..0fbf5e4 100644 --- a/acrate-nodeinfo/src/lib.rs +++ b/acrate-nodeinfo/src/lib.rs @@ -1,76 +1,489 @@ -//! Serde-based loose [NodeInfo] fetcher and parser. +//! Serde-based NodeInfo fetcher and loose parser. //! -//! [NodeInfo]: https://github.com/jhass/nodeinfo/blob/main/PROTOCOL.md +//! > NodeInfo is an effort to create a standardized way of exposing metadata about a server running one of the distributed social networks. + +use std::collections::HashMap; use serde::Deserialize; +/// A variant of a NodeInfo document. +/// +/// ## Specification +/// +/// - https://github.com/jhass/nodeinfo/blob/main/PROTOCOL.md +#[derive(Debug, Clone)] +pub enum NodeInfo { + V1(NodeInfo1), + V2(NodeInfo2), +} + +/// A NodeInfo document at version 1.X. +/// +/// ## Specification +/// +/// - https://github.com/jhass/nodeinfo/blob/main/schemas/1.0/schema.json +/// - https://github.com/jhass/nodeinfo/blob/main/schemas/1.1/schema.json +/// #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NodeInfo1 { + /// The schema version. pub version: String, + + /// Metadata about server software in use. pub software: NodeInfo1Software, + + /// The protocols supported on this server. pub protocols: NodeInfo1Protocols, + + /// The third party sites this server can connect to via their application API. pub services: NodeInfo1Services, + + /// Whether this server allows open self-registration. pub open_registrations: bool, + + /// Usage statistics for this server. pub usage: NodeInfo1Usage, + + /// Free form key value pairs for software specific values. + /// + /// Clients should not rely on any specific key present. pub metadata: serde_json::Value, } +/// A NodeInfo document at version 2.X. +/// +/// ## Specification +/// +/// - https://github.com/jhass/nodeinfo/blob/main/schemas/2.0/schema.json +/// - https://github.com/jhass/nodeinfo/blob/main/schemas/2.1/schema.json +/// - https://github.com/jhass/nodeinfo/blob/main/schemas/2.2/schema.json +/// #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NodeInfo2 { + /// The schema version. pub version: String, + + /// Metadata specific to the instance. An instance is a the concrete installation of a software running on a server. pub instance: Option, + + /// Metadata about server software in use. pub software: NodeInfo1Software, - pub protocols: NodeInfo1Protocols, - pub services: NodeInfo1Services, + + /// The protocols supported on this server. + pub protocols: Vec, + + /// The third party sites this server can connect to via their application API. + pub services: Option, + + /// Whether this server allows open self-registration. pub open_registrations: bool, - pub usage: NodeInfo1Usage, + + /// Usage statistics for this server. + pub usage: Option, + + /// Free form key value pairs for software specific values. + /// + /// Clients should not rely on any specific key present. pub metadata: serde_json::Value, } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct NodeInfo1Software { +pub struct NodeInfo1Software { + /// The canonical name of this server software. pub name: String, + + /// The version of this server software. pub version: String, + pub repository: Option, + pub homepage: Option, } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NodeInfo1Protocols { + /// The protocols this server can receive traffic for. pub inbound: Vec, + + /// The protocols this server can generate traffic for. pub outbound: Vec, } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NodeInfo1Services { + /// The third party sites this server can retrieve messages from for combined display with regular traffic. pub inbound: Vec, + + /// The third party sites this server can publish messages to on the behalf of a user. pub outbound: Vec, } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NodeInfo1Usage { - pub users: NodeInfo1UsageUsers, - pub local_posts: i32, - pub local_comments: i32, + /// Statistics about the users of this server. + pub users: Option, + + /// The amount of posts that were made by users that are registered on this server. + pub local_posts: Option, + + /// The amount of comments that were made by users that are registered on this server. + pub local_comments: Option, } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NodeInfo1UsageUsers { - pub total: i32, - pub active_halfyear: i32, - pub active_month: i32, + /// The total amount of on this server registered users. + pub total: Option, + + /// The amount of users that signed in at least once in the last 180 days. + pub active_halfyear: Option, + + /// The amount of users that signed in at least once in the last 30 days. + pub active_month: Option, + + /// The amount of users that signed in at least once in the last 7 days. + pub active_week: Option, } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NodeInfo2Instance { + /// If supported by the software, the administrator-configured name of this instance. pub name: String, + + /// If supported by the software, the administrator-configured long form description of this instance. pub description: String, } + +impl NodeInfo { + /// Discover and get the latest NodeInfo version available given a certain base URL. + /// + /// ## Examples + /// + /// ``` + /// # tokio_test::block_on(async { + /// use acrate_nodeinfo::NodeInfo; + /// + /// let client = reqwest::Client::new(); + /// let url: reqwest::Url = "https://mastodon.online".parse() + /// .expect("URL to be valid"); + /// + /// let ni = NodeInfo::get_latest_wellknown(&client, url) + /// .await + /// .expect("NodeInfo to be processed correctly"); + /// + /// let version = match ni { + /// NodeInfo::V1(ni) => ni.version, + /// NodeInfo::V2(ni) => ni.version, + /// }; + /// + /// assert_eq!(version, "2.0"); + /// # }) + pub async fn get_latest_wellknown(client: &reqwest::Client, url: reqwest::Url) -> Result { + use NodeInfoGetWellknownError::*; + + log::debug!("Getting well-known NodeInfo document at base: {url}"); + + log::trace!("Discovering NodeInfo document locations..."); + let discovery = acrate_hostmeta::ResourceDescriptor::discover(client, url) + .await + .map_err(Discovery)?; + + log::trace!("Getting a list of NodeInfo document links..."); + let mut links = discovery.links.unwrap_or_default(); + links.sort_unstable_by_key(|o| o.rel.clone()); // TODO: Performance can be improved. + links.reverse(); + + for link in links.into_iter() { + + log::trace!("Checking discovered link href..."); + let url = match link.href { + None => { + log::warn!("Discovered link does not have an href, skipping..."); + continue + }, + Some(href) => { + log::trace!("Discovered link has an href, processing..."); + href + }, + }; + + log::trace!("Parsing discovered link href..."); + let url: reqwest::Url = match url.parse() { + Err(e) => { + log::warn!("Discovered link has an invalid URL as href, skipping: {e:#?}"); + continue + }, + Ok(url) => { + log::trace!("Discovered link has a valid URL, processing..."); + url + }, + }; + + let rel = link.rel; + + let nodeinfo = match rel.as_str() { + "http://nodeinfo.diaspora.software/ns/schema/1.0" => match NodeInfo1::get(client, url).await { + Err(e) => { + log::warn!("Failed to get NodeInfo v1.0 document, skipping: {e:#?}"); + continue; + }, + Ok(nodeinfo) => { + log::trace!("Successfully processed NodeInfo v1.0 document!"); + Self::V1(nodeinfo) + } + } + "http://nodeinfo.diaspora.software/ns/schema/1.1" => match NodeInfo1::get(client, url).await { + Err(e) => { + log::warn!("Failed to get NodeInfo v1.1 document, skipping: {e:#?}"); + continue; + }, + Ok(nodeinfo) => { + log::trace!("Successfully processed NodeInfo v1.1 document!"); + Self::V1(nodeinfo) + } + } + "http://nodeinfo.diaspora.software/ns/schema/2.0" => match NodeInfo2::get(client, url).await { + Err(e) => { + log::warn!("Failed to get NodeInfo v2.0 document, skipping: {e:#?}"); + continue; + }, + Ok(nodeinfo) => { + log::trace!("Successfully processed NodeInfo v2.0 document!"); + Self::V2(nodeinfo) + } + } + "http://nodeinfo.diaspora.software/ns/schema/2.1" => match NodeInfo2::get(client, url).await { + Err(e) => { + log::warn!("Failed to get NodeInfo v2.1 document, skipping: {e:#?}"); + continue; + }, + Ok(nodeinfo) => { + log::trace!("Successfully processed NodeInfo v2.1 document!"); + Self::V2(nodeinfo) + } + } + "http://nodeinfo.diaspora.software/ns/schema/2.2" => match NodeInfo2::get(client, url).await { + Err(e) => { + log::warn!("Failed to get NodeInfo v2.2 document, skipping: {e:#?}"); + continue; + }, + Ok(nodeinfo) => { + log::trace!("Successfully processed NodeInfo v2.2 document!"); + Self::V2(nodeinfo) + } + } + _ => { + log::warn!("Discovered link has unknown rel `{rel}`, skipping."); + continue; + }, + }; + + log::trace!("Successfully retrieved latest NodeInfo: {nodeinfo:#?}") + return Ok(nodeinfo); + } + + log::warn!("Ran out of possible NodeInfo sources, returning an Unsupported error."); + Err(Unsupported) + } + + /// Well-known path for NodeInfo documents. + /// + /// ## Specification + /// + /// - https://github.com/jhass/nodeinfo/blob/main/PROTOCOL.md#discovery + pub const WELLKNOWN_NODEINFO_PATH: &str = "/.well-known/nodeinfo"; + + /// Attempt to discover a NodeInfo document at the given base URL. + /// + /// ## Examples + /// + /// ``` + /// # tokio_test::block_on(async { + /// use acrate_hostmeta::ResourceDescriptor; + /// + /// let client = reqwest::Client::new(); + /// let base: reqwest::Url = "https://junimo.party".parse() + /// .expect("URL to be valid"); + /// + /// let rd = NodeInfo::discover(&client, base) + /// .await + /// .expect("NodeInfo to be discovered correctly"); + /// # }) + /// ``` + /// + pub async fn discover(client: &reqwest::Client, mut base: reqwest::Url) -> Result { + base.set_path(Self::WELLKNOWN_NODEINFO_PATH); + + Self::get_latest_wellknown(client, base) + .await + } +} + +/// An error occurred during [`NodeInfo::get_latest_wellknown`]. +#[derive(Debug)] +pub enum NodeInfoGetWellknownError { + /// The discovery of possible locations for NodeInfo documents failed. + Discovery(acrate_hostmeta::ResourceDescriptorDiscoveryError), + /// No compatible NodeInfo documents were detected at the given URL. + Unsupported, +} + +impl NodeInfo1 { + /// Get a NodeInfo v1.X document. + pub async fn get(client: &reqwest::Client, url: reqwest::Url) -> Result { + use NodeInfoGetError::*; + + log::debug!("Getting NodeInfo v1.X document at: {url}"); + + log::trace!("Building request..."); + let request = { + log::trace!("Creating new request..."); + let mut request = reqwest::Request::new(reqwest::Method::GET, url); + + log::trace!("Setting request headers..."); + let headers = request.headers_mut(); + + log::trace!("Setting `Accept: application/json`..."); + let _ = headers.insert(reqwest::header::ACCEPT, "application/json".parse().unwrap()); + + request + }; + + log::trace!("Sending request..."); + let response = client.execute(request) + .await + .map_err(Request)?; + + log::trace!("Checking `Content-Type` of the response..."); + let content_type = response + .headers() + .get(reqwest::header::CONTENT_TYPE) + .ok_or(ContentTypeMissing)?; + + log::trace!("Extracting MIME type from the `Content-Type` header..."); + let mime_type = extract_mime_from_content_type(content_type) + .ok_or(ContentTypeInvalid)?; + + log::trace!("Ensuring MIME type is acceptable for NodeInfo documents..."); + if mime_type != "application/json" { + log::error!("MIME type `{mime_type}` is not acceptable for NodeInfo documents."); + return Err(ContentTypeInvalid) + } + + log::trace!("Attempting to parse response as JSON..."); + let data = response.json::() + .await + .map_err(Parse)?; + + log::trace!("Making sure version is compatible with 1.X..."); + if !data.version.starts_with("1.") { + return Err(Version); + } + + Ok(data) + } +} + +impl NodeInfo2 { + /// Get a NodeInfo v2.X document. + /// + /// ## Examples + /// + /// ``` + /// # tokio_test::block_on(async { + /// use acrate_nodeinfo::NodeInfo2; + /// + /// let client = reqwest::Client::new(); + /// let url: reqwest::Url = "https://junimo.party/nodeinfo/2.1.json".parse() + /// .expect("URL to be valid"); + /// + /// let rd = NodeInfo2::get(&client, url) + /// .await + /// .expect("NodeInfo to be obtained correctly"); + /// # }) + /// ``` + pub async fn get(client: &reqwest::Client, url: reqwest::Url) -> Result { + use NodeInfoGetError::*; + + log::debug!("Getting NodeInfo v2.X document at: {url}"); + + log::trace!("Building request..."); + let request = { + log::trace!("Creating new request..."); + let mut request = reqwest::Request::new(reqwest::Method::GET, url); + + log::trace!("Setting request headers..."); + let headers = request.headers_mut(); + + log::trace!("Setting `Accept: application/json`..."); + let _ = headers.insert(reqwest::header::ACCEPT, "application/json".parse().unwrap()); + + request + }; + + log::trace!("Sending request..."); + let response = client.execute(request) + .await + .map_err(Request)?; + + log::trace!("Checking `Content-Type` of the response..."); + let content_type = response + .headers() + .get(reqwest::header::CONTENT_TYPE) + .ok_or(ContentTypeMissing)?; + + log::trace!("Extracting MIME type from the `Content-Type` header..."); + let mime_type = extract_mime_from_content_type(content_type) + .ok_or(ContentTypeInvalid)?; + + log::trace!("Ensuring MIME type is acceptable for NodeInfo documents..."); + if mime_type != "application/json" { + log::error!("MIME type `{mime_type}` is not acceptable for NodeInfo documents."); + return Err(ContentTypeInvalid) + } + + log::trace!("Attempting to parse response as JSON..."); + let data = response.json::() + .await + .map_err(Parse)?; + + log::trace!("Making sure version is compatible with 2.X..."); + if !data.version.starts_with("2.") { + return Err(Version) + } + + Ok(data) + } +} + +/// An error encountered during [`NodeInfo1::get`] or [`NodeInfo2::get`]. +#[derive(Debug)] +pub enum NodeInfoGetError { + /// The HTTP request failed. + Request(reqwest::Error), + /// The `Content-Type` header of the response is missing. + ContentTypeMissing, + /// The `Content-Type` header of the response is invalid. + ContentTypeInvalid, + /// The document failed to be parsed as JSON by [`reqwest`]. + Parse(reqwest::Error), + /// The returned version does not match the version of the created struct. + Version, +} + +/// Extract the MIME type from the value of the `Content-Type` header. +fn extract_mime_from_content_type(value: &reqwest::header::HeaderValue) -> Option { + let value = value.to_str().ok()?; + match value.split_once("; ") { + None => Some(value.to_string()), + Some((mime, _)) => Some(mime.to_string()), + } +} diff --git a/acrate-nodeinfo/tests/nodeinfo_tests.rs b/acrate-nodeinfo/tests/nodeinfo_tests.rs new file mode 100644 index 0000000..e9e2fba --- /dev/null +++ b/acrate-nodeinfo/tests/nodeinfo_tests.rs @@ -0,0 +1,70 @@ +const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME"); +const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); +const CARGO_PKG_REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY"); + + +fn init_log() { + let mut builder = pretty_env_logger::formatted_builder(); + builder.target(pretty_env_logger::env_logger::Target::Stdout); + builder.filter_level(log::LevelFilter::max()); + builder.is_test(true); + + if builder.try_init().is_ok() { + log::debug!("Initialized logging!"); + } +} + +fn make_client() -> reqwest::Client { + let user_agent = format!("{CARGO_PKG_NAME}/{CARGO_PKG_VERSION} ({CARGO_PKG_REPOSITORY})"); + + reqwest::Client::builder() + .user_agent(user_agent) + .build() + .expect("reqwest client to build") +} + + +macro_rules! test { + ($id:ident, $url:literal, $version:literal) => { + test!($id, $url, $version,); + }; + ($id:ident, $url:literal, $version:literal, $($tag:meta),*) => { + mod $id { + use acrate_nodeinfo::*; + use super::*; + + #[tokio::test] + $(#[$tag])* + async fn test_version() { + init_log(); + let client = make_client(); + + let base: reqwest::Url = $url.parse() + .expect("a valid URL"); + + let doc = NodeInfo::get_latest_wellknown(&client, base) + .await + .expect("NodeInfo discovery to succeed"); + + log::info!("Parsed NodeInfo document: {doc:#?}"); + + let version = match doc { + NodeInfo::V1(d) => d.version, + NodeInfo::V2(d) => d.version, + }; + + assert_eq!(version, $version); + } + } + }; +} + +test!(akkoma, "https://junimo.party", "2.1"); +test!(mastodon, "https://mastodon.social", "2.0"); +test!(misskey, "https://misskey.io", "2.1"); +test!(iceshrimpnet, "https://ice.frieren.quest", "2.1"); +test!(gotosocial, "https://alpha.polymaths.social", "2.0"); +test!(bridgyfed, "https://fed.brid.gy", "2.1"); +test!(threads, "https://threads.net", "", ignore = "does not support NodeInfo"); +test!(snac, "https://ngoa.giao.loan", "2.0"); +test!(hollo, "https://hollo.social", "2.1"); From b46a995c09efd68cc7f4e64468c118c3357fda80 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 11 Nov 2024 08:31:09 +0100 Subject: [PATCH 40/43] i lied and i did it now --- acrate-nodeinfo/src/lib.rs | 52 ++++++++++---------------------------- 1 file changed, 14 insertions(+), 38 deletions(-) diff --git a/acrate-nodeinfo/src/lib.rs b/acrate-nodeinfo/src/lib.rs index 0fbf5e4..fe8ea02 100644 --- a/acrate-nodeinfo/src/lib.rs +++ b/acrate-nodeinfo/src/lib.rs @@ -2,8 +2,6 @@ //! //! > NodeInfo is an effort to create a standardized way of exposing metadata about a server running one of the distributed social networks. -use std::collections::HashMap; - use serde::Deserialize; /// A variant of a NodeInfo document. @@ -163,6 +161,13 @@ pub struct NodeInfo2Instance { } impl NodeInfo { + /// Well-known path for NodeInfo documents. + /// + /// ## Specification + /// + /// - https://github.com/jhass/nodeinfo/blob/main/PROTOCOL.md#discovery + pub const WELLKNOWN_NODEINFO_PATH: &str = "/.well-known/nodeinfo"; + /// Discover and get the latest NodeInfo version available given a certain base URL. /// /// ## Examples @@ -186,13 +191,16 @@ impl NodeInfo { /// /// assert_eq!(version, "2.0"); /// # }) - pub async fn get_latest_wellknown(client: &reqwest::Client, url: reqwest::Url) -> Result { + pub async fn get_latest_wellknown(client: &reqwest::Client, mut base: reqwest::Url) -> Result { use NodeInfoGetWellknownError::*; - log::debug!("Getting well-known NodeInfo document at base: {url}"); + log::debug!("Getting well-known NodeInfo document at base: {base}"); + + log::trace!("Setting URL path to the well-known NodeInfo value..."); + base.set_path(Self::WELLKNOWN_NODEINFO_PATH); log::trace!("Discovering NodeInfo document locations..."); - let discovery = acrate_hostmeta::ResourceDescriptor::discover(client, url) + let discovery = acrate_hostmeta::ResourceDescriptor::discover(client, base) .await .map_err(Discovery)?; @@ -286,45 +294,13 @@ impl NodeInfo { }, }; - log::trace!("Successfully retrieved latest NodeInfo: {nodeinfo:#?}") + log::trace!("Successfully retrieved latest NodeInfo: {nodeinfo:#?}"); return Ok(nodeinfo); } log::warn!("Ran out of possible NodeInfo sources, returning an Unsupported error."); Err(Unsupported) } - - /// Well-known path for NodeInfo documents. - /// - /// ## Specification - /// - /// - https://github.com/jhass/nodeinfo/blob/main/PROTOCOL.md#discovery - pub const WELLKNOWN_NODEINFO_PATH: &str = "/.well-known/nodeinfo"; - - /// Attempt to discover a NodeInfo document at the given base URL. - /// - /// ## Examples - /// - /// ``` - /// # tokio_test::block_on(async { - /// use acrate_hostmeta::ResourceDescriptor; - /// - /// let client = reqwest::Client::new(); - /// let base: reqwest::Url = "https://junimo.party".parse() - /// .expect("URL to be valid"); - /// - /// let rd = NodeInfo::discover(&client, base) - /// .await - /// .expect("NodeInfo to be discovered correctly"); - /// # }) - /// ``` - /// - pub async fn discover(client: &reqwest::Client, mut base: reqwest::Url) -> Result { - base.set_path(Self::WELLKNOWN_NODEINFO_PATH); - - Self::get_latest_wellknown(client, base) - .await - } } /// An error occurred during [`NodeInfo::get_latest_wellknown`]. From a7d095fee0f8da347b937d80f7ac26aa49f409a4 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 11 Nov 2024 08:43:22 +0100 Subject: [PATCH 41/43] Add some sort of docs --- acrate-hostmeta/src/lib.rs | 107 +++++++++++++++++++++---------------- acrate-nodeinfo/src/lib.rs | 30 ++++++----- 2 files changed, 79 insertions(+), 58 deletions(-) diff --git a/acrate-hostmeta/src/lib.rs b/acrate-hostmeta/src/lib.rs index 81dcb55..57ecd2b 100644 --- a/acrate-hostmeta/src/lib.rs +++ b/acrate-hostmeta/src/lib.rs @@ -1,7 +1,9 @@ //! Resource descriptior deserializer. //! -//! [RFC 6415]: https://datatracker.ietf.org/doc/html/rfc6415 -//! [RFC 7033]: https://datatracker.ietf.org/doc/html/rfc7033#section-4.4 +//! # Specification +//! +//! - +//! - use std::collections::HashMap; @@ -9,105 +11,118 @@ use serde::Deserialize; /// A resource descriptor object. /// -/// ## Specification +/// # Specification +/// +/// - +/// - /// -/// - https://datatracker.ietf.org/doc/html/rfc6415#section-3 -/// - https://datatracker.ietf.org/doc/html/rfc7033#section-4.4 #[derive(Debug, Clone, Deserialize)] pub struct ResourceDescriptor { /// The resource this document refers to. /// - /// ## Specification + /// # Specification + /// + /// - /// - /// - https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.1 #[serde(alias = "Subject")] pub subject: Option, /// Other names the resource described by this document can be referred to. /// - /// ## Specification + /// # Specification + /// + /// - /// - /// - https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.2 #[serde(alias = "Alias")] pub aliases: Option>, /// Additional information about the resource described by this document. /// - /// ## Specification + /// # Specification + /// + /// - /// - /// - https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.3 #[serde(alias = "Property")] pub properties: Option>, /// Links established between the [`Self::subject`] and other resources. /// - /// ## Specification + /// # Specification + /// + /// - + /// - /// - /// - https://datatracker.ietf.org/doc/html/rfc6415#section-3.1.1 - /// - https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4 #[serde(alias = "Link")] pub links: Option>, } /// A link element, which puts the subject resource in relation with another. /// -/// ## Specification +/// # Specification +/// +/// - /// -/// - https://datatracker.ietf.org/doc/html/rfc6415#section-3.1.1 #[derive(Debug, Clone, Deserialize)] pub struct ResourceDescriptorLink { /// The kind of relation established by the subject with the attached resource. /// - /// ## Specification + /// # Specification + /// + /// - /// - /// https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.1 #[serde(alias = "@rel")] pub rel: String, /// The media type of the resource put in relation. /// - /// ## Specification + /// # Specification + /// + /// - /// - /// - https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.2 #[serde(alias = "@type")] pub r#type: Option, /// URI to the resource put in relation. /// - /// ## Specification + /// # Specification + /// + /// - /// - /// - https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.3 #[serde(alias = "@href")] pub href: Option, /// Titles of the resource put in relation in various languages. /// - /// ## Specification + /// # Specification + /// + /// - /// - /// - https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.4 pub titles: Option>>, /// Additional information about the resource put in relation. /// - /// ## Specification + /// # Specification + /// + /// - /// - /// - https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.5 pub properties: Option>, /// Template to fill to get the URL to resource-specific information. /// - /// ## Specification + /// # Specification + /// + /// - /// - /// - https://datatracker.ietf.org/doc/html/rfc6415#section-4.2 #[serde(alias = "@template")] pub template: Option, } /// A property element, which describes a certain aspect of the subject resource. /// -/// ## Specification +/// # Specification +/// +/// - /// -/// - https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.3 #[derive(Debug, Clone, Deserialize)] pub struct ResourceDescriptorProperty { /// The property identifier, or type. @@ -121,11 +136,11 @@ pub struct ResourceDescriptorProperty { impl ResourceDescriptor { /// Get a JRD (JSON [`ResourceDescriptor`]). /// - /// ## Notes + /// # Notes /// /// This follows redirects until the redirect chain is 10 hops; see [`reqwest::redirect`] for more info. /// - /// ## Examples + /// # Examples /// /// ``` /// # tokio_test::block_on(async { @@ -191,11 +206,11 @@ impl ResourceDescriptor { /// Get a XRD (Extensible [`ResourceDescriptor`]). /// - /// ## Notes + /// # Notes /// /// This follows redirects until the redirect chain is 10 hops; see [`reqwest::redirect`] for more info. /// - /// ## Examples + /// # Examples /// /// ``` /// # tokio_test::block_on(async { @@ -210,6 +225,7 @@ impl ResourceDescriptor { /// .expect("XRD to be processed correctly"); /// # }) /// ``` + /// pub async fn get_xrd(client: &reqwest::Client, url: reqwest::Url) -> Result { use GetXRDError::*; @@ -266,18 +282,18 @@ impl ResourceDescriptor { /// /// In order, this method attempts: /// - /// 1. [HTTPS] [XRD](Self::get_xrd) - /// 2. [HTTPS] [JRD](Self::get_jrd) - /// 3. [HTTPS] [JRD](Self::get_jrd) with .json path extension - /// 4. [HTTP] [XRD](Self::get_xrd) - /// 5. [HTTP] [JRD](Self::get_jrd) - /// 6. [HTTP] [JRD](Self::get_jrd) with .json path extension + /// 1. HTTPS [XRD](Self::get_xrd) + /// 2. HTTPS [JRD](Self::get_jrd) + /// 3. HTTPS [JRD](Self::get_jrd) with .json path extension + /// 4. HTTP [XRD](Self::get_xrd) + /// 5. HTTP [JRD](Self::get_jrd) + /// 6. HTTP [JRD](Self::get_jrd) with .json path extension /// - /// ## Notes + /// # Notes /// /// This follows redirects until the redirect chain is 10 hops; see [`reqwest::redirect`] for more info. /// - /// ## Examples + /// # Examples /// /// ``` /// # tokio_test::block_on(async { @@ -428,14 +444,15 @@ impl ResourceDescriptor { /// Well-known path for host-meta documents. /// - /// ## Specification + /// # Specification + /// + /// - /// - /// - https://datatracker.ietf.org/doc/html/rfc6415#section-2 pub const WELLKNOWN_HOSTMETA_PATH: &str = "/.well-known/host-meta"; /// Attempt to discover a host-meta document at the given base URL. /// - /// ## Examples + /// # Examples /// /// ``` /// # tokio_test::block_on(async { diff --git a/acrate-nodeinfo/src/lib.rs b/acrate-nodeinfo/src/lib.rs index fe8ea02..0123a3d 100644 --- a/acrate-nodeinfo/src/lib.rs +++ b/acrate-nodeinfo/src/lib.rs @@ -1,14 +1,18 @@ //! Serde-based NodeInfo fetcher and loose parser. //! //! > NodeInfo is an effort to create a standardized way of exposing metadata about a server running one of the distributed social networks. +//! +//! # Specification +//! +//! - use serde::Deserialize; /// A variant of a NodeInfo document. /// -/// ## Specification +/// # Specification /// -/// - https://github.com/jhass/nodeinfo/blob/main/PROTOCOL.md +/// - #[derive(Debug, Clone)] pub enum NodeInfo { V1(NodeInfo1), @@ -17,10 +21,10 @@ pub enum NodeInfo { /// A NodeInfo document at version 1.X. /// -/// ## Specification +/// # Specification /// -/// - https://github.com/jhass/nodeinfo/blob/main/schemas/1.0/schema.json -/// - https://github.com/jhass/nodeinfo/blob/main/schemas/1.1/schema.json +/// - +/// - /// #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] @@ -51,11 +55,11 @@ pub struct NodeInfo1 { /// A NodeInfo document at version 2.X. /// -/// ## Specification +/// # Specification /// -/// - https://github.com/jhass/nodeinfo/blob/main/schemas/2.0/schema.json -/// - https://github.com/jhass/nodeinfo/blob/main/schemas/2.1/schema.json -/// - https://github.com/jhass/nodeinfo/blob/main/schemas/2.2/schema.json +/// - +/// - +/// - /// #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] @@ -163,14 +167,14 @@ pub struct NodeInfo2Instance { impl NodeInfo { /// Well-known path for NodeInfo documents. /// - /// ## Specification + /// # Specification /// - /// - https://github.com/jhass/nodeinfo/blob/main/PROTOCOL.md#discovery + /// - pub const WELLKNOWN_NODEINFO_PATH: &str = "/.well-known/nodeinfo"; /// Discover and get the latest NodeInfo version available given a certain base URL. /// - /// ## Examples + /// # Examples /// /// ``` /// # tokio_test::block_on(async { @@ -371,7 +375,7 @@ impl NodeInfo1 { impl NodeInfo2 { /// Get a NodeInfo v2.X document. /// - /// ## Examples + /// # Examples /// /// ``` /// # tokio_test::block_on(async { From 77e76d9dfb92e0af3a709cc3a40f00e45e6fbf62 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 11 Nov 2024 08:43:35 +0100 Subject: [PATCH 42/43] `core`: Blank out the file --- acrate-core/src/lib.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/acrate-core/src/lib.rs b/acrate-core/src/lib.rs index b93cf3f..e69de29 100644 --- a/acrate-core/src/lib.rs +++ b/acrate-core/src/lib.rs @@ -1,14 +0,0 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} From c2da1723cf2f3a967c1b4605b8d74ac4cf044068 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 11 Nov 2024 08:46:16 +0100 Subject: [PATCH 43/43] `core`: Re-export all subcrates --- acrate-core/Cargo.toml | 2 ++ acrate-core/src/lib.rs | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/acrate-core/Cargo.toml b/acrate-core/Cargo.toml index bef29b4..6823787 100644 --- a/acrate-core/Cargo.toml +++ b/acrate-core/Cargo.toml @@ -6,6 +6,8 @@ edition = "2021" [dependencies] diesel = "2.2.4" diesel_migrations = "2.2.0" +acrate-hostmeta = { path = "../acrate-hostmeta" } +acrate-nodeinfo = { path = "../acrate-nodeinfo" } [lints.clippy] tabs-in-doc-comments = "allow" diff --git a/acrate-core/src/lib.rs b/acrate-core/src/lib.rs index e69de29..dae5ee7 100644 --- a/acrate-core/src/lib.rs +++ b/acrate-core/src/lib.rs @@ -0,0 +1,4 @@ +//! Core crate of the `acrate` project. + +pub use acrate_nodeinfo as nodeinfo; +pub use acrate_hostmeta as hostmeta;