tower_serve_embedded/
service.rs1use std::convert::Infallible;
2use std::future::{Ready, ready};
3use std::task::{Context, Poll};
4
5use bytes::Bytes;
6use http::header::{ALLOW, CACHE_CONTROL, CONTENT_TYPE, ETAG, IF_NONE_MATCH};
7use http::{HeaderValue, Method, Request, Response, StatusCode};
8use http_body_util::Full;
9use tower_service::Service;
10
11use crate::Assets;
12
13#[derive(Clone, Copy, Debug)]
29pub struct ServeEmbedded {
30 assets: &'static Assets,
31}
32
33impl ServeEmbedded {
34 pub(crate) fn new(assets: &'static Assets) -> Self {
35 Self { assets }
36 }
37
38 fn respond(
39 &self,
40 method: &Method,
41 path: &str,
42 if_none_match: Option<&HeaderValue>,
43 ) -> Response<Full<Bytes>> {
44 if method != Method::GET && method != Method::HEAD {
45 return Response::builder()
46 .status(StatusCode::METHOD_NOT_ALLOWED)
47 .header(ALLOW, HeaderValue::from_static("GET, HEAD"))
48 .body(empty())
49 .unwrap();
50 }
51
52 let Some(resolved) = self.assets.resolve(path) else {
53 return Response::builder()
54 .status(StatusCode::NOT_FOUND)
55 .body(empty())
56 .unwrap();
57 };
58 let file = resolved.file;
59
60 if if_none_match.is_some_and(|inm| etag_matches(inm.as_bytes(), file.etag)) {
61 let mut builder = Response::builder()
62 .status(StatusCode::NOT_MODIFIED)
63 .header(ETAG, file.etag);
64 if let Some(cache_control) = resolved.cache_control {
65 builder = builder.header(CACHE_CONTROL, cache_control);
66 }
67 return builder.body(empty()).unwrap();
68 }
69
70 let body = if method == Method::HEAD {
71 empty()
72 } else {
73 Full::new(Bytes::from_static(file.bytes))
74 };
75
76 let mut builder = Response::builder()
77 .status(StatusCode::OK)
78 .header(CONTENT_TYPE, file.content_type)
79 .header(ETAG, file.etag);
80 if let Some(cache_control) = resolved.cache_control {
81 builder = builder.header(CACHE_CONTROL, cache_control);
82 }
83 builder.body(body).unwrap()
84 }
85}
86
87impl<B> Service<Request<B>> for ServeEmbedded {
88 type Response = Response<Full<Bytes>>;
89 type Error = Infallible;
90 type Future = Ready<Result<Self::Response, Self::Error>>;
91
92 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
93 Poll::Ready(Ok(()))
94 }
95
96 fn call(&mut self, req: Request<B>) -> Self::Future {
97 let resp = self.respond(
98 req.method(),
99 req.uri().path(),
100 req.headers().get(IF_NONE_MATCH),
101 );
102 ready(Ok(resp))
103 }
104}
105
106fn empty() -> Full<Bytes> {
107 Full::new(Bytes::new())
108}
109
110fn etag_matches(if_none_match: &[u8], etag: &str) -> bool {
116 let Ok(header) = std::str::from_utf8(if_none_match) else {
117 return false;
118 };
119 header.split(',').any(|candidate| {
120 let c = candidate.trim();
121 c == "*" || c == etag || c.strip_prefix("W/").is_some_and(|weak| weak == etag)
122 })
123}
124
125#[cfg(test)]
126mod tests {
127 use super::etag_matches;
128
129 #[test]
130 fn matches_exact_and_wildcard_and_weak() {
131 assert!(etag_matches(b"\"abc\"", "\"abc\""));
132 assert!(etag_matches(b"*", "\"abc\""));
133 assert!(etag_matches(b"W/\"abc\"", "\"abc\""));
134 assert!(etag_matches(b"\"x\", \"abc\", \"y\"", "\"abc\""));
135 assert!(!etag_matches(b"\"nope\"", "\"abc\""));
136 assert!(!etag_matches(b"", "\"abc\""));
137 }
138}