//! Serde-based [RFC 6415] `host-meta` parser. //! //! [RFC 6415]: https://datatracker.ietf.org/doc/html/rfc6415 use serde::{Serialize, Deserialize}; /// 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, } /// 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 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 host-meta JRD 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 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 response as JSON..."); let data = response.json::() .await .map_err(Parse)?; Ok(data) } /// 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::*; log::debug!("Getting host-meta XRD 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/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); 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 XRD host-meta via HTTPS..."); let https_xrd = match Self::get_xrd(client, url.clone()).await { Ok(data) => { log::trace!("HTTPS XRD retrieval was successful, returning..."); return Ok(data) } Err(err) => { 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 } }; log::trace!("Setting URL scheme to HTTP..."); 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 { Ok(data) => { log::trace!("HTTP XRD retrieval was successful, returning..."); return Ok(data) } Err(err) => { 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( 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 } } #[derive(Debug)] 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), } #[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 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, } #[derive(Debug)] 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 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), } #[derive(Debug)] 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), }