use serde_json::{Value, json};
use tokio::sync::mpsc;
use crate::browser::listener::{ListenFilter, parse_headers};
use crate::protocol::Connection;
use crate::util::base64_encode;
use crate::Result;
#[derive(Debug, Clone, Default)]
pub struct ResumeOptions {
pub url: Option<String>,
pub method: Option<String>,
pub headers: Option<Vec<(String, String)>>,
pub post_data: Option<String>,
}
impl ResumeOptions {
pub fn new() -> Self {
Self::default()
}
pub fn url(mut self, url: impl Into<String>) -> Self {
self.url = Some(url.into());
self
}
pub fn method(mut self, method: impl Into<String>) -> Self {
self.method = Some(method.into());
self
}
pub fn headers(mut self, headers: Vec<(String, String)>) -> Self {
self.headers = Some(headers);
self
}
pub fn post_data(mut self, post_data: impl Into<String>) -> Self {
self.post_data = Some(post_data.into());
self
}
}
pub struct InterceptedRequest {
pub url: String,
pub method: String,
pub resource_type: String,
pub headers: Vec<(String, String)>,
pub post_data: Option<String>,
request_id: String,
conn: Connection,
session_id: String,
}
impl InterceptedRequest {
pub async fn resume(self) -> Result<()> {
self.conn
.send(
"Network.resumeInterceptedRequest",
json!({ "requestId": self.request_id }),
Some(&self.session_id),
)
.await?;
Ok(())
}
pub async fn resume_with(self, opts: ResumeOptions) -> Result<()> {
let mut p = serde_json::Map::new();
p.insert("requestId".into(), json!(self.request_id));
if let Some(u) = opts.url {
p.insert("url".into(), json!(u));
}
if let Some(m) = opts.method {
p.insert("method".into(), json!(m));
}
if let Some(h) = opts.headers {
p.insert("headers".into(), json!(to_header_array(&h)));
}
if let Some(d) = opts.post_data {
p.insert("postData".into(), json!(d));
}
self.conn
.send(
"Network.resumeInterceptedRequest",
Value::Object(p),
Some(&self.session_id),
)
.await?;
Ok(())
}
pub async fn fulfill(
self,
status: u16,
headers: Vec<(String, String)>,
body: &str,
) -> Result<()> {
let p = json!({
"requestId": self.request_id,
"status": status,
"statusText": status_text(status),
"headers": to_header_array(&headers),
"base64body": base64_encode(body.as_bytes()),
});
self.conn
.send("Network.fulfillInterceptedRequest", p, Some(&self.session_id))
.await?;
Ok(())
}
pub async fn abort(self, error_code: &str) -> Result<()> {
self.conn
.send(
"Network.abortInterceptedRequest",
json!({ "requestId": self.request_id, "errorCode": error_code }),
Some(&self.session_id),
)
.await?;
Ok(())
}
pub fn request_id(&self) -> &str {
&self.request_id
}
}
pub(crate) enum Decision {
Delivered,
AutoResume(String),
Ignore,
}
pub(crate) struct InterceptorState {
filter: ListenFilter,
tx: mpsc::UnboundedSender<InterceptedRequest>,
conn: Connection,
session_id: String,
}
impl InterceptorState {
pub(crate) fn new(
filter: ListenFilter,
conn: Connection,
session_id: String,
) -> (Self, mpsc::UnboundedReceiver<InterceptedRequest>) {
let (tx, rx) = mpsc::unbounded_channel();
(
Self {
filter,
tx,
conn,
session_id,
},
rx,
)
}
pub(crate) fn on_request_will_be_sent(&self, params: &Value) -> Decision {
if !params["isIntercepted"].as_bool().unwrap_or(false) {
return Decision::Ignore;
}
let Some(request_id) = params["requestId"].as_str() else {
return Decision::Ignore;
};
let url = params["url"].as_str().unwrap_or_default().to_string();
let cause = params["cause"].as_str().unwrap_or_default();
let internal = params["internalCause"].as_str().unwrap_or_default();
let resource_type = if !cause.is_empty() { cause } else { internal }.to_string();
if !self.filter.matches(&url, &resource_type) {
return Decision::AutoResume(request_id.to_string());
}
let req = InterceptedRequest {
url,
method: params["method"].as_str().unwrap_or_default().to_string(),
resource_type,
headers: parse_headers(¶ms["headers"]),
post_data: params["postData"].as_str().map(str::to_string),
request_id: request_id.to_string(),
conn: self.conn.clone(),
session_id: self.session_id.clone(),
};
if self.tx.send(req).is_err() {
return Decision::AutoResume(request_id.to_string());
}
Decision::Delivered
}
}
fn to_header_array(headers: &[(String, String)]) -> Vec<Value> {
headers
.iter()
.map(|(n, v)| json!({ "name": n, "value": v }))
.collect()
}
fn status_text(status: u16) -> &'static str {
match status {
200 => "OK",
201 => "Created",
202 => "Accepted",
204 => "No Content",
301 => "Moved Permanently",
302 => "Found",
303 => "See Other",
304 => "Not Modified",
307 => "Temporary Redirect",
308 => "Permanent Redirect",
400 => "Bad Request",
401 => "Unauthorized",
403 => "Forbidden",
404 => "Not Found",
405 => "Method Not Allowed",
429 => "Too Many Requests",
500 => "Internal Server Error",
502 => "Bad Gateway",
503 => "Service Unavailable",
504 => "Gateway Timeout",
_ => "",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn header_array_shape() {
let h = vec![("Content-Type".to_string(), "text/html".to_string())];
let arr = to_header_array(&h);
assert_eq!(arr[0]["name"], "Content-Type");
assert_eq!(arr[0]["value"], "text/html");
}
#[test]
fn status_text_known() {
assert_eq!(status_text(200), "OK");
assert_eq!(status_text(404), "Not Found");
assert_eq!(status_text(599), "");
}
}