diff --git a/server/src/web/mod.rs b/server/src/web/mod.rs index 030b5ff..9c4e3f3 100644 --- a/server/src/web/mod.rs +++ b/server/src/web/mod.rs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception. mod path; +mod static_file; use self::path::Path; use crate::body::Body; @@ -899,42 +900,6 @@ impl Service { Ok(http_serve::serve(mp4, req)) } - async fn static_file(&self, req: Request) -> ResponseResult { - let dir = self - .ui_dir - .clone() - .ok_or_else(|| not_found("--ui-dir not configured; 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)) - } - async fn user(&self, req: Request, caller: Caller, id: i32) -> ResponseResult { if caller.user.map(|u| u.id) != Some(id) { bail_t!(Unauthenticated, "must be authenticated as supplied user"); @@ -1257,60 +1222,9 @@ fn encode_sid(sid: db::RawSessionId, flags: i32) -> String { cookie } -#[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 should. - 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 super::{Segments, StaticFileRequest}; + use super::Segments; use db::testutil::{self, TestDb}; use futures::future::FutureExt; use log::info; @@ -1417,30 +1331,6 @@ mod tests { } } - #[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, - } - ); - } - #[test] #[rustfmt::skip] fn test_segments() { diff --git a/server/src/web/static_file.rs b/server/src/web/static_file.rs new file mode 100644 index 0000000..25347ff --- /dev/null +++ b/server/src/web/static_file.rs @@ -0,0 +1,130 @@ +// 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; 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, + } + ); + } +}