1use bytes::Bytes;
19use heck::ToTrainCase;
20use http::HeaderMap;
21use http::HeaderValue;
22use http::StatusCode;
23use nu_ansi_term::Color::{LightCyan, LightGreen, LightRed};
24use std::fmt;
25use std::time::Duration;
26use unicode_truncate::Alignment;
27use unicode_truncate::UnicodeTruncateStr;
28
29pub static ALPN_HTTP2: &str = "h2";
30pub static ALPN_HTTP1: &str = "http/1.1";
31
32fn format_duration(duration: Duration) -> String {
33 if duration > Duration::from_secs(1) {
34 return format!("{:.2}s", duration.as_secs_f64());
35 }
36 if duration > Duration::from_millis(1) {
37 return format!("{}ms", duration.as_millis());
38 }
39 format!("{}µs", duration.as_micros())
40}
41
42struct Timeline {
43 name: String,
44 duration: Duration,
45}
46
47#[derive(Default, Debug)]
48pub struct HttpStat {
49 pub dns_lookup: Option<Duration>,
50 pub tcp_connect: Option<Duration>,
51 pub tls_handshake: Option<Duration>,
52 pub server_processing: Option<Duration>,
53 pub content_transfer: Option<Duration>,
54 pub total: Option<Duration>,
55 pub addr: Option<String>,
56 pub status: Option<StatusCode>,
57 pub tls: Option<String>,
58 pub alpn: Option<String>,
59 pub cert_not_before: Option<String>,
60 pub cert_not_after: Option<String>,
61 pub cert_cipher: Option<String>,
62 pub cert_domains: Option<Vec<String>>,
63 pub body: Option<Bytes>,
64 pub headers: Option<HeaderMap<HeaderValue>>,
65 pub error: Option<String>,
66}
67
68impl fmt::Display for HttpStat {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 if let Some(addr) = &self.addr {
71 write!(
72 f,
73 "{} {}\n\n",
74 LightGreen.paint("Connected to"),
75 LightCyan.paint(addr)
76 )?;
77 }
78 if let Some(error) = &self.error {
79 writeln!(f, "Error: {}", LightRed.paint(error))?;
80 }
81 if let Some(status) = &self.status {
82 let alpn = self.alpn.clone().unwrap_or_else(|| ALPN_HTTP1.to_string());
83 let status_code = status.as_u16();
84 let status = if status_code < 400 {
85 LightGreen.paint(status.to_string())
86 } else {
87 LightRed.paint(status.to_string())
88 };
89 writeln!(f, "{} {}", LightCyan.paint(alpn.to_uppercase()), status)?;
90 }
91 if let Some(tls) = &self.tls {
92 writeln!(f, "{}: {}", "tls".to_train_case(), LightCyan.paint(tls))?;
93 writeln!(
94 f,
95 "{}: {}",
96 "cipher".to_train_case(),
97 LightCyan.paint(self.cert_cipher.clone().unwrap_or_default())
98 )?;
99 writeln!(
100 f,
101 "{}: {}",
102 "not before".to_train_case(),
103 LightCyan.paint(self.cert_not_before.clone().unwrap_or_default())
104 )?;
105 writeln!(
106 f,
107 "{}: {}",
108 "not after".to_train_case(),
109 LightCyan.paint(self.cert_not_after.clone().unwrap_or_default())
110 )?;
111 writeln!(f)?;
112 }
113
114 if let Some(headers) = &self.headers {
115 for (key, value) in headers.iter() {
116 writeln!(
117 f,
118 "{}: {}",
119 key.to_string().to_train_case(),
120 LightCyan.paint(value.to_str().unwrap_or_default())
121 )?;
122 }
123 writeln!(f)?;
124 }
125
126 if let Some(body) = &self.body {
127 let status = self.status.unwrap_or(StatusCode::OK).as_u16();
128 if status >= 400 {
129 let body = std::str::from_utf8(self.body.as_ref().unwrap()).unwrap_or_default();
130 writeln!(f, "Body: {}", LightRed.paint(body))?;
131 } else {
132 let text = format!("Body discarded {} bytes", body.len());
133 writeln!(f, "{}", LightCyan.paint(text))?;
134 }
135 }
136
137 let width = 20;
138
139 let mut timelines = vec![];
140 if let Some(value) = self.dns_lookup {
141 timelines.push(Timeline {
142 name: "DNS Lookup".to_string(),
143 duration: value,
144 });
145 }
146
147 if let Some(value) = self.tcp_connect {
148 timelines.push(Timeline {
149 name: "TCP Connect".to_string(),
150 duration: value,
151 });
152 }
153
154 if let Some(value) = self.tls_handshake {
155 timelines.push(Timeline {
156 name: "TLS Handshake".to_string(),
157 duration: value,
158 });
159 }
160
161 if let Some(value) = self.server_processing {
162 timelines.push(Timeline {
163 name: "Server Processing".to_string(),
164 duration: value,
165 });
166 }
167
168 if let Some(value) = self.content_transfer {
169 timelines.push(Timeline {
170 name: "Content Transfer".to_string(),
171 duration: value,
172 });
173 }
174
175 write!(f, " ")?;
177 for (i, timeline) in timelines.iter().enumerate() {
178 write!(
179 f,
180 "{}",
181 timeline.name.unicode_pad(width, Alignment::Center, true)
182 )?;
183 if i < timelines.len() - 1 {
184 write!(f, " ")?;
185 }
186 }
187 writeln!(f)?;
188
189 write!(f, "[")?;
191 for (i, timeline) in timelines.iter().enumerate() {
192 write!(
193 f,
194 "{}",
195 LightCyan.paint(
196 format_duration(timeline.duration)
197 .unicode_pad(width, Alignment::Center, true)
198 .to_string(),
199 )
200 )?;
201 if i < timelines.len() - 1 {
202 write!(f, "|")?;
203 }
204 }
205 writeln!(f, "]")?;
206
207 write!(f, " ")?;
209 for _ in 0..timelines.len() {
210 write!(f, "{}", " ".repeat(width))?;
211 write!(f, "|")?;
212 }
213 writeln!(f)?;
214
215 write!(f, "{}", " ".repeat(width * timelines.len()))?;
216 write!(
217 f,
218 "total:{}",
219 LightCyan.paint(format_duration(self.total.unwrap_or_default()))
220 )?;
221
222 Ok(())
223 }
224}