Skip to main content

eva_sdk/
http.rs

1use eva_common::op::Op;
2use eva_common::prelude::*;
3use http_body_util::{BodyExt, Empty, Full};
4use hyper::body::{Bytes, Incoming};
5use hyper::{StatusCode, Uri};
6use hyper_tls::HttpsConnector;
7use hyper_util::client::legacy::{Client as HyperClient, connect::HttpConnector};
8use hyper_util::rt::TokioExecutor;
9use serde::{Deserialize, Serialize};
10use simple_pool::ResourcePool;
11use std::collections::BTreeMap;
12use std::time::Duration;
13
14type Resource = HyperClient<HttpsConnector<HttpConnector>, Empty<Bytes>>;
15
16pub type RawResponse = hyper::Response<Incoming>;
17pub type BufferedResponse = hyper::Response<Full<Bytes>>;
18
19pub const MAX_REDIRECTS: usize = 10;
20
21pub struct Client {
22    pool: ResourcePool<Resource>,
23    timeout: Duration,
24    max_redirects: usize,
25    follow_redirects: bool,
26}
27
28#[derive(Serialize, Deserialize, Debug, Clone)]
29pub struct Response {
30    status: u16,
31    headers: BTreeMap<String, String>,
32    body: Vec<u8>,
33}
34
35impl Response {
36    #[inline]
37    pub fn status(&self) -> u16 {
38        self.status
39    }
40    #[inline]
41    pub fn headers(&self) -> &BTreeMap<String, String> {
42        &self.headers
43    }
44    #[inline]
45    pub fn body(&self) -> &[u8] {
46        &self.body
47    }
48}
49
50impl TryFrom<Response> for BufferedResponse {
51    type Error = Error;
52    fn try_from(resp: Response) -> EResult<Self> {
53        let mut r = hyper::http::Response::builder();
54        for (header, value) in resp.headers {
55            r = r.header(header, value);
56        }
57        r.status(StatusCode::from_u16(resp.status).map_err(Error::failed)?)
58            .body(Full::new(Bytes::from(resp.body)))
59            .map_err(Error::failed)
60    }
61}
62
63impl Client {
64    pub fn new(pool_size: usize, timeout: Duration) -> Self {
65        let pool: ResourcePool<Resource> = <_>::default();
66        for _ in 0..=pool_size {
67            let https = HttpsConnector::new();
68            let client = HyperClient::builder(TokioExecutor::new())
69                .pool_idle_timeout(timeout)
70                .build(https);
71            pool.append(client);
72        }
73        Self {
74            pool,
75            timeout,
76            max_redirects: MAX_REDIRECTS,
77            follow_redirects: true,
78        }
79    }
80    #[inline]
81    pub fn max_redirects(mut self, max_redirects: usize) -> Self {
82        self.max_redirects = max_redirects;
83        self
84    }
85    #[inline]
86    pub fn follow_redirects(mut self, follow: bool) -> Self {
87        self.follow_redirects = follow;
88        self
89    }
90    pub async fn get(&self, url: &str) -> EResult<RawResponse> {
91        let op = Op::new(self.timeout);
92        let mut target_uri: Uri = {
93            if url.starts_with("http://") || url.starts_with("https://") {
94                url.parse()
95            } else {
96                format!("http://{url}").parse()
97            }
98        }
99        .map_err(|e| Error::invalid_params(format!("invalid url {}: {}", url, e)))?;
100        let client = tokio::time::timeout(op.timeout()?, self.pool.get()).await?;
101        let mut rdr = 0;
102        loop {
103            let res = tokio::time::timeout(op.timeout()?, client.get(target_uri.clone()))
104                .await?
105                .map_err(Error::io)?;
106            if self.follow_redirects
107                && (res.status() == StatusCode::MOVED_PERMANENTLY
108                    || res.status() == StatusCode::TEMPORARY_REDIRECT
109                    || res.status() == StatusCode::FOUND)
110            {
111                if rdr > self.max_redirects {
112                    return Err(Error::io("too many redirects"));
113                }
114                rdr += 1;
115                if let Some(loc) = res.headers().get(hyper::header::LOCATION) {
116                    let location_uri: Uri = loc
117                        .to_str()
118                        .map_err(|e| Error::invalid_params(format!("invalid redirect url: {e}")))?
119                        .parse()
120                        .map_err(|e| Error::invalid_params(format!("invalid redirect url: {e}")))?;
121                    let loc_parts = location_uri.into_parts();
122                    let mut parts = target_uri.into_parts();
123                    if loc_parts.scheme.is_some() {
124                        parts.scheme = loc_parts.scheme;
125                    }
126                    if loc_parts.authority.is_some() {
127                        parts.authority = loc_parts.authority;
128                    }
129                    parts.path_and_query = loc_parts.path_and_query;
130                    target_uri = Uri::from_parts(parts)
131                        .map_err(|e| Error::invalid_params(format!("invalid redirect url: {e}")))?;
132                } else {
133                    return Err(Error::io("invalid redirect"));
134                }
135            } else {
136                return Ok(res);
137            }
138        }
139    }
140    pub async fn get_response(&self, url: &str) -> EResult<Response> {
141        let op = Op::new(self.timeout);
142        let resp = self.get(url).await?;
143        let status = resp.status().as_u16();
144        let mut headers = BTreeMap::new();
145        for (header, value) in resp.headers() {
146            headers.insert(
147                header.to_string(),
148                value.to_str().unwrap_or_default().to_owned(),
149            );
150        }
151        let body = tokio::time::timeout(op.timeout()?, resp.into_body().collect())
152            .await?
153            .map_err(Error::io)?
154            .to_bytes()
155            .to_vec();
156        Ok(Response {
157            status,
158            headers,
159            body,
160        })
161    }
162}