gatel_core/hoops/
replace.rs1use http::header::{CONTENT_LENGTH, CONTENT_TYPE};
2use salvo::{Depot, FlowCtrl, Request, Response, async_trait};
3use tracing::debug;
4
5pub struct ReplaceHoop {
15 rules: Vec<(String, String)>,
16 once: bool,
17 content_types: Vec<String>,
18}
19
20impl ReplaceHoop {
21 pub fn new(rules: Vec<(String, String)>, once: bool) -> Self {
22 Self {
23 rules,
24 once,
25 content_types: vec!["text/html".to_string()],
26 }
27 }
28
29 fn content_type_matches(&self, headers: &http::HeaderMap) -> bool {
32 let ct = headers
33 .get(CONTENT_TYPE)
34 .and_then(|v| v.to_str().ok())
35 .unwrap_or("");
36 self.content_types
37 .iter()
38 .any(|allowed| ct.contains(allowed.as_str()))
39 }
40}
41
42#[async_trait]
43impl salvo::Handler for ReplaceHoop {
44 async fn handle(
45 &self,
46 req: &mut Request,
47 depot: &mut Depot,
48 res: &mut Response,
49 ctrl: &mut FlowCtrl,
50 ) {
51 ctrl.call_next(req, depot, res).await;
52
53 if !self.content_type_matches(res.headers()) {
55 return;
56 }
57
58 let body = res.take_body();
60 let body_bytes = match super::compress::collect_res_body_bytes(body).await {
61 Ok(b) => b,
62 Err(_) => return,
63 };
64
65 let text = match std::str::from_utf8(&body_bytes) {
67 Ok(s) => s.to_string(),
68 Err(_) => {
69 debug!("response body is not valid UTF-8; skipping replace middleware");
70 res.body(body_bytes);
71 return;
72 }
73 };
74
75 let mut output = text;
77 for (search, replacement) in &self.rules {
78 output = if self.once {
79 output.replacen(search.as_str(), replacement.as_str(), 1)
80 } else {
81 output.replace(search.as_str(), replacement.as_str())
82 };
83 debug!(
84 search = search.as_str(),
85 replacement = replacement.as_str(),
86 once = self.once,
87 "applied body replacement rule"
88 );
89 }
90
91 let new_bytes = output.into_bytes();
93 res.headers_mut()
94 .insert(CONTENT_LENGTH, http::HeaderValue::from(new_bytes.len()));
95 res.body(new_bytes);
96 }
97}