1use core::str::FromStr;
4use core::time::Duration;
5
6use async_trait::async_trait;
7use reqwest::{header, Proxy};
8
9use tendermint::{block::Height, evidence::Evidence, Hash};
10use tendermint_config::net;
11
12use crate::client::{Client, CompatMode};
13use crate::dialect::{v0_34, v0_37, v0_38, Dialect, LatestDialect};
14use crate::endpoint;
15use crate::prelude::*;
16use crate::query::Query;
17use crate::request::RequestMessage;
18use crate::response::Response;
19use crate::{Error, Order, Scheme, SimpleRequest, Url};
20
21use super::auth;
22
23const USER_AGENT: &str = concat!("tendermint.rs/", env!("CARGO_PKG_VERSION"));
24
25#[derive(Debug, Clone)]
52pub struct HttpClient {
53 inner: reqwest::Client,
54 url: reqwest::Url,
55 compat: CompatMode,
56}
57
58pub struct Builder {
60 url: HttpClientUrl,
61 compat: CompatMode,
62 proxy_url: Option<HttpClientUrl>,
63 user_agent: Option<String>,
64 timeout: Duration,
65 client: Option<reqwest::Client>,
66}
67
68impl Builder {
69 pub fn compat_mode(mut self, mode: CompatMode) -> Self {
73 self.compat = mode;
74 self
75 }
76
77 pub fn proxy_url(mut self, url: HttpClientUrl) -> Self {
84 self.proxy_url = Some(url);
85 self
86 }
87
88 pub fn timeout(mut self, duration: Duration) -> Self {
93 self.timeout = duration;
94 self
95 }
96
97 pub fn user_agent(mut self, agent: String) -> Self {
99 self.user_agent = Some(agent);
100 self
101 }
102
103 pub fn client(mut self, client: reqwest::Client) -> Self {
109 self.client = Some(client);
110 self
111 }
112
113 pub fn build(self) -> Result<HttpClient, Error> {
115 let inner = if let Some(inner) = self.client {
116 inner
117 } else {
118 let builder = reqwest::ClientBuilder::new()
119 .user_agent(self.user_agent.unwrap_or_else(|| USER_AGENT.to_string()))
120 .timeout(self.timeout);
121
122 match self.proxy_url {
123 None => builder.build().map_err(Error::http)?,
124 Some(proxy_url) => {
125 let proxy = if self.url.0.is_secure() {
126 Proxy::https(reqwest::Url::from(proxy_url.0))
127 .map_err(Error::invalid_proxy)?
128 } else {
129 Proxy::http(reqwest::Url::from(proxy_url.0))
130 .map_err(Error::invalid_proxy)?
131 };
132 builder.proxy(proxy).build().map_err(Error::http)?
133 },
134 }
135 };
136
137 Ok(HttpClient {
138 inner,
139 url: self.url.into(),
140 compat: self.compat,
141 })
142 }
143}
144
145impl HttpClient {
146 pub fn new_from_parts(inner: reqwest::Client, url: reqwest::Url, compat: CompatMode) -> Self {
150 Self { inner, url, compat }
151 }
152
153 pub fn new<U>(url: U) -> Result<Self, Error>
156 where
157 U: TryInto<HttpClientUrl, Error = Error>,
158 {
159 let url = url.try_into()?;
160 Self::builder(url).build()
161 }
162
163 pub fn new_with_proxy<U, P>(url: U, proxy_url: P) -> Result<Self, Error>
171 where
172 U: TryInto<HttpClientUrl, Error = Error>,
173 P: TryInto<HttpClientUrl, Error = Error>,
174 {
175 let url = url.try_into()?;
176 Self::builder(url).proxy_url(proxy_url.try_into()?).build()
177 }
178
179 pub fn builder(url: HttpClientUrl) -> Builder {
183 Builder {
184 url,
185 compat: Default::default(),
186 proxy_url: None,
187 user_agent: None,
188 timeout: Duration::from_secs(30),
189 client: None,
190 }
191 }
192
193 pub fn set_compat_mode(&mut self, compat: CompatMode) {
199 self.compat = compat;
200 }
201
202 fn build_request<R>(&self, request: R) -> Result<reqwest::Request, Error>
203 where
204 R: RequestMessage,
205 {
206 let request_body = request.into_json();
207
208 tracing::debug!(url = %self.url, body = %request_body, "outgoing request");
209
210 let mut builder = self
211 .inner
212 .post(auth::strip_authority(self.url.clone()))
213 .header(header::CONTENT_TYPE, "application/json")
214 .body(request_body.into_bytes());
215
216 if let Some(auth) = auth::authorize(&self.url) {
217 builder = builder.header(header::AUTHORIZATION, auth.to_string());
218 }
219
220 builder.build().map_err(Error::http)
221 }
222
223 async fn perform_with_dialect<R, S>(&self, request: R, _dialect: S) -> Result<R::Output, Error>
224 where
225 R: SimpleRequest<S>,
226 S: Dialect,
227 {
228 let request = self.build_request(request)?;
229 let response = self.inner.execute(request).await.map_err(Error::http)?;
230 let response_status = response.status();
231 let response_body = response.bytes().await.map_err(Error::http)?;
232
233 tracing::debug!(
234 status = %response_status,
235 body = %String::from_utf8_lossy(&response_body),
236 "incoming response"
237 );
238
239 if response_status != reqwest::StatusCode::OK {
244 return Err(Error::http_request_failed(response_status));
245 }
246
247 R::Response::from_string(&response_body).map(Into::into)
248 }
249}
250
251#[async_trait]
252impl Client for HttpClient {
253 async fn perform<R>(&self, request: R) -> Result<R::Output, Error>
254 where
255 R: SimpleRequest,
256 {
257 self.perform_with_dialect(request, LatestDialect).await
258 }
259
260 async fn block<H>(&self, height: H) -> Result<endpoint::block::Response, Error>
261 where
262 H: Into<Height> + Send,
263 {
264 perform_with_compat!(self, endpoint::block::Request::new(height.into()))
265 }
266
267 async fn block_by_hash(
268 &self,
269 hash: tendermint::Hash,
270 ) -> Result<endpoint::block_by_hash::Response, Error> {
271 perform_with_compat!(self, endpoint::block_by_hash::Request::new(hash))
272 }
273
274 async fn latest_block(&self) -> Result<endpoint::block::Response, Error> {
275 perform_with_compat!(self, endpoint::block::Request::default())
276 }
277
278 async fn block_results<H>(&self, height: H) -> Result<endpoint::block_results::Response, Error>
279 where
280 H: Into<Height> + Send,
281 {
282 perform_with_compat!(self, endpoint::block_results::Request::new(height.into()))
283 }
284
285 async fn latest_block_results(&self) -> Result<endpoint::block_results::Response, Error> {
286 perform_with_compat!(self, endpoint::block_results::Request::default())
287 }
288
289 async fn block_search(
290 &self,
291 query: Query,
292 page: u32,
293 per_page: u8,
294 order: Order,
295 ) -> Result<endpoint::block_search::Response, Error> {
296 perform_with_compat!(
297 self,
298 endpoint::block_search::Request::new(query, page, per_page, order)
299 )
300 }
301
302 async fn header<H>(&self, height: H) -> Result<endpoint::header::Response, Error>
303 where
304 H: Into<Height> + Send,
305 {
306 let height = height.into();
307 match self.compat {
308 CompatMode::V0_38 => {
309 self.perform_with_dialect(endpoint::header::Request::new(height), v0_38::Dialect)
310 .await
311 },
312 CompatMode::V0_37 => {
313 self.perform_with_dialect(endpoint::header::Request::new(height), v0_37::Dialect)
314 .await
315 },
316 CompatMode::V0_34 => {
317 let resp = self
320 .perform_with_dialect(endpoint::block::Request::new(height), v0_34::Dialect)
321 .await?;
322 Ok(resp.into())
323 },
324 }
325 }
326
327 async fn header_by_hash(
328 &self,
329 hash: Hash,
330 ) -> Result<endpoint::header_by_hash::Response, Error> {
331 match self.compat {
332 CompatMode::V0_38 => {
333 self.perform_with_dialect(
334 endpoint::header_by_hash::Request::new(hash),
335 v0_38::Dialect,
336 )
337 .await
338 },
339 CompatMode::V0_37 => {
340 self.perform_with_dialect(
341 endpoint::header_by_hash::Request::new(hash),
342 v0_37::Dialect,
343 )
344 .await
345 },
346 CompatMode::V0_34 => {
347 let resp = self
350 .perform_with_dialect(
351 endpoint::block_by_hash::Request::new(hash),
352 v0_34::Dialect,
353 )
354 .await?;
355 Ok(resp.into())
356 },
357 }
358 }
359
360 async fn broadcast_evidence(
362 &self,
363 evidence: Evidence,
364 ) -> Result<endpoint::evidence::Response, Error> {
365 match self.compat {
366 CompatMode::V0_38 => {
367 let request = endpoint::evidence::Request::new(evidence);
368 self.perform_with_dialect(request, crate::dialect::v0_38::Dialect)
369 .await
370 },
371 CompatMode::V0_37 => {
372 let request = endpoint::evidence::Request::new(evidence);
373 self.perform_with_dialect(request, crate::dialect::v0_37::Dialect)
374 .await
375 },
376 CompatMode::V0_34 => {
377 let request = endpoint::evidence::Request::new(evidence);
378 self.perform_with_dialect(request, crate::dialect::v0_34::Dialect)
379 .await
380 },
381 }
382 }
383
384 async fn tx(&self, hash: Hash, prove: bool) -> Result<endpoint::tx::Response, Error> {
385 perform_with_compat!(self, endpoint::tx::Request::new(hash, prove))
386 }
387
388 async fn tx_search(
389 &self,
390 query: Query,
391 prove: bool,
392 page: u32,
393 per_page: u8,
394 order: Order,
395 ) -> Result<endpoint::tx_search::Response, Error> {
396 perform_with_compat!(
397 self,
398 endpoint::tx_search::Request::new(query, prove, page, per_page, order)
399 )
400 }
401
402 async fn broadcast_tx_commit<T>(
403 &self,
404 tx: T,
405 ) -> Result<endpoint::broadcast::tx_commit::Response, Error>
406 where
407 T: Into<Vec<u8>> + Send,
408 {
409 perform_with_compat!(self, endpoint::broadcast::tx_commit::Request::new(tx))
410 }
411}
412
413#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
417pub struct HttpClientUrl(Url);
418
419impl TryFrom<Url> for HttpClientUrl {
420 type Error = Error;
421
422 fn try_from(value: Url) -> Result<Self, Error> {
423 match value.scheme() {
424 Scheme::Http | Scheme::Https => Ok(Self(value)),
425 _ => Err(Error::invalid_url(value)),
426 }
427 }
428}
429
430impl FromStr for HttpClientUrl {
431 type Err = Error;
432
433 fn from_str(s: &str) -> Result<Self, Error> {
434 let url: Url = s.parse()?;
435 url.try_into()
436 }
437}
438
439impl TryFrom<&str> for HttpClientUrl {
440 type Error = Error;
441
442 fn try_from(value: &str) -> Result<Self, Error> {
443 value.parse()
444 }
445}
446
447impl TryFrom<net::Address> for HttpClientUrl {
448 type Error = Error;
449
450 fn try_from(value: net::Address) -> Result<Self, Error> {
451 match value {
452 net::Address::Tcp {
453 peer_id: _,
454 host,
455 port,
456 } => format!("http://{host}:{port}").parse(),
457 net::Address::Unix { .. } => Err(Error::invalid_network_address()),
458 }
459 }
460}
461
462impl From<HttpClientUrl> for Url {
463 fn from(url: HttpClientUrl) -> Self {
464 url.0
465 }
466}
467
468impl From<HttpClientUrl> for url::Url {
469 fn from(url: HttpClientUrl) -> Self {
470 url.0.into()
471 }
472}
473
474#[cfg(test)]
475mod tests {
476 use core::str::FromStr;
477
478 use reqwest::{header::AUTHORIZATION, Request};
479
480 use super::HttpClient;
481 use crate::endpoint::abci_info;
482 use crate::Url;
483
484 fn authorization(req: &Request) -> Option<&str> {
485 req.headers()
486 .get(AUTHORIZATION)
487 .map(|h| h.to_str().unwrap())
488 }
489
490 #[test]
491 fn without_basic_auth() {
492 let url = Url::from_str("http://example.com").unwrap();
493 let client = HttpClient::new(url).unwrap();
494 let req = HttpClient::build_request(&client, abci_info::Request).unwrap();
495
496 assert_eq!(authorization(&req), None);
497 }
498
499 #[test]
500 fn with_basic_auth() {
501 let url = Url::from_str("http://toto:tata@example.com").unwrap();
502 let client = HttpClient::new(url).unwrap();
503 let req = HttpClient::build_request(&client, abci_info::Request).unwrap();
504
505 assert_eq!(authorization(&req), Some("Basic dG90bzp0YXRh"));
506 let num_auth_headers = req
507 .headers()
508 .iter()
509 .filter(|h| h.0 == AUTHORIZATION)
510 .count();
511 assert_eq!(num_auth_headers, 1);
512 }
513}