1use crate::flow::{Flow, Layer};
2use serde_json::{Value, json};
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
14 .headers
15 .iter()
16 .map(|(k, v)| json!({ "name": k, "value": v }))
17 .collect();
18 let req_query: Vec<Value> = request
19 .query
20 .iter()
21 .map(|(k, v)| json!({ "name": k, "value": v }))
22 .collect();
23 let req_cookies: Vec<Value> = request
24 .cookies
25 .iter()
26 .map(|c| json!({ "name": c.name, "value": c.value }))
27 .collect();
28
29 let req_content_type = request
30 .headers
31 .iter()
32 .find(|(k, _)| k.eq_ignore_ascii_case("content-type"))
33 .map(|(_, v)| v.clone());
34
35 let mut req_json = json!({
36 "method": request.method,
37 "url": request.url.to_string(),
38 "httpVersion": request.version,
39 "headers": req_headers,
40 "queryString": req_query,
41 "cookies": req_cookies,
42 "headersSize": har_headers_size(&request.headers, &request.method, request.url.path(), request.url.query(), &request.version),
43 "bodySize": request.body.as_ref().map(|b| b.size).unwrap_or(0),
44 });
45 if let Some(body) = &request.body
46 && !body.content.is_empty()
47 {
48 req_json["postData"] = json!({
49 "mimeType": req_content_type.unwrap_or_default(),
50 "text": body.content,
51 });
52 }
53
54 let resp_headers: Vec<Value> = response
55 .map(|r| {
56 r.headers
57 .iter()
58 .map(|(k, v)| json!({ "name": k, "value": v }))
59 .collect()
60 })
61 .unwrap_or_default();
62 let resp_cookies: Vec<Value> = response
63 .map(|r| {
64 r.cookies
65 .iter()
66 .map(|c| json!({ "name": c.name, "value": c.value }))
67 .collect()
68 })
69 .unwrap_or_default();
70
71 let mut resp_json = json!({});
72 let mut timings = json!({ "send": 0, "wait": 0, "receive": 0, "connect": -1, "ssl": -1, "dns": -1, "blocked": -1 });
73
74 if let Some(resp) = response {
75 let resp_content_type = resp
76 .headers
77 .iter()
78 .find(|(k, _)| k.eq_ignore_ascii_case("content-type"))
79 .map(|(_, v)| v.clone())
80 .unwrap_or_default();
81 let redirect_url = resp
82 .headers
83 .iter()
84 .find(|(k, _)| k.eq_ignore_ascii_case("location"))
85 .map(|(_, v)| v.clone())
86 .unwrap_or_default();
87
88 resp_json = json!({
89 "status": resp.status,
90 "statusText": resp.status_text,
91 "httpVersion": resp.version,
92 "headers": resp_headers,
93 "cookies": resp_cookies,
94 "content": {
95 "size": resp.body.as_ref().map(|b| b.size).unwrap_or(0),
96 "mimeType": resp_content_type,
97 "text": resp.body.as_ref().map(|b| b.content.as_str()).unwrap_or(""),
98 },
99 "redirectURL": redirect_url,
100 "headersSize": har_headers_size(&resp.headers, "", "", None, &resp.version),
101 "bodySize": resp.body.as_ref().map(|b| b.size).unwrap_or(0),
102 });
103
104 timings["wait"] = json!(resp.timing.time_to_first_byte.unwrap_or(0));
105 let ttlbs = resp.timing.time_to_last_byte.unwrap_or(0);
106 let wait = resp.timing.time_to_first_byte.unwrap_or(0);
107 timings["receive"] = json!(ttlbs.saturating_sub(wait));
108 if let Some(c) = resp.timing.connect_time_ms {
109 timings["connect"] = json!(c);
110 }
111 if let Some(s) = resp.timing.ssl_time_ms {
112 timings["ssl"] = json!(s);
113 }
114 }
115
116 let total_time = response
117 .map(|r| r.timing.time_to_last_byte.unwrap_or(0))
118 .unwrap_or(0);
119
120 json!({
121 "startedDateTime": flow.start_time.to_rfc3339(),
122 "time": total_time,
123 "request": req_json,
124 "response": resp_json,
125 "timings": timings,
126 "cache": {},
127 "_relaycore": {
128 "flow_id": flow.id.to_string(),
129 "client_ip": flow.network.client_ip,
130 "server_ip": flow.network.server_ip,
131 "tags": flow.tags,
132 }
133 })
134}
135
136fn har_headers_size(
137 headers: &[(String, String)],
138 method: &str,
139 path: &str,
140 query: Option<&str>,
141 version: &str,
142) -> u64 {
143 let start_line = if method.is_empty() {
144 version.len() + 1 + 3 + 1 + 3 + 2
145 } else {
146 let q = query.map(|q| q.len() + 1).unwrap_or(0);
147 method.len() + 1 + path.len() + q + 1 + version.len() + 2
148 };
149 let headers_bytes: usize = headers.iter().map(|(k, v)| k.len() + 2 + v.len() + 2).sum();
150 (start_line + headers_bytes + 2) as u64
151}