1use std::collections::HashSet;
2
3use crate::*;
4
5pub fn mount(extensions: &mut Extensions, manager: SmartPush) -> &mut Extensions {
11 let manager = Mutex::new(manager);
12
13 extensions.add_post(
14 post!(
15 request,
16 host,
17 response_pipe,
18 identity_body,
19 addr,
20 move |manager: Mutex<SmartPush>| {
21 push(
22 request,
23 host,
24 response_pipe,
25 identity_body,
26 addr,
27 Some(manager),
28 )
29 .await
30 }
31 ),
32 Id::new(-32, "HTTP/2 push"),
33 );
34 extensions
35}
36
37pub fn always<'a>(
51 request: &'a FatRequest,
52 host: &'a Host,
53 response_pipe: &'a mut application::ResponseBodyPipe,
54 bytes: Bytes,
55 addr: SocketAddr,
56) -> RetFut<'a, ()> {
57 Box::pin(push(request, host, response_pipe, bytes, addr, None))
58}
59
60pub struct SmartPush {
61 db: HashSet<SocketAddr>,
62 last_clear: Instant,
63 clear_interval: Duration,
64 check_every_request: u32,
65 iteration: u32,
66}
67impl SmartPush {
68 pub fn new(clear_interval: Duration, check_every_request: u32) -> Self {
73 Self {
74 db: HashSet::new(),
75 last_clear: Instant::now(),
76 clear_interval,
77 check_every_request,
78 iteration: 0,
79 }
80 }
81 fn accept(&mut self, remote: SocketAddr) -> bool {
82 if self.iteration >= self.check_every_request {
83 let now = Instant::now();
84 let elapsed = now - self.last_clear;
85 if elapsed > self.clear_interval {
86 self.last_clear = now;
87 self.db.clear();
88 }
89 }
90 self.iteration += 1;
91
92 !self.db.contains(&remote)
93 }
94 fn register(&mut self, remote: SocketAddr) {
95 self.db.insert(remote);
96 }
97}
98impl Default for SmartPush {
99 fn default() -> Self {
100 Self::new(Duration::from_secs(60 * 2), 8)
101 }
102}
103
104async fn push<'a>(
105 request: &'a FatRequest,
106 host: &'a Host,
107 response_pipe: &'a mut application::ResponseBodyPipe,
108 bytes: Bytes,
109 addr: SocketAddr,
110 manager: Option<&'a Mutex<SmartPush>>,
111) {
112 use internals::*;
113 #[allow(irrefutable_let_patterns)]
118 if !matches!(response_pipe, ResponseBodyPipe::Http2(_, _)) {
119 return;
120 }
121
122 if let Some(manager) = manager {
123 let mut lock = manager.lock().await;
125 if !lock.accept(addr) {
126 return;
127 }
128 }
129
130 if request
135 .headers()
136 .get("user-agent")
137 .and_then(|user_agent| user_agent.to_str().ok())
138 .map_or(false, |user_agent| user_agent.contains("Firefox/"))
139 {
140 return;
141 }
142
143 const HTML_START: &str = "<!DOCTYPE html>";
144
145 match str::from_utf8(&bytes) {
146 Ok(string)
148 if string
149 .get(..HTML_START.len())
150 .map_or(false, |s| s.eq_ignore_ascii_case(HTML_START)) =>
151 {
152 let mut urls: Vec<_> = url_crawl::get_urls(string).map(String::from).collect();
153
154 urls.retain(|url| {
156 !url.contains(".jpg")
157 && !url.contains(".avif")
158 && !url.contains("png")
159 && !url.contains(".webp")
160 && !url.contains(".gif")
161 });
162
163 for url in &mut urls {
164 if !url.starts_with('/') && !url.contains(':') {
165 let path = request.uri().path();
166 let mut last_slash = 0;
167 for (pos, c) in path.chars().enumerate() {
168 if c == '/' {
169 last_slash = pos;
170 }
171 }
172 url.insert_str(0, &path[..=last_slash]);
173 }
174 }
175
176 debug!("Pushing urls {:?}", urls);
177
178 urls.sort_unstable();
179 urls.dedup();
180
181 for url in urls {
182 let mut uri = request.uri().clone().into_parts();
183 if let Some(uri) =
184 uri::PathAndQuery::from_maybe_shared::<Bytes>(url.into_bytes().into())
185 .ok()
186 .and_then(|path| {
187 uri.path_and_query = Some(path);
188 Uri::from_parts(uri).ok()
189 })
190 {
191 let mut push_request = Request::builder().uri(uri);
192 macro_rules! copy_header {
193 ($builder: expr, $headers: expr, $name: expr) => {
194 for header in $headers.get_all($name) {
195 $builder = $builder.header($name, header);
196 }
197 };
198 }
199 let headers = request.headers();
200
201 copy_header!(push_request, headers, "accept-encoding");
202 copy_header!(push_request, headers, "accept-language");
203 copy_header!(push_request, headers, "user-agent");
204 copy_header!(push_request, headers, "host");
205 copy_header!(push_request, headers, "origin");
206 copy_header!(push_request, headers, "cookies");
207
208 let push_request = push_request
209 .body(())
210 .expect("failed to construct a request only from another valid request.");
211
212 let empty_request = utils::empty_clone_request(&push_request);
213
214 let response_pipe = match response_pipe.push_request(empty_request) {
215 Ok(pipe) => pipe,
216 Err(_) => return,
217 };
218
219 let mut push_request =
220 push_request.map(|_| kvarn::application::Body::Bytes(Bytes::new().into()));
221
222 let response = kvarn::handle_cache(&mut push_request, addr, host).await;
223
224 if let Err(err) = kvarn::SendKind::Push(response_pipe)
225 .send(response, request, host, addr)
226 .await
227 {
228 error!("Error occurred when pushing request. {:?}", err);
229 }
230 }
231 }
232
233 debug!("Push done.");
234 }
235 _ => {}
237 }
238 if let Some(manager) = manager {
239 let mut lock = manager.lock().await;
241 lock.register(addr);
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 #[tokio::test]
249 async fn run() {
250 let mut extensions = Extensions::new();
251 mount(&mut extensions, SmartPush::default());
252 let _server = kvarn_testing::ServerBuilder::from(extensions).run().await;
253 }
254 #[test]
255 fn exclusive() {
256 let mut extensions = new();
257 extensions.add_post(Box::new(always), Id::new(-32, "HTTP/2 push"));
258
259 let debug = format!("{extensions:?}");
260 assert_eq!(debug.match_indices("push").count(), 1);
261 mount(&mut extensions, SmartPush::default());
262 assert_eq!(debug.match_indices("push").count(), 1);
263 }
264}