1use std::time::Duration;
4
5use tokio::io::{AsyncReadExt, AsyncWriteExt};
6use tokio::net::TcpStream;
7use url::Url;
8
9use super::NEX_PORT;
10
11#[derive(Debug, Clone)]
13pub struct FetchOptions {
14 pub timeout: Duration,
16 pub max_body: usize,
18 pub connect_addr: Option<std::net::SocketAddr>,
21}
22
23impl Default for FetchOptions {
24 fn default() -> Self {
25 Self {
26 timeout: Duration::from_secs(30),
27 max_body: 16 * 1024 * 1024,
28 connect_addr: None,
29 }
30 }
31}
32
33#[derive(Debug)]
36pub enum ClientError {
37 BadUrl(String),
38 Io(String),
39 Timeout(&'static str),
40 BodyTooLarge { max: usize },
41}
42
43impl std::fmt::Display for ClientError {
44 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 match self {
46 Self::BadUrl(message) => write!(formatter, "bad nex URL: {message}"),
47 Self::Io(message) => write!(formatter, "nex IO error: {message}"),
48 Self::Timeout(step) => write!(formatter, "nex {step} timed out"),
49 Self::BodyTooLarge { max } => {
50 write!(formatter, "nex response exceeds {max} bytes")
51 }
52 }
53 }
54}
55
56impl std::error::Error for ClientError {}
57
58pub async fn fetch(url: &str, options: &FetchOptions) -> Result<Vec<u8>, ClientError> {
60 let url = Url::parse(url).map_err(|error| ClientError::BadUrl(error.to_string()))?;
61 if url.scheme() != "nex" {
62 return Err(ClientError::BadUrl(format!(
63 "expected nex:// scheme, got {}://",
64 url.scheme()
65 )));
66 }
67 let host = url
68 .host_str()
69 .ok_or_else(|| ClientError::BadUrl("URL has no host".to_string()))?;
70 let port = url.port().unwrap_or(NEX_PORT);
71 fetch_path(host, port, url.path(), options).await
72}
73
74pub async fn fetch_path(
77 host: &str,
78 port: u16,
79 path: &str,
80 options: &FetchOptions,
81) -> Result<Vec<u8>, ClientError> {
82 let connect = async {
83 match options.connect_addr {
84 Some(addr) => TcpStream::connect(addr).await,
85 None => TcpStream::connect((host, port)).await,
86 }
87 };
88 let mut stream = tokio::time::timeout(options.timeout, connect)
89 .await
90 .map_err(|_| ClientError::Timeout("connect"))?
91 .map_err(|error| ClientError::Io(error.to_string()))?;
92
93 let request = format!("{path}\r\n");
94 tokio::time::timeout(options.timeout, stream.write_all(request.as_bytes()))
95 .await
96 .map_err(|_| ClientError::Timeout("request write"))?
97 .map_err(|error| ClientError::Io(error.to_string()))?;
98
99 let read = async {
100 let mut body = Vec::new();
101 let mut chunk = [0u8; 8192];
102 loop {
103 let count = stream.read(&mut chunk).await?;
104 if count == 0 {
105 break;
106 }
107 body.extend_from_slice(&chunk[..count]);
108 if body.len() > options.max_body {
109 return Ok::<_, std::io::Error>(None);
110 }
111 }
112 Ok(Some(body))
113 };
114 tokio::time::timeout(options.timeout, read)
115 .await
116 .map_err(|_| ClientError::Timeout("response read"))?
117 .map_err(|error| ClientError::Io(error.to_string()))?
118 .ok_or(ClientError::BodyTooLarge {
119 max: options.max_body,
120 })
121}