1use crate::flow::{Flow, Layer};
2use serde_json::{json, Value};
3
4pub fn flow_to_har_entry(flow: &Flow) -> Value {
7 let (request, response) = match &flow.layer {
8 Layer::Http(http) => (&http.request, http.response.as_ref()),
9 Layer::WebSocket(ws) => (&ws.handshake_request, Some(&ws.handshake_response)),
10 _ => return json!({ "request": {}, "response": {}, "timings": {} }),
11 };
12
13 let req_headers: Vec<Value> = request.headers.iter()
14 .map(|(k, v)| json!({ "name": k, "value": v })).collect();
15 let req_query: Vec<Value> = request.query.iter()
16 .map(|(k, v)| json!({ "name": k, "value": v })).collect();
17 let req_cookies: Vec<Value> = request.cookies.iter()
18 .map(|c| json!({ "name": c.name, "value": c.value })).collect();
19
20 let req_content_type = request.headers.iter()
21 .find(|(k, _)| k.eq_ignore_ascii_case("content-type"))
22 .map(|(_, v)| v.clone());
23
24 let mut req_json = json!({
25 "method": request.method,
26 "url": request.url.to_string(),
27 "httpVersion": request.version,
28 "headers": req_headers,
29 "queryString": req_query,
30 "cookies": req_cookies,
31 "headersSize": har_headers_size(&request.headers, &request.method, request.url.path(), request.url.query(), &request.version),
32 "bodySize": request.body.as_ref().map(|b| b.size).unwrap_or(0),
33 });
34 if let Some(body) = &request.body
35 && !body.content.is_empty() {
36 req_json["postData"] = json!({
37 "mimeType": req_content_type.unwrap_or_default(),
38 "text": body.content,
39 });
40 }
41
42 let resp_headers: Vec<Value> = response.map(|r| r.headers.iter()
43 .map(|(k, v)| json!({ "name": k, "value": v })).collect()).unwrap_or_default();
44 let resp_cookies: Vec<Value> = response.map(|r| r.cookies.iter()
45 .map(|c| json!({ "name": c.name, "value": c.value })).collect()).unwrap_or_default();
46
47 let mut resp_json = json!({});
48 let mut timings = json!({ "send": 0, "wait": 0, "receive": 0, "connect": -1, "ssl": -1, "dns": -1, "blocked": -1 });
49
50 if let Some(resp) = response {
51 let resp_content_type = resp.headers.iter()
52 .find(|(k, _)| k.eq_ignore_ascii_case("content-type"))
53 .map(|(_, v)| v.clone())
54 .unwrap_or_default();
55 let redirect_url = resp.headers.iter()
56 .find(|(k, _)| k.eq_ignore_ascii_case("location"))
57 .map(|(_, v)| v.clone())
58 .unwrap_or_default();
59
60 resp_json = json!({
61 "status": resp.status,
62 "statusText": resp.status_text,
63 "httpVersion": resp.version,
64 "headers": resp_headers,
65 "cookies": resp_cookies,
66 "content": {
67 "size": resp.body.as_ref().map(|b| b.size).unwrap_or(0),
68 "mimeType": resp_content_type,
69 "text": resp.body.as_ref().map(|b| b.content.as_str()).unwrap_or(""),
70 },
71 "redirectURL": redirect_url,
72 "headersSize": har_headers_size(&resp.headers, "", "", None, &resp.version),
73 "bodySize": resp.body.as_ref().map(|b| b.size).unwrap_or(0),
74 });
75
76 timings["wait"] = json!(resp.timing.time_to_first_byte.unwrap_or(0));
77 let ttlbs = resp.timing.time_to_last_byte.unwrap_or(0);
78 let wait = resp.timing.time_to_first_byte.unwrap_or(0);
79 timings["receive"] = json!(ttlbs.saturating_sub(wait));
80 if let Some(c) = resp.timing.connect_time_ms { timings["connect"] = json!(c); }
81 if let Some(s) = resp.timing.ssl_time_ms { timings["ssl"] = json!(s); }
82 }
83
84 let total_time = response.map(|r| r.timing.time_to_last_byte.unwrap_or(0))
85 .unwrap_or(0);
86
87 json!({
88 "startedDateTime": flow.start_time.to_rfc3339(),
89 "time": total_time,
90 "request": req_json,
91 "response": resp_json,
92 "timings": timings,
93 "cache": {},
94 "_relaycore": {
95 "flow_id": flow.id.to_string(),
96 "client_ip": flow.network.client_ip,
97 "server_ip": flow.network.server_ip,
98 "tags": flow.tags,
99 }
100 })
101}
102
103fn har_headers_size(headers: &[(String, String)], method: &str, path: &str, query: Option<&str>, version: &str) -> u64 {
104 let start_line = if method.is_empty() {
105 version.len() + 1 + 3 + 1 + 3 + 2
106 } else {
107 let q = query.map(|q| q.len() + 1).unwrap_or(0);
108 method.len() + 1 + path.len() + q + 1 + version.len() + 2
109 };
110 let headers_bytes: usize = headers.iter().map(|(k, v)| k.len() + 2 + v.len() + 2).sum();
111 (start_line + headers_bytes + 2) as u64
112}