use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{debug, trace, warn};
use crate::error::NetworkError;
use crate::network::{Route, UrlPattern};
use super::har::{Har, HarEntry};
#[derive(Debug, Clone)]
pub struct HarReplayOptions {
pub url_filter: Option<UrlPattern>,
pub strict: bool,
pub update: bool,
pub update_content: UpdateContentMode,
pub update_timings: TimingMode,
pub use_original_timing: bool,
}
impl Default for HarReplayOptions {
fn default() -> Self {
Self {
url_filter: None,
strict: false,
update: false,
update_content: UpdateContentMode::Embed,
update_timings: TimingMode::Placeholder,
use_original_timing: false,
}
}
}
impl HarReplayOptions {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn url<M: Into<UrlPattern>>(mut self, pattern: M) -> Self {
self.url_filter = Some(pattern.into());
self
}
#[must_use]
pub fn strict(mut self, strict: bool) -> Self {
self.strict = strict;
self
}
#[must_use]
pub fn update(mut self, update: bool) -> Self {
self.update = update;
self
}
#[must_use]
pub fn update_content(mut self, mode: UpdateContentMode) -> Self {
self.update_content = mode;
self
}
#[must_use]
pub fn update_timings(mut self, mode: TimingMode) -> Self {
self.update_timings = mode;
self
}
#[must_use]
pub fn use_original_timing(mut self, use_timing: bool) -> Self {
self.use_original_timing = use_timing;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UpdateContentMode {
Embed,
Attach,
Omit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TimingMode {
Placeholder,
Actual,
}
pub struct HarReplayHandler {
har: Har,
options: HarReplayOptions,
har_path: Option<std::path::PathBuf>,
new_entries: Arc<RwLock<Vec<HarEntry>>>,
}
impl HarReplayHandler {
pub async fn from_file(path: impl AsRef<Path>) -> Result<Self, NetworkError> {
let path = path.as_ref();
debug!("Loading HAR from: {}", path.display());
let content = tokio::fs::read_to_string(path)
.await
.map_err(|e| NetworkError::IoError(format!("Failed to read HAR file: {e}")))?;
let har: Har = serde_json::from_str(&content)
.map_err(|e| NetworkError::HarError(format!("Failed to parse HAR: {e}")))?;
debug!("Loaded HAR with {} entries", har.log.entries.len());
Ok(Self {
har,
options: HarReplayOptions::default(),
har_path: Some(path.to_path_buf()),
new_entries: Arc::new(RwLock::new(Vec::new())),
})
}
pub fn from_har(har: Har) -> Self {
Self {
har,
options: HarReplayOptions::default(),
har_path: None,
new_entries: Arc::new(RwLock::new(Vec::new())),
}
}
pub fn with_options(mut self, options: HarReplayOptions) -> Self {
self.options = options;
self
}
pub fn find_entry(
&self,
url: &str,
method: &str,
post_data: Option<&str>,
) -> Option<&HarEntry> {
if let Some(ref filter) = self.options.url_filter {
if !filter.matches(url) {
trace!("URL {} doesn't match filter, skipping HAR lookup", url);
return None;
}
}
for entry in &self.har.log.entries {
if self.entry_matches(entry, url, method, post_data) {
debug!("Found HAR match for {} {}", method, url);
return Some(entry);
}
}
debug!("No HAR match found for {} {}", method, url);
None
}
fn entry_matches(
&self,
entry: &HarEntry,
url: &str,
method: &str,
post_data: Option<&str>,
) -> bool {
if !self.url_matches(&entry.request.url, url) {
return false;
}
if entry.request.method.to_uppercase() != method.to_uppercase() {
return false;
}
if let Some(request_post_data) = post_data {
if let Some(ref har_post_data) = entry.request.post_data {
if !self.post_data_matches(&har_post_data.text, request_post_data) {
return false;
}
}
}
true
}
fn url_matches(&self, har_url: &str, request_url: &str) -> bool {
let har_parsed = url::Url::parse(har_url);
let request_parsed = url::Url::parse(request_url);
match (har_parsed, request_parsed) {
(Ok(har), Ok(req)) => {
if har.scheme() != req.scheme() {
return false;
}
if har.host_str() != req.host_str() {
return false;
}
if har.path() != req.path() {
return false;
}
let har_params: HashMap<_, _> = har.query_pairs().collect();
let req_params: HashMap<_, _> = req.query_pairs().collect();
for (key, value) in &har_params {
if req_params.get(key) != Some(value) {
return false;
}
}
true
}
_ => {
har_url == request_url
}
}
}
fn post_data_matches(&self, har_post_data: &str, request_post_data: &str) -> bool {
let har_json: Result<serde_json::Value, _> = serde_json::from_str(har_post_data);
let req_json: Result<serde_json::Value, _> = serde_json::from_str(request_post_data);
match (har_json, req_json) {
(Ok(har), Ok(req)) => har == req,
_ => {
har_post_data == request_post_data
}
}
}
pub fn build_response(&self, entry: &HarEntry) -> HarResponseData {
let response = &entry.response;
HarResponseData {
status: response.status as u16,
status_text: response.status_text.clone(),
headers: response
.headers
.iter()
.map(|h| (h.name.clone(), h.value.clone()))
.collect(),
body: response.content.text.clone(),
timing_ms: if self.options.use_original_timing {
Some(entry.time as u64)
} else {
None
},
}
}
pub fn options(&self) -> &HarReplayOptions {
&self.options
}
pub fn har(&self) -> &Har {
&self.har
}
pub async fn record_entry(&self, entry: HarEntry) {
let mut entries = self.new_entries.write().await;
entries.push(entry);
}
pub async fn save_updates(&self) -> Result<(), NetworkError> {
if !self.options.update {
return Ok(());
}
let path = self
.har_path
.as_ref()
.ok_or_else(|| NetworkError::HarError("No HAR path set for updates".to_string()))?;
let new_entries = self.new_entries.read().await;
if new_entries.is_empty() {
return Ok(());
}
let mut updated_har = self.har.clone();
for entry in new_entries.iter() {
updated_har.log.entries.push(entry.clone());
}
let content = serde_json::to_string_pretty(&updated_har)
.map_err(|e| NetworkError::HarError(format!("Failed to serialize HAR: {e}")))?;
tokio::fs::write(path, content)
.await
.map_err(|e| NetworkError::IoError(format!("Failed to write HAR: {e}")))?;
debug!("Saved {} new entries to HAR", new_entries.len());
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct HarResponseData {
pub status: u16,
pub status_text: String,
pub headers: Vec<(String, String)>,
pub body: Option<String>,
pub timing_ms: Option<u64>,
}
pub fn create_har_route_handler(
handler: Arc<HarReplayHandler>,
) -> impl Fn(
Route,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), NetworkError>> + Send>>
+ Send
+ Sync
+ Clone
+ 'static {
move |route: Route| {
let handler = handler.clone();
Box::pin(async move {
let request = route.request();
let url = request.url();
let method = request.method();
let post_data = request.post_data();
if let Some(entry) = handler.find_entry(url, method, post_data) {
let response_data = handler.build_response(entry);
if let Some(timing_ms) = response_data.timing_ms {
tokio::time::sleep(std::time::Duration::from_millis(timing_ms)).await;
}
let mut builder = route.fulfill().status(response_data.status);
for (name, value) in response_data.headers {
builder = builder.header(&name, &value);
}
if let Some(body) = response_data.body {
builder = builder.body(body);
}
builder.send().await
} else if handler.options().strict {
warn!("HAR strict mode: no match for {} {}", method, url);
route.abort().await
} else {
route.continue_().await
}
})
}
}
#[cfg(test)]
mod tests;