1use bytes::Bytes;
19use bytesize::ByteSize;
20use heck::ToTrainCase;
21use http::HeaderMap;
22use http::HeaderValue;
23use http::StatusCode;
24use nu_ansi_term::Color::{LightCyan, LightGreen, LightRed};
25use std::fmt;
26use std::io::Write;
27use std::time::Duration;
28use tempfile::NamedTempFile;
29use unicode_truncate::Alignment;
30use unicode_truncate::UnicodeTruncateStr;
31
32pub static ALPN_HTTP2: &str = "h2";
33pub static ALPN_HTTP1: &str = "http/1.1";
34pub static ALPN_HTTP3: &str = "h3";
35
36fn format_duration(duration: Duration) -> String {
37 if duration > Duration::from_secs(1) {
38 return format!("{:.2}s", duration.as_secs_f64());
39 }
40 if duration > Duration::from_millis(1) {
41 return format!("{}ms", duration.as_millis());
42 }
43 format!("{}µs", duration.as_micros())
44}
45
46struct Timeline {
47 name: String,
48 duration: Duration,
49}
50
51#[derive(Default, Debug, Clone)]
77pub struct HttpStat {
78 pub is_grpc: bool,
79 pub request_headers: HeaderMap<HeaderValue>,
80 pub dns_lookup: Option<Duration>,
81 pub quic_connect: Option<Duration>,
82 pub tcp_connect: Option<Duration>,
83 pub tls_handshake: Option<Duration>,
84 pub server_processing: Option<Duration>,
85 pub content_transfer: Option<Duration>,
86 pub total: Option<Duration>,
87 pub addr: Option<String>,
88 pub grpc_status: Option<String>,
89 pub status: Option<StatusCode>,
90 pub tls: Option<String>,
91 pub alpn: Option<String>,
92 pub subject: Option<String>,
93 pub issuer: Option<String>,
94 pub cert_not_before: Option<String>,
95 pub cert_not_after: Option<String>,
96 pub cert_cipher: Option<String>,
97 pub cert_domains: Option<Vec<String>>,
98 pub certificates: Option<Vec<Certificate>>,
99 pub body: Option<Bytes>,
100 pub body_size: Option<usize>,
101 pub headers: Option<HeaderMap<HeaderValue>>,
102 pub error: Option<String>,
103 pub silent: bool,
104 pub verbose: bool,
105 pub pretty: bool,
106}
107
108#[derive(Debug, Clone)]
109pub struct Certificate {
110 pub subject: String,
111 pub issuer: String,
112 pub not_before: String,
113 pub not_after: String,
114}
115
116impl HttpStat {
117 pub fn is_success(&self) -> bool {
118 if self.error.is_some() {
119 return false;
120 }
121 if self.is_grpc {
122 if let Some(grpc_status) = &self.grpc_status {
123 return grpc_status == "0";
124 }
125 return false;
126 }
127 let Some(status) = &self.status else {
128 return false;
129 };
130 if status.as_u16() >= 400 {
131 return false;
132 }
133 true
134 }
135}
136
137impl fmt::Display for HttpStat {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 if let Some(addr) = &self.addr {
140 let mut text = format!(
141 "{} {}",
142 LightGreen.paint("Connected to"),
143 LightCyan.paint(addr)
144 );
145 if self.silent {
146 if let Some(status) = &self.status {
147 let alpn = self.alpn.clone().unwrap_or_else(|| ALPN_HTTP1.to_string());
148 let status_code = status.as_u16();
149 let status = if status_code < 400 {
150 LightGreen.paint(status.to_string())
151 } else {
152 LightRed.paint(status.to_string())
153 };
154 text = format!(
155 "{text} --> {} {}",
156 LightCyan.paint(alpn.to_uppercase()),
157 status
158 );
159 } else {
160 text = format!("{text} --> {}", LightRed.paint("FAIL"));
161 }
162 text = format!("{text} {}", format_duration(self.total.unwrap_or_default()));
163 }
164 writeln!(f, "{text}")?;
165 }
166 if let Some(error) = &self.error {
167 writeln!(f, "Error: {}", LightRed.paint(error))?;
168 }
169 if self.silent {
170 return Ok(());
171 }
172 if self.verbose {
173 for (key, value) in self.request_headers.iter() {
174 writeln!(
175 f,
176 "{}: {}",
177 key.to_string().to_train_case(),
178 LightCyan.paint(value.to_str().unwrap_or_default())
179 )?;
180 }
181 writeln!(f)?;
182 }
183
184 if let Some(status) = &self.status {
185 let alpn = self.alpn.clone().unwrap_or_else(|| ALPN_HTTP1.to_string());
186 let status_code = status.as_u16();
187 let status = if status_code < 400 {
188 LightGreen.paint(status.to_string())
189 } else {
190 LightRed.paint(status.to_string())
191 };
192 writeln!(f, "{} {}", LightCyan.paint(alpn.to_uppercase()), status)?;
193 }
194 if self.is_grpc {
195 if self.is_success() {
196 writeln!(f, "{}", LightGreen.paint("GRPC OK"))?;
197 }
198 writeln!(f)?;
199 }
200
201 if let Some(tls) = &self.tls {
202 writeln!(f)?;
203 writeln!(f, "Tls: {}", LightCyan.paint(tls))?;
204 writeln!(
205 f,
206 "Cipher: {}",
207 LightCyan.paint(self.cert_cipher.clone().unwrap_or_default())
208 )?;
209 writeln!(
210 f,
211 "Not Before: {}",
212 LightCyan.paint(self.cert_not_before.clone().unwrap_or_default())
213 )?;
214 writeln!(
215 f,
216 "Not After: {}",
217 LightCyan.paint(self.cert_not_after.clone().unwrap_or_default())
218 )?;
219 if self.verbose {
220 writeln!(
221 f,
222 "Subject: {}",
223 LightCyan.paint(self.subject.clone().unwrap_or_default())
224 )?;
225 writeln!(
226 f,
227 "Issuer: {}",
228 LightCyan.paint(self.issuer.clone().unwrap_or_default())
229 )?;
230 writeln!(
231 f,
232 "Certificate Domains: {}",
233 LightCyan.paint(self.cert_domains.clone().unwrap_or_default().join(", "))
234 )?;
235 }
236 writeln!(f)?;
237
238 if self.verbose {
239 if let Some(certificates) = &self.certificates {
240 writeln!(f, "Certificate Chain")?;
241 for (index, cert) in certificates.iter().enumerate() {
242 writeln!(
243 f,
244 " {index} Subject: {}",
245 LightCyan.paint(cert.subject.clone())
246 )?;
247 writeln!(f, " Issuer: {}", LightCyan.paint(cert.issuer.clone()))?;
248 writeln!(
249 f,
250 " Not Before: {}",
251 LightCyan.paint(cert.not_before.clone())
252 )?;
253 writeln!(
254 f,
255 " Not After: {}",
256 LightCyan.paint(cert.not_after.clone())
257 )?;
258 writeln!(f)?;
259 }
260 }
261 }
262 }
263
264 let mut is_text = false;
265 let mut is_json = false;
266 if let Some(headers) = &self.headers {
267 for (key, value) in headers.iter() {
268 let value = value.to_str().unwrap_or_default();
269 if key.to_string().to_lowercase() == "content-type" {
270 if value.contains("text/") || value.contains("application/json") {
271 is_text = true;
272 }
273 if value.contains("application/json") {
274 is_json = true;
275 }
276 }
277 writeln!(
278 f,
279 "{}: {}",
280 key.to_string().to_train_case(),
281 LightCyan.paint(value)
282 )?;
283 }
284 writeln!(f)?;
285 }
286
287 let width = 20;
288
289 let mut timelines = vec![];
290 if let Some(value) = self.dns_lookup {
291 timelines.push(Timeline {
292 name: "DNS Lookup".to_string(),
293 duration: value,
294 });
295 }
296
297 if let Some(value) = self.tcp_connect {
298 timelines.push(Timeline {
299 name: "TCP Connect".to_string(),
300 duration: value,
301 });
302 }
303
304 if let Some(value) = self.tls_handshake {
305 timelines.push(Timeline {
306 name: "TLS Handshake".to_string(),
307 duration: value,
308 });
309 }
310 if let Some(value) = self.quic_connect {
311 timelines.push(Timeline {
312 name: "QUIC Connect".to_string(),
313 duration: value,
314 });
315 }
316
317 if let Some(value) = self.server_processing {
318 timelines.push(Timeline {
319 name: "Server Processing".to_string(),
320 duration: value,
321 });
322 }
323
324 if let Some(value) = self.content_transfer {
325 timelines.push(Timeline {
326 name: "Content Transfer".to_string(),
327 duration: value,
328 });
329 }
330
331 if !timelines.is_empty() {
332 write!(f, " ")?;
334 for (i, timeline) in timelines.iter().enumerate() {
335 write!(
336 f,
337 "{}",
338 timeline.name.unicode_pad(width, Alignment::Center, true)
339 )?;
340 if i < timelines.len() - 1 {
341 write!(f, " ")?;
342 }
343 }
344 writeln!(f)?;
345
346 write!(f, "[")?;
348 for (i, timeline) in timelines.iter().enumerate() {
349 write!(
350 f,
351 "{}",
352 LightCyan.paint(
353 format_duration(timeline.duration)
354 .unicode_pad(width, Alignment::Center, true)
355 .to_string(),
356 )
357 )?;
358 if i < timelines.len() - 1 {
359 write!(f, "|")?;
360 }
361 }
362 writeln!(f, "]")?;
363 }
364
365 write!(f, " ")?;
367 for _ in 0..timelines.len() {
368 write!(f, "{}", " ".repeat(width))?;
369 write!(f, "|")?;
370 }
371 writeln!(f)?;
372 write!(f, "{}", " ".repeat(width * timelines.len()))?;
373 write!(
374 f,
375 "Total:{}\n\n",
376 LightCyan.paint(format_duration(self.total.unwrap_or_default()))
377 )?;
378
379 if let Some(body) = &self.body {
380 let status = self.status.unwrap_or(StatusCode::OK).as_u16();
381 let mut body = std::str::from_utf8(body.as_ref())
382 .unwrap_or_default()
383 .to_string();
384 if self.pretty && is_json {
385 if let Ok(json_body) = serde_json::from_str::<serde_json::Value>(&body) {
386 if let Ok(value) = serde_json::to_string_pretty(&json_body) {
387 body = value;
388 }
389 }
390 }
391 if self.verbose || (is_text && body.len() < 1024) {
392 let text = format!(
393 "Body size: {}",
394 ByteSize(self.body_size.unwrap_or(0) as u64)
395 );
396 writeln!(f, "{}\n", LightCyan.paint(text))?;
397 if status >= 400 {
398 writeln!(f, "{}", LightRed.paint(body))?;
399 } else {
400 writeln!(f, "{body}")?;
401 }
402 } else {
403 let mut save_tips = "".to_string();
404 if let Ok(mut file) = NamedTempFile::new() {
405 if let Ok(()) = file.write_all(body.as_bytes()) {
406 save_tips = format!("saved to: {}", file.path().display());
407 let _ = file.keep();
408 }
409 }
410 let text = format!(
411 "Body discarded {}",
412 ByteSize(self.body_size.unwrap_or(0) as u64)
413 );
414 writeln!(f, "{} {}", LightCyan.paint(text), save_tips)?;
415 }
416 }
417
418 Ok(())
419 }
420}