http_manager/
lib.rs

1use std::{
2    fs::File,
3    io::{self, copy, Cursor, Error, ErrorKind},
4    time::Duration,
5};
6
7use hyper::{body::Bytes, client::HttpConnector, Body, Client, Method, Request, Response};
8use hyper_tls::HttpsConnector;
9use reqwest::{header::CONTENT_TYPE, ClientBuilder};
10use tokio::time::timeout;
11use url::Url;
12
13/// Creates a simple HTTP GET request with no header and no body.
14pub fn create_get(url: &str, path: &str) -> io::Result<Request<Body>> {
15    let uri = match join_uri(url, path) {
16        Ok(u) => u,
17        Err(e) => return Err(e),
18    };
19
20    let req = match Request::builder()
21        .method(Method::GET)
22        .uri(uri.as_str())
23        .body(Body::empty())
24    {
25        Ok(r) => r,
26        Err(e) => {
27            return Err(Error::new(
28                ErrorKind::Other,
29                format!("failed to create request {}", e),
30            ));
31        }
32    };
33
34    Ok(req)
35}
36
37const JSON_CONTENT_TYPE: &str = "application/json";
38
39/// Creates a simple HTTP POST request with JSON header and body.
40pub fn create_json_post(url: &str, path: &str, d: &str) -> io::Result<Request<Body>> {
41    let uri = join_uri(url, path)?;
42
43    let req = match Request::builder()
44        .method(Method::POST)
45        .header("content-type", JSON_CONTENT_TYPE)
46        .uri(uri.as_str())
47        .body(Body::from(String::from(d)))
48    {
49        Ok(r) => r,
50        Err(e) => {
51            return Err(Error::new(
52                ErrorKind::Other,
53                format!("failed to create request {}", e),
54            ));
55        }
56    };
57
58    Ok(req)
59}
60
61/// Sends a HTTP request, reads response in "hyper::body::Bytes".
62pub async fn read_bytes(
63    req: Request<Body>,
64    timeout_dur: Duration,
65    is_https: bool,
66    check_status_code: bool,
67) -> io::Result<Bytes> {
68    let resp = send_req(req, timeout_dur, is_https).await?;
69    if !resp.status().is_success() {
70        log::warn!(
71            "unexpected HTTP response code {} (server error {})",
72            resp.status(),
73            resp.status().is_server_error()
74        );
75        if check_status_code {
76            return Err(Error::new(
77                ErrorKind::Other,
78                format!(
79                    "unexpected HTTP response code {} (server error {})",
80                    resp.status(),
81                    resp.status().is_server_error()
82                ),
83            ));
84        }
85    }
86
87    // set timeouts for reads
88    // https://github.com/hyperium/hyper/issues/1097
89    let future_task = hyper::body::to_bytes(resp);
90    let ret = timeout(timeout_dur, future_task).await;
91
92    let bytes;
93    match ret {
94        Ok(result) => match result {
95            Ok(b) => bytes = b,
96            Err(e) => {
97                return Err(Error::new(
98                    ErrorKind::Other,
99                    format!("failed to read response {}", e),
100                ));
101            }
102        },
103        Err(e) => {
104            return Err(Error::new(
105                ErrorKind::Other,
106                format!("failed to read response {}", e),
107            ));
108        }
109    }
110
111    Ok(bytes)
112}
113
114/// Sends a HTTP(s) request and wait for its response.
115async fn send_req(
116    req: Request<Body>,
117    timeout_dur: Duration,
118    is_https: bool,
119) -> io::Result<Response<Body>> {
120    // ref. https://github.com/tokio-rs/tokio-tls/blob/master/examples/hyper-client.rs
121    // ref. https://docs.rs/hyper/latest/hyper/client/struct.HttpConnector.html
122    // ref. https://github.com/hyperium/hyper-tls/blob/master/examples/client.rs
123    let mut connector = HttpConnector::new();
124    // ref. https://github.com/hyperium/hyper/issues/1097
125    connector.set_connect_timeout(Some(Duration::from_secs(5)));
126
127    let task = {
128        if !is_https {
129            let cli = Client::builder().build(connector);
130            cli.request(req)
131        } else {
132            // TODO: implement "curl --insecure"
133            let https_connector = HttpsConnector::new_with_connector(connector);
134            let cli = Client::builder().build(https_connector);
135            cli.request(req)
136        }
137    };
138
139    let res = timeout(timeout_dur, task).await?;
140    match res {
141        Ok(resp) => Ok(resp),
142        Err(e) => {
143            return Err(Error::new(
144                ErrorKind::Other,
145                format!("failed to fetch response {}", e),
146            ))
147        }
148    }
149}
150
151#[test]
152fn test_read_bytes_timeout() {
153    let _ = env_logger::builder()
154        .filter_level(log::LevelFilter::Info)
155        .is_test(true)
156        .try_init();
157
158    macro_rules! ab {
159        ($e:expr) => {
160            tokio_test::block_on($e)
161        };
162    }
163
164    let ret = join_uri("http://localhost:12", "invalid");
165    assert!(ret.is_ok());
166    let u = ret.unwrap();
167    let u = u.to_string();
168
169    let ret = Request::builder()
170        .method(hyper::Method::POST)
171        .uri(u)
172        .body(Body::empty());
173    assert!(ret.is_ok());
174    let req = ret.unwrap();
175    let ret = ab!(read_bytes(req, Duration::from_secs(1), false, true));
176    assert!(!ret.is_ok());
177}
178
179pub fn join_uri(url: &str, path: &str) -> io::Result<Url> {
180    let mut uri = match Url::parse(url) {
181        Ok(u) => u,
182        Err(e) => {
183            return Err(Error::new(
184                ErrorKind::Other,
185                format!("failed to parse client URL {}", e),
186            ))
187        }
188    };
189
190    if !path.is_empty() {
191        match uri.join(path) {
192            Ok(u) => uri = u,
193            Err(e) => {
194                return Err(Error::new(
195                    ErrorKind::Other,
196                    format!("failed to join parsed URL {}", e),
197                ));
198            }
199        }
200    }
201
202    Ok(uri)
203}
204
205#[test]
206fn test_join_uri() {
207    let ret = Url::parse("http://localhost:9850/ext/X/sendMultiple");
208    let expected = ret.unwrap();
209
210    let ret = join_uri("http://localhost:9850/", "/ext/X/sendMultiple");
211    assert!(ret.is_ok());
212    let t = ret.unwrap();
213    assert_eq!(t, expected);
214
215    let ret = join_uri("http://localhost:9850", "/ext/X/sendMultiple");
216    assert!(ret.is_ok());
217    let t = ret.unwrap();
218    assert_eq!(t, expected);
219
220    let ret = join_uri("http://localhost:9850", "ext/X/sendMultiple");
221    assert!(ret.is_ok());
222    let t = ret.unwrap();
223    assert_eq!(t, expected);
224}
225
226/// Downloads a file to the "file_path".
227pub async fn download_file(ep: &str, file_path: &str) -> io::Result<()> {
228    log::info!("downloading the file via {}", ep);
229    let resp = reqwest::get(ep)
230        .await
231        .map_err(|e| Error::new(ErrorKind::Other, format!("failed reqwest::get {}", e)))?;
232
233    let mut content = Cursor::new(
234        resp.bytes()
235            .await
236            .map_err(|e| Error::new(ErrorKind::Other, format!("failed bytes {}", e)))?,
237    );
238
239    let mut f = File::create(file_path)?;
240    copy(&mut content, &mut f)?;
241
242    Ok(())
243}
244
245/// TODO: implement this with native Rust
246pub async fn get_non_tls(url: &str, url_path: &str) -> io::Result<Vec<u8>> {
247    let joined = join_uri(url, url_path)?;
248    log::debug!("non-TLS HTTP get for {:?}", joined);
249
250    let output = {
251        if url.starts_with("https") {
252            log::info!("sending via danger_accept_invalid_certs");
253            let cli = ClientBuilder::new()
254                .user_agent(env!("CARGO_PKG_NAME"))
255                .danger_accept_invalid_certs(true)
256                .timeout(Duration::from_secs(15))
257                .connection_verbose(true)
258                .build()
259                .map_err(|e| {
260                    Error::new(
261                        ErrorKind::Other,
262                        format!("failed ClientBuilder build {}", e),
263                    )
264                })?;
265            let resp = cli.get(joined.as_str()).send().await.map_err(|e| {
266                Error::new(ErrorKind::Other, format!("failed ClientBuilder send {}", e))
267            })?;
268            let out = resp.bytes().await.map_err(|e| {
269                Error::new(ErrorKind::Other, format!("failed ClientBuilder send {}", e))
270            })?;
271            out.into()
272        } else {
273            let req = create_get(url, url_path)?;
274            let buf = match read_bytes(
275                req,
276                Duration::from_secs(15),
277                url.starts_with("https"),
278                false,
279            )
280            .await
281            {
282                Ok(b) => b,
283                Err(e) => return Err(e),
284            };
285            buf.to_vec()
286        }
287    };
288    Ok(output)
289}
290
291/// RUST_LOG=debug cargo test --lib -- test_get_non_tls --exact --show-output
292#[test]
293fn test_get_non_tls() {
294    use tokio::runtime::Runtime;
295
296    let _ = env_logger::builder().is_test(true).try_init();
297
298    let rt = Runtime::new().unwrap();
299    let out = rt
300        .block_on(get_non_tls(
301            "https://api.github.com",
302            "repos/ava-labs/avalanchego/releases/latest",
303        ))
304        .unwrap();
305    println!("out: {}", String::from_utf8(out).unwrap());
306}
307
308/// Posts JSON body.
309pub async fn post_non_tls(url: &str, url_path: &str, data: &str) -> io::Result<Vec<u8>> {
310    let joined = join_uri(url, url_path)?;
311    log::debug!("non-TLS HTTP post {}-byte data to {:?}", data.len(), joined);
312
313    let output = {
314        if url.starts_with("https") {
315            log::info!("sending via danger_accept_invalid_certs");
316
317            let cli = ClientBuilder::new()
318                .user_agent(env!("CARGO_PKG_NAME"))
319                .danger_accept_invalid_certs(true)
320                .timeout(Duration::from_secs(15))
321                .connection_verbose(true)
322                .build()
323                .map_err(|e| {
324                    Error::new(
325                        ErrorKind::Other,
326                        format!("failed ClientBuilder build {}", e),
327                    )
328                })?;
329            let resp = cli
330                .post(joined.as_str())
331                .header(CONTENT_TYPE, "application/json")
332                .body(data.to_string())
333                .send()
334                .await
335                .map_err(|e| {
336                    Error::new(ErrorKind::Other, format!("failed ClientBuilder send {}", e))
337                })?;
338            let out = resp.bytes().await.map_err(|e| {
339                Error::new(ErrorKind::Other, format!("failed ClientBuilder send {}", e))
340            })?;
341            out.into()
342        } else {
343            let req = create_json_post(url, url_path, data)?;
344            let buf = match read_bytes(req, Duration::from_secs(15), false, false).await {
345                Ok(b) => b,
346                Err(e) => return Err(e),
347            };
348            buf.to_vec()
349        }
350    };
351    Ok(output)
352}