1use crate::cdp::session::CdpSession;
8use crate::cdp::CdpEvent;
9use crate::error::Result;
10use crate::network::NetworkStore;
11use crate::request::Request;
12use crate::types::Headers;
13use base64::Engine;
14use parking_lot::Mutex;
15use serde_json::{json, Value};
16use std::future::Future;
17use std::pin::Pin;
18use std::sync::atomic::{AtomicBool, Ordering};
19use std::sync::{Arc, Weak};
20use tokio::sync::broadcast;
21
22pub(crate) type RouteHandler =
24 Arc<dyn Fn(Route) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
25
26#[derive(Clone)]
28pub(crate) struct RouteEntry {
29 pub pattern: String,
30 pub handler: RouteHandler,
31}
32
33pub(crate) async fn route_listener(
35 mut rx: broadcast::Receiver<CdpEvent>,
36 session: Arc<CdpSession>,
37 store: Arc<NetworkStore>,
38 handlers: Arc<Mutex<Vec<RouteEntry>>>,
39) {
40 loop {
41 match rx.recv().await {
42 Ok(ev) if ev.method == "Fetch.requestPaused" => {
43 let url = ev
44 .params
45 .get("request")
46 .and_then(|r| r.get("url"))
47 .and_then(|v| v.as_str())
48 .unwrap_or("")
49 .to_string();
50 let fetch_id = ev
51 .params
52 .get("requestId")
53 .and_then(|v| v.as_str())
54 .unwrap_or("")
55 .to_string();
56 let handler = {
57 let hs = handlers.lock();
58 hs.iter()
59 .find(|e| pattern_matches(&e.pattern, &url))
60 .map(|e| Arc::clone(&e.handler))
61 };
62 match (handler, request_from_paused(&ev.params, Arc::downgrade(&store))) {
63 (Some(h), Some(req)) => {
64 let route = Route::new(req, fetch_id, Arc::clone(&session));
65 tokio::spawn(async move {
66 (h)(route).await;
67 });
68 }
69 _ => {
70 let _ = session
72 .send("Fetch.continueRequest", json!({ "requestId": fetch_id }))
73 .await;
74 }
75 }
76 }
77 Ok(_) => {}
78 Err(broadcast::error::RecvError::Closed) => break,
79 Err(broadcast::error::RecvError::Lagged(_)) => continue,
80 }
81 }
82}
83
84pub(crate) fn pattern_matches(pattern: &str, url: &str) -> bool {
106 if pattern == url {
108 return true;
109 }
110
111 if let Some(result) = legacy_simple_wildcard(pattern, url) {
116 return result;
117 }
118
119 match glob_to_regex(pattern) {
121 Some(re) => re.is_match(url),
122 None => url.contains(pattern),
124 }
125}
126
127fn legacy_simple_wildcard(pattern: &str, url: &str) -> Option<bool> {
133 let has_leading = pattern.starts_with('*');
134 let has_trailing = pattern.ends_with('*') && pattern.len() > 1;
135 let inner_start = if has_leading { 1 } else { 0 };
137 let inner_end = if has_trailing {
138 pattern.len() - 1
139 } else {
140 pattern.len()
141 };
142 if inner_start > inner_end {
143 return None;
145 }
146 let inner = &pattern[inner_start..inner_end];
147 if inner.contains('*') || inner.contains('?') || inner.contains('[') {
149 return None;
150 }
151 match (has_leading, has_trailing) {
152 (true, true) => Some(url.contains(inner)),
153 (true, false) => Some(url.ends_with(inner)),
154 (false, true) => Some(url.starts_with(inner)),
155 (false, false) => {
156 Some(url.contains(pattern))
158 }
159 }
160}
161
162fn glob_to_regex(pattern: &str) -> Option<regex::Regex> {
165 let mut out = String::with_capacity(pattern.len() * 2);
166 out.push('^');
167 let chars: Vec<char> = pattern.chars().collect();
168 let mut i = 0;
169 while i < chars.len() {
170 let c = chars[i];
171 match c {
172 '*' => {
173 if i + 1 < chars.len() && chars[i + 1] == '*' {
176 out.push_str(".*");
177 i += 2;
178 while i < chars.len() && chars[i] == '*' {
180 i += 1;
181 }
182 } else {
183 out.push_str("[^/]*");
184 i += 1;
185 }
186 }
187 '?' => {
188 out.push_str("[^/]");
189 i += 1;
190 }
191 '[' => {
192 let start = i + 1;
195 let mut j = start;
196 if j < chars.len() && (chars[j] == '!' || chars[j] == '^') {
197 j += 1;
198 }
199 if j < chars.len() && chars[j] == ']' {
200 j += 1;
201 }
202 while j < chars.len() && chars[j] != ']' {
203 j += 1;
204 }
205 if j >= chars.len() {
206 out.push_str("\\[");
208 i += 1;
209 } else {
210 out.push('[');
211 let mut k = start;
212 if k < chars.len() && chars[k] == '!' {
213 out.push('^');
214 k += 1;
215 }
216 while k < j {
217 let ch = chars[k];
218 if "\\.+*?(){}|^$".contains(ch) {
221 out.push('\\');
222 }
223 out.push(ch);
224 k += 1;
225 }
226 out.push(']');
227 i = j + 1;
228 }
229 }
230 _ => {
231 out.push_str(®ex::escape(&c.to_string()));
233 i += 1;
234 }
235 }
236 }
237 out.push('$');
238 regex::Regex::new(&out).ok()
239}
240
241#[derive(Debug, Clone, Default)]
243#[non_exhaustive]
244pub struct RouteContinueOptions {
245 pub url: Option<String>,
246 pub method: Option<String>,
247 pub headers: Option<Headers>,
248 pub post_data: Option<String>,
249}
250
251#[derive(Debug, Clone, Default)]
253#[non_exhaustive]
254pub struct RouteFulfillOptions {
255 pub status: Option<u16>,
256 pub headers: Option<Headers>,
257 pub content_type: Option<String>,
258 pub body: Option<String>,
259}
260
261impl RouteFulfillOptions {
262 pub fn new() -> Self {
263 Self::default()
264 }
265 pub fn status(mut self, v: u16) -> Self {
266 self.status = Some(v);
267 self
268 }
269 pub fn body(mut self, v: impl Into<String>) -> Self {
270 self.body = Some(v.into());
271 self
272 }
273 pub fn content_type(mut self, v: impl Into<String>) -> Self {
274 self.content_type = Some(v.into());
275 self
276 }
277}
278
279#[derive(Debug, Clone, Default)]
284#[non_exhaustive]
285pub struct RouteFetchOptions {
286 pub url: Option<String>,
287 pub method: Option<String>,
288 pub headers: Option<Headers>,
289 pub post_data: Option<String>,
290 pub post_data_bytes: Option<Vec<u8>>,
291 pub timeout: Option<u64>,
293 pub max_redirects: Option<u32>,
295}
296
297impl RouteFetchOptions {
298 pub fn new() -> Self {
299 Self::default()
300 }
301 pub fn url(mut self, v: impl Into<String>) -> Self {
302 self.url = Some(v.into());
303 self
304 }
305 pub fn method(mut self, v: impl Into<String>) -> Self {
306 self.method = Some(v.into());
307 self
308 }
309 pub fn headers(mut self, v: Headers) -> Self {
310 self.headers = Some(v);
311 self
312 }
313 pub fn post_data(mut self, v: impl Into<String>) -> Self {
314 self.post_data = Some(v.into());
315 self
316 }
317 pub fn post_data_bytes(mut self, v: Vec<u8>) -> Self {
318 self.post_data_bytes = Some(v);
319 self
320 }
321 pub fn timeout(mut self, v: u64) -> Self {
322 self.timeout = Some(v);
323 self
324 }
325 pub fn max_redirects(mut self, v: u32) -> Self {
326 self.max_redirects = Some(v);
327 self
328 }
329}
330
331#[derive(Debug, Clone)]
337pub struct RouteFetchResponse {
338 url: String,
339 status: u16,
340 status_text: String,
341 headers: Headers,
342 body: Vec<u8>,
343}
344
345impl RouteFetchResponse {
346 pub fn url(&self) -> &str {
347 &self.url
348 }
349 pub fn status(&self) -> u16 {
350 self.status
351 }
352 pub fn status_text(&self) -> &str {
353 &self.status_text
354 }
355 pub fn headers(&self) -> &Headers {
356 &self.headers
357 }
358 pub fn body(&self) -> &[u8] {
359 &self.body
360 }
361
362 pub fn text(&self) -> String {
364 String::from_utf8_lossy(&self.body).into_owned()
365 }
366}
367
368#[derive(Clone)]
370pub struct Route {
371 inner: Arc<RouteInner>,
372}
373
374struct RouteInner {
375 request: Request,
376 fetch_request_id: String,
377 session: Arc<CdpSession>,
378 handled: AtomicBool,
379}
380
381impl Route {
382 pub(crate) fn new(
383 request: Request,
384 fetch_request_id: String,
385 session: Arc<CdpSession>,
386 ) -> Self {
387 Self {
388 inner: Arc::new(RouteInner {
389 request,
390 fetch_request_id,
391 session,
392 handled: AtomicBool::new(false),
393 }),
394 }
395 }
396
397 pub fn request(&self) -> Request {
399 self.inner.request.clone()
400 }
401
402 fn claim(&self) -> bool {
403 !self.inner.handled.swap(true, Ordering::SeqCst)
404 }
405
406 pub async fn continue_(&self, options: Option<RouteContinueOptions>) -> Result<()> {
408 if !self.claim() {
409 return Ok(());
410 }
411 let opts = options.unwrap_or_default();
412 let mut params = json!({ "requestId": self.inner.fetch_request_id });
413 if let Some(u) = opts.url {
414 params["url"] = json!(u);
415 }
416 if let Some(m) = opts.method {
417 params["method"] = json!(m);
418 }
419 if let Some(h) = opts.headers {
420 params["headers"] = json!(headers_to_array(&h));
421 }
422 if let Some(p) = opts.post_data {
423 params["postData"] = json!(base64::engine::general_purpose::STANDARD.encode(p));
424 }
425 self.inner
426 .session
427 .send("Fetch.continueRequest", params)
428 .await
429 .map(|_: Value| ())
430 }
431
432 pub async fn fallback(&self) -> Result<()> {
434 self.continue_(None).await
435 }
436
437 pub async fn fulfill(&self, options: RouteFulfillOptions) -> Result<()> {
439 if !self.claim() {
440 return Ok(());
441 }
442 let status = options.status.unwrap_or(200);
443 let mut headers = options.headers.unwrap_or_default();
444 if let Some(ct) = options.content_type {
445 headers.entry("content-type".into()).or_insert(ct);
446 }
447 let body_b64 = options
448 .body
449 .map(|b| base64::engine::general_purpose::STANDARD.encode(b))
450 .unwrap_or_default();
451 self.inner
452 .session
453 .send(
454 "Fetch.fulfillRequest",
455 json!({
456 "requestId": self.inner.fetch_request_id,
457 "responseCode": status,
458 "responseHeaders": headers_to_array(&headers),
459 "body": body_b64,
460 }),
461 )
462 .await
463 .map(|_: Value| ())
464 }
465
466 pub async fn abort(&self, error_code: Option<&str>) -> Result<()> {
468 if !self.claim() {
469 return Ok(());
470 }
471 let reason = error_code.unwrap_or("Failed");
472 self.inner
473 .session
474 .send(
475 "Fetch.failRequest",
476 json!({ "requestId": self.inner.fetch_request_id, "errorReason": reason }),
477 )
478 .await
479 .map(|_: Value| ())
480 }
481
482 pub async fn fetch(&self, options: Option<RouteFetchOptions>) -> Result<RouteFetchResponse> {
499 let opts = options.unwrap_or_default();
500 let request = self.request();
501
502 let url = opts
503 .url
504 .clone()
505 .unwrap_or_else(|| request.url().to_string());
506
507 let method_str = opts
508 .method
509 .clone()
510 .unwrap_or_else(|| request.method().to_string());
511 let method = reqwest::Method::from_bytes(method_str.as_bytes())
512 .map_err(|e| crate::error::Error::InvalidArgument(format!("invalid method: {e}")))?;
513
514 let mut headers = request.headers().clone();
516 if let Some(extra) = opts.headers.as_ref() {
517 for (k, v) in extra {
518 headers.insert(k.clone(), v.clone());
519 }
520 }
521
522 let body: Vec<u8> = if let Some(b) = opts.post_data_bytes.clone() {
525 b
526 } else if let Some(s) = opts.post_data.clone() {
527 s.into_bytes()
528 } else {
529 request.post_data_buffer().unwrap_or_default()
530 };
531
532 let mut client_builder = reqwest::Client::builder();
534 if let Some(n) = opts.max_redirects {
535 client_builder = client_builder.redirect(reqwest::redirect::Policy::limited(
536 n.max(1) as usize,
537 ));
538 }
539 let client = client_builder
540 .build()
541 .map_err(|e| crate::error::Error::Http(format!("failed to build client: {e}")))?;
542
543 let mut builder = client.request(method, &url);
544
545 const SKIP: &[&str] = &[
550 "host",
551 "connection",
552 "content-length",
553 "transfer-encoding",
554 "accept-encoding",
555 "content-encoding",
556 "keep-alive",
557 "proxy-connection",
558 "upgrade",
559 ];
560 for (k, v) in &headers {
561 if SKIP.contains(&k.as_str()) {
562 continue;
563 }
564 if let (Ok(name), Ok(val)) = (
565 reqwest::header::HeaderName::from_bytes(k.as_bytes()),
566 reqwest::header::HeaderValue::from_str(v),
567 ) {
568 builder = builder.header(name, val);
569 }
570 }
571
572 let is_bodyless =
574 method_str.eq_ignore_ascii_case("GET") || method_str.eq_ignore_ascii_case("HEAD");
575 if !body.is_empty() && !is_bodyless {
576 builder = builder.body(body);
577 }
578
579 let timeout_ms = opts.timeout.unwrap_or(30_000);
582 builder = builder.timeout(std::time::Duration::from_millis(timeout_ms));
583
584 let resp = builder.send().await.map_err(|e| {
585 let mut s = format!("{e}");
589 let mut src = std::error::Error::source(&e);
590 while let Some(c) = src {
591 s.push_str(" :: ");
592 s.push_str(&c.to_string());
593 src = c.source();
594 }
595 crate::error::Error::Http(format!("request failed: {s}"))
596 })?;
597
598 let final_url = resp.url().to_string();
599 let status = resp.status().as_u16();
600 let status_text = resp.status().canonical_reason().unwrap_or("").to_string();
601
602 let mut resp_headers: Headers = Headers::with_capacity(resp.headers().len());
603 for (name, value) in resp.headers().iter() {
604 let key = name.as_str().to_ascii_lowercase();
605 let val = value.to_str().map(String::from).unwrap_or_else(|_| {
606 String::from_utf8_lossy(value.as_bytes()).into_owned()
607 });
608 resp_headers.insert(key, val);
609 }
610
611 let body = resp
612 .bytes()
613 .await
614 .map_err(|e| crate::error::Error::Http(format!("failed to read body: {e}")))?;
615
616 Ok(RouteFetchResponse {
617 url: final_url,
618 status,
619 status_text,
620 headers: resp_headers,
621 body: body.to_vec(),
622 })
623 }
624}
625
626pub(crate) fn request_from_paused(params: &Value, store: Weak<NetworkStore>) -> Option<Request> {
628 let req = params.get("request")?;
629 let request_id = params.get("requestId").and_then(|v| v.as_str())?.to_string();
630 let url = req.get("url").and_then(|v| v.as_str())?.to_string();
631 let method = req
632 .get("method")
633 .and_then(|v| v.as_str())
634 .unwrap_or("GET")
635 .to_string();
636 let headers = headers_from_object(req.get("headers"));
637 let resource_type = params
638 .get("resourceType")
639 .and_then(|v| v.as_str())
640 .unwrap_or("Other")
641 .to_string();
642 Some(Request::new(
643 url,
644 method,
645 headers,
646 resource_type.clone(),
647 request_id,
648 resource_type == "Document",
649 store,
650 ))
651}
652
653fn headers_to_array(h: &Headers) -> Vec<Value> {
654 h.iter().map(|(k, v)| json!({ "name": k, "value": v })).collect()
655}
656
657fn headers_from_object(v: Option<&Value>) -> Headers {
658 let mut out = Headers::new();
659 if let Some(obj) = v.and_then(|v| v.as_object()) {
660 for (k, val) in obj {
661 if let Some(s) = val.as_str() {
662 out.insert(k.to_lowercase(), s.to_string());
663 }
664 }
665 }
666 out
667}
668
669#[cfg(test)]
670mod tests {
671 use super::*;
672
673 #[test]
676 fn legacy_exact_match() {
677 assert!(pattern_matches("http://x/y", "http://x/y"));
678 assert!(!pattern_matches("http://x/y", "http://x/z"));
679 }
680
681 #[test]
682 fn legacy_leading_star_is_suffix() {
683 assert!(pattern_matches("*mocked", "http://host/mock-api-mocked"));
685 assert!(!pattern_matches("*mocked", "http://host/api"));
686 }
687
688 #[test]
689 fn legacy_trailing_star_is_prefix() {
690 assert!(pattern_matches("http://x*", "http://x/api"));
692 assert!(!pattern_matches("http://x*", "http://y/api"));
693 }
694
695 #[test]
696 fn legacy_both_stars_is_substring() {
697 assert!(pattern_matches("*mocked*", "http://host/mock-api-mocked/path"));
699 assert!(pattern_matches("*blocked*", "http://host/blocked"));
700 assert!(!pattern_matches("*mocked*", "http://host/api"));
701 }
702
703 #[test]
704 fn legacy_no_wildcard_is_substring() {
705 assert!(pattern_matches("api/v1", "http://host/api/v1/users"));
707 assert!(!pattern_matches("api/v1", "http://host/api/v2"));
708 }
709
710 #[test]
713 fn double_star_matches_anything_including_path() {
714 assert!(pattern_matches("**", "http://x/"));
716 assert!(pattern_matches("**", "http://x/a/b/c"));
717 assert!(pattern_matches("**", "https://example.com/api/v1?x=1#frag"));
718 assert!(pattern_matches("**", ""));
720 }
721
722 #[test]
723 fn double_star_slash_star_matches_any() {
724 assert!(pattern_matches("**/*", "http://x/api/y"));
726 assert!(pattern_matches("**/*", "http://x/"));
727 }
728
729 #[test]
730 fn bare_single_star_matches_any() {
731 assert!(pattern_matches("*", "http://x/a/b"));
734 }
735
736 #[test]
737 fn single_star_does_not_cross_slash() {
738 assert!(pattern_matches("*/api/*", "http://x/api/y"));
740 assert!(pattern_matches("*/api/*", "http://x/a/b/api/c"));
741 assert!(!pattern_matches("*/api/*", "http://x/users"));
746 }
747
748 #[test]
749 fn question_mark_matches_single_char() {
750 assert!(pattern_matches("http://x/a?c", "http://x/abc"));
751 assert!(pattern_matches("http://x/a?c", "http://x/aZc"));
752 assert!(!pattern_matches("http://x/a?c", "http://x/a/c"));
754 }
755
756 #[test]
757 fn char_class_positive() {
758 assert!(pattern_matches("http://x/v[12]", "http://x/v1"));
759 assert!(pattern_matches("http://x/v[12]", "http://x/v2"));
760 assert!(!pattern_matches("http://x/v[12]", "http://x/v3"));
761 }
762
763 #[test]
764 fn char_class_negation() {
765 assert!(pattern_matches("http://x/v[!12]", "http://x/v3"));
766 assert!(!pattern_matches("http://x/v[!12]", "http://x/v1"));
767 }
768
769 #[test]
770 fn real_world_typical_pattern() {
771 let pattern = "**/api/users/*";
773 assert!(pattern_matches(pattern, "https://app.example.com/api/users/42"));
774 assert!(pattern_matches(pattern, "https://app.example.com/v2/api/users/abc"));
775 assert!(pattern_matches(pattern, "https://app.example.com/api/users/"));
777 assert!(!pattern_matches(pattern, "https://app.example.com/api/groups/1"));
778 assert!(!pattern_matches(pattern, "https://other.example.com/feed"));
780 }
781
782 #[test]
783 fn regex_metacharacters_in_pattern_are_literal() {
784 assert!(pattern_matches("http://x/.png", "http://x/.png"));
786 assert!(!pattern_matches("http://x/.png", "http://x/xpng"));
787 }
788}