httpstat/
lib.rs

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			// "CONNECT" => Self::Connect,
55			"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	// Set post_field_size for anything other than a PUT request if the user has passed in data.
304	// Note: https://httpwg.org/specs/rfc7231.html#method.definitions
305	// > A payload within a {METHOD} request message has no defined semantics; sending a payload
306	// > body on a {METHOD} request might cause some existing implementations to reject the
307	// > request.
308	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	// hmmm
325	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	// Force handler to drop so we can access the body references held by the collector
341	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}