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");