use etag::EntityTag;
use salvo_core::http::header::{ETAG, IF_NONE_MATCH};
use salvo_core::http::headers::{self, HeaderMapExt};
use salvo_core::http::{ResBody, StatusCode};
use salvo_core::{Depot, FlowCtrl, Handler, Request, Response, async_trait};
#[derive(Default, Clone, Copy, Debug)]
pub struct ETag {
strong: bool,
}
impl ETag {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn strong(mut self) -> Self {
self.strong = true;
self
}
}
#[async_trait]
impl Handler for ETag {
async fn handle(
&self,
req: &mut Request,
depot: &mut Depot,
res: &mut Response,
ctrl: &mut FlowCtrl,
) {
ctrl.call_next(req, depot, res).await;
if ctrl.is_ceased() {
return;
}
let if_none_match = req
.headers()
.get(IF_NONE_MATCH)
.and_then(|etag| etag.to_str().ok())
.and_then(|etag| etag.parse::<EntityTag>().ok());
let res_etag = res
.headers()
.get(ETAG)
.and_then(|etag| etag.to_str().ok())
.and_then(|etag| etag.parse::<EntityTag>().ok());
let etag = res_etag.or_else(|| {
let etag = match &res.body {
ResBody::Once(bytes) => Some(EntityTag::from_data(bytes)),
ResBody::Chunks(bytes) => {
let tags = bytes
.iter()
.map(|item| EntityTag::from_data(item).tag().to_owned())
.collect::<Vec<_>>()
.concat();
Some(EntityTag::from_data(tags.as_bytes()))
}
ResBody::Stream(_) => {
tracing::debug!("etag not supported for streaming body");
None
}
ResBody::None => {
tracing::debug!("etag not supported for empty body");
None
}
_ => None,
};
if let Some(etag) = &etag {
match etag.to_string().parse::<headers::ETag>() {
Ok(etag) => res.headers_mut().typed_insert(etag),
Err(e) => {
tracing::error!(error = ?e, "failed to parse etag");
}
}
}
etag
});
if let (Some(etag), Some(if_none_match)) = (etag, if_none_match) {
let eq = if self.strong {
etag.strong_eq(&if_none_match)
} else {
etag.weak_eq(&if_none_match)
};
if eq {
res.body(ResBody::None);
res.status_code(StatusCode::NOT_MODIFIED);
}
}
}
}
#[derive(Clone, Debug, Copy, Default)]
pub struct Modified {
_private: (),
}
impl Modified {
#[must_use]
pub fn new() -> Self {
Self { _private: () }
}
}
#[async_trait]
impl Handler for Modified {
async fn handle(
&self,
req: &mut Request,
depot: &mut Depot,
res: &mut Response,
ctrl: &mut FlowCtrl,
) {
ctrl.call_next(req, depot, res).await;
if ctrl.is_ceased() {
return;
}
if let (Some(if_modified_since), Some(last_modified)) = (
req.headers().typed_get::<headers::IfModifiedSince>(),
res.headers().typed_get::<headers::LastModified>(),
) && !if_modified_since.is_modified(last_modified.into())
{
res.body(ResBody::None);
res.status_code(StatusCode::NOT_MODIFIED);
}
}
}
#[derive(Clone, Debug, Copy, Default)]
pub struct CachingHeaders(Modified, ETag);
impl CachingHeaders {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
#[async_trait]
impl Handler for CachingHeaders {
async fn handle(
&self,
req: &mut Request,
depot: &mut Depot,
res: &mut Response,
ctrl: &mut FlowCtrl,
) {
self.0.handle(req, depot, res, ctrl).await;
if res.status_code != Some(StatusCode::NOT_MODIFIED) {
self.1.handle(req, depot, res, ctrl).await;
}
}
}
#[cfg(test)]
mod tests {
use salvo_core::http::header::*;
use salvo_core::prelude::*;
use salvo_core::test::TestClient;
use super::*;
#[handler]
async fn hello() -> &'static str {
"Hello World"
}
#[tokio::test]
async fn test_caching_headers() {
let router = Router::with_hoop(CachingHeaders::new()).get(hello);
let service = Service::new(router);
let response = TestClient::get("http://127.0.0.1:8698/")
.send(&service)
.await;
assert_eq!(response.status_code, Some(StatusCode::OK));
let etag = response.headers().get(ETAG).unwrap();
let response = TestClient::get("http://127.0.0.1:8698/")
.add_header(IF_NONE_MATCH, etag, true)
.send(&service)
.await;
assert_eq!(response.status_code, Some(StatusCode::NOT_MODIFIED));
assert!(response.body.is_none());
}
#[tokio::test]
async fn test_caching_headers_ignores_forged_request_etag() {
let router = Router::with_hoop(CachingHeaders::new()).get(hello);
let service = Service::new(router);
let response = TestClient::get("http://127.0.0.1:8699/")
.add_header(ETAG, "\"forged\"", true)
.add_header(IF_NONE_MATCH, "\"forged\"", true)
.send(&service)
.await;
assert_eq!(response.status_code, Some(StatusCode::OK));
let etag = response.headers().get(ETAG).unwrap();
assert_ne!(etag, "\"forged\"");
}
}