Skip to main content

webfinger_rs/
reqwest.rs

1use std::sync::Once;
2
3use http::Uri;
4use tracing::trace;
5
6use crate::error::Error;
7use crate::{WebFingerRequest, WebFingerResponse};
8
9struct EmptyBody;
10
11static DEFAULT_CRYPTO_PROVIDER: Once = Once::new();
12
13fn install_default_crypto_provider() {
14    DEFAULT_CRYPTO_PROVIDER.call_once(|| {
15        let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
16    });
17}
18
19fn webfinger_reqwest_client() -> Result<reqwest::Client, reqwest::Error> {
20    install_default_crypto_provider();
21    reqwest::Client::builder().https_only(true).build()
22}
23
24impl From<EmptyBody> for reqwest::Body {
25    fn from(_: EmptyBody) -> reqwest::Body {
26        reqwest::Body::default()
27    }
28}
29
30impl TryFrom<&WebFingerRequest> for http::Request<EmptyBody> {
31    type Error = http::Error;
32
33    fn try_from(query: &WebFingerRequest) -> Result<http::Request<EmptyBody>, http::Error> {
34        let uri = Uri::try_from(query)?;
35        http::Request::builder()
36            .method("GET")
37            .uri(uri)
38            .body(EmptyBody)
39    }
40}
41
42impl TryFrom<&WebFingerRequest> for reqwest::Request {
43    type Error = crate::Error;
44
45    fn try_from(query: &WebFingerRequest) -> Result<reqwest::Request, crate::Error> {
46        let request = http::Request::try_from(query)?;
47        let request = reqwest::Request::try_from(request)?;
48        Ok(request)
49    }
50}
51
52impl WebFingerRequest {
53    /// Executes the WebFinger request with a fresh [`reqwest::Client`].
54    ///
55    /// This is the shortest path from a [`WebFingerRequest`] to a parsed [`WebFingerResponse`].
56    /// The method:
57    ///
58    /// 1. Converts the WebFinger query into a `GET` [`reqwest::Request`].
59    /// 1. Creates a new [`reqwest::Client`] that only sends HTTPS requests, including redirects.
60    /// 1. Sends the request with that client.
61    /// 1. Rejects non-success HTTP statuses with [`reqwest::Response::error_for_status`].
62    /// 1. Deserializes the response body as JSON into [`WebFingerResponse`].
63    ///
64    /// Use this when the first-party WebFinger client configuration is sufficient. This path
65    /// follows RFC 7033's HTTPS-only transport requirements by rejecting redirects to non-HTTPS
66    /// targets. If you need shared connection pooling, custom headers, middleware, proxies,
67    /// timeouts, or TLS settings, prefer [`Self::execute_reqwest_with_client`] instead.
68    ///
69    /// Errors are returned as [`crate::Error`]:
70    ///
71    /// - Request-construction failures surface as [`crate::Error::Http`] or
72    ///   [`crate::Error::InvalidUri`].
73    /// - Reqwest client-construction failures, transport failures, non-success HTTP statuses, and
74    ///   JSON decoding failures surface as [`crate::Error::Reqwest`].
75    ///
76    /// # Examples
77    ///
78    /// ```rust,no_run
79    /// use webfinger_rs::WebFingerRequest;
80    ///
81    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
82    /// let request = WebFingerRequest::builder("acct:carol@example.com")?
83    ///     .host("example.com")
84    ///     .rel("http://webfinger.net/rel/profile-page")
85    ///     .build();
86    ///
87    /// let response = request.execute_reqwest().await?;
88    /// println!("{response:#?}");
89    /// # Ok(())
90    /// # }
91    /// ```
92    #[tracing::instrument]
93    pub async fn execute_reqwest(&self) -> Result<WebFingerResponse, Error> {
94        let client = webfinger_reqwest_client()?;
95        self.execute_reqwest_with_client(&client).await
96    }
97
98    /// Executes the WebFinger request with a caller-provided [`reqwest::Client`].
99    ///
100    /// This follows the same conversion, status handling, and JSON decoding path as
101    /// [`Self::execute_reqwest`], but reuses the client you provide instead of constructing a new
102    /// WebFinger-specific one for each call.
103    ///
104    /// RFC 7033 requires clients to query WebFinger resources using HTTPS only and allows redirects
105    /// only to HTTPS URIs. Caller-provided clients are used as-is, so configure them to reject
106    /// non-HTTPS requests and redirect targets when you need RFC-compliant WebFinger execution.
107    ///
108    /// Use this when your application already owns a configured client, for example to:
109    ///
110    /// - reuse connection pools across multiple requests;
111    /// - set default headers, user agents, or auth;
112    /// - configure timeouts, proxies, redirects, or TLS behavior; or
113    /// - integrate with Reqwest middleware or client-wide instrumentation.
114    ///
115    /// Non-success HTTP statuses and JSON decoding failures still surface as
116    /// [`crate::Error::Reqwest`], because they originate from Reqwest's response handling.
117    ///
118    /// # Examples
119    ///
120    /// ```rust,no_run
121    /// use std::time::Duration;
122    ///
123    /// use reqwest::Client;
124    /// use webfinger_rs::WebFingerRequest;
125    ///
126    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
127    /// let client = Client::builder()
128    ///     .timeout(Duration::from_secs(10))
129    ///     .user_agent("webfinger-rs docs example")
130    ///     .https_only(true)
131    ///     .build()?;
132    ///
133    /// let request = WebFingerRequest::builder("acct:carol@example.com")?
134    ///     .host("example.com")
135    ///     .build();
136    ///
137    /// let response = request.execute_reqwest_with_client(&client).await?;
138    /// println!("{response:#?}");
139    /// # Ok(())
140    /// # }
141    /// ```
142    #[tracing::instrument]
143    pub async fn execute_reqwest_with_client(
144        &self,
145        client: &reqwest::Client,
146    ) -> Result<WebFingerResponse, Error> {
147        let request = self.try_into()?;
148        trace!("request: {:?}", request);
149        let response = client.execute(request).await?;
150        trace!("response: {:?}", response);
151        async_convert::TryFrom::try_from(response).await
152    }
153
154    /// Converts this WebFinger query into a [`reqwest::Request`] without executing it.
155    ///
156    /// This is useful when you want to inspect or modify the outgoing request before sending it,
157    /// or when another part of your application is responsible for execution.
158    ///
159    /// The resulting request is an HTTPS `GET` to the WebFinger well-known endpoint with the
160    /// current `resource`, `host`, and `rel` values encoded into the URL.
161    ///
162    /// This only performs request construction. It does not send anything over the network.
163    ///
164    /// # Examples
165    ///
166    /// ```rust
167    /// use webfinger_rs::WebFingerRequest;
168    ///
169    /// let request = WebFingerRequest::builder("acct:carol@example.com")?
170    ///     .host("example.com")
171    ///     .rel("http://webfinger.net/rel/profile-page")
172    ///     .build();
173    ///
174    /// let reqwest_request = request.try_into_reqwest()?;
175    /// assert_eq!(reqwest_request.method(), reqwest::Method::GET);
176    /// assert_eq!(
177    ///     reqwest_request.url().as_str(),
178    ///     "https://example.com/.well-known/webfinger?resource=acct%3Acarol%40example.com&rel=http%3A%2F%2Fwebfinger.net%2Frel%2Fprofile-page"
179    /// );
180    /// # Ok::<(), Box<dyn std::error::Error>>(())
181    /// ```
182    pub fn try_into_reqwest(&self) -> Result<reqwest::Request, Error> {
183        self.try_into()
184    }
185}
186
187impl WebFingerResponse {
188    /// Converts a completed [`reqwest::Response`] into a [`WebFingerResponse`].
189    ///
190    /// This is useful when you execute the HTTP request yourself, but still want this crate's
191    /// WebFinger response parsing behavior.
192    ///
193    /// The conversion:
194    ///
195    /// 1. Rejects non-success HTTP statuses with [`reqwest::Response::error_for_status`].
196    /// 1. Deserializes the response body as JSON into [`WebFingerResponse`].
197    ///
198    /// Both status failures and JSON decoding failures surface as [`crate::Error::Reqwest`].
199    ///
200    /// # Examples
201    ///
202    /// ```rust,no_run
203    /// use reqwest::Client;
204    /// use webfinger_rs::{WebFingerRequest, WebFingerResponse};
205    ///
206    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
207    /// let client = Client::new();
208    /// let request = WebFingerRequest::builder("acct:carol@example.com")?
209    ///     .host("example.com")
210    ///     .build()
211    ///     .try_into_reqwest()?;
212    ///
213    /// let response = client.execute(request).await?;
214    /// let webfinger = WebFingerResponse::try_from_reqwest(response).await?;
215    /// println!("{webfinger:#?}");
216    /// # Ok(())
217    /// # }
218    /// ```
219    pub async fn try_from_reqwest(response: reqwest::Response) -> Result<WebFingerResponse, Error> {
220        async_convert::TryFrom::try_from(response).await
221    }
222}
223
224#[async_convert::async_trait]
225impl async_convert::TryFrom<reqwest::Response> for WebFingerResponse {
226    type Error = crate::Error;
227
228    async fn try_from(response: reqwest::Response) -> Result<WebFingerResponse, crate::Error> {
229        let response = response.error_for_status()?;
230        let response = response.json().await?;
231        Ok(response)
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use reqwest::Method;
238
239    use super::*;
240
241    /// RFC 7033 sections 4.2 and 9.1 require WebFinger clients to use HTTPS-only transport. The
242    /// first-party client should reject an HTTP URL before attempting network I/O; the same Reqwest
243    /// setting also applies to redirect targets.
244    #[tokio::test]
245    async fn default_webfinger_client_rejects_non_https_requests() {
246        let client = webfinger_reqwest_client().unwrap();
247        let url = "http://127.0.0.1:9/.well-known/webfinger?resource=acct:carol@example.org"
248            .parse()
249            .unwrap();
250        let request = reqwest::Request::new(Method::GET, url);
251
252        let error = client.execute(request).await.unwrap_err();
253
254        assert!(error.is_builder());
255        assert_eq!(error.url().map(reqwest::Url::scheme), Some("http"));
256    }
257}