tendermint_rpc/client/transport/
http.rs

1//! HTTP-based transport for Tendermint RPC Client.
2
3use 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/// A JSON-RPC/HTTP Tendermint RPC client (implements [`crate::Client`]).
26///
27/// Supports both HTTP and HTTPS connections to Tendermint RPC endpoints, and
28/// allows for the use of HTTP proxies (see [`HttpClient::new_with_proxy`] for
29/// details).
30///
31/// Does not provide [`crate::event::Event`] subscription facilities (see
32/// [`crate::WebSocketClient`] for a client that does).
33///
34/// ## Examples
35///
36/// ```rust,ignore
37/// use tendermint_rpc::{HttpClient, Client};
38///
39/// #[tokio::main]
40/// async fn main() {
41///     let client = HttpClient::new("http://127.0.0.1:26657")
42///         .unwrap();
43///
44///     let abci_info = client.abci_info()
45///         .await
46///         .unwrap();
47///
48///     println!("Got ABCI info: {:?}", abci_info);
49/// }
50/// ```
51#[derive(Debug, Clone)]
52pub struct HttpClient {
53    inner: reqwest::Client,
54    url: reqwest::Url,
55    compat: CompatMode,
56}
57
58/// The builder pattern constructor for [`HttpClient`].
59pub 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    /// Use the specified compatibility mode for the Tendermint RPC protocol.
70    ///
71    /// The default is the latest protocol version supported by this crate.
72    pub fn compat_mode(mut self, mode: CompatMode) -> Self {
73        self.compat = mode;
74        self
75    }
76
77    /// Specify the URL of a proxy server for the client to connect through.
78    ///
79    /// If the RPC endpoint is secured (HTTPS), the proxy will automatically
80    /// attempt to connect using the [HTTP CONNECT] method.
81    ///
82    /// [HTTP CONNECT]: https://en.wikipedia.org/wiki/HTTP_tunnel
83    pub fn proxy_url(mut self, url: HttpClientUrl) -> Self {
84        self.proxy_url = Some(url);
85        self
86    }
87
88    /// The timeout is applied from when the request starts connecting until
89    /// the response body has finished.
90    ///
91    /// The default is 30 seconds.
92    pub fn timeout(mut self, duration: Duration) -> Self {
93        self.timeout = duration;
94        self
95    }
96
97    /// Specify the custom User-Agent header used by the client.
98    pub fn user_agent(mut self, agent: String) -> Self {
99        self.user_agent = Some(agent);
100        self
101    }
102
103    /// Use the provided client instead of building one internally.
104    ///
105    /// ## Warning
106    /// This will override the following options set on the builder:
107    /// `timeout`, `user_agent`, and `proxy_url`.
108    pub fn client(mut self, client: reqwest::Client) -> Self {
109        self.client = Some(client);
110        self
111    }
112
113    /// Try to create a client with the options specified for this builder.
114    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    /// Construct a new Tendermint RPC HTTP/S client connecting to the given
147    /// URL. This avoids using the `Builder` and thus does not perform any
148    /// validation of the configuration.
149    pub fn new_from_parts(inner: reqwest::Client, url: reqwest::Url, compat: CompatMode) -> Self {
150        Self { inner, url, compat }
151    }
152
153    /// Construct a new Tendermint RPC HTTP/S client connecting to the given
154    /// URL.
155    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    /// Construct a new Tendermint RPC HTTP/S client connecting to the given
164    /// URL, but via the specified proxy's URL.
165    ///
166    /// If the RPC endpoint is secured (HTTPS), the proxy will automatically
167    /// attempt to connect using the [HTTP CONNECT] method.
168    ///
169    /// [HTTP CONNECT]: https://en.wikipedia.org/wiki/HTTP_tunnel
170    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    /// Initiate a builder for a Tendermint RPC HTTP/S client connecting
180    /// to the given URL, so that more configuration options can be specified
181    /// with the builder.
182    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    /// Set compatibility mode on the instantiated client.
194    ///
195    /// As the HTTP client is stateless and does not support subscriptions,
196    /// the protocol version it uses can be changed at will, for example,
197    /// as a result of version discovery over the `/status` endpoint.
198    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        // Successful JSON-RPC requests are expected to return a 200 OK HTTP status.
240        // Otherwise, this means that the HTTP request failed as a whole,
241        // as opposed to the JSON-RPC request returning an error,
242        // and we cannot expect the response body to be a valid JSON-RPC response.
243        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                // Back-fill with a request to /block endpoint and
318                // taking just the header from the response.
319                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                // Back-fill with a request to /block_by_hash endpoint and
348                // taking just the header from the response.
349                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    /// `/broadcast_evidence`: broadcast an evidence.
361    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/// A URL limited to use with HTTP clients.
414///
415/// Facilitates useful type conversions and inferences.
416#[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}