viewpoint_core/network/har_replay/
mod.rs1use std::collections::HashMap;
7use std::path::Path;
8use std::sync::Arc;
9
10use tokio::sync::RwLock;
11use tracing::{debug, trace, warn};
12
13use crate::error::NetworkError;
14use crate::network::{Route, UrlPattern};
15
16use super::har::{Har, HarEntry};
17
18#[derive(Debug, Clone)]
20pub struct HarReplayOptions {
21 pub url_filter: Option<UrlPattern>,
24 pub strict: bool,
27 pub update: bool,
29 pub update_content: UpdateContentMode,
31 pub update_timings: TimingMode,
33 pub use_original_timing: bool,
35}
36
37impl Default for HarReplayOptions {
38 fn default() -> Self {
39 Self {
40 url_filter: None,
41 strict: false,
42 update: false,
43 update_content: UpdateContentMode::Embed,
44 update_timings: TimingMode::Placeholder,
45 use_original_timing: false,
46 }
47 }
48}
49
50impl HarReplayOptions {
51 pub fn new() -> Self {
53 Self::default()
54 }
55
56 #[must_use]
58 pub fn url<M: Into<UrlPattern>>(mut self, pattern: M) -> Self {
59 self.url_filter = Some(pattern.into());
60 self
61 }
62
63 #[must_use]
65 pub fn strict(mut self, strict: bool) -> Self {
66 self.strict = strict;
67 self
68 }
69
70 #[must_use]
72 pub fn update(mut self, update: bool) -> Self {
73 self.update = update;
74 self
75 }
76
77 #[must_use]
79 pub fn update_content(mut self, mode: UpdateContentMode) -> Self {
80 self.update_content = mode;
81 self
82 }
83
84 #[must_use]
86 pub fn update_timings(mut self, mode: TimingMode) -> Self {
87 self.update_timings = mode;
88 self
89 }
90
91 #[must_use]
93 pub fn use_original_timing(mut self, use_timing: bool) -> Self {
94 self.use_original_timing = use_timing;
95 self
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum UpdateContentMode {
102 Embed,
104 Attach,
106 Omit,
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub enum TimingMode {
113 Placeholder,
115 Actual,
117}
118
119pub struct HarReplayHandler {
121 har: Har,
123 options: HarReplayOptions,
125 har_path: Option<std::path::PathBuf>,
127 new_entries: Arc<RwLock<Vec<HarEntry>>>,
129}
130
131impl HarReplayHandler {
132 pub async fn from_file(path: impl AsRef<Path>) -> Result<Self, NetworkError> {
134 let path = path.as_ref();
135 debug!("Loading HAR from: {}", path.display());
136
137 let content = tokio::fs::read_to_string(path)
138 .await
139 .map_err(|e| NetworkError::IoError(format!("Failed to read HAR file: {e}")))?;
140
141 let har: Har = serde_json::from_str(&content)
142 .map_err(|e| NetworkError::HarError(format!("Failed to parse HAR: {e}")))?;
143
144 debug!("Loaded HAR with {} entries", har.log.entries.len());
145
146 Ok(Self {
147 har,
148 options: HarReplayOptions::default(),
149 har_path: Some(path.to_path_buf()),
150 new_entries: Arc::new(RwLock::new(Vec::new())),
151 })
152 }
153
154 pub fn from_har(har: Har) -> Self {
156 Self {
157 har,
158 options: HarReplayOptions::default(),
159 har_path: None,
160 new_entries: Arc::new(RwLock::new(Vec::new())),
161 }
162 }
163
164 pub fn with_options(mut self, options: HarReplayOptions) -> Self {
166 self.options = options;
167 self
168 }
169
170 pub fn find_entry(&self, url: &str, method: &str, post_data: Option<&str>) -> Option<&HarEntry> {
172 if let Some(ref filter) = self.options.url_filter {
174 if !filter.matches(url) {
175 trace!("URL {} doesn't match filter, skipping HAR lookup", url);
176 return None;
177 }
178 }
179
180 for entry in &self.har.log.entries {
182 if self.entry_matches(entry, url, method, post_data) {
183 debug!("Found HAR match for {} {}", method, url);
184 return Some(entry);
185 }
186 }
187
188 debug!("No HAR match found for {} {}", method, url);
189 None
190 }
191
192 fn entry_matches(&self, entry: &HarEntry, url: &str, method: &str, post_data: Option<&str>) -> bool {
194 if !self.url_matches(&entry.request.url, url) {
196 return false;
197 }
198
199 if entry.request.method.to_uppercase() != method.to_uppercase() {
201 return false;
202 }
203
204 if let Some(request_post_data) = post_data {
206 if let Some(ref har_post_data) = entry.request.post_data {
207 if !self.post_data_matches(&har_post_data.text, request_post_data) {
208 return false;
209 }
210 }
211 }
212
213 true
214 }
215
216 fn url_matches(&self, har_url: &str, request_url: &str) -> bool {
218 let har_parsed = url::Url::parse(har_url);
220 let request_parsed = url::Url::parse(request_url);
221
222 match (har_parsed, request_parsed) {
223 (Ok(har), Ok(req)) => {
224 if har.scheme() != req.scheme() {
226 return false;
227 }
228 if har.host_str() != req.host_str() {
229 return false;
230 }
231 if har.path() != req.path() {
232 return false;
233 }
234
235 let har_params: HashMap<_, _> = har.query_pairs().collect();
238 let req_params: HashMap<_, _> = req.query_pairs().collect();
239
240 for (key, value) in &har_params {
241 if req_params.get(key) != Some(value) {
242 return false;
243 }
244 }
245
246 true
247 }
248 _ => {
249 har_url == request_url
251 }
252 }
253 }
254
255 fn post_data_matches(&self, har_post_data: &str, request_post_data: &str) -> bool {
257 let har_json: Result<serde_json::Value, _> = serde_json::from_str(har_post_data);
259 let req_json: Result<serde_json::Value, _> = serde_json::from_str(request_post_data);
260
261 match (har_json, req_json) {
262 (Ok(har), Ok(req)) => har == req,
263 _ => {
264 har_post_data == request_post_data
266 }
267 }
268 }
269
270 pub fn build_response(&self, entry: &HarEntry) -> HarResponseData {
272 let response = &entry.response;
273
274 HarResponseData {
275 status: response.status as u16,
276 status_text: response.status_text.clone(),
277 headers: response
278 .headers
279 .iter()
280 .map(|h| (h.name.clone(), h.value.clone()))
281 .collect(),
282 body: response.content.text.clone(),
283 timing_ms: if self.options.use_original_timing {
284 Some(entry.time as u64)
285 } else {
286 None
287 },
288 }
289 }
290
291 pub fn options(&self) -> &HarReplayOptions {
293 &self.options
294 }
295
296 pub fn har(&self) -> &Har {
298 &self.har
299 }
300
301 pub async fn record_entry(&self, entry: HarEntry) {
303 let mut entries = self.new_entries.write().await;
304 entries.push(entry);
305 }
306
307 pub async fn save_updates(&self) -> Result<(), NetworkError> {
309 if !self.options.update {
310 return Ok(());
311 }
312
313 let path = self.har_path.as_ref().ok_or_else(|| {
314 NetworkError::HarError("No HAR path set for updates".to_string())
315 })?;
316
317 let new_entries = self.new_entries.read().await;
318 if new_entries.is_empty() {
319 return Ok(());
320 }
321
322 let mut updated_har = self.har.clone();
324 for entry in new_entries.iter() {
325 updated_har.log.entries.push(entry.clone());
326 }
327
328 let content = serde_json::to_string_pretty(&updated_har)
330 .map_err(|e| NetworkError::HarError(format!("Failed to serialize HAR: {e}")))?;
331
332 tokio::fs::write(path, content)
333 .await
334 .map_err(|e| NetworkError::IoError(format!("Failed to write HAR: {e}")))?;
335
336 debug!("Saved {} new entries to HAR", new_entries.len());
337 Ok(())
338 }
339}
340
341#[derive(Debug, Clone)]
343pub struct HarResponseData {
344 pub status: u16,
346 pub status_text: String,
348 pub headers: Vec<(String, String)>,
350 pub body: Option<String>,
352 pub timing_ms: Option<u64>,
354}
355
356pub fn create_har_route_handler(
358 handler: Arc<HarReplayHandler>,
359) -> impl Fn(Route) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), NetworkError>> + Send>> + Send + Sync + Clone + 'static
360{
361 move |route: Route| {
362 let handler = handler.clone();
363 Box::pin(async move {
364 let request = route.request();
365 let url = request.url();
366 let method = request.method();
367 let post_data = request.post_data();
368
369 if let Some(entry) = handler.find_entry(url, method, post_data) {
371 let response_data = handler.build_response(entry);
372
373 if let Some(timing_ms) = response_data.timing_ms {
375 tokio::time::sleep(std::time::Duration::from_millis(timing_ms)).await;
376 }
377
378 let mut builder = route.fulfill().status(response_data.status);
380
381 for (name, value) in response_data.headers {
383 builder = builder.header(&name, &value);
384 }
385
386 if let Some(body) = response_data.body {
388 builder = builder.body(body);
389 }
390
391 builder.send().await
392 } else if handler.options().strict {
393 warn!("HAR strict mode: no match for {} {}", method, url);
395 route.abort().await
396 } else {
397 route.continue_().await
399 }
400 })
401 }
402}
403
404#[cfg(test)]
405mod tests;