moonfire-nvr/server/src/web/static_file.rs
Scott Lamb 6a5b751bd6 log error messages in web paths
HTTP requests were only returning the error message to the caller, not
logging locally. In most cases the problem could be understood
client-side, but there are some exceptions. E.g. if Moonfire returns
a 403 on WebSocket update, even in the Chrome debug tools's network
tab the HTTP response body seems to be unavailable. And in general,
it's nice to have more context server-side.

Logging a `response::Body` isn't practical (it could be a stream), so
convert all the web stuff to use `base::Error` err returns.

Convert the `METHOD_NOT_ALLOWED` paths to return `Ok` for now. This is a
bit lame but punts on having some way of plumbing an explicit/overridden
status code in `base::Error`, as no gRPC error kind cleanly maps to
that.

Also convert `db::auth`, rather than making up an error kind in the web
layer.

This is also a small step toward getting rid of `failure::Error`.
2023-07-09 10:15:56 -07:00

130 lines
4.0 KiB
Rust

// 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 base::{bail_t, format_err_t, Error, ErrorKind, ResultExt};
use http::{header, HeaderValue, Request};
use super::{ResponseResult, Service};
impl Service {
/// Serves a static file if possible.
pub(super) async fn static_file(&self, req: Request<hyper::Body>) -> ResponseResult {
let Some(dir) = self.ui_dir.clone() else {
bail_t!(NotFound, "ui dir not configured or missing; no static files available.")
};
let Some(static_req) = StaticFileRequest::parse(req.uri().path()) else {
bail_t!(NotFound, "static file not found");
};
let f = dir.get(static_req.path, req.headers());
let node = f.await.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
format_err_t!(NotFound, "no such static file")
} else {
Error::wrap(ErrorKind::Internal, 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).err_kind(ErrorKind::Internal)?;
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<Self> {
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,
}
);
}
}