cometbft_rpc/client/transport/
http.rs1use core::{
4 convert::{TryFrom, TryInto},
5 str::FromStr,
6};
7
8use async_trait::async_trait;
9use reqwest::{header, Proxy};
10
11use cometbft::{block::Height, evidence::Evidence, Hash};
12use cometbft_config::net;
13
14use super::auth;
15use crate::prelude::*;
16use crate::{
17 client::{Client, CompatMode},
18 dialect::{v0_34, Dialect, LatestDialect},
19 endpoint,
20 query::Query,
21 request::RequestMessage,
22 response::Response,
23 Error, Order, Scheme, SimpleRequest, Url,
24};
25
26const USER_AGENT: &str = concat!("cometbft.rs/", env!("CARGO_PKG_VERSION"));
27
28#[derive(Debug, Clone)]
55pub struct HttpClient {
56 inner: reqwest::Client,
57 url: reqwest::Url,
58 compat: CompatMode,
59}
60
61pub struct Builder {
63 url: HttpClientUrl,
64 compat: CompatMode,
65 proxy_url: Option<HttpClientUrl>,
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 build(self) -> Result<HttpClient, Error> {
90 let builder = reqwest::ClientBuilder::new().user_agent(USER_AGENT);
91 let inner = match self.proxy_url {
92 None => builder.build().map_err(Error::http)?,
93 Some(proxy_url) => {
94 let proxy = if self.url.0.is_secure() {
95 Proxy::https(reqwest::Url::from(proxy_url.0)).map_err(Error::invalid_proxy)?
96 } else {
97 Proxy::http(reqwest::Url::from(proxy_url.0)).map_err(Error::invalid_proxy)?
98 };
99 builder.proxy(proxy).build().map_err(Error::http)?
100 },
101 };
102 Ok(HttpClient {
103 inner,
104 url: self.url.into(),
105 compat: self.compat,
106 })
107 }
108}
109
110impl HttpClient {
111 pub fn new<U>(url: U) -> Result<Self, Error>
114 where
115 U: TryInto<HttpClientUrl, Error = Error>,
116 {
117 let url = url.try_into()?;
118 Self::builder(url).build()
119 }
120
121 pub fn new_with_proxy<U, P>(url: U, proxy_url: P) -> Result<Self, Error>
129 where
130 U: TryInto<HttpClientUrl, Error = Error>,
131 P: TryInto<HttpClientUrl, Error = Error>,
132 {
133 let url = url.try_into()?;
134 Self::builder(url).proxy_url(proxy_url.try_into()?).build()
135 }
136
137 pub fn builder(url: HttpClientUrl) -> Builder {
141 Builder {
142 url,
143 compat: Default::default(),
144 proxy_url: None,
145 }
146 }
147
148 pub fn set_compat_mode(&mut self, compat: CompatMode) {
154 self.compat = compat;
155 }
156
157 fn build_request<R>(&self, request: R) -> Result<reqwest::Request, Error>
158 where
159 R: RequestMessage,
160 {
161 let request_body = request.into_json();
162
163 tracing::debug!(url = %self.url, body = %request_body, "outgoing request");
164
165 let mut builder = self
166 .inner
167 .post(self.url.clone())
168 .header(header::CONTENT_TYPE, "application/json")
169 .body(request_body.into_bytes());
170
171 if let Some(auth) = auth::authorize(&self.url) {
172 builder = builder.header(header::AUTHORIZATION, auth.to_string());
173 }
174
175 builder.build().map_err(Error::http)
176 }
177
178 async fn perform_with_dialect<R, S>(&self, request: R, _dialect: S) -> Result<R::Output, Error>
179 where
180 R: SimpleRequest<S>,
181 S: Dialect,
182 {
183 let request = self.build_request(request)?;
184 let response = self.inner.execute(request).await.map_err(Error::http)?;
185 let response_status = response.status();
186 let response_body = response.bytes().await.map_err(Error::http)?;
187
188 tracing::debug!(
189 status = %response_status,
190 body = %String::from_utf8_lossy(&response_body),
191 "incoming response"
192 );
193
194 if response_status != reqwest::StatusCode::OK {
199 return Err(Error::http_request_failed(response_status));
200 }
201
202 R::Response::from_string(&response_body).map(Into::into)
203 }
204}
205
206#[async_trait]
207impl Client for HttpClient {
208 async fn perform<R>(&self, request: R) -> Result<R::Output, Error>
209 where
210 R: SimpleRequest,
211 {
212 self.perform_with_dialect(request, LatestDialect).await
213 }
214
215 async fn block_results<H>(&self, height: H) -> Result<endpoint::block_results::Response, Error>
216 where
217 H: Into<Height> + Send,
218 {
219 perform_with_compat!(self, endpoint::block_results::Request::new(height.into()))
220 }
221
222 async fn latest_block_results(&self) -> Result<endpoint::block_results::Response, Error> {
223 perform_with_compat!(self, endpoint::block_results::Request::default())
224 }
225
226 async fn header<H>(&self, height: H) -> Result<endpoint::header::Response, Error>
227 where
228 H: Into<Height> + Send,
229 {
230 let height = height.into();
231 match self.compat {
232 CompatMode::V0_37 => self.perform(endpoint::header::Request::new(height)).await,
233 CompatMode::V0_34 => {
234 let resp = self
237 .perform_with_dialect(endpoint::block::Request::new(height), v0_34::Dialect)
238 .await?;
239 Ok(resp.into())
240 },
241 }
242 }
243
244 async fn header_by_hash(
245 &self,
246 hash: Hash,
247 ) -> Result<endpoint::header_by_hash::Response, Error> {
248 match self.compat {
249 CompatMode::V0_37 => {
250 self.perform(endpoint::header_by_hash::Request::new(hash))
251 .await
252 },
253 CompatMode::V0_34 => {
254 let resp = self
257 .perform_with_dialect(
258 endpoint::block_by_hash::Request::new(hash),
259 v0_34::Dialect,
260 )
261 .await?;
262 Ok(resp.into())
263 },
264 }
265 }
266
267 async fn broadcast_evidence(&self, e: Evidence) -> Result<endpoint::evidence::Response, Error> {
269 match self.compat {
270 CompatMode::V0_37 => self.perform(endpoint::evidence::Request::new(e)).await,
271 CompatMode::V0_34 => {
272 self.perform_with_dialect(endpoint::evidence::Request::new(e), v0_34::Dialect)
273 .await
274 },
275 }
276 }
277
278 async fn tx(&self, hash: Hash, prove: bool) -> Result<endpoint::tx::Response, Error> {
279 perform_with_compat!(self, endpoint::tx::Request::new(hash, prove))
280 }
281
282 async fn tx_search(
283 &self,
284 query: Query,
285 prove: bool,
286 page: u32,
287 per_page: u8,
288 order: Order,
289 ) -> Result<endpoint::tx_search::Response, Error> {
290 perform_with_compat!(
291 self,
292 endpoint::tx_search::Request::new(query, prove, page, per_page, order)
293 )
294 }
295
296 async fn broadcast_tx_commit<T>(
297 &self,
298 tx: T,
299 ) -> Result<endpoint::broadcast::tx_commit::Response, Error>
300 where
301 T: Into<Vec<u8>> + Send,
302 {
303 perform_with_compat!(self, endpoint::broadcast::tx_commit::Request::new(tx))
304 }
305}
306
307#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
311pub struct HttpClientUrl(Url);
312
313impl TryFrom<Url> for HttpClientUrl {
314 type Error = Error;
315
316 fn try_from(value: Url) -> Result<Self, Error> {
317 match value.scheme() {
318 Scheme::Http | Scheme::Https => Ok(Self(value)),
319 _ => Err(Error::invalid_url(value)),
320 }
321 }
322}
323
324impl FromStr for HttpClientUrl {
325 type Err = Error;
326
327 fn from_str(s: &str) -> Result<Self, Error> {
328 let url: Url = s.parse()?;
329 url.try_into()
330 }
331}
332
333impl TryFrom<&str> for HttpClientUrl {
334 type Error = Error;
335
336 fn try_from(value: &str) -> Result<Self, Error> {
337 value.parse()
338 }
339}
340
341impl TryFrom<net::Address> for HttpClientUrl {
342 type Error = Error;
343
344 fn try_from(value: net::Address) -> Result<Self, Error> {
345 match value {
346 net::Address::Tcp {
347 peer_id: _,
348 host,
349 port,
350 } => format!("http://{host}:{port}").parse(),
351 net::Address::Unix { .. } => Err(Error::invalid_network_address()),
352 }
353 }
354}
355
356impl From<HttpClientUrl> for Url {
357 fn from(url: HttpClientUrl) -> Self {
358 url.0
359 }
360}
361
362impl From<HttpClientUrl> for url::Url {
363 fn from(url: HttpClientUrl) -> Self {
364 url.0.into()
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use core::str::FromStr;
371
372 use reqwest::{header::AUTHORIZATION, Request};
373
374 use super::HttpClient;
375 use crate::endpoint::abci_info;
376 use crate::Url;
377
378 fn authorization(req: &Request) -> Option<&str> {
379 req.headers()
380 .get(AUTHORIZATION)
381 .map(|h| h.to_str().unwrap())
382 }
383
384 #[test]
385 fn without_basic_auth() {
386 let url = Url::from_str("http://example.com").unwrap();
387 let client = HttpClient::new(url).unwrap();
388 let req = HttpClient::build_request(&client, abci_info::Request).unwrap();
389
390 assert_eq!(authorization(&req), None);
391 }
392
393 #[test]
394 fn with_basic_auth() {
395 let url = Url::from_str("http://toto:tata@example.com").unwrap();
396 let client = HttpClient::new(url).unwrap();
397 let req = HttpClient::build_request(&client, abci_info::Request).unwrap();
398
399 assert_eq!(authorization(&req), Some("Basic dG90bzp0YXRh"));
400 }
401}