1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
use eva_common::op::Op;
use eva_common::prelude::*;
use hyper::{client::HttpConnector, Body, StatusCode, Uri};
use hyper_tls::HttpsConnector;
use serde::{Deserialize, Serialize};
use simple_pool::ResourcePool;
use std::collections::BTreeMap;
use std::time::Duration;

type Resource = hyper::Client<HttpsConnector<HttpConnector>>;

pub const MAX_REDIRECTS: usize = 10;

pub struct Client {
    pool: ResourcePool<Resource>,
    timeout: Duration,
    max_redirects: usize,
    follow_redirects: bool,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Response {
    status: u16,
    headers: BTreeMap<String, String>,
    body: Vec<u8>,
}

impl Response {
    #[inline]
    pub fn status(&self) -> u16 {
        self.status
    }
    #[inline]
    pub fn headers(&self) -> &BTreeMap<String, String> {
        &self.headers
    }
    #[inline]
    pub fn body(&self) -> &[u8] {
        &self.body
    }
}

impl TryFrom<Response> for hyper::http::Response<Body> {
    type Error = Error;
    fn try_from(resp: Response) -> EResult<Self> {
        let mut r = hyper::http::Response::builder();
        for (header, value) in resp.headers {
            r = r.header(header, value);
        }
        r.status(StatusCode::from_u16(resp.status).map_err(Error::failed)?)
            .body(Body::from(resp.body))
            .map_err(Error::failed)
    }
}

impl Client {
    pub fn new(pool_size: usize, timeout: Duration) -> Self {
        let pool: ResourcePool<Resource> = <_>::default();
        for _ in 0..=pool_size {
            let https = HttpsConnector::new();
            let client: hyper::Client<_> = hyper::Client::builder()
                .pool_idle_timeout(timeout)
                .build(https);
            pool.append(client);
        }
        Self {
            pool,
            timeout,
            max_redirects: MAX_REDIRECTS,
            follow_redirects: true,
        }
    }
    #[inline]
    pub fn max_redirects(mut self, max_redirects: usize) -> Self {
        self.max_redirects = max_redirects;
        self
    }
    #[inline]
    pub fn follow_redirects(mut self, follow: bool) -> Self {
        self.follow_redirects = follow;
        self
    }
    pub async fn get(&self, url: &str) -> EResult<hyper::Response<Body>> {
        let op = Op::new(self.timeout);
        let mut target_uri: Uri = {
            if url.starts_with("http://") || url.starts_with("https://") {
                url.parse()
            } else {
                format!("http://{url}").parse()
            }
        }
        .map_err(|e| Error::invalid_params(format!("invalid url {}: {}", url, e)))?;
        let client = tokio::time::timeout(op.timeout()?, self.pool.get()).await?;
        let mut rdr = 0;
        loop {
            let res = tokio::time::timeout(op.timeout()?, client.get(target_uri.clone()))
                .await?
                .map_err(Error::io)?;
            if self.follow_redirects
                && (res.status() == StatusCode::MOVED_PERMANENTLY
                    || res.status() == StatusCode::TEMPORARY_REDIRECT
                    || res.status() == StatusCode::FOUND)
            {
                if rdr > self.max_redirects {
                    return Err(Error::io("too many redirects"));
                }
                rdr += 1;
                if let Some(loc) = res.headers().get(hyper::header::LOCATION) {
                    let location_uri: Uri = loc
                        .to_str()
                        .map_err(|e| Error::invalid_params(format!("invalid redirect url: {e}")))?
                        .parse()
                        .map_err(|e| Error::invalid_params(format!("invalid redirect url: {e}")))?;
                    let loc_parts = location_uri.into_parts();
                    let mut parts = target_uri.into_parts();
                    if loc_parts.scheme.is_some() {
                        parts.scheme = loc_parts.scheme;
                    }
                    if loc_parts.authority.is_some() {
                        parts.authority = loc_parts.authority;
                    }
                    parts.path_and_query = loc_parts.path_and_query;
                    target_uri = Uri::from_parts(parts)
                        .map_err(|e| Error::invalid_params(format!("invalid redirect url: {e}")))?;
                } else {
                    return Err(Error::io("invalid redirect"));
                }
            } else {
                return Ok(res);
            }
        }
    }
    pub async fn get_response(&self, url: &str) -> EResult<Response> {
        let op = Op::new(self.timeout);
        let resp = self.get(url).await?;
        let status = resp.status().as_u16();
        let mut headers = BTreeMap::new();
        for (header, value) in resp.headers() {
            headers.insert(
                header.to_string(),
                value.to_str().unwrap_or_default().to_owned(),
            );
        }
        let body = tokio::time::timeout(op.timeout()?, hyper::body::to_bytes(resp))
            .await?
            .map_err(Error::io)?
            .to_vec();
        Ok(Response {
            status,
            headers,
            body,
        })
    }
}