viewpoint_core/network/har/
mod.rs

1//! HAR (HTTP Archive) format support.
2//!
3//! This module provides types for generating HAR files from network traffic,
4//! which can be used for tracing and debugging.
5
6use std::collections::HashMap;
7
8// Re-export types from har_types
9pub use super::har_types::{
10    Har, HarCache, HarCacheEntry, HarContent, HarCookie, HarCreator, HarEntry, HarHeader, HarLog,
11    HarPage, HarPageTimings, HarParam, HarPostData, HarQueryParam, HarRequest, HarResponse,
12    HarTimings,
13};
14
15impl Har {
16    /// Create a new HAR with the given creator name.
17    pub fn new(creator_name: &str, creator_version: &str) -> Self {
18        Self {
19            log: HarLog {
20                version: "1.2".to_string(),
21                creator: HarCreator {
22                    name: creator_name.to_string(),
23                    version: creator_version.to_string(),
24                },
25                browser: None,
26                pages: Vec::new(),
27                entries: Vec::new(),
28                comment: None,
29            },
30        }
31    }
32
33    /// Add a page to the HAR.
34    pub fn add_page(&mut self, page: HarPage) {
35        self.log.pages.push(page);
36    }
37
38    /// Add an entry to the HAR.
39    pub fn add_entry(&mut self, entry: HarEntry) {
40        self.log.entries.push(entry);
41    }
42
43    /// Set the browser info.
44    pub fn set_browser(&mut self, name: &str, version: &str) {
45        self.log.browser = Some(HarCreator {
46            name: name.to_string(),
47            version: version.to_string(),
48        });
49    }
50}
51
52impl HarPage {
53    /// Create a new page entry.
54    pub fn new(id: &str, title: &str, started: &str) -> Self {
55        Self {
56            started_date_time: started.to_string(),
57            id: id.to_string(),
58            title: title.to_string(),
59            page_timings: HarPageTimings::default(),
60            comment: None,
61        }
62    }
63
64    /// Set page timing information.
65    pub fn set_timings(&mut self, on_content_load: Option<f64>, on_load: Option<f64>) {
66        self.page_timings.on_content_load = on_content_load;
67        self.page_timings.on_load = on_load;
68    }
69}
70
71impl HarEntry {
72    /// Create a new entry.
73    pub fn new(started: &str) -> Self {
74        Self {
75            pageref: None,
76            started_date_time: started.to_string(),
77            time: 0.0,
78            request: HarRequest::default(),
79            response: HarResponse::default(),
80            cache: HarCache::default(),
81            timings: HarTimings::default(),
82            server_ip_address: None,
83            connection: None,
84            comment: None,
85        }
86    }
87
88    /// Set the request.
89    pub fn set_request(&mut self, request: HarRequest) {
90        self.request = request;
91    }
92
93    /// Set the response.
94    pub fn set_response(&mut self, response: HarResponse) {
95        self.response = response;
96    }
97
98    /// Set timing information.
99    pub fn set_timings(&mut self, timings: HarTimings) {
100        self.time = timings.total();
101        self.timings = timings;
102    }
103
104    /// Set server IP address.
105    pub fn set_server_ip(&mut self, ip: &str) {
106        self.server_ip_address = Some(ip.to_string());
107    }
108}
109
110impl HarRequest {
111    /// Create a new request.
112    pub fn new(method: &str, url: &str) -> Self {
113        Self {
114            method: method.to_string(),
115            url: url.to_string(),
116            http_version: "HTTP/1.1".to_string(),
117            cookies: Vec::new(),
118            headers: Vec::new(),
119            query_string: Vec::new(),
120            post_data: None,
121            headers_size: -1,
122            body_size: -1,
123            comment: None,
124        }
125    }
126
127    /// Set headers from a `HashMap`.
128    pub fn set_headers(&mut self, headers: &HashMap<String, String>) {
129        self.headers = headers
130            .iter()
131            .map(|(name, value)| HarHeader {
132                name: name.clone(),
133                value: value.clone(),
134            })
135            .collect();
136
137        // Calculate headers size
138        self.headers_size = self
139            .headers
140            .iter()
141            .map(|h| (h.name.len() + h.value.len() + 4) as i64) // ": " + "\r\n"
142            .sum();
143    }
144
145    /// Set POST data.
146    pub fn set_post_data(&mut self, data: Option<&str>, mime_type: Option<&str>) {
147        if let Some(text) = data {
148            self.post_data = Some(HarPostData {
149                mime_type: mime_type.unwrap_or("application/octet-stream").to_string(),
150                text: text.to_string(),
151                params: None,
152            });
153            self.body_size = text.len() as i64;
154        }
155    }
156
157    /// Parse query string from URL.
158    pub fn parse_query_string(&mut self) {
159        if let Ok(url) = url::Url::parse(&self.url) {
160            self.query_string = url
161                .query_pairs()
162                .map(|(name, value)| HarQueryParam {
163                    name: name.to_string(),
164                    value: value.to_string(),
165                })
166                .collect();
167        }
168    }
169}
170
171impl HarResponse {
172    /// Create a new response.
173    pub fn new(status: i32, status_text: &str) -> Self {
174        Self {
175            status,
176            status_text: status_text.to_string(),
177            http_version: "HTTP/1.1".to_string(),
178            cookies: Vec::new(),
179            headers: Vec::new(),
180            content: HarContent::default(),
181            redirect_url: String::new(),
182            headers_size: -1,
183            body_size: -1,
184            comment: None,
185        }
186    }
187
188    /// Create an error response.
189    pub fn error(error_text: &str) -> Self {
190        Self {
191            status: 0,
192            status_text: error_text.to_string(),
193            http_version: "HTTP/1.1".to_string(),
194            cookies: Vec::new(),
195            headers: Vec::new(),
196            content: HarContent {
197                size: 0,
198                compression: None,
199                mime_type: "x-unknown".to_string(),
200                text: Some(error_text.to_string()),
201                encoding: None,
202                comment: None,
203            },
204            redirect_url: String::new(),
205            headers_size: -1,
206            body_size: -1,
207            comment: None,
208        }
209    }
210
211    /// Set headers from a `HashMap`.
212    pub fn set_headers(&mut self, headers: &HashMap<String, String>) {
213        self.headers = headers
214            .iter()
215            .map(|(name, value)| HarHeader {
216                name: name.clone(),
217                value: value.clone(),
218            })
219            .collect();
220
221        // Calculate headers size
222        self.headers_size = self
223            .headers
224            .iter()
225            .map(|h| (h.name.len() + h.value.len() + 4) as i64)
226            .sum();
227    }
228
229    /// Set response content.
230    pub fn set_content(&mut self, text: Option<&str>, mime_type: &str, encoding: Option<&str>) {
231        self.content = HarContent {
232            size: text.map_or(0, |t| t.len() as i64),
233            compression: None,
234            mime_type: mime_type.to_string(),
235            text: text.map(String::from),
236            encoding: encoding.map(String::from),
237            comment: None,
238        };
239        self.body_size = self.content.size;
240    }
241
242    /// Set cookies from name-value pairs.
243    pub fn set_cookies(&mut self, cookies: &[(String, String)]) {
244        self.cookies = cookies
245            .iter()
246            .map(|(name, value)| HarCookie {
247                name: name.clone(),
248                value: value.clone(),
249                ..Default::default()
250            })
251            .collect();
252    }
253
254    /// Set redirect URL.
255    pub fn set_redirect_url(&mut self, url: &str) {
256        self.redirect_url = url.to_string();
257    }
258}
259
260impl HarTimings {
261    /// Calculate total time.
262    pub fn total(&self) -> f64 {
263        let mut total = 0.0;
264        if self.blocked > 0.0 {
265            total += self.blocked;
266        }
267        if self.dns > 0.0 {
268            total += self.dns;
269        }
270        if self.connect > 0.0 {
271            total += self.connect;
272        }
273        if self.send > 0.0 {
274            total += self.send;
275        }
276        if self.wait > 0.0 {
277            total += self.wait;
278        }
279        if self.receive > 0.0 {
280            total += self.receive;
281        }
282        total
283    }
284
285    /// Create from CDP resource timing.
286    pub fn from_resource_timing(
287        dns_start: f64,
288        dns_end: f64,
289        connect_start: f64,
290        connect_end: f64,
291        ssl_start: f64,
292        ssl_end: f64,
293        send_start: f64,
294        send_end: f64,
295        receive_headers_end: f64,
296    ) -> Self {
297        Self {
298            blocked: if dns_start > 0.0 { dns_start } else { -1.0 },
299            dns: if dns_end > dns_start {
300                dns_end - dns_start
301            } else {
302                -1.0
303            },
304            connect: if connect_end > connect_start && ssl_start <= 0.0 {
305                connect_end - connect_start
306            } else if connect_end > connect_start {
307                ssl_start - connect_start
308            } else {
309                -1.0
310            },
311            ssl: if ssl_end > ssl_start {
312                ssl_end - ssl_start
313            } else {
314                -1.0
315            },
316            send: if send_end > send_start {
317                send_end - send_start
318            } else {
319                -1.0
320            },
321            wait: if receive_headers_end > send_end {
322                receive_headers_end - send_end
323            } else {
324                -1.0
325            },
326            receive: -1.0, // Will be calculated when response finishes
327            comment: None,
328        }
329    }
330}