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::time::Duration;
27use unicode_truncate::Alignment;
28use unicode_truncate::UnicodeTruncateStr;
29
30pub static ALPN_HTTP2: &str = "h2";
31pub static ALPN_HTTP1: &str = "http/1.1";
32pub static ALPN_HTTP3: &str = "h3";
33
34fn format_duration(duration: Duration) -> String {
35 if duration > Duration::from_secs(1) {
36 return format!("{:.2}s", duration.as_secs_f64());
37 }
38 if duration > Duration::from_millis(1) {
39 return format!("{}ms", duration.as_millis());
40 }
41 format!("{}µs", duration.as_micros())
42}
43
44struct Timeline {
45 name: String,
46 duration: Duration,
47}
48
49#[derive(Default, Debug)]
75pub struct HttpStat {
76 pub dns_lookup: Option<Duration>,
77 pub quic_connect: Option<Duration>,
78 pub tcp_connect: Option<Duration>,
79 pub tls_handshake: Option<Duration>,
80 pub server_processing: Option<Duration>,
81 pub content_transfer: Option<Duration>,
82 pub total: Option<Duration>,
83 pub addr: Option<String>,
84 pub status: Option<StatusCode>,
85 pub tls: Option<String>,
86 pub alpn: Option<String>,
87 pub subject: Option<String>,
88 pub issuer: Option<String>,
89 pub cert_not_before: Option<String>,
90 pub cert_not_after: Option<String>,
91 pub cert_cipher: Option<String>,
92 pub cert_domains: Option<Vec<String>>,
93 pub body: Option<Bytes>,
94 pub body_size: Option<usize>,
95 pub headers: Option<HeaderMap<HeaderValue>>,
96 pub error: Option<String>,
97 pub silent: bool,
98}
99
100impl fmt::Display for HttpStat {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 if let Some(addr) = &self.addr {
103 let mut text = format!(
104 "{} {}",
105 LightGreen.paint("Connected to"),
106 LightCyan.paint(addr)
107 );
108 if self.silent {
109 if let Some(status) = &self.status {
110 let alpn = self.alpn.clone().unwrap_or_else(|| ALPN_HTTP1.to_string());
111 let status_code = status.as_u16();
112 let status = if status_code < 400 {
113 LightGreen.paint(status.to_string())
114 } else {
115 LightRed.paint(status.to_string())
116 };
117 text = format!(
118 "{text} --> {} {}",
119 LightCyan.paint(alpn.to_uppercase()),
120 status
121 );
122 } else {
123 text = format!("{text} --> {}", LightRed.paint("FAIL"));
124 }
125 }
126 writeln!(f, "{}", text)?;
127 }
128 if let Some(error) = &self.error {
129 writeln!(f, "Error: {}", LightRed.paint(error))?;
130 }
131 if self.silent {
132 return Ok(());
133 }
134 if let Some(status) = &self.status {
135 let alpn = self.alpn.clone().unwrap_or_else(|| ALPN_HTTP1.to_string());
136 let status_code = status.as_u16();
137 let status = if status_code < 400 {
138 LightGreen.paint(status.to_string())
139 } else {
140 LightRed.paint(status.to_string())
141 };
142 writeln!(f, "{} {}", LightCyan.paint(alpn.to_uppercase()), status)?;
143 }
144
145 if let Some(tls) = &self.tls {
146 writeln!(f, "{}: {}", "tls".to_train_case(), LightCyan.paint(tls))?;
147 writeln!(
148 f,
149 "{}: {}",
150 "cipher".to_train_case(),
151 LightCyan.paint(self.cert_cipher.clone().unwrap_or_default())
152 )?;
153 writeln!(
154 f,
155 "{}: {}",
156 "not before".to_train_case(),
157 LightCyan.paint(self.cert_not_before.clone().unwrap_or_default())
158 )?;
159 writeln!(
160 f,
161 "{}: {}",
162 "not after".to_train_case(),
163 LightCyan.paint(self.cert_not_after.clone().unwrap_or_default())
164 )?;
165 writeln!(f)?;
166 }
167
168 if let Some(headers) = &self.headers {
169 for (key, value) in headers.iter() {
170 writeln!(
171 f,
172 "{}: {}",
173 key.to_string().to_train_case(),
174 LightCyan.paint(value.to_str().unwrap_or_default())
175 )?;
176 }
177 writeln!(f)?;
178 }
179
180 if let Some(body) = &self.body {
181 let status = self.status.unwrap_or(StatusCode::OK).as_u16();
182 if status >= 400 {
183 let body = std::str::from_utf8(body.as_ref()).unwrap_or_default();
184 writeln!(f, "Body: {}", LightRed.paint(body))?;
185 } else {
186 let text = format!(
187 "Body discarded {}",
188 ByteSize(self.body_size.unwrap_or(0) as u64)
189 );
190 writeln!(f, "{}", LightCyan.paint(text))?;
191 }
192 }
193
194 let width = 20;
195
196 let mut timelines = vec![];
197 if let Some(value) = self.dns_lookup {
198 timelines.push(Timeline {
199 name: "DNS Lookup".to_string(),
200 duration: value,
201 });
202 }
203
204 if let Some(value) = self.tcp_connect {
205 timelines.push(Timeline {
206 name: "TCP Connect".to_string(),
207 duration: value,
208 });
209 }
210
211 if let Some(value) = self.tls_handshake {
212 timelines.push(Timeline {
213 name: "TLS Handshake".to_string(),
214 duration: value,
215 });
216 }
217 if let Some(value) = self.quic_connect {
218 timelines.push(Timeline {
219 name: "QUIC Connect".to_string(),
220 duration: value,
221 });
222 }
223
224 if let Some(value) = self.server_processing {
225 timelines.push(Timeline {
226 name: "Server Processing".to_string(),
227 duration: value,
228 });
229 }
230
231 if let Some(value) = self.content_transfer {
232 timelines.push(Timeline {
233 name: "Content Transfer".to_string(),
234 duration: value,
235 });
236 }
237
238 write!(f, " ")?;
240 for (i, timeline) in timelines.iter().enumerate() {
241 write!(
242 f,
243 "{}",
244 timeline.name.unicode_pad(width, Alignment::Center, true)
245 )?;
246 if i < timelines.len() - 1 {
247 write!(f, " ")?;
248 }
249 }
250 writeln!(f)?;
251
252 write!(f, "[")?;
254 for (i, timeline) in timelines.iter().enumerate() {
255 write!(
256 f,
257 "{}",
258 LightCyan.paint(
259 format_duration(timeline.duration)
260 .unicode_pad(width, Alignment::Center, true)
261 .to_string(),
262 )
263 )?;
264 if i < timelines.len() - 1 {
265 write!(f, "|")?;
266 }
267 }
268 writeln!(f, "]")?;
269
270 write!(f, " ")?;
272 for _ in 0..timelines.len() {
273 write!(f, "{}", " ".repeat(width))?;
274 write!(f, "|")?;
275 }
276 writeln!(f)?;
277
278 write!(f, "{}", " ".repeat(width * timelines.len()))?;
279 write!(
280 f,
281 "total:{}",
282 LightCyan.paint(format_duration(self.total.unwrap_or_default()))
283 )?;
284
285 Ok(())
286 }
287}