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}