use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use crate::Result;
use crate::cdp::core::CdpCore;
use crate::cdp::extras::{HarPlayer, HarRecorder, HarReplayOptions};
use crate::cdp::interceptor::CdpIntercept;
use crate::cdp::listener::CdpListen;
use crate::cdp::tab::ChromiumTab;
use crate::net::{ListenFilter, ResumeOptions};
#[derive(Clone)]
pub struct NetworkHandle {
core: Arc<CdpCore>,
filter: ListenFilter,
}
impl NetworkHandle {
pub(crate) fn new(core: Arc<CdpCore>) -> Self {
Self {
core,
filter: ListenFilter::default(),
}
}
pub fn filter(mut self, keyword: impl Into<String>) -> Self {
self.filter.url_keywords.push(keyword.into());
self
}
pub fn method(mut self, method: impl Into<String>) -> Self {
self.filter.methods.push(method.into().to_ascii_uppercase());
self
}
pub fn xhr_only(self) -> Self {
let mut s = self;
s.filter.xhr_only = true;
s
}
pub async fn listen(self) -> Result<CdpListen> {
let h = CdpListen::new(self.core);
h.start_filter(self.filter).await?;
Ok(h)
}
pub async fn intercept(self) -> Result<CdpIntercept> {
let h = CdpIntercept::new(self.core);
h.start_filter(self.filter).await?;
Ok(h)
}
pub async fn block(self) -> Result<NetworkRoute> {
self.route(RouteRule::Block).await
}
pub async fn mock(
self,
status: u16,
headers: Vec<(String, String)>,
body: impl Into<String>,
) -> Result<NetworkRoute> {
self.route(RouteRule::Mock {
status,
headers,
body: body.into(),
})
.await
}
pub async fn rewrite(self, opts: ResumeOptions) -> Result<NetworkRoute> {
self.route(RouteRule::Modify(opts)).await
}
pub async fn record(self) -> Result<HarRecorder> {
ChromiumTab::new(self.core).har_record().await
}
pub async fn replay(
self,
path: impl AsRef<Path>,
opts: &HarReplayOptions,
) -> Result<HarPlayer> {
ChromiumTab::new(self.core).route_from_har(path, opts).await
}
async fn route(self, rule: RouteRule) -> Result<NetworkRoute> {
let intercept = CdpIntercept::new(self.core);
intercept.start_filter(self.filter).await?;
let worker = intercept.clone();
let task = tokio::spawn(async move {
loop {
match worker.next(Some(Duration::from_secs(30))).await {
Ok(Some(req)) => match &rule {
RouteRule::Allow => {
let _ = req.resume().await;
}
RouteRule::Block => {
let _ = req.abort("blockedbyclient").await;
}
RouteRule::Modify(opts) => {
let _ = req.resume_with(opts.clone()).await;
}
RouteRule::Mock {
status,
headers,
body,
} => {
let _ = req.fulfill(*status, headers.clone(), body).await;
}
},
Ok(None) => {
if !worker.is_intercepting().await {
break;
}
}
Err(_) => break,
}
}
});
Ok(NetworkRoute {
intercept,
abort: task.abort_handle(),
})
}
}
#[derive(Clone)]
enum RouteRule {
#[allow(dead_code)]
Allow,
Block,
Modify(ResumeOptions),
Mock {
status: u16,
headers: Vec<(String, String)>,
body: String,
},
}
pub struct NetworkRoute {
intercept: CdpIntercept,
abort: tokio::task::AbortHandle,
}
impl NetworkRoute {
pub async fn stop(self) -> Result<()> {
self.abort.abort();
self.intercept.stop().await
}
pub async fn is_active(&self) -> bool {
self.intercept.is_intercepting().await
}
}
impl Drop for NetworkRoute {
fn drop(&mut self) {
self.abort.abort();
}
}
#[cfg(test)]
mod tests {
use crate::net::ListenFilter;
#[test]
fn filter_builder_collects() {
let mut h_filter = ListenFilter {
url_keywords: vec!["/api/".into()],
xhr_only: true,
methods: vec!["POST".into()],
};
assert!(h_filter.matches_request("https://x.com/api/v", "xhr", "POST"));
assert!(!h_filter.matches_request("https://x.com/api/v", "xhr", "GET"));
h_filter.methods.clear();
assert!(h_filter.matches_request("https://x.com/api/v", "fetch", "GET"));
}
}