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