1use serde::Serialize;
2use serde_json::{json, Value};
3use std::collections::HashMap;
4use std::sync::Arc;
5use tokio::sync::Mutex;
6
7use crate::cdp::CDPClient;
8use crate::error::Result;
9
10#[derive(Debug, Clone, Serialize)]
12#[serde(rename_all = "camelCase")]
13pub struct HarEntry {
14 pub pageref: String,
16 pub started_date_time: String,
18 pub time: f64,
20 pub request: HarRequest,
22 pub response: HarResponse,
24 pub cache: Value,
26 pub timings: HarTimings,
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub server_ip_address: Option<String>,
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub connection: Option<String>,
34}
35
36#[derive(Debug, Clone, Serialize)]
38#[serde(rename_all = "camelCase")]
39pub struct HarRequest {
40 pub method: String,
42 pub url: String,
44 pub http_version: String,
46 pub headers: Vec<HarHeader>,
48 pub query_string: Vec<HarQueryParam>,
50 pub headers_size: i64,
52 pub body_size: i64,
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub post_data: Option<HarPostData>,
57}
58
59#[derive(Debug, Clone, Serialize)]
61#[serde(rename_all = "camelCase")]
62pub struct HarResponse {
63 pub status: i64,
65 pub status_text: String,
67 pub http_version: String,
69 pub headers: Vec<HarHeader>,
71 pub cookies: Vec<HarCookie>,
73 pub content: HarContent,
75 pub redirect_url: String,
77 pub headers_size: i64,
79 pub body_size: i64,
81}
82
83#[derive(Debug, Clone, Serialize)]
85pub struct HarTimings {
86 pub dns: f64,
88 pub connect: f64,
90 #[serde(skip_serializing_if = "Option::is_none")]
92 pub ssl: Option<f64>,
93 pub send: f64,
95 pub wait: f64,
97 pub receive: f64,
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub blocked: Option<f64>,
102}
103
104#[derive(Debug, Clone, Serialize)]
106pub struct HarHeader {
107 pub name: String,
109 pub value: String,
111}
112
113#[derive(Debug, Clone, Serialize)]
115#[serde(rename_all = "camelCase")]
116pub struct HarQueryParam {
117 pub name: String,
119 pub value: String,
121}
122
123#[derive(Debug, Clone, Serialize)]
125#[serde(rename_all = "camelCase")]
126pub struct HarPostData {
127 pub mime_type: String,
129 pub text: String,
131}
132
133#[derive(Debug, Clone, Serialize)]
135pub struct HarCookie {
136 pub name: String,
138 pub value: String,
140 #[serde(skip_serializing_if = "Option::is_none")]
142 pub path: Option<String>,
143 #[serde(skip_serializing_if = "Option::is_none")]
145 pub domain: Option<String>,
146 #[serde(skip_serializing_if = "Option::is_none")]
148 pub expires: Option<String>,
149 #[serde(skip_serializing_if = "Option::is_none")]
151 pub http_only: Option<bool>,
152 #[serde(skip_serializing_if = "Option::is_none")]
154 pub secure: Option<bool>,
155}
156
157#[derive(Debug, Clone, Serialize)]
159#[serde(rename_all = "camelCase")]
160pub struct HarContent {
161 pub size: i64,
163 pub mime_type: String,
165 #[serde(skip_serializing_if = "Option::is_none")]
167 pub compression: Option<i64>,
168}
169
170#[derive(Debug, Clone, Serialize)]
172pub struct HarLog {
173 pub version: String,
175 pub creator: Value,
177 pub entries: Vec<HarEntry>,
179}
180
181#[derive(Debug, Clone, Serialize)]
183pub struct HarArchive {
184 pub log: HarLog,
186}
187
188#[derive(Debug, Clone)]
191struct PendingRequest {
192 method: String,
193 url: String,
194 http_version: String,
195 request_headers: Vec<HarHeader>,
196 query_string: Vec<HarQueryParam>,
197 post_data: Option<HarPostData>,
198 request_timestamp: f64,
199 status: Option<i64>,
200 status_text: Option<String>,
201 response_headers: Vec<HarHeader>,
202 response_cookies: Vec<HarCookie>,
203 mime_type: Option<String>,
204 redirect_url: String,
205 headers_size: i64,
206 body_size: i64,
207 server_ip_address: Option<String>,
208 connection: Option<String>,
209 timing_dns: f64,
210 timing_connect: f64,
211 timing_ssl: Option<f64>,
212 timing_send: f64,
213 timing_wait: f64,
214 timing_receive: f64,
215 timing_blocked: Option<f64>,
216}
217
218impl PendingRequest {
219 fn new(method: String, url: String, request_timestamp: f64) -> Self {
220 Self {
221 method,
222 url,
223 http_version: String::new(),
224 request_headers: Vec::new(),
225 query_string: Vec::new(),
226 post_data: None,
227 request_timestamp,
228 status: None,
229 status_text: None,
230 response_headers: Vec::new(),
231 response_cookies: Vec::new(),
232 mime_type: None,
233 redirect_url: String::new(),
234 headers_size: -1,
235 body_size: -1,
236 server_ip_address: None,
237 connection: None,
238 timing_dns: -1.0,
239 timing_connect: -1.0,
240 timing_ssl: None,
241 timing_send: -1.0,
242 timing_wait: -1.0,
243 timing_receive: -1.0,
244 timing_blocked: None,
245 }
246 }
247
248 fn finish(self, response_timestamp: f64) -> HarEntry {
249 let time_ms = ((response_timestamp - self.request_timestamp) * 1000.0).max(0.0);
250 let iso = iso_timestamp(self.request_timestamp);
251
252 let http_version = self.http_version;
253 let method = self.method;
254 let url = self.url;
255 let request_headers = self.request_headers;
256 let query_string = self.query_string;
257 let headers_size = self.headers_size;
258 let body_size = self.body_size;
259 let post_data = self.post_data;
260 let status = self.status.unwrap_or(0);
261 let status_text = self.status_text.unwrap_or_default();
262 let response_headers = self.response_headers;
263 let response_cookies = self.response_cookies;
264 let mime_type = self.mime_type.unwrap_or_default();
265 let redirect_url = self.redirect_url;
266 let server_ip_address = self.server_ip_address;
267 let connection = self.connection;
268 let timing_dns = self.timing_dns;
269 let timing_connect = self.timing_connect;
270 let timing_ssl = self.timing_ssl;
271 let timing_send = self.timing_send;
272 let timing_wait = self.timing_wait;
273 let timing_receive = self.timing_receive;
274 let timing_blocked = self.timing_blocked;
275
276 HarEntry {
277 pageref: "page_1".to_string(),
278 started_date_time: iso,
279 time: time_ms,
280 request: HarRequest {
281 method,
282 url,
283 http_version: http_version.clone(),
284 headers: request_headers,
285 query_string,
286 headers_size,
287 body_size,
288 post_data,
289 },
290 response: HarResponse {
291 status,
292 status_text,
293 http_version,
294 headers: response_headers,
295 cookies: response_cookies,
296 content: HarContent {
297 size: body_size.max(0),
298 mime_type,
299 compression: None,
300 },
301 redirect_url,
302 headers_size,
303 body_size,
304 },
305 cache: json!({}),
306 timings: HarTimings {
307 dns: timing_dns,
308 connect: timing_connect,
309 ssl: timing_ssl,
310 send: timing_send,
311 wait: timing_wait,
312 receive: timing_receive,
313 blocked: timing_blocked,
314 },
315 server_ip_address,
316 connection,
317 }
318 }
319}
320
321struct CaptureState {
322 pending: HashMap<String, PendingRequest>,
323 entries: Vec<HarEntry>,
324}
325
326pub struct HarCapture {
348 cdp: Arc<CDPClient>,
349 session_id: String,
350 state: Arc<Mutex<CaptureState>>,
351}
352
353impl HarCapture {
354 pub(crate) fn new(cdp: Arc<CDPClient>, session_id: String) -> Self {
355 Self {
356 cdp,
357 session_id,
358 state: Arc::new(Mutex::new(CaptureState {
359 pending: HashMap::new(),
360 entries: Vec::new(),
361 })),
362 }
363 }
364
365 pub async fn start(&self) -> Result<()> {
369 self.cdp
371 .send_command_with_session(&self.session_id, "Network.enable".to_string(), None)
372 .await?;
373
374 let mut rx = self.cdp.subscribe_events();
375 let session_id = self.session_id.clone();
376 let state = self.state.clone();
377
378 tokio::spawn(async move {
379 loop {
380 match rx.recv().await {
381 Ok(msg) if msg.session_id.as_deref() == Some(&session_id) => {
382 Self::handle_event(&state, msg.method.as_deref(), msg.params).await;
383 }
384 Ok(_) => {} Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {}
386 Err(_) => return, }
388 }
389 });
390
391 Ok(())
392 }
393
394 async fn handle_event(
395 state: &Arc<Mutex<CaptureState>>,
396 method: Option<&str>,
397 params: Option<Value>,
398 ) {
399 let Some(params) = params else { return };
400 match method {
401 Some("Network.requestWillBeSent") => {
402 Self::handle_request_will_be_sent(state, params).await;
403 }
404 Some("Network.responseReceived") => {
405 Self::handle_response_received(state, params).await;
406 }
407 Some("Network.loadingFinished") => {
408 Self::handle_loading_finished(state, params).await;
409 }
410 Some("Network.loadingFailed") => {
411 Self::handle_loading_failed(state, params).await;
412 }
413 _ => {}
414 }
415 }
416
417 async fn handle_request_will_be_sent(state: &Arc<Mutex<CaptureState>>, params: Value) {
418 let request_id = params
419 .get("requestId")
420 .and_then(|v| v.as_str())
421 .map(|s| s.to_string());
422 let request = match params.get("request") {
423 Some(r) => r,
424 None => return,
425 };
426 let method = match request.get("method").and_then(|v| v.as_str()) {
427 Some(m) => m.to_string(),
428 None => return,
429 };
430 let url = match request.get("url").and_then(|v| v.as_str()) {
431 Some(u) => u.to_string(),
432 None => return,
433 };
434 let ts = params
435 .get("timestamp")
436 .and_then(|v| v.as_f64())
437 .unwrap_or(0.0);
438
439 let http_version = {
441 if let Some(ver) = request.get("headers").and_then(|h| h.get(":version")) {
442 ver.as_str().unwrap_or("HTTP/2.0").to_string()
443 } else if url.starts_with("https") {
444 "HTTP/2.0".to_string()
445 } else {
446 "HTTP/1.1".to_string()
447 }
448 };
449
450 let mut pending = PendingRequest::new(method, url, ts);
451 pending.http_version = http_version;
452
453 if let Some(headers) = request.get("headers").and_then(|h| h.as_object()) {
455 for (name, value) in headers {
456 if let Some(val) = value.as_str() {
457 if !name.starts_with(':') {
458 pending.request_headers.push(HarHeader {
459 name: name.clone(),
460 value: val.to_string(),
461 });
462 }
463 }
464 }
465 }
466
467 if let Some(qs) = request.get("queryString").and_then(|v| v.as_array()) {
469 for param in qs {
470 if let (Some(name), Some(value)) = (
471 param.get("name").and_then(|v| v.as_str()),
472 param.get("value").and_then(|v| v.as_str()),
473 ) {
474 pending.query_string.push(HarQueryParam {
475 name: name.to_string(),
476 value: value.to_string(),
477 });
478 }
479 }
480 }
481
482 if let Some(post) = request.get("postData") {
484 if let (Some(text), mime) = (
485 post.get("text").and_then(|v| v.as_str()),
486 post.get("mimeType")
487 .and_then(|v| v.as_str())
488 .unwrap_or("application/octet-stream"),
489 ) {
490 pending.post_data = Some(HarPostData {
491 mime_type: mime.to_string(),
492 text: text.to_string(),
493 });
494 }
495 }
496
497 let mut guard = state.lock().await;
498 if let Some(id) = request_id {
499 guard.pending.insert(id, pending);
500 }
501 }
502
503 async fn handle_response_received(state: &Arc<Mutex<CaptureState>>, params: Value) {
504 let request_id = params
505 .get("requestId")
506 .and_then(|v| v.as_str())
507 .map(|s| s.to_string());
508 let response = match params.get("response") {
509 Some(r) => r,
510 None => return,
511 };
512
513 let status = response.get("status").and_then(|v| v.as_i64()).unwrap_or(0);
514 let status_text = response
515 .get("statusText")
516 .and_then(|v| v.as_str())
517 .unwrap_or("")
518 .to_string();
519 let mime_type = response
520 .get("mimeType")
521 .and_then(|v| v.as_str())
522 .unwrap_or("")
523 .to_string();
524 let remote_ip = response
525 .get("remoteIPAddress")
526 .and_then(|v| v.as_str())
527 .map(|s| s.to_string());
528 let connection_id = response
529 .get("connectionId")
530 .and_then(|v| v.as_str())
531 .map(|s| s.to_string());
532
533 let mut resp_headers = Vec::new();
535 if let Some(headers) = response.get("headers").and_then(|h| h.as_object()) {
536 for (name, value) in headers {
537 if let Some(val) = value.as_str() {
538 if !name.starts_with(':') {
539 resp_headers.push(HarHeader {
540 name: name.clone(),
541 value: val.to_string(),
542 });
543 }
544 }
545 }
546 }
547
548 let mut cookies = Vec::new();
550 if let Some(cookie_array) = response.get("cookies").and_then(|c| c.as_array()) {
551 for c in cookie_array {
552 let name = c.get("name").and_then(|v| v.as_str()).unwrap_or("");
553 let value = c.get("value").and_then(|v| v.as_str()).unwrap_or("");
554 cookies.push(HarCookie {
555 name: name.to_string(),
556 value: value.to_string(),
557 path: c
558 .get("path")
559 .and_then(|v| v.as_str())
560 .map(|s| s.to_string()),
561 domain: c
562 .get("domain")
563 .and_then(|v| v.as_str())
564 .map(|s| s.to_string()),
565 expires: c
566 .get("expires")
567 .and_then(|v| v.as_str())
568 .map(|s| s.to_string()),
569 http_only: c.get("httpOnly").and_then(|v| v.as_bool()),
570 secure: c.get("secure").and_then(|v| v.as_bool()),
571 });
572 }
573 }
574
575 let timings = response.get("timing");
577 let dns = timings
578 .and_then(|t| t.get("dns"))
579 .and_then(|v| v.as_f64())
580 .unwrap_or(-1.0);
581 let connect = timings
582 .and_then(|t| t.get("connect"))
583 .and_then(|v| v.as_f64())
584 .unwrap_or(-1.0);
585 let ssl = timings
586 .and_then(|t| t.get("ssl"))
587 .and_then(|v| v.as_f64())
588 .filter(|&v| v >= 0.0);
589 let send = timings
590 .and_then(|t| t.get("send"))
591 .and_then(|v| v.as_f64())
592 .unwrap_or(-1.0);
593 let wait = timings
594 .and_then(|t| t.get("wait"))
595 .and_then(|v| v.as_f64())
596 .unwrap_or(-1.0);
597 let receive = timings
598 .and_then(|t| t.get("receive"))
599 .and_then(|v| v.as_f64())
600 .unwrap_or(-1.0);
601 let blocked = timings
602 .and_then(|t| t.get("blocked"))
603 .and_then(|v| v.as_f64())
604 .filter(|&v| v >= 0.0);
605
606 let redirect_url = response
608 .get("redirectURL")
609 .and_then(|v| v.as_str())
610 .unwrap_or("")
611 .to_string();
612
613 let headers_size = response
615 .get("headersSize")
616 .and_then(|v| v.as_i64())
617 .unwrap_or(-1);
618
619 let mut guard = state.lock().await;
620 if let Some(ref id) = request_id {
621 if let Some(p) = guard.pending.get_mut(id) {
622 p.status = Some(status);
623 p.status_text = Some(status_text);
624 p.mime_type = Some(mime_type);
625 p.server_ip_address = remote_ip;
626 p.connection = connection_id;
627 p.response_headers = resp_headers;
628 p.response_cookies = cookies;
629 p.timing_dns = dns;
630 p.timing_connect = connect;
631 p.timing_ssl = ssl;
632 p.timing_send = send;
633 p.timing_wait = wait;
634 p.timing_receive = receive;
635 p.timing_blocked = blocked;
636 p.redirect_url = redirect_url;
637 p.headers_size = headers_size;
638 }
639 }
640 }
641
642 async fn handle_loading_finished(state: &Arc<Mutex<CaptureState>>, params: Value) {
643 let request_id = params
644 .get("requestId")
645 .and_then(|v| v.as_str())
646 .map(|s| s.to_string());
647 let ts = params
648 .get("timestamp")
649 .and_then(|v| v.as_f64())
650 .unwrap_or(0.0);
651 let encoded_size = params
652 .get("encodedDataLength")
653 .and_then(|v| v.as_i64())
654 .unwrap_or(-1);
655
656 let mut guard = state.lock().await;
657 if let Some(id) = request_id {
658 if let Some(p) = guard.pending.remove(&id) {
659 let mut entry = p.finish(ts);
660 entry.response.content.size = encoded_size.max(0);
661 entry.response.body_size = encoded_size;
662 guard.entries.push(entry);
663 }
664 }
665 }
666
667 async fn handle_loading_failed(state: &Arc<Mutex<CaptureState>>, params: Value) {
668 let request_id = params
669 .get("requestId")
670 .and_then(|v| v.as_str())
671 .map(|s| s.to_string());
672 let ts = params
673 .get("timestamp")
674 .and_then(|v| v.as_f64())
675 .unwrap_or(0.0);
676
677 let mut guard = state.lock().await;
678 if let Some(id) = request_id {
679 if let Some(p) = guard.pending.remove(&id) {
680 let mut entry = p.finish(ts);
681 entry.response.status = 0;
682 entry.response.status_text = params
683 .get("errorText")
684 .and_then(|v| v.as_str())
685 .unwrap_or("Failed")
686 .to_string();
687 guard.entries.push(entry);
688 }
689 }
690 }
691
692 pub async fn stop(&self) -> HarArchive {
697 let mut guard = self.state.lock().await;
698 let now = std::time::SystemTime::now()
699 .duration_since(std::time::UNIX_EPOCH)
700 .unwrap_or_default()
701 .as_secs_f64();
702
703 let mut all_entries = std::mem::take(&mut guard.entries);
705 for (_, pending) in guard.pending.drain() {
706 all_entries.push(pending.finish(now));
707 }
708
709 HarArchive {
710 log: HarLog {
711 version: "1.2".to_string(),
712 creator: json!({
713 "name": "ferrous-browser",
714 "version": env!("CARGO_PKG_VERSION"),
715 }),
716 entries: all_entries,
717 },
718 }
719 }
720
721 pub async fn export(&self) -> HarArchive {
723 let guard = self.state.lock().await;
724 let now = std::time::SystemTime::now()
725 .duration_since(std::time::UNIX_EPOCH)
726 .unwrap_or_default()
727 .as_secs_f64();
728
729 let mut entries = guard.entries.clone();
730 for (_, pending) in guard.pending.iter() {
731 entries.push(pending.clone().finish(now));
732 }
733
734 HarArchive {
735 log: HarLog {
736 version: "1.2".to_string(),
737 creator: json!({
738 "name": "ferrous-browser",
739 "version": env!("CARGO_PKG_VERSION"),
740 }),
741 entries,
742 },
743 }
744 }
745
746 pub async fn clear(&self) {
748 let mut guard = self.state.lock().await;
749 guard.pending.clear();
750 guard.entries.clear();
751 }
752}
753
754fn iso_timestamp(ts: f64) -> String {
756 let secs = ts as i64;
757 let subsec_nanos = ((ts - secs as f64) * 1_000_000_000.0).round() as u32;
758
759 let days = secs / 86400;
761 let time_secs = (secs % 86400).abs();
762 let hours = time_secs / 3600;
763 let minutes = (time_secs % 3600) / 60;
764 let seconds = time_secs % 60;
765
766 let (year, month, day) = days_to_date(days);
768
769 format!(
770 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
771 year,
772 month,
773 day,
774 hours,
775 minutes,
776 seconds,
777 subsec_nanos / 1_000_000
778 )
779}
780
781fn days_to_date(mut days: i64) -> (i64, u32, u32) {
783 days += 719468;
785 let era = if days >= 0 { days } else { days - 146096 } / 146097;
786 let doe = days - era * 146097;
787 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
788 let y = yoe + era * 400;
789 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
790 let mp = (5 * doy + 2) / 153;
791 let d = doy - (153 * mp + 2) / 5 + 1;
792 let m = if mp < 10 { mp + 3 } else { mp - 9 };
793 let y = if m <= 2 { y + 1 } else { y };
794 (y, m as u32, d as u32)
795}