Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,18 @@ url = "2.5.8"
reqwest = { version = "0.13.2", features = ["rustls", "stream", "socks", "cookies"], default-features = false }
clap = { version = "4.6.0", features = ["derive"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
serde_json = { version = "1.0.149", features = ["raw_value"] }
tokio = { version = "1.50.0", features = ["rt-multi-thread", "macros", "signal"] }
tokio-util = { version = "0.7.18", features = ["io"] }
futures = "0.3.32"
warp = { version = "0.4.2", features = ["server"] }
chrono = "0.4.44"
chrono = { version = "0.4.44", features = ["serde"] }
anyhow = "1.0.102"
indexmap = { version = "2.13.0", features = ["serde"] }
sha1 = "0.10.6"
thiserror = "2.0.18"
har = "0.8.1"
base64 = "0.22.1"

[dev-dependencies]
insta = "1.46.3"
Expand Down
6 changes: 6 additions & 0 deletions NOTICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@ This software bundles the following third-party libraries:
- anyhow version 1.0.102 (https://github.com/dtolnay/anyhow)
Author: David Tolnay <dtolnay@gmail.com>
License: MIT OR Apache-2.0
- base64 version 0.22.1 (https://github.com/marshallpierce/rust-base64)
Author: Marshall Pierce <marshall@mpierce.org>
License: MIT OR Apache-2.0
- chrono version 0.4.44 (https://github.com/chronotope/chrono)
License: MIT OR Apache-2.0
- clap version 4.6.0 (https://github.com/clap-rs/clap)
License: MIT OR Apache-2.0
- futures version 0.3.32 (https://github.com/rust-lang/futures-rs)
License: MIT OR Apache-2.0
- har version 0.8.1 (https://github.com/mandrean/har-rs)
Author: Sebastian Mandrean <sebastian.mandrean@gmail.com>
License: MIT
- indexmap version 2.13.0 (https://github.com/indexmap-rs/indexmap)
License: Apache-2.0 OR MIT
- m3u8-rs version 6.0.0 (https://github.com/rutgersc/m3u8-rs)
Expand Down
133 changes: 133 additions & 0 deletions src/import/har.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
use base64::prelude::*;
use chrono::{DateTime, Utc};
pub use har::v1_3::*;
use serde::Deserialize;
use serde_json::value::RawValue;
use std::io::{Error, Result};

#[derive(Clone, Debug, Deserialize, Default)]
pub enum Version {
/// Version 1.2 of the HAR specification.
///
/// Refer to the official
/// [specification](https://w3c.github.io/web-performance/specs/HAR/Overview.html)
/// for more information.
#[allow(non_camel_case_types)]
#[serde(rename = "1.2")]
#[default]
V1_2,

// Version 1.3 of the HAR specification.
//
// Refer to the draft
// [specification](https://github.com/ahmadnassri/har-spec/blob/master/versions/1.3.md)
// for more information.
#[allow(non_camel_case_types)]
#[serde(rename = "1.3")]
V1_3,
}

#[derive(Clone, Debug, Deserialize)]
pub struct Har {
pub log: Log,
}

#[derive(Clone, Debug, Deserialize, Default)]
pub struct Log {
pub version: Version,
pub creator: Creator,
pub browser: Option<Creator>,
pub pages: Option<Vec<Pages>>,
pub entries: Vec<Entries>,
pub comment: Option<String>,
}

#[derive(Clone, Debug, Deserialize, Default)]
pub struct Pages {
#[serde(rename = "startedDateTime")]
pub started_date_time: DateTime<Utc>,
pub id: String,
pub title: String,
#[serde(rename = "pageTimings")]
pub page_timings: PageTimings,
pub comment: Option<String>,
}

#[derive(Clone, Debug, Deserialize, Default)]
pub struct Entries {
pub pageref: Option<String>,
#[serde(rename = "startedDateTime")]
pub started_date_time: DateTime<Utc>,
pub time: f64,
pub request: Request,
pub response: Response,
pub cache: Cache,
pub timings: Timings,
#[serde(rename = "serverIPAddress")]
pub server_ip_address: Option<String>,
pub connection: Option<String>,
pub comment: Option<String>,
}

#[derive(Clone, Debug, Deserialize, Default)]
pub struct Response {
pub status: i64,
#[serde(rename = "statusText")]
pub status_text: String,
#[serde(rename = "httpVersion")]
pub http_version: String,
pub cookies: Vec<Cookies>,
pub headers: Vec<Headers>,
pub content: Content,
#[serde(rename = "redirectURL")]
pub redirect_url: Option<String>,
#[serde(rename = "headersSize", default = "default_isize")]
pub headers_size: i64,
#[serde(rename = "bodySize", default = "default_isize")]
pub body_size: i64,
pub comment: Option<String>,
#[serde(rename = "headersCompression")]
pub headers_compression: Option<i64>,
}

#[derive(Clone, Debug, Deserialize, Default)]
pub struct Content {
#[serde(default = "default_isize")]
pub size: i64,
pub compression: Option<i64>,
#[serde(rename = "mimeType")]
pub mime_type: Option<String>,
// CHANGED: Parse as raw value, to avoid keeping a ton of strings in memory.
// pub text: Option<String>,
text: Option<Box<RawValue>>,
pub encoding: Option<String>,
pub comment: Option<String>,
}

impl Content {
pub(crate) fn text(&self) -> Result<String> {
let Some(raw_text) = self.text.as_ref() else {
// Response has no content.
return Ok(String::new());
};
// Parse back into a string.
let text = serde_json::from_str::<String>(raw_text.as_ref().get())
.map_err(|_| Error::other("invalid content"))?;
Ok(text)
}

pub(crate) fn as_bytes(&self) -> Result<Vec<u8>> {
let text = self.text()?;
// Decode as base64 (if needed).
match self.encoding.as_ref() {
Some(encoding) if encoding == "base64" => BASE64_STANDARD
.decode(text.as_bytes())
.map_err(|_| Error::other("invalid base64")),
_ => Ok(text.into_bytes()),
}
}
}

fn default_isize() -> i64 {
-1
}
43 changes: 43 additions & 0 deletions src/import/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
mod har;

use crate::record::{HarSource, RecordError, RecordOptions, Recorder};
use crate::shared::url_file_extension;
use anyhow::anyhow;
pub(crate) use har::Har;
use std::io::Error;
use std::path::Path;
use tokio::fs;
use tokio_util::sync::CancellationToken;
use url::Url;

pub async fn import_har(
har: Har,
dest: &Path,
options: RecordOptions,
token: CancellationToken,
) -> Result<(), RecordError> {
fs::create_dir_all(dest).await?;

// Find the first .m3u8 request
let first_playlist_request = har
.log
.entries
.iter()
.find(|entry| {
let url = Url::parse(&entry.request.url).unwrap();
matches!(url_file_extension(&url), Some("m3u8"))
})
.ok_or_else(|| Error::other("no playlist found"))?;
let url = first_playlist_request
.request
.url
.parse::<Url>()
.map_err(|e| RecordError::Parse(anyhow!("Invalid URL: {e}")))?;
let time = first_playlist_request.started_date_time;

// Create a source that reads from the HAR
let source = HarSource::new(har, time);

let recorder = Recorder::new(source, dest, options).await?;
recorder.run(&url, token).await
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod import;
pub mod record;
pub mod replay;
pub mod shared;
Loading
Loading