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(
172 &self,
173 url: &str,
174 method: &str,
175 post_data: Option<&str>,
176 ) -> Option<&HarEntry> {
177 if let Some(ref filter) = self.options.url_filter {
179 if !filter.matches(url) {
180 trace!("URL {} doesn't match filter, skipping HAR lookup", url);
181 return None;
182 }
183 }
184
185 for entry in &self.har.log.entries {
187 if self.entry_matches(entry, url, method, post_data) {
188 debug!("Found HAR match for {} {}", method, url);
189 return Some(entry);
190 }
191 }
192
193 debug!("No HAR match found for {} {}", method, url);
194 None
195 }
196
197 fn entry_matches(
199 &self,
200 entry: &HarEntry,
201 url: &str,
202 method: &str,
203 post_data: Option<&str>,
204 ) -> bool {
205 if !self.url_matches(&entry.request.url, url) {
207 return false;
208 }
209
210 if entry.request.method.to_uppercase() != method.to_uppercase() {
212 return false;
213 }
214
215 if let Some(request_post_data) = post_data {
217 if let Some(ref har_post_data) = entry.request.post_data {
218 if !self.post_data_matches(&har_post_data.text, request_post_data) {
219 return false;
220 }
221 }
222 }
223
224 true
225 }
226
227 fn url_matches(&self, har_url: &str, request_url: &str) -> bool {
229 let har_parsed = url::Url::parse(har_url);
231 let request_parsed = url::Url::parse(request_url);
232
233 match (har_parsed, request_parsed) {
234 (Ok(har), Ok(req)) => {
235 if har.scheme() != req.scheme() {
237 return false;
238 }
239 if har.host_str() != req.host_str() {
240 return false;
241 }
242 if har.path() != req.path() {
243 return false;
244 }
245
246 let har_params: HashMap<_, _> = har.query_pairs().collect();
249 let req_params: HashMap<_, _> = req.query_pairs().collect();
250
251 for (key, value) in &har_params {
252 if req_params.get(key) != Some(value) {
253 return false;
254 }
255 }
256
257 true
258 }
259 _ => {
260 har_url == request_url
262 }
263 }
264 }
265
266 fn post_data_matches(&self, har_post_data: &str, request_post_data: &str) -> bool {
268 let har_json: Result<serde_json::Value, _> = serde_json::from_str(har_post_data);
270 let req_json: Result<serde_json::Value, _> = serde_json::from_str(request_post_data);
271
272 match (har_json, req_json) {
273 (Ok(har), Ok(req)) => har == req,
274 _ => {
275 har_post_data == request_post_data
277 }
278 }
279 }
280
281 pub fn build_response(&self, entry: &HarEntry) -> HarResponseData {
283 let response = &entry.response;
284
285 HarResponseData {
286 status: response.status as u16,
287 status_text: response.status_text.clone(),
288 headers: response
289 .headers
290 .iter()
291 .map(|h| (h.name.clone(), h.value.clone()))
292 .collect(),
293 body: response.content.text.clone(),
294 timing_ms: if self.options.use_original_timing {
295 Some(entry.time as u64)
296 } else {
297 None
298 },
299 }
300 }
301
302 pub fn options(&self) -> &HarReplayOptions {
304 &self.options
305 }
306
307 pub fn har(&self) -> &Har {
309 &self.har
310 }
311
312 pub async fn record_entry(&self, entry: HarEntry) {
314 let mut entries = self.new_entries.write().await;
315 entries.push(entry);
316 }
317
318 pub async fn save_updates(&self) -> Result<(), NetworkError> {
320 if !self.options.update {
321 return Ok(());
322 }
323
324 let path = self
325 .har_path
326 .as_ref()
327 .ok_or_else(|| NetworkError::HarError("No HAR path set for updates".to_string()))?;
328
329 let new_entries = self.new_entries.read().await;
330 if new_entries.is_empty() {
331 return Ok(());
332 }
333
334 let mut updated_har = self.har.clone();
336 for entry in new_entries.iter() {
337 updated_har.log.entries.push(entry.clone());
338 }
339
340 let content = serde_json::to_string_pretty(&updated_har)
342 .map_err(|e| NetworkError::HarError(format!("Failed to serialize HAR: {e}")))?;
343
344 tokio::fs::write(path, content)
345 .await
346 .map_err(|e| NetworkError::IoError(format!("Failed to write HAR: {e}")))?;
347
348 debug!("Saved {} new entries to HAR", new_entries.len());
349 Ok(())
350 }
351}
352
353#[derive(Debug, Clone)]
355pub struct HarResponseData {
356 pub status: u16,
358 pub status_text: String,
360 pub headers: Vec<(String, String)>,
362 pub body: Option<String>,
364 pub timing_ms: Option<u64>,
366}
367
368pub fn create_har_route_handler(
370 handler: Arc<HarReplayHandler>,
371) -> impl Fn(
372 Route,
373) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), NetworkError>> + Send>>
374+ Send
375+ Sync
376+ Clone
377+ 'static {
378 move |route: Route| {
379 let handler = handler.clone();
380 Box::pin(async move {
381 let request = route.request();
382 let url = request.url();
383 let method = request.method();
384 let post_data = request.post_data();
385
386 if let Some(entry) = handler.find_entry(url, method, post_data) {
388 let response_data = handler.build_response(entry);
389
390 if let Some(timing_ms) = response_data.timing_ms {
392 tokio::time::sleep(std::time::Duration::from_millis(timing_ms)).await;
393 }
394
395 let mut builder = route.fulfill().status(response_data.status);
397
398 for (name, value) in response_data.headers {
400 builder = builder.header(&name, &value);
401 }
402
403 if let Some(body) = response_data.body {
405 builder = builder.body(body);
406 }
407
408 builder.send().await
409 } else if handler.options().strict {
410 warn!("HAR strict mode: no match for {} {}", method, url);
412 route.abort().await
413 } else {
414 route.continue_().await
416 }
417 })
418 }
419}
420
421#[cfg(test)]
422mod tests;