// This file is part of Moonfire NVR, a security camera network video recorder. // Copyright (C) 2021 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt. // SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception. //! Static file serving. use http::{header, HeaderValue, Request}; use super::{internal_server_err, not_found, ResponseResult, Service}; impl Service { /// Serves a static file if possible. pub(super) async fn static_file(&self, req: Request) -> ResponseResult { let dir = self.ui_dir.clone().ok_or_else(|| { not_found("ui dir not configured or missing; no static files available.") })?; let static_req = match StaticFileRequest::parse(req.uri().path()) { None => return Err(not_found("static file not found")), Some(r) => r, }; let f = dir.get(static_req.path, req.headers()); let node = f.await.map_err(|e| { if e.kind() == std::io::ErrorKind::NotFound { not_found("no such static file") } else { internal_server_err(e) } })?; let mut hdrs = http::HeaderMap::new(); node.add_encoding_headers(&mut hdrs); hdrs.insert( header::CACHE_CONTROL, HeaderValue::from_static(if static_req.immutable { // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Caching_static_assets "public, max-age=604800, immutable" } else { "public" }), ); hdrs.insert( header::CONTENT_TYPE, HeaderValue::from_static(static_req.mime), ); let e = node.into_file_entity(hdrs).map_err(internal_server_err)?; Ok(http_serve::serve(e, &req)) } } #[derive(Debug, Eq, PartialEq)] struct StaticFileRequest<'a> { path: &'a str, immutable: bool, mime: &'static str, } impl<'a> StaticFileRequest<'a> { fn parse(path: &'a str) -> Option { if !path.starts_with('/') || path == "/index.html" { return None; } let (path, immutable) = match &path[1..] { // These well-known URLs don't have content hashes in them, and // thus aren't immutable. "" => ("index.html", false), "robots.txt" => ("robots.txt", false), "site.webmanifest" => ("site.webmanifest", false), // Everything else is assumed to contain a hash and be immutable. p => (p, true), }; let last_dot = match path.rfind('.') { None => return None, Some(d) => d, }; let ext = &path[last_dot + 1..]; let mime = match ext { "css" => "text/css", "html" => "text/html", "ico" => "image/x-icon", "js" | "map" => "text/javascript", "json" => "application/json", "png" => "image/png", "svg" => "image/svg+xml", "txt" => "text/plain", "webmanifest" => "application/manifest+json", "woff2" => "font/woff2", _ => return None, }; Some(StaticFileRequest { path, immutable, mime, }) } } #[cfg(test)] mod tests { use db::testutil; use super::StaticFileRequest; #[test] fn static_file() { testutil::init(); let r = StaticFileRequest::parse("/jquery-ui.b6d3d46c828800e78499.js").unwrap(); assert_eq!( r, StaticFileRequest { path: "jquery-ui.b6d3d46c828800e78499.js", mime: "text/javascript", immutable: true, } ); let r = StaticFileRequest::parse("/").unwrap(); assert_eq!( r, StaticFileRequest { path: "index.html", mime: "text/html", immutable: false, } ); } }