69f12aa
use axum::http::{HeaderMap, HeaderValue, Method, StatusCode, Uri, header};
use axum::response::{IntoResponse, Response};
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::sync::OnceLock;
const CACHE_IMMUTABLE: &str = "public, max-age=31536000, immutable";
const CACHE_NO_CACHE: &str = "no-cache";
const CACHE_REVALIDATE: &str = "public, max-age=3600, must-revalidate";
/// Service worker path, mirrored from `web/public/sw.js`.
const SERVICE_WORKER: &str = "sw.js";
/// PWA manifest path, mirrored from `web/public/manifest.webmanifest`.
const WEB_MANIFEST: &str = "manifest.webmanifest";
fn etags() -> &'static HashMap<&'static str, HeaderValue> {
static ETAGS: OnceLock<HashMap<&'static str, HeaderValue>> = OnceLock::new();
ETAGS.get_or_init(|| {
let mut m = HashMap::new();
let index = web::index();
m.insert(index.path, etag_of(index.content));
for p in web::paths() {
if let Some(a) = web::file(p) {
m.insert(a.path, etag_of(a.content));
}
}
m
})
}
fn etag_of(content: &[u8]) -> HeaderValue {
let mut h = std::collections::hash_map::DefaultHasher::new();
content.hash(&mut h);
h.write_u64(content.len() as u64);
HeaderValue::from_maybe_shared(format!("W/\"{:016x}\"", h.finish()).into_bytes())
.expect("valid etag header")
}
fn cache_control(path: &str) -> &'static str {
if path.starts_with("assets/") {
CACHE_IMMUTABLE
} else if path == "index.html" || path == SERVICE_WORKER || path == WEB_MANIFEST {
// The service worker is the push receiver: it must never be served from
// an hour-long cache, or a fixed push handler keeps running old logic.
// The manifest is what makes iOS treat the site as installable, so it
// rides along on the same rule.
CACHE_NO_CACHE
} else {
CACHE_REVALIDATE
}
}
pub async fn fallback(method: Method, uri: Uri, headers: HeaderMap) -> Response {
if !matches!(method, Method::GET | Method::HEAD) {
return StatusCode::METHOD_NOT_ALLOWED.into_response();
}
let path = uri.path();
if path.starts_with("/api") {
return StatusCode::NOT_FOUND.into_response();
}
let asset = match web::file(path) {
Some(asset) => asset,
None => {
if path.rsplit('/').next().is_some_and(|s| s.contains('.')) {
return StatusCode::NOT_FOUND.into_response();
}
web::index()
}
};
serve(asset, &headers)
}
fn serve(asset: web::Asset, headers: &HeaderMap) -> Response {
let etag = etags()
.get(asset.path)
.cloned()
.unwrap_or_else(|| etag_of(asset.content));
let cache_control = HeaderValue::from_static(cache_control(asset.path));
if let Some(if_none_match) = headers.get(header::IF_NONE_MATCH)
&& etag_matches(if_none_match, &etag)
{
return (
StatusCode::NOT_MODIFIED,
[(header::CACHE_CONTROL, cache_control), (header::ETAG, etag)],
)
.into_response();
}
(
[
(header::CONTENT_TYPE, HeaderValue::from_static(asset.mime)),
(header::CACHE_CONTROL, cache_control),
(header::ETAG, etag),
],
asset.content,
)
.into_response()
}
fn etag_matches(if_none_match: &HeaderValue, etag: &HeaderValue) -> bool {
let Ok(value) = if_none_match.to_str() else {
return false;
};
let Ok(target) = etag.to_str() else {
return false;
};
fn strong(s: &str) -> &str {
s.strip_prefix("W/").unwrap_or(s)
}
value
.split(',')
.map(str::trim)
.any(|c| c == "*" || strong(c) == strong(target))
}