1use crate::{Cache, CacheError};
4use http::header::PROXY_AUTHORIZATION;
5use http::header::{ACCEPT, ACCEPT_ENCODING, AUTHORIZATION};
6use http::header::{CACHE_CONTROL, COOKIE};
7use http::header::{HOST, SET_COOKIE, VARY};
8use http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode, header};
9use siderite_core::middleware::TrustedProxies;
10use siderite_core::middleware::{BoxService, Next, from_fn};
11use siderite_core::{ApiError, Body, IntoResponse, Request, Response};
12use std::sync::Arc;
13use std::sync::atomic::{AtomicU8, Ordering};
14use std::time::Duration;
15use tower::Layer;
16
17mod codec;
18mod generation;
19use codec::{decode, encode};
20use generation::representation_key as make_key;
21type CacheResult<T> = Result<T, CacheError>;
22type Directive = (String, Option<String>);
23type Hit = Option<(HeaderMap, Vec<u8>)>;
24
25const X_CACHE: HeaderName = HeaderName::from_static("x-cache");
26const HIT: HeaderValue = HeaderValue::from_static("hit");
27const MISS: HeaderValue = HeaderValue::from_static("miss");
28
29pub const DEFAULT_MAX_BODY_BYTES: u64 = 1024 * 1024;
31
32pub const DEFAULT_GENERATION_TTL: Duration = Duration::from_secs(3600);
34
35pub const DEFAULT_MAX_NEW_TARGETS: u64 = 10_000;
38
39const DISABLED: u8 = 1;
41const NO_MARKERS: u8 = 2;
44const ADMIT_WARNED: u8 = 4;
46
47const KEYED_HEADERS: [HeaderName; 2] = [ACCEPT, ACCEPT_ENCODING];
50
51fn is_hop_by_hop(name: &HeaderName) -> bool {
53 matches!(
54 name.as_str(),
55 "connection"
56 | "keep-alive"
57 | "proxy-authenticate"
58 | "proxy-authorization"
59 | "te"
60 | "trailer"
61 | "trailers"
62 | "transfer-encoding"
63 | "upgrade"
64 | "x-cache"
65 )
66}
67
68#[derive(Clone, Debug)]
139pub struct RouteCache<C> {
140 cache: Arc<C>,
141 default_ttl: Option<Duration>,
142 max_body: u64,
143 bypass: Arc<Vec<HeaderName>>,
144 keys: Arc<generation::Keys>,
145 local: Arc<generation::Local>,
146 proxies: TrustedProxies,
147 flags: Arc<AtomicU8>,
148 max_key: usize,
149 max_headers: usize,
150 max_encoded: usize,
151 generation_ttl: Duration,
152 max_new_targets: u64,
153}
154
155impl<C: Cache> RouteCache<C> {
156 #[must_use]
159 pub fn new(cache: C) -> Self {
160 Self {
161 cache: Arc::new(cache),
162 default_ttl: None,
163 max_body: DEFAULT_MAX_BODY_BYTES,
164 keys: Arc::new(generation::Keys::new(&uuid::Uuid::new_v4().to_string())),
165 local: Arc::new(generation::Local::new()),
166 proxies: TrustedProxies::default(),
167 flags: Arc::new(AtomicU8::new(0)),
168 max_key: 4096,
169 max_headers: 8192,
170 max_encoded: 2 * 1024 * 1024,
171 generation_ttl: DEFAULT_GENERATION_TTL,
172 max_new_targets: DEFAULT_MAX_NEW_TARGETS,
173 bypass: Arc::new(vec![
174 AUTHORIZATION,
175 PROXY_AUTHORIZATION,
176 COOKIE,
177 HeaderName::from_static("x-api-key"),
178 ]),
179 }
180 }
181
182 #[must_use]
190 pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
191 self.keys = Arc::new(generation::Keys::new(&namespace.into()));
192 self
193 }
194
195 #[must_use]
203 pub fn trusted_proxies(mut self, proxies: TrustedProxies) -> Self {
204 self.proxies = proxies;
205 self
206 }
207
208 #[must_use]
218 pub fn byte_limits(self, key: usize, hdrs: usize, encoded: usize) -> Self {
219 let mut settings = self;
220 settings.max_key = key;
221 settings.max_headers = hdrs;
222 settings.max_encoded = encoded;
223 settings
224 }
225
226 #[must_use]
249 pub fn generation_ttl(mut self, ttl: Duration) -> Self {
250 self.generation_ttl = ttl;
251 if ttl.is_zero() {
252 self.flags.fetch_or(NO_MARKERS, Ordering::Release);
253 } else {
254 self.flags.fetch_and(!NO_MARKERS, Ordering::Release);
255 }
256 self
257 }
258
259 #[must_use]
300 pub fn max_new_targets(mut self, max: u64) -> Self {
301 self.max_new_targets = max;
302 self
303 }
304
305 pub async fn invalidate_target(&self, req: &Request) -> CacheResult<()> {
318 let policy = self.policy();
319 let failure = |_| CacheError::Backend("invalid context".into());
320 let context = self.proxies.resolve(req).map_err(failure)?;
321 let target = generation::target(req, &policy, context.scheme);
322 if target.len() > self.max_key {
323 return Err(CacheError::Backend("cache key too large".into()));
324 }
325 let backend = self.cache.as_ref();
326 let result = generation::invalidate(backend, &target, &policy).await;
327 if result.is_err() {
328 disable(&self.flags);
329 }
330 result
331 }
332
333 fn policy(&self) -> Policy {
334 Policy {
335 default_ttl: self.default_ttl,
336 max_body: self.max_body,
337 bypass: Arc::clone(&self.bypass),
338 keys: Arc::clone(&self.keys),
339 local: Arc::clone(&self.local),
340 proxies: self.proxies.clone(),
341 flags: Arc::clone(&self.flags),
342 max_key: self.max_key,
343 max_headers: self.max_headers,
344 max_encoded: self.max_encoded,
345 generation_ttl: self.generation_ttl,
346 max_new_targets: self.max_new_targets,
347 }
348 }
349
350 #[must_use]
354 pub fn default_ttl(mut self, ttl: Duration) -> Self {
355 self.default_ttl = Some(ttl);
356 self
357 }
358
359 #[must_use]
361 pub fn bypass_header(mut self, name: HeaderName) -> Self {
362 Arc::make_mut(&mut self.bypass).push(name);
363 self
364 }
365
366 #[must_use]
369 pub fn max_body_bytes(mut self, bytes: u64) -> Self {
370 self.max_body = bytes;
371 self
372 }
373}
374
375struct Policy {
377 default_ttl: Option<Duration>,
378 max_body: u64,
379 bypass: Arc<Vec<HeaderName>>,
380 keys: Arc<generation::Keys>,
381 local: Arc<generation::Local>,
382 proxies: TrustedProxies,
383 flags: Arc<AtomicU8>,
384 max_key: usize,
385 max_headers: usize,
386 max_encoded: usize,
387 generation_ttl: Duration,
388 max_new_targets: u64,
389}
390
391impl<C: Cache> Layer<BoxService> for RouteCache<C> {
392 type Service = BoxService;
393
394 fn layer(&self, inner: BoxService) -> Self::Service {
395 let cache = Arc::clone(&self.cache);
396 let policy = Arc::new(self.policy());
397 from_fn(move |req: Request, next: Next| {
398 let cache = Arc::clone(&cache);
399 let policy = Arc::clone(&policy);
400 async move { Dispatch { cache, policy }.run(req, next).await }
401 })
402 .layer(inner)
403 }
404}
405
406fn is_cacheable_method(method: &Method) -> bool {
407 method == Method::GET || method == Method::HEAD
408}
409
410fn request_bypasses_cache(headers: &HeaderMap, bypass: &[HeaderName]) -> bool {
411 bypass.iter().any(|name| headers.contains_key(name))
412 || headers.keys().any(looks_like_credential)
413}
414
415fn looks_like_credential(name: &HeaderName) -> bool {
420 const NEEDLES: [&str; 8] = [
421 "auth",
422 "token",
423 "session",
424 "jwt",
425 "secret",
426 "api-key",
427 "apikey",
428 "access-key",
429 ];
430 let name = name.as_str();
431 NEEDLES.iter().any(|needle| name.contains(needle))
432}
433
434fn varies_outside_key(headers: &HeaderMap) -> bool {
436 headers
437 .get_all(VARY)
438 .iter()
439 .flat_map(|value| value.to_str().unwrap_or("*").split(','))
440 .map(str::trim)
441 .filter(|name| !name.is_empty())
442 .any(|name| {
443 !KEYED_HEADERS
444 .iter()
445 .any(|keyed| keyed.as_str().eq_ignore_ascii_case(name))
446 })
447}
448
449fn cache_control(headers: &HeaderMap) -> Vec<(String, Option<String>)> {
451 headers
452 .get_all(CACHE_CONTROL)
453 .iter()
454 .filter_map(|value| value.to_str().ok())
455 .flat_map(|text| text.split(','))
456 .filter_map(|directive| {
457 let (name, value) = match directive.split_once('=') {
458 Some((name, value)) => {
459 let value = value.trim().trim_matches('"').to_owned();
460 (name, Some(value))
461 }
462 None => (directive, None),
463 };
464 let name = name.trim().trim_matches('"').to_ascii_lowercase();
465 (!name.is_empty()).then_some((name, value))
466 })
467 .collect()
468}
469
470pub(crate) fn cache_control_forbids_store(headers: &HeaderMap) -> bool {
474 cache_control(headers)
475 .iter()
476 .any(|(name, _)| forbids_store(name))
477}
478
479fn forbids_store(name: &str) -> bool {
480 matches!(name, "no-store" | "no-cache" | "private")
481}
482
483fn storable_ttl(response: &Response, policy: &Policy) -> Option<Duration> {
485 let headers = response.headers();
486 let eligible = response.status() == StatusCode::OK
487 && !headers.contains_key(SET_COOKIE)
488 && !cache_control_forbids_store(headers)
489 && !varies_outside_key(headers)
490 && !headers.contains_key(header::TRAILER)
491 && response
492 .body()
493 .exact_len()
494 .is_some_and(|len| len <= policy.max_body);
495 if !eligible {
496 return None;
497 }
498 let directives = cache_control(headers);
499 let public = directives.iter().any(|(name, _)| name == "public");
500 if !public && policy.default_ttl.is_none() {
501 return None;
502 }
503 let seconds = |wanted: &str| {
504 directives
505 .iter()
506 .find(|(name, _)| name == wanted)
507 .and_then(|(_, value)| value.as_deref()?.parse::<u64>().ok())
508 };
509 let ttl = seconds("s-maxage")
510 .or_else(|| seconds("max-age"))
511 .map(Duration::from_secs)
512 .or(policy.default_ttl)?;
513 let remaining = ttl.checked_sub(codec::initial_age(headers)?)?;
514 let remaining = remaining.min(policy.generation_ttl);
515 (remaining > Duration::ZERO).then_some(remaining)
516}
517
518fn cache_key(req: &Request, scheme: &str) -> String {
522 let mut key = String::new();
523 push_part(&mut key, req.method().as_str().as_bytes());
524 push_part(&mut key, scheme.as_bytes());
525 let host = req
526 .headers()
527 .get(HOST)
528 .map(HeaderValue::as_bytes)
529 .or_else(|| req.uri().authority().map(|a| a.as_str().as_bytes()))
530 .unwrap_or_default();
531 push_part(&mut key, &host.to_ascii_lowercase());
532 let target = req
533 .uri()
534 .path_and_query()
535 .map_or("/", http::uri::PathAndQuery::as_str);
536 push_part(&mut key, target.as_bytes());
537 for name in &KEYED_HEADERS {
538 let values = req.headers().get_all(name);
539 key.push_str(&format!("#{}", values.iter().count()));
540 for value in values {
541 push_part(&mut key, value.as_bytes());
542 }
543 }
544 key
545}
546
547fn push_part(key: &mut String, bytes: &[u8]) {
549 use std::fmt::Write as _;
550 match std::str::from_utf8(bytes) {
551 Ok(text) => {
552 let _ = write!(key, "s{}:{text}", text.len());
553 }
554 Err(_) => {
555 const HEX: &[u8; 16] = b"0123456789abcdef";
556 let _ = write!(key, "x{}:", bytes.len());
557 for byte in bytes {
558 key.push(char::from(HEX[usize::from(byte >> 4)]));
559 key.push(char::from(HEX[usize::from(byte & 15)]));
560 }
561 }
562 }
563}
564
565fn with_x_cache(mut response: Response, value: HeaderValue) -> Response {
566 response.headers_mut().insert(X_CACHE, value);
567 response
568}
569
570fn restore(method: &Method, headers: HeaderMap, body: Vec<u8>) -> Response {
573 let len = HeaderValue::from(body.len());
574 let mut response = Response::new(Body::from(body));
575 *response.status_mut() = StatusCode::OK;
576 *response.headers_mut() = headers;
577 if method != Method::HEAD {
578 response.headers_mut().insert(header::CONTENT_LENGTH, len);
579 }
580 with_x_cache(response, HIT)
581}
582
583fn disable(flags: &AtomicU8) {
584 flags.fetch_or(DISABLED, Ordering::Release);
585 tracing::warn!("cache invalidation failed; cache layer disabled");
586}
587
588fn header_bytes(headers: &HeaderMap) -> usize {
589 headers.iter().fold(0usize, |size, (name, value)| {
590 size.saturating_add(name.as_str().len())
591 .saturating_add(value.as_bytes().len())
592 })
593}
594
595fn unsupported_request(headers: &HeaderMap) -> bool {
596 [
597 header::RANGE,
598 header::IF_RANGE,
599 header::IF_MATCH,
600 header::IF_NONE_MATCH,
601 header::IF_MODIFIED_SINCE,
602 header::IF_UNMODIFIED_SINCE,
603 ]
604 .iter()
605 .any(|header| headers.contains_key(header))
606}
607
608fn requests_refresh(items: &[Directive], h: &HeaderMap) -> bool {
609 let forced = items.iter().any(|(name, _)| needs_refresh(name));
610 let pragma = h.get(header::PRAGMA).is_some_and(pragma_no_cache);
611 forced || pragma
612}
613
614fn needs_refresh(name: &str) -> bool {
615 matches!(name, "no-cache" | "max-age" | "min-fresh")
616}
617
618fn pragma_no_cache(value: &HeaderValue) -> bool {
619 value.as_bytes().eq_ignore_ascii_case(b"no-cache")
620}
621
622async fn lookup<C: Cache>(cache: &C, key: &str, policy: &Policy) -> Hit {
623 let bytes = cache.get(key).await.ok()??;
624 if bytes.len() > policy.max_encoded {
625 return None;
626 }
627 decode(&bytes, policy.max_headers, policy.max_body)
628}
629
630enum Slot {
632 Known(String),
633 Absent(generation::Started, String),
634}
635
636struct Dispatch<C> {
637 cache: Arc<C>,
638 policy: Arc<Policy>,
639}
640
641impl<C: Cache> Dispatch<C> {
642 async fn run(self, req: Request, next: Next) -> Response {
643 let Self { cache, policy } = self;
644 let flags = policy.flags.load(Ordering::Acquire);
645 if flags & DISABLED != 0 {
646 return with_x_cache(next.run(req).await, MISS);
647 }
648 let context = match policy.proxies.resolve(&req) {
649 Ok(context) => context,
650 Err(_) => {
651 let error = ApiError::bad_request("Invalid proxy context.");
652 return error.into_response();
653 }
654 };
655 let target = generation::target(&req, &policy, context.scheme);
656 if target.len() > policy.max_key {
657 return with_x_cache(next.run(req).await, MISS);
658 }
659 if !is_cacheable_method(req.method()) {
660 let write = generation::unsafe_method(req.method());
661 let invalidate = write && flags & NO_MARKERS == 0;
662 let response = next.run(req).await;
663 let status = response.status();
664 let succeeded = status.is_success() || status.is_redirection();
665 if invalidate
666 && succeeded
667 && generation::invalidate(cache.as_ref(), &target, &policy)
668 .await
669 .is_err()
670 {
671 disable(&policy.flags);
674 }
675 return response;
676 }
677 let directives = cache_control(req.headers());
678 let no_store = directives.iter().any(|(name, _)| name == "no-store");
679 let refresh = requests_refresh(&directives, req.headers());
680 if no_store
681 || unsupported_request(req.headers())
682 || request_bypasses_cache(req.headers(), &policy.bypass)
683 {
684 return with_x_cache(next.run(req).await, MISS);
685 }
686 if flags & NO_MARKERS != 0 || policy.max_new_targets == 0 {
687 return with_x_cache(next.run(req).await, MISS);
690 }
691 let backend = cache.as_ref();
692 let known = match generation::peek(backend, &target).await {
693 Ok(known) => known,
694 Err(_) => return with_x_cache(next.run(req).await, MISS),
695 };
696 let scheme = context.scheme;
697 let (slot, hit) = match known {
698 Some(generation) => {
699 let key = make_key(&req, &policy, scheme, generation);
700 if key.len() > policy.max_key {
701 return with_x_cache(next.run(req).await, MISS);
702 }
703 let hit = if refresh {
704 None
705 } else {
706 lookup(backend, &key, &policy).await
707 };
708 (Slot::Known(key), hit)
709 }
710 None if policy.local.spent() => {
713 return with_x_cache(next.run(req).await, MISS);
716 }
717 None => match generation::begin(backend, &policy, &target).await {
718 Ok(started) => {
719 let suffix = cache_key(&req, scheme);
720 (Slot::Absent(started, suffix), None)
721 }
722 Err(_) => return with_x_cache(next.run(req).await, MISS),
723 },
724 };
725 let method = req.method().clone();
726 if let Some((headers, body)) = hit {
727 return restore(&method, headers, body);
728 }
729 let response = next.run(req).await;
730 let Some(ttl) = storable_ttl(&response, &policy) else {
731 return with_x_cache(response, MISS);
732 };
733 if header_bytes(response.headers()) > policy.max_headers {
734 return with_x_cache(response, MISS);
735 }
736 let key = match slot {
737 Slot::Known(key) => Some(key),
738 Slot::Absent(started, suffix) => {
739 let admitted = generation::admit(backend, &target, &policy, &started);
740 match admitted.await {
741 Ok(Some(generation)) => {
742 let key = generation::key_for(&policy, generation, &suffix);
743 (key.len() <= policy.max_key).then_some(key)
744 }
745 _ => None,
746 }
747 }
748 };
749 let Some(key) = key else {
750 return with_x_cache(response, MISS);
751 };
752 let (mut parts, body) = response.into_parts();
753 let limit = usize::try_from(policy.max_body).unwrap_or(usize::MAX);
754 let bytes = match body.into_bytes_limited(limit).await {
755 Ok(bytes) => bytes,
756 Err(_) => {
757 let error = ApiError::internal("Cache body read failed.");
758 return error.into_response();
759 }
760 };
761 if let Ok(payload) = encode(&parts.headers, &bytes)
762 && payload.len() <= policy.max_encoded
763 {
764 let _ = cache.set(&key, payload, Some(ttl)).await;
765 }
766 parts.headers.insert(X_CACHE, MISS);
767 Response::from_parts(parts, Body::from(bytes))
768 }
769}
770
771#[cfg(test)]
772mod tests {
773 use super::*;
774 use http::HeaderValue;
775
776 fn headers(pairs: &[(&str, &str)]) -> HeaderMap {
777 let mut map = HeaderMap::new();
778 for (name, value) in pairs {
779 map.append(
780 HeaderName::try_from(*name).unwrap(),
781 HeaderValue::from_str(value).unwrap(),
782 );
783 }
784 map
785 }
786
787 #[test]
788 fn cache_control_detects_no_store_no_cache_and_private() {
789 assert!(cache_control_forbids_store(&headers(&[(
790 "cache-control",
791 "no-store"
792 )])));
793 assert!(cache_control_forbids_store(&headers(&[(
794 "cache-control",
795 "max-age=0, Private"
796 )])));
797 assert!(cache_control_forbids_store(&headers(&[(
798 "cache-control",
799 "public, no-store, max-age=60"
800 )])));
801 assert!(cache_control_forbids_store(&headers(&[
802 ("cache-control", "max-age=60"),
803 ("cache-control", "private=\"Set-Cookie\""),
804 ])));
805 assert!(!cache_control_forbids_store(&headers(&[(
806 "cache-control",
807 "public, max-age=60"
808 )])));
809 assert!(cache_control_forbids_store(&headers(&[(
810 "cache-control",
811 "No-Cache"
812 )])));
813 assert!(!cache_control_forbids_store(&HeaderMap::new()));
814 }
815
816 #[test]
817 fn encode_decode_roundtrip_strips_hop_by_hop() {
818 let mut map = headers(&[
819 ("content-type", "text/plain"),
820 ("x-foo", "bar"),
821 ("transfer-encoding", "chunked"),
822 ("x-cache", "miss"),
823 ]);
824 map.append("x-foo", HeaderValue::from_static("baz"));
825 let encoded = encode(&map, b"hello").unwrap();
826 let (decoded, body) = decode(&encoded, 8192, 1024).unwrap();
827 assert_eq!(body, b"hello");
828 assert_eq!(
829 decoded
830 .get_all("x-foo")
831 .iter()
832 .map(|v| v.as_bytes())
833 .collect::<Vec<_>>(),
834 [b"bar".as_slice(), b"baz".as_slice()]
835 );
836 assert!(decoded.get("transfer-encoding").is_none());
837 assert!(decoded.get("x-cache").is_none());
838 assert_eq!(
839 decoded.get("content-type").unwrap().as_bytes(),
840 b"text/plain"
841 );
842 }
843
844 #[test]
845 fn decode_rejects_garbage() {
846 assert!(decode(b"not-json", 8192, 1024).is_none());
847 assert!(decode(b"{}", 8192, 1024).is_none());
848 }
849}