1#[cfg(test)]
37mod tests;
38#[cfg(not(target_arch = "wasm32"))]
39pub(crate) mod disk;
40
41use std::collections::{HashMap, VecDeque};
42use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
43use std::sync::{Arc, Mutex};
44use std::time::{Duration, Instant};
45
46use crate::application::Application;
47use crate::header::Header;
48use crate::middleware::Middleware;
49use crate::request::{METHOD, Request};
50use crate::response::Response;
51use crate::server::ConnectionInfo;
52
53struct CachedEntry {
56 response: Response,
57 inserted_at: Instant,
58 ttl: Duration,
61}
62
63struct CacheStore {
64 entries: HashMap<String, CachedEntry>,
65 order: VecDeque<String>,
67}
68
69impl CacheStore {
70 fn new() -> Self {
71 CacheStore { entries: HashMap::new(), order: VecDeque::new() }
72 }
73
74 fn get(&self, key: &str) -> Option<&CachedEntry> {
75 self.entries.get(key).filter(|e| e.inserted_at.elapsed() < e.ttl)
76 }
77
78 fn insert(&mut self, key: String, entry: CachedEntry, capacity: usize) {
79 if self.entries.contains_key(&key) {
81 self.entries.insert(key, entry);
82 return;
83 }
84 if self.entries.len() >= capacity {
87 if let Some(oldest) = self.order.pop_front() {
88 self.entries.remove(&oldest);
89 }
90 }
91 self.order.push_back(key.clone());
92 self.entries.insert(key, entry);
93 }
94
95 fn purge_expired(&mut self) {
96 let expired: Vec<String> = self.entries.iter()
97 .filter(|(_, e)| e.inserted_at.elapsed() >= e.ttl)
98 .map(|(k, _)| k.clone())
99 .collect();
100 for k in &expired {
101 self.entries.remove(k);
102 self.order.retain(|o| o != k);
103 }
104 }
105}
106
107pub(crate) fn freshness(response: &Response, cap: Duration) -> Duration {
112 let mut s_maxage = None;
113 let mut max_age = None;
114 for h in response.headers.iter().filter(|h| h.name.eq_ignore_ascii_case(Header::_CACHE_CONTROL)) {
115 for directive in h.value.split(',') {
116 let Some((name, value)) = directive.split_once('=') else { continue };
117 let Ok(secs) = value.trim().trim_matches('"').parse::<u64>() else { continue };
118 match name.trim().to_ascii_lowercase().as_str() {
119 "s-maxage" => s_maxage = Some(s_maxage.map_or(secs, |v: u64| v.min(secs))),
120 "max-age" => max_age = Some(max_age.map_or(secs, |v: u64| v.min(secs))),
121 _ => {}
122 }
123 }
124 }
125 match s_maxage.or(max_age) {
126 Some(secs) => cap.min(Duration::from_secs(secs)),
127 None => cap,
128 }
129}
130
131#[derive(Debug, Clone, PartialEq, Eq)]
137pub enum Purge {
138 All,
140 Exact(String),
142 Prefix(String),
144}
145
146impl Purge {
147 fn matches(&self, key: &str) -> bool {
148 let target = key.split('\x00').next().unwrap_or(key);
149 match self {
150 Purge::All => true,
151 Purge::Exact(t) => target == t,
152 Purge::Prefix(p) => target.starts_with(p.as_str()),
153 }
154 }
155
156 pub fn from_target(target: &str) -> Purge {
158 match target.strip_suffix('*') {
159 Some(prefix) => Purge::Prefix(prefix.to_string()),
160 None => Purge::Exact(target.to_string()),
161 }
162 }
163}
164
165fn same_bytes(a: &[u8], b: &[u8]) -> bool {
167 a.len() == b.len() && a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
168}
169
170pub mod registry {
173 use super::CacheLayer;
174 use std::collections::BTreeMap;
175 use std::sync::{Mutex, OnceLock};
176
177 fn caches() -> &'static Mutex<BTreeMap<String, CacheLayer>> {
178 static CACHES: OnceLock<Mutex<BTreeMap<String, CacheLayer>>> = OnceLock::new();
179 CACHES.get_or_init(Mutex::default)
180 }
181
182 pub fn register(name: impl Into<String>, cache: CacheLayer) {
184 caches().lock().unwrap_or_else(|e| e.into_inner()).insert(name.into(), cache);
185 }
186
187 pub(crate) fn replace_group(group: &str, new: Vec<(String, CacheLayer)>) {
190 let mut map = caches().lock().unwrap_or_else(|e| e.into_inner());
191 map.retain(|name, _| !name.starts_with(group));
192 map.extend(new);
193 }
194
195 pub fn list() -> Vec<(String, CacheLayer)> {
197 caches().lock().unwrap_or_else(|e| e.into_inner()).iter().map(|(n, c)| (n.clone(), c.clone())).collect()
198 }
199
200 pub fn get(name: &str) -> Option<CacheLayer> {
201 caches().lock().unwrap_or_else(|e| e.into_inner()).get(name).cloned()
202 }
203}
204
205#[derive(Clone)]
216pub struct CacheLayer {
217 store: Arc<Mutex<CacheStore>>,
218 stored: Arc<AtomicUsize>,
223 hits: Arc<AtomicU64>,
224 misses: Arc<AtomicU64>,
225 capacity: usize,
226 ttl: Duration,
227 vary_headers: Vec<String>,
228 #[cfg(not(target_arch = "wasm32"))]
230 disk: Option<Arc<disk::DiskStore>>,
231 purge_token: Option<Arc<String>>,
233}
234
235impl CacheLayer {
236 pub fn memory(capacity: usize) -> Self {
240 CacheLayer {
241 store: Arc::new(Mutex::new(CacheStore::new())),
242 stored: Arc::new(AtomicUsize::new(0)),
243 hits: Arc::new(AtomicU64::new(0)),
244 misses: Arc::new(AtomicU64::new(0)),
245 capacity,
246 ttl: Duration::from_secs(60),
247 vary_headers: vec![],
248 #[cfg(not(target_arch = "wasm32"))]
249 disk: None,
250 purge_token: None,
251 }
252 }
253
254 pub fn purge_token(mut self, token: impl Into<String>) -> Self {
261 let token = token.into();
262 self.purge_token = (!token.is_empty()).then(|| Arc::new(token));
263 self
264 }
265
266 pub fn purge(&self, target: &Purge) -> usize {
268 if let Some(disk) = self.disk_store() {
269 let n = disk.remove_where(|key| target.matches(key));
270 self.stored.store(disk.len(), Ordering::Relaxed);
271 return n;
272 }
273 let mut guard = self.store.lock().unwrap_or_else(|e| e.into_inner());
274 let keys: Vec<String> = guard.entries.keys().filter(|k| target.matches(k)).cloned().collect();
275 for key in &keys {
276 guard.entries.remove(key);
277 }
278 guard.order.retain(|k| !keys.contains(k));
279 self.stored.store(guard.entries.len(), Ordering::Relaxed);
280 keys.len()
281 }
282
283 pub fn kind(&self) -> &'static str {
285 if self.disk_store().is_some() { "disk" } else { "memory" }
286 }
287
288 fn answer_purge(&self, request: &Request) -> Option<Response> {
290 let token = self.purge_token.as_ref()?;
291 if !request.method.eq_ignore_ascii_case("PURGE") {
292 return None;
293 }
294 use crate::response::STATUS_CODE_REASON_PHRASE;
295 let sent = request.header(Header::_AUTHORIZATION).and_then(|h| h.value.strip_prefix("Bearer ").map(str::trim));
296 let authorized = sent.is_some_and(|s| same_bytes(s.as_bytes(), token.as_bytes()));
297 if !authorized {
298 let mut response = Response::json(STATUS_CODE_REASON_PHRASE.n401_unauthorized, br#"{"error":"PURGE needs the purge token"}"#.to_vec());
299 response.headers.push(Header::new("WWW-Authenticate", r#"Bearer realm="cache purge""#));
300 return Some(response);
301 }
302 let n = self.purge(&Purge::from_target(&request.request_uri));
303 Some(Response::json(STATUS_CODE_REASON_PHRASE.n200_ok, format!(r#"{{"purged":{n}}}"#).into_bytes()))
304 }
305
306 #[cfg(not(target_arch = "wasm32"))]
319 pub fn disk(dir: impl Into<std::path::PathBuf>, max_bytes: u64) -> std::io::Result<Self> {
320 let store = disk::DiskStore::open(dir, max_bytes)?;
321 let mut layer = CacheLayer::memory(0);
322 layer.stored.store(store.len(), Ordering::Relaxed);
323 layer.disk = Some(Arc::new(store));
324 Ok(layer)
325 }
326
327 #[cfg(not(target_arch = "wasm32"))]
329 fn disk_store(&self) -> Option<&disk::DiskStore> {
330 self.disk.as_deref()
331 }
332
333 #[cfg(target_arch = "wasm32")]
334 fn disk_store(&self) -> Option<&NoDisk> {
335 None
336 }
337
338 pub fn hits(&self) -> u64 {
340 self.hits.load(Ordering::Relaxed)
341 }
342
343 pub fn misses(&self) -> u64 {
345 self.misses.load(Ordering::Relaxed)
346 }
347
348 pub fn size(&self) -> usize {
351 if let Some(disk) = self.disk_store() {
352 return disk.len();
353 }
354 self.store.lock().unwrap().entries.len()
355 }
356
357 pub fn stored_bytes(&self) -> Option<u64> {
359 #[cfg(not(target_arch = "wasm32"))]
360 return self.disk_store().map(disk::DiskStore::bytes);
361 #[cfg(target_arch = "wasm32")]
362 None
363 }
364
365 pub fn clear(&self) {
367 if let Some(disk) = self.disk_store() {
368 disk.remove_where(|_| true);
369 self.stored.store(0, Ordering::Relaxed);
370 return;
371 }
372 let mut guard = self.store.lock().unwrap();
373 guard.entries.clear();
374 guard.order.clear();
375 self.stored.store(0, Ordering::Relaxed);
376 }
377
378 pub fn ttl(mut self, secs: u64) -> Self {
380 self.ttl = Duration::from_secs(secs);
381 self
382 }
383
384 pub fn vary_by_header(mut self, name: &str) -> Self {
398 self.vary_headers.push(name.to_ascii_lowercase());
399 self
400 }
401
402 fn store(&self) -> &Mutex<CacheStore> {
403 &self.store
404 }
405
406 fn cache_key(&self, request: &Request) -> String {
408 let mut key = request.request_uri.clone();
409 for vh in &self.vary_headers {
410 let val = request.headers.iter()
411 .find(|h| h.name.eq_ignore_ascii_case(vh))
412 .map(|h| h.value.as_ref())
413 .unwrap_or("");
414 key.push('\x00');
415 key.push_str(val);
416 }
417 key
418 }
419
420 fn request_bypasses_cache(request: &Request) -> bool {
423 request.headers.iter().any(|h| {
424 h.name.eq_ignore_ascii_case(Header::_CACHE_CONTROL)
425 && h.value.to_ascii_lowercase().contains("no-cache")
426 })
427 }
428
429 fn request_is_private(request: &Request) -> bool {
433 request.headers.iter().any(|h| {
434 h.name.eq_ignore_ascii_case(Header::_AUTHORIZATION)
435 || h.name.eq_ignore_ascii_case(Header::_COOKIE)
436 || h.name.eq_ignore_ascii_case(Header::_RANGE)
437 })
438 }
439
440 fn response_is_cacheable(&self, request: &Request, response: &Response) -> bool {
453 if response.status_code < 200 || response.status_code >= 300 {
454 return false;
455 }
456 if request.header(Header::_RANGE).is_some() {
457 return false;
458 }
459 if response.stream_file.is_some() || response.stream_pipe.is_some() {
462 return false;
463 }
464 if response.headers.iter().any(|h| h.name.eq_ignore_ascii_case(Header::_SET_COOKIE)) {
465 return false;
466 }
467 let has = |name: &str| response.headers.iter().any(|h| h.name.eq_ignore_ascii_case(name));
468 let varies_harmlessly = |name: &str| {
469 (name.eq_ignore_ascii_case(Header::_ORIGIN) && !has(Header::_ACCESS_CONTROL_ALLOW_ORIGIN))
470 || (name.eq_ignore_ascii_case(Header::_ACCEPT_ENCODING) && !has(Header::_CONTENT_ENCODING))
471 };
472 response.headers.iter().all(|h| {
473 if h.name.eq_ignore_ascii_case(Header::_CACHE_CONTROL) {
474 let v = h.value.to_ascii_lowercase();
475 !(v.contains("no-store") || v.contains("private") || v.contains("no-cache"))
476 } else if h.name.eq_ignore_ascii_case(Header::_VARY) {
477 h.value.split(',').map(str::trim).filter(|name| !name.is_empty()).all(|name| {
478 name != "*" && (self.vary_headers.iter().any(|vh| vh.eq_ignore_ascii_case(name)) || varies_harmlessly(name))
479 })
480 } else {
481 true
482 }
483 })
484 }
485
486 fn age_secs(entry: &CachedEntry) -> u64 {
488 entry.inserted_at.elapsed().as_secs()
489 }
490
491 fn cached_response(entry: &CachedEntry) -> Response {
493 let mut response = entry.response.clone();
494 let age = Self::age_secs(entry);
495 if let Some(h) = response.headers.iter_mut().find(|h| h.name.eq_ignore_ascii_case("Age")) {
497 h.value = age.to_string().into();
498 } else {
499 response.headers.push(Header { name: "Age".into(), value: age.to_string().into() });
500 }
501 response
502 }
503}
504
505impl Middleware for CacheLayer {
506 fn accepts_streamed_body(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
508 true
509 }
510
511
512 fn handle(
513 &self,
514 request: &Request,
515 connection: &ConnectionInfo,
516 next: &dyn Application,
517 ) -> Result<Response, String> {
518 if let Some(answer) = self.answer_purge(request) {
519 return Ok(answer);
520 }
521 if request.method != METHOD.get || Self::request_is_private(request) {
523 return next.execute(request, connection);
524 }
525
526 let bypass = Self::request_bypasses_cache(request);
527
528 if let Some(disk) = self.disk_store() {
529 if !bypass && !disk.is_empty() {
530 let key = self.cache_key(request);
531 if let Some((mut response, age)) = disk.get(&key) {
532 self.hits.fetch_add(1, Ordering::Relaxed);
533 response.headers.retain(|h| !h.name.eq_ignore_ascii_case("Age"));
534 response.headers.push(Header { name: "Age".into(), value: age.as_secs().to_string().into() });
535 return Ok(response);
536 }
537 }
538 self.misses.fetch_add(1, Ordering::Relaxed);
539 let response = next.execute(request, connection)?;
540 let ttl = freshness(&response, self.ttl);
541 if !ttl.is_zero() && self.response_is_cacheable(request, &response) {
542 disk.put(&self.cache_key(request), &response, ttl);
543 self.stored.store(disk.len(), Ordering::Relaxed);
544 }
545 return Ok(response);
546 }
547
548 if !bypass && self.stored.load(Ordering::Relaxed) > 0 {
549 let key = self.cache_key(request);
551 let guard = self.store().lock().unwrap();
552 if let Some(entry) = guard.get(&key) {
553 self.hits.fetch_add(1, Ordering::Relaxed);
554 return Ok(Self::cached_response(entry));
555 }
556 }
557 self.misses.fetch_add(1, Ordering::Relaxed);
558
559 let response = next.execute(request, connection)?;
561
562 let ttl = freshness(&response, self.ttl);
563 if !ttl.is_zero() && self.response_is_cacheable(request, &response) {
564 let key = self.cache_key(request);
565 let mut guard = self.store().lock().unwrap();
566 guard.purge_expired();
567 guard.insert(
568 key,
569 CachedEntry { response: response.clone(), inserted_at: Instant::now(), ttl },
570 self.capacity,
571 );
572 self.stored.store(guard.entries.len(), Ordering::Relaxed);
573 }
574
575 Ok(response)
576 }
577
578 fn runs_inline(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
581 self.disk_store().is_none()
582 }
583}
584
585#[cfg(target_arch = "wasm32")]
587struct NoDisk;
588
589#[cfg(target_arch = "wasm32")]
590impl NoDisk {
591 fn len(&self) -> usize { 0 }
592 fn is_empty(&self) -> bool { true }
593 fn remove_where(&self, _matches: impl Fn(&str) -> bool) -> usize { 0 }
594 fn get(&self, _key: &str) -> Option<(Response, Duration)> { None }
595 fn put(&self, _key: &str, _response: &Response, _ttl: Duration) {}
596}