1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
|
/*
This file is part of gnix (https://codeberg.org/metamuffin/gnix)
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2025 metamuffin <metamuffin.org>
*/
//! Response caching module
//!
//! Considerations:
//! - check cache header
//! - ignore responses that get too large
//! - ignore requests with body (or too large body)
//! - LRU cache pruning
//! - different backends:
//! - in memory (HashMap)
//! - on disk (redb? filesystem?)
//! - external db?
use super::{Node, NodeContext, NodeKind, NodeRequest, NodeResponse};
use crate::{
config::{DynNode, DynNodeConfig},
error::ServiceError,
modules::InstContext,
};
use anyhow::Result;
use bytes::Bytes;
use headers::{CacheControl, HeaderMapExt};
use http::Response;
use http_body_util::{BodyExt, Full};
use log::debug;
use serde::Deserialize;
use sha2::{Digest, Sha256};
use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc, time::Duration};
use tokio::sync::RwLock;
pub struct CacheKind;
#[derive(Deserialize)]
struct CacheConfig {
next: DynNodeConfig,
}
struct Cache {
_config: CacheConfig,
next: DynNode,
entries: RwLock<HashMap<[u8; 32], Response<Bytes>>>,
}
impl NodeKind for CacheKind {
fn name(&self) -> &'static str {
"cache"
}
fn instanciate(&self, ic: InstContext) -> Result<DynNode> {
let config: CacheConfig = ic.config().parse()?;
Ok(Arc::new(Cache {
next: ic.instanciate_child(config.next.clone())?,
_config: config,
entries: HashMap::new().into(),
}))
}
}
impl Node for Cache {
fn handle<'a>(
&'a self,
context: &'a mut NodeContext,
request: NodeRequest,
) -> Pin<Box<dyn Future<Output = Result<NodeResponse, ServiceError>> + Send + Sync + 'a>> {
Box::pin(async move {
let allow_cache = request.method().is_safe();
if !allow_cache {
return self.next.handle(context, request).await;
}
// not very fast
let mut hasher = Sha256::new();
hasher.update(request.method().as_str().len().to_be_bytes());
hasher.update(request.method().as_str());
hasher.update(request.uri().path().len().to_be_bytes());
hasher.update(request.uri().path());
hasher.update([request.uri().query().is_some() as u8]);
if let Some(q) = request.uri().query() {
hasher.update(q.len().to_be_bytes());
hasher.update(q);
}
// TODO which headers are important to caching?
hasher.update(request.headers().len().to_be_bytes());
for (k, v) in request.headers() {
hasher.update(k.as_str().len().to_be_bytes());
hasher.update(v.as_bytes().len().to_be_bytes());
hasher.update(k);
hasher.update(v);
}
let key: [u8; 32] = hasher.finalize().into();
if let Some(resp) = self.entries.read().await.get(&key) {
debug!("hit");
return Ok(resp
.to_owned()
.map(|b| Full::new(b).map_err(|e| match e {}).boxed()));
}
debug!("miss");
let response = self.next.handle(context, request).await?;
let cache_control = response
.headers()
.typed_get::<CacheControl>()
.unwrap_or(CacheControl::new().with_max_age(Duration::from_secs(120))); // TODO what is the correct default?
let allow_store = !cache_control.no_store() && !cache_control.no_cache();
let response = if allow_store {
debug!("store");
let h = response.headers().to_owned();
let s = response.status().to_owned();
let body = response.collect().await?.to_bytes();
let mut r1 = Response::new(Full::new(body.clone()).map_err(|e| match e {}).boxed());
*r1.headers_mut() = h.clone();
*r1.status_mut() = s;
let mut r2 = Response::new(body);
*r2.headers_mut() = h;
*r2.status_mut() = s;
self.entries.write().await.insert(key, r2);
r1
} else {
debug!("no store");
response
};
Ok(response)
})
}
}
|