1#[cfg(test)]
38mod tests;
39mod value;
40pub use value::ValueCache;
41#[cfg(not(target_arch = "wasm32"))]
42#[doc(hidden)]
43pub mod disk;
44
45use std::collections::{HashMap, VecDeque};
46use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
47use std::sync::{Arc, Mutex};
48use std::time::{Duration, Instant};
49
50use crate::application::Application;
51use crate::header::Header;
52use crate::middleware::Middleware;
53use crate::request::{METHOD, Request};
54use crate::response::Response;
55use crate::server::ConnectionInfo;
56
57struct CachedEntry {
60 response: Response,
61 inserted_at: Instant,
62 ttl: Duration,
65}
66
67struct CacheStore {
68 entries: HashMap<String, CachedEntry>,
69 order: VecDeque<String>,
71}
72
73impl CacheStore {
74 fn new() -> Self {
75 CacheStore { entries: HashMap::new(), order: VecDeque::new() }
76 }
77
78 fn get(&self, key: &str) -> Option<&CachedEntry> {
79 self.entries.get(key).filter(|e| e.inserted_at.elapsed() < e.ttl)
80 }
81
82 fn insert(&mut self, key: String, entry: CachedEntry, capacity: usize) {
83 if self.entries.contains_key(&key) {
85 self.entries.insert(key, entry);
86 return;
87 }
88 if self.entries.len() >= capacity {
91 if let Some(oldest) = self.order.pop_front() {
92 self.entries.remove(&oldest);
93 }
94 }
95 self.order.push_back(key.clone());
96 self.entries.insert(key, entry);
97 }
98
99 fn purge_expired(&mut self) {
100 let expired: Vec<String> = self.entries.iter()
101 .filter(|(_, e)| e.inserted_at.elapsed() >= e.ttl)
102 .map(|(k, _)| k.clone())
103 .collect();
104 for k in &expired {
105 self.entries.remove(k);
106 self.order.retain(|o| o != k);
107 }
108 }
109}
110
111#[doc(hidden)]
116pub fn freshness(response: &Response, cap: Duration) -> Duration {
117 let mut s_maxage = None;
118 let mut max_age = None;
119 for h in response.headers.iter().filter(|h| h.name.eq_ignore_ascii_case(Header::_CACHE_CONTROL)) {
120 for directive in h.value.split(',') {
121 let Some((name, value)) = directive.split_once('=') else { continue };
122 let Ok(secs) = value.trim().trim_matches('"').parse::<u64>() else { continue };
123 match name.trim().to_ascii_lowercase().as_str() {
124 "s-maxage" => s_maxage = Some(s_maxage.map_or(secs, |v: u64| v.min(secs))),
125 "max-age" => max_age = Some(max_age.map_or(secs, |v: u64| v.min(secs))),
126 _ => {}
127 }
128 }
129 }
130 match s_maxage.or(max_age) {
131 Some(secs) => cap.min(Duration::from_secs(secs)),
132 None => cap,
133 }
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
142pub enum Purge {
143 All,
145 Exact(String),
147 Prefix(String),
149}
150
151impl Purge {
152 fn matches(&self, key: &str) -> bool {
153 let target = key.split('\x00').next().unwrap_or(key);
154 match self {
155 Purge::All => true,
156 Purge::Exact(t) => target == t,
157 Purge::Prefix(p) => target.starts_with(p.as_str()),
158 }
159 }
160
161 pub fn from_target(target: &str) -> Purge {
163 match target.strip_suffix('*') {
164 Some(prefix) => Purge::Prefix(prefix.to_string()),
165 None => Purge::Exact(target.to_string()),
166 }
167 }
168}
169
170fn same_bytes(a: &[u8], b: &[u8]) -> bool {
172 a.len() == b.len() && a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
173}
174
175pub mod registry {
178 use super::CacheLayer;
179 use std::collections::BTreeMap;
180 use std::sync::{Mutex, OnceLock};
181
182 fn caches() -> &'static Mutex<BTreeMap<String, CacheLayer>> {
183 static CACHES: OnceLock<Mutex<BTreeMap<String, CacheLayer>>> = OnceLock::new();
184 CACHES.get_or_init(Mutex::default)
185 }
186
187 pub fn register(name: impl Into<String>, cache: CacheLayer) {
189 caches().lock().unwrap_or_else(|e| e.into_inner()).insert(name.into(), cache);
190 }
191
192 #[doc(hidden)]
195 pub fn replace_group(group: &str, new: Vec<(String, CacheLayer)>) {
196 let mut map = caches().lock().unwrap_or_else(|e| e.into_inner());
197 map.retain(|name, _| !name.starts_with(group));
198 map.extend(new);
199 }
200
201 pub fn list() -> Vec<(String, CacheLayer)> {
203 caches().lock().unwrap_or_else(|e| e.into_inner()).iter().map(|(n, c)| (n.clone(), c.clone())).collect()
204 }
205
206 pub fn get(name: &str) -> Option<CacheLayer> {
207 caches().lock().unwrap_or_else(|e| e.into_inner()).get(name).cloned()
208 }
209}
210
211#[derive(Clone)]
222pub struct CacheLayer {
223 store: Arc<Mutex<CacheStore>>,
224 stored: Arc<AtomicUsize>,
229 hits: Arc<AtomicU64>,
230 misses: Arc<AtomicU64>,
231 capacity: usize,
232 ttl: Duration,
233 vary_headers: Vec<String>,
234 #[cfg(not(target_arch = "wasm32"))]
236 disk: Option<Arc<disk::DiskStore>>,
237 purge_token: Option<Arc<String>>,
239}
240
241impl CacheLayer {
242 pub fn memory(capacity: usize) -> Self {
246 CacheLayer {
247 store: Arc::new(Mutex::new(CacheStore::new())),
248 stored: Arc::new(AtomicUsize::new(0)),
249 hits: Arc::new(AtomicU64::new(0)),
250 misses: Arc::new(AtomicU64::new(0)),
251 capacity,
252 ttl: Duration::from_secs(60),
253 vary_headers: vec![],
254 #[cfg(not(target_arch = "wasm32"))]
255 disk: None,
256 purge_token: None,
257 }
258 }
259
260 pub fn purge_token(mut self, token: impl Into<String>) -> Self {
267 let token = token.into();
268 self.purge_token = (!token.is_empty()).then(|| Arc::new(token));
269 self
270 }
271
272 pub fn purge(&self, target: &Purge) -> usize {
274 if let Some(disk) = self.disk_store() {
275 let n = disk.remove_where(|key| target.matches(key));
276 self.stored.store(disk.len(), Ordering::Relaxed);
277 return n;
278 }
279 let mut guard = self.store.lock().unwrap_or_else(|e| e.into_inner());
280 let keys: Vec<String> = guard.entries.keys().filter(|k| target.matches(k)).cloned().collect();
281 for key in &keys {
282 guard.entries.remove(key);
283 }
284 guard.order.retain(|k| !keys.contains(k));
285 self.stored.store(guard.entries.len(), Ordering::Relaxed);
286 keys.len()
287 }
288
289 pub fn kind(&self) -> &'static str {
291 if self.disk_store().is_some() { "disk" } else { "memory" }
292 }
293
294 fn answer_purge(&self, request: &Request) -> Option<Response> {
296 let token = self.purge_token.as_ref()?;
297 if !request.method.eq_ignore_ascii_case("PURGE") {
298 return None;
299 }
300 use crate::response::STATUS_CODE_REASON_PHRASE;
301 let sent = request.header(Header::_AUTHORIZATION).and_then(|h| h.value.strip_prefix("Bearer ").map(str::trim));
302 let authorized = sent.is_some_and(|s| same_bytes(s.as_bytes(), token.as_bytes()));
303 if !authorized {
304 let mut response = crate::error::problem::problem_with_text(STATUS_CODE_REASON_PHRASE.n401_unauthorized, "PURGE needs the purge token", r#"{"error":"PURGE needs the purge token"}"#);
305 response.headers.push(Header::new("WWW-Authenticate", r#"Bearer realm="cache purge""#));
306 return Some(response);
307 }
308 let n = self.purge(&Purge::from_target(&request.request_uri));
309 Some(Response::json(STATUS_CODE_REASON_PHRASE.n200_ok, format!(r#"{{"purged":{n}}}"#).into_bytes()))
310 }
311
312 #[cfg(not(target_arch = "wasm32"))]
326 pub fn disk(dir: impl Into<std::path::PathBuf>, max_bytes: u64) -> std::io::Result<Self> {
327 let store = disk::DiskStore::open(dir, max_bytes)?;
328 let mut layer = CacheLayer::memory(0);
329 layer.stored.store(store.len(), Ordering::Relaxed);
330 layer.disk = Some(Arc::new(store));
331 Ok(layer)
332 }
333
334 #[cfg(not(target_arch = "wasm32"))]
336 fn disk_store(&self) -> Option<&disk::DiskStore> {
337 self.disk.as_deref()
338 }
339
340 #[cfg(target_arch = "wasm32")]
341 fn disk_store(&self) -> Option<&NoDisk> {
342 None
343 }
344
345 pub fn hits(&self) -> u64 {
347 self.hits.load(Ordering::Relaxed)
348 }
349
350 pub fn misses(&self) -> u64 {
352 self.misses.load(Ordering::Relaxed)
353 }
354
355 pub fn size(&self) -> usize {
358 if let Some(disk) = self.disk_store() {
359 return disk.len();
360 }
361 self.store.lock().unwrap().entries.len()
362 }
363
364 pub fn stored_bytes(&self) -> Option<u64> {
366 #[cfg(not(target_arch = "wasm32"))]
367 return self.disk_store().map(disk::DiskStore::bytes);
368 #[cfg(target_arch = "wasm32")]
369 None
370 }
371
372 pub fn clear(&self) {
374 if let Some(disk) = self.disk_store() {
375 disk.remove_where(|_| true);
376 self.stored.store(0, Ordering::Relaxed);
377 return;
378 }
379 let mut guard = self.store.lock().unwrap();
380 guard.entries.clear();
381 guard.order.clear();
382 self.stored.store(0, Ordering::Relaxed);
383 }
384
385 pub fn ttl(mut self, secs: u64) -> Self {
387 self.ttl = Duration::from_secs(secs);
388 self
389 }
390
391 pub fn vary_by_header(mut self, name: &str) -> Self {
406 self.vary_headers.push(name.to_ascii_lowercase());
407 self
408 }
409
410 fn store(&self) -> &Mutex<CacheStore> {
411 &self.store
412 }
413
414 fn cache_key(&self, request: &Request) -> String {
416 let mut key = request.request_uri.clone();
417 for vh in &self.vary_headers {
418 let val = request.headers.iter()
419 .find(|h| h.name.eq_ignore_ascii_case(vh))
420 .map(|h| h.value.as_ref())
421 .unwrap_or("");
422 key.push('\x00');
423 key.push_str(val);
424 }
425 key
426 }
427
428 fn request_bypasses_cache(request: &Request) -> bool {
431 request.headers.iter().any(|h| {
432 h.name.eq_ignore_ascii_case(Header::_CACHE_CONTROL)
433 && h.value.to_ascii_lowercase().contains("no-cache")
434 })
435 }
436
437 fn request_is_private(request: &Request) -> bool {
441 request.headers.iter().any(|h| {
442 h.name.eq_ignore_ascii_case(Header::_AUTHORIZATION)
443 || h.name.eq_ignore_ascii_case(Header::_COOKIE)
444 || h.name.eq_ignore_ascii_case(Header::_RANGE)
445 })
446 }
447
448 fn response_is_cacheable(&self, request: &Request, response: &Response) -> bool {
461 if response.status_code < 200 || response.status_code >= 300 {
462 return false;
463 }
464 if request.header(Header::_RANGE).is_some() {
465 return false;
466 }
467 if response.stream_file.is_some() || response.stream_pipe.is_some() {
470 return false;
471 }
472 if response.headers.iter().any(|h| h.name.eq_ignore_ascii_case(Header::_SET_COOKIE)) {
473 return false;
474 }
475 let has = |name: &str| response.headers.iter().any(|h| h.name.eq_ignore_ascii_case(name));
476 let varies_harmlessly = |name: &str| {
477 (name.eq_ignore_ascii_case(Header::_ORIGIN) && !has(Header::_ACCESS_CONTROL_ALLOW_ORIGIN))
478 || (name.eq_ignore_ascii_case(Header::_ACCEPT_ENCODING) && !has(Header::_CONTENT_ENCODING))
479 };
480 response.headers.iter().all(|h| {
481 if h.name.eq_ignore_ascii_case(Header::_CACHE_CONTROL) {
482 let v = h.value.to_ascii_lowercase();
483 !(v.contains("no-store") || v.contains("private") || v.contains("no-cache"))
484 } else if h.name.eq_ignore_ascii_case(Header::_VARY) {
485 h.value.split(',').map(str::trim).filter(|name| !name.is_empty()).all(|name| {
486 name != "*" && (self.vary_headers.iter().any(|vh| vh.eq_ignore_ascii_case(name)) || varies_harmlessly(name))
487 })
488 } else {
489 true
490 }
491 })
492 }
493
494 fn age_secs(entry: &CachedEntry) -> u64 {
496 entry.inserted_at.elapsed().as_secs()
497 }
498
499 fn cached_response(entry: &CachedEntry) -> Response {
501 let mut response = entry.response.clone();
502 let age = Self::age_secs(entry);
503 if let Some(h) = response.headers.iter_mut().find(|h| h.name.eq_ignore_ascii_case("Age")) {
505 h.value = age.to_string().into();
506 } else {
507 response.headers.push(Header { name: "Age".into(), value: age.to_string().into() });
508 }
509 response
510 }
511}
512
513impl Middleware for CacheLayer {
514 fn accepts_streamed_body(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
516 true
517 }
518
519
520 fn handle(
521 &self,
522 request: &Request,
523 connection: &ConnectionInfo,
524 next: &dyn Application,
525 ) -> Result<Response, String> {
526 if let Some(answer) = self.answer_purge(request) {
527 return Ok(answer);
528 }
529 if request.method != METHOD.get {
531 return Ok(cache_status(next.execute(request, connection)?, "fwd=method"));
532 }
533 if Self::request_is_private(request) {
534 return Ok(cache_status(next.execute(request, connection)?, "fwd=bypass; detail=private"));
535 }
536
537 let bypass = Self::request_bypasses_cache(request);
538
539 if let Some(disk) = self.disk_store() {
540 if !bypass && !disk.is_empty() {
541 let key = self.cache_key(request);
542 if let Some((mut response, age)) = disk.get(&key) {
543 self.hits.fetch_add(1, Ordering::Relaxed);
544 response.headers.retain(|h| !h.name.eq_ignore_ascii_case("Age"));
545 response.headers.push(Header { name: "Age".into(), value: age.as_secs().to_string().into() });
546 let left = freshness(&response, self.ttl).saturating_sub(age).as_secs();
547 return Ok(cache_status(response, &format!("hit; ttl={left}")));
548 }
549 }
550 self.misses.fetch_add(1, Ordering::Relaxed);
551 let response = next.execute(request, connection)?;
552 let ttl = freshness(&response, self.ttl);
553 let forwarded = if bypass { "fwd=request" } else { "fwd=uri-miss" };
554 if !ttl.is_zero() && self.response_is_cacheable(request, &response) {
555 disk.put(&self.cache_key(request), &response, ttl);
556 self.stored.store(disk.len(), Ordering::Relaxed);
557 return Ok(cache_status(response, &format!("{forwarded}; stored")));
558 }
559 return Ok(cache_status(response, forwarded));
560 }
561
562 let mut forwarded = if bypass { "fwd=request" } else { "fwd=uri-miss" };
563 if !bypass && self.stored.load(Ordering::Relaxed) > 0 {
564 let key = self.cache_key(request);
566 let guard = self.store().lock().unwrap();
567 if let Some(entry) = guard.get(&key) {
568 self.hits.fetch_add(1, Ordering::Relaxed);
569 let left = entry.ttl.saturating_sub(entry.inserted_at.elapsed()).as_secs();
570 return Ok(cache_status(Self::cached_response(entry), &format!("hit; ttl={left}")));
571 }
572 if guard.entries.contains_key(&key) {
573 forwarded = "fwd=stale";
574 }
575 }
576 self.misses.fetch_add(1, Ordering::Relaxed);
577
578 let response = next.execute(request, connection)?;
580
581 let ttl = freshness(&response, self.ttl);
582 if !ttl.is_zero() && self.response_is_cacheable(request, &response) {
583 let key = self.cache_key(request);
584 let mut guard = self.store().lock().unwrap();
585 guard.purge_expired();
586 guard.insert(
587 key,
588 CachedEntry { response: response.clone(), inserted_at: Instant::now(), ttl },
589 self.capacity,
590 );
591 self.stored.store(guard.entries.len(), Ordering::Relaxed);
592 return Ok(cache_status(response, &format!("{forwarded}; stored")));
593 }
594
595 Ok(cache_status(response, forwarded))
596 }
597
598 fn runs_inline(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
601 self.disk_store().is_none()
602 }
603}
604
605fn cache_status(mut response: Response, parameters: &str) -> Response {
610 let config = crate::server_config::ServerConfig::current();
611 if !config.cache_status {
612 return response;
613 }
614 let member = format!("{}; {parameters}", crate::header::sf::item(&config.proxy_name));
615 match response.headers.iter_mut().find(|h| h.name.eq_ignore_ascii_case("cache-status")) {
616 Some(existing) => existing.value = format!("{}, {member}", existing.value).into(),
617 None => response.headers.push(Header::new("Cache-Status", member)),
618 }
619 response
620}
621
622#[cfg(target_arch = "wasm32")]
624struct NoDisk;
625
626#[cfg(target_arch = "wasm32")]
627impl NoDisk {
628 fn len(&self) -> usize { 0 }
629 fn is_empty(&self) -> bool { true }
630 fn remove_where(&self, _matches: impl Fn(&str) -> bool) -> usize { 0 }
631 fn get(&self, _key: &str) -> Option<(Response, Duration)> { None }
632 fn put(&self, _key: &str, _response: &Response, _ttl: Duration) {}
633}