1use anyhow::{anyhow, Result};
2use core::fmt;
3use curl::easy::{Easy2, Handler, List, ReadError, WriteError};
4use curl::multi::{Easy2Handle, Multi};
5use serde::{Deserialize, Serialize};
6use std::future::Future;
7use std::io::Read;
8use std::pin::Pin;
9use std::str::{self, FromStr};
10use std::task::{Context, Poll};
11use std::time::Duration;
12
13macro_rules! set_handle_optional {
14 ($field:expr, $handle:ident, $fn:ident) => {
15 if let Some(f) = $field {
16 $handle.$fn(f)?;
17 }
18 };
19}
20
21#[derive(Debug, Clone)]
22pub enum RequestMethod {
23 Delete,
24 Get,
25 Head,
26 Options,
27 Patch,
28 Post,
29 Put,
30 Trace,
31 Custom(String),
32}
33
34impl<'a> From<&'a RequestMethod> for &'a str {
35 fn from(request_method: &'a RequestMethod) -> &'a str {
36 match request_method {
37 RequestMethod::Delete => "DELETE",
38 RequestMethod::Get => "GET",
39 RequestMethod::Head => "HEAD",
40 RequestMethod::Options => "OPTIONS",
41 RequestMethod::Patch => "PATCH",
42 RequestMethod::Post => "POST",
43 RequestMethod::Put => "PUT",
44 RequestMethod::Trace => "TRACE",
45 RequestMethod::Custom(request_method) => request_method,
46 }
47 }
48}
49
50impl From<String> for RequestMethod {
51 fn from(request_method: String) -> Self {
52 let request_method = request_method.to_uppercase();
53 match request_method.as_str() {
54 "DELETE" => RequestMethod::Delete,
56 "GET" => RequestMethod::Get,
57 "HEAD" => RequestMethod::Head,
58 "OPTIONS" => RequestMethod::Options,
59 "PATCH" => RequestMethod::Patch,
60 "POST" => RequestMethod::Post,
61 "PUT" => RequestMethod::Put,
62 "TRACE" => RequestMethod::Trace,
63 _ => Self::Custom(request_method),
64 }
65 }
66}
67
68#[derive(Debug, Clone)]
69pub struct Config {
70 pub location: bool,
71 pub connect_timeout: Option<Duration>,
72 pub request_method: RequestMethod,
73 pub data: Option<String>,
74 pub headers: Vec<Header>,
75 pub insecure: bool,
76 pub client_cert: Option<String>,
77 pub client_key: Option<String>,
78 pub ca_cert: Option<String>,
79 pub url: String,
80 pub verbose: bool,
81 pub max_response_size: Option<usize>,
82}
83
84impl Default for Config {
85 fn default() -> Self {
86 Self {
87 location: Default::default(),
88 connect_timeout: Default::default(),
89 request_method: RequestMethod::Get,
90 data: Default::default(),
91 headers: Default::default(),
92 insecure: Default::default(),
93 client_cert: Default::default(),
94 client_key: Default::default(),
95 ca_cert: Default::default(),
96 url: Default::default(),
97 verbose: Default::default(),
98 max_response_size: Default::default(),
99 }
100 }
101}
102
103#[derive(Deserialize, Serialize, Debug, Clone)]
104pub struct Header {
105 pub name: String,
106 pub value: String,
107}
108
109impl fmt::Display for Header {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 write!(f, "{}: {}", self.name, self.value)
112 }
113}
114
115impl FromStr for Header {
116 type Err = anyhow::Error;
117 fn from_str(s: &str) -> Result<Self, Self::Err> {
118 match s.split_once(':') {
119 Some(header_tuple) => Ok(Self {
120 name: header_tuple.0.into(),
121 value: header_tuple.1.trim().into(),
122 }),
123 None => Err(anyhow!("Invalid header \"{}\"", s)),
124 }
125 }
126}
127
128#[derive(Deserialize, Serialize, Debug, Clone)]
129pub struct HttpResponseHeader {
130 pub http_version: String,
131 pub response_code: i32,
132 pub response_message: Option<String>,
133}
134
135impl From<String> for HttpResponseHeader {
136 fn from(line: String) -> Self {
137 let cleaned = line.trim().replace("\r", "").replace("\n", "");
138 let header_tuple: (&str, &str) = cleaned.split_once('/').unwrap();
139 let response_arr: Vec<&str> = header_tuple.1.split(' ').collect();
140
141 let http_version: String = response_arr.get(0).unwrap().to_string();
142 let response_code: i32 = response_arr.get(1).unwrap().parse().unwrap();
143 let response_message: Option<String> = response_arr.get(2).map(|msg| msg.to_string());
144
145 Self {
146 http_version,
147 response_code,
148 response_message,
149 }
150 }
151}
152
153#[derive(Deserialize, Serialize, Debug, Clone)]
154pub struct Timing {
155 pub namelookup: Duration,
156 pub connect: Duration,
157 pub pretransfer: Duration,
158 pub starttransfer: Duration,
159 pub total: Duration,
160 pub dns_resolution: Duration,
161 pub tcp_connection: Duration,
162 pub tls_connection: Duration,
163 pub server_processing: Duration,
164 pub content_transfer: Duration,
165}
166
167impl Timing {
168 pub fn new(handle: &mut Easy2Handle<Collector>) -> Self {
169 let namelookup = handle.namelookup_time().unwrap();
170 let connect = handle.connect_time().unwrap();
171 let pretransfer = handle.pretransfer_time().unwrap();
172 let starttransfer = handle.starttransfer_time().unwrap();
173 let total = handle.total_time().unwrap();
174 let dns_resolution = namelookup;
175 let tcp_connection = connect - namelookup;
176 let tls_connection = pretransfer - connect;
177 let server_processing = starttransfer - pretransfer;
178 let content_transfer = total - starttransfer;
179
180 Self {
181 namelookup,
182 connect,
183 pretransfer,
184 starttransfer,
185 total,
186 dns_resolution,
187 tcp_connection,
188 tls_connection,
189 server_processing,
190 content_transfer,
191 }
192 }
193}
194
195#[derive(Deserialize, Serialize, Debug, Clone)]
196pub struct StatResult {
197 pub http_version: String,
198 pub response_code: i32,
199 pub response_message: Option<String>,
200 pub headers: Vec<Header>,
201 pub timing: Timing,
202 pub body: Vec<u8>,
203}
204
205pub struct Collector<'a> {
206 config: &'a Config,
207 headers: &'a mut Vec<u8>,
208 data: &'a mut Vec<u8>,
209}
210
211impl<'a> Collector<'a> {
212 pub fn new(config: &'a Config, data: &'a mut Vec<u8>, headers: &'a mut Vec<u8>) -> Self {
213 Self {
214 config,
215 data,
216 headers,
217 }
218 }
219}
220
221impl<'a> Handler for Collector<'a> {
222 fn write(&mut self, data: &[u8]) -> Result<usize, WriteError> {
223 self.data.extend_from_slice(data);
224 if let Some(ref max_response_size) = self.config.max_response_size {
225 if self.data.len() > *max_response_size {
226 return Ok(0);
227 }
228 }
229 Ok(data.len())
230 }
231
232 fn read(&mut self, into: &mut [u8]) -> Result<usize, ReadError> {
233 match &self.config.data {
234 Some(data) => Ok(data.as_bytes().read(into).unwrap()),
235 None => Ok(0),
236 }
237 }
238
239 fn header(&mut self, data: &[u8]) -> bool {
240 self.headers.extend_from_slice(data);
241 true
242 }
243}
244
245pub struct HttpstatFuture<'a>(&'a Multi);
246
247impl<'a> Future for HttpstatFuture<'a> {
248 type Output = Result<()>;
249
250 fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
251 match self.0.perform() {
252 Ok(running) => {
253 if running > 0 {
254 context.waker().wake_by_ref();
255 Poll::Pending
256 } else {
257 Poll::Ready(Ok(()))
258 }
259 }
260 Err(error) => Poll::Ready(Err(error.into())),
261 }
262 }
263}
264
265pub async fn httpstat(config: &Config) -> Result<StatResult> {
266 let mut body = Vec::new();
267 let mut headers = Vec::new();
268 let mut handle = Easy2::new(Collector::new(config, &mut body, &mut headers));
269
270 handle.url(&config.url)?;
271 handle.show_header(false)?;
272 handle.progress(true)?;
273 handle.verbose(config.verbose)?;
274
275 if config.insecure {
276 handle.ssl_verify_host(false)?;
277 handle.ssl_verify_peer(false)?;
278 }
279
280 set_handle_optional!(&config.client_cert, handle, ssl_cert);
281 set_handle_optional!(&config.client_key, handle, ssl_key);
282 set_handle_optional!(&config.ca_cert, handle, cainfo);
283 set_handle_optional!(config.connect_timeout, handle, connect_timeout);
284
285 if config.location {
286 handle.follow_location(true)?;
287 }
288
289 let data_len = config.data.as_ref().map(|data| data.len() as u64);
290
291 let request_method = &config.request_method;
292 match request_method {
293 RequestMethod::Put => {
294 handle.upload(true)?;
295 set_handle_optional!(data_len, handle, in_filesize);
296 }
297 RequestMethod::Get => handle.get(true)?,
298 RequestMethod::Head => handle.nobody(true)?,
299 RequestMethod::Post => handle.post(true)?,
300 _ => handle.custom_request(request_method.into())?,
301 }
302
303 if data_len.is_some() && !matches!(request_method, RequestMethod::Put) {
309 handle.post_field_size(data_len.unwrap())?;
310 }
311
312 if !&config.headers.is_empty() {
313 let mut headers = List::new();
314 for header in &config.headers {
315 headers.append(&header.to_string())?;
316 }
317 handle.http_headers(headers)?;
318 }
319
320 let multi = Multi::new();
321 let mut handle = multi.add2(handle)?;
322 HttpstatFuture(&multi).await?;
323
324 let mut transfer_result: Result<()> = Ok(());
326 multi.messages(|m| {
327 if let Ok(()) = transfer_result {
328 if let Some(Err(error)) = m.result_for2(&handle) {
329 if error.is_write_error() {
330 transfer_result = Err(anyhow!("Maximum response size reached"));
331 } else {
332 transfer_result = Err(error.into());
333 }
334 }
335 }
336 });
337 transfer_result?;
338
339 let timing = Timing::new(&mut handle);
340 drop(handle);
342
343 let header_lines = str::from_utf8(&headers[..])?.lines();
344
345 let mut http_response_header: Option<HttpResponseHeader> = None;
346 let mut headers: Vec<Header> = Vec::new();
347
348 let header_iter = header_lines
349 .map(|line| line.replace("\r", "").replace("\n", ""))
350 .filter(|line| !line.is_empty());
351
352 for line in header_iter {
353 if line.to_uppercase().starts_with("HTTP/") {
354 http_response_header = Some(HttpResponseHeader::from(line.to_string()));
355 } else if let Ok(header) = Header::from_str(&line) {
356 headers.push(header);
357 }
358 }
359
360 Ok(StatResult {
361 http_version: http_response_header
362 .as_ref()
363 .map_or_else(|| "Unknown".into(), |h| h.http_version.clone()),
364 response_code: http_response_header
365 .as_ref()
366 .map_or(-1, |h| h.response_code),
367 response_message: http_response_header
368 .as_ref()
369 .and_then(|h| h.response_message.clone()),
370 headers,
371 body,
372 timing,
373 })
374}