introduce typed errors and use in mp4 code

Fixes #46. If there are no video_sample_entries, it returns
InvalidArgument, which gets mapped to a HTTP 400. Various other failures
turn into non-500s as well.

There are many places that can & should be using typed errors, but it's
a start.
This commit is contained in:
Scott Lamb 2018-12-28 17:30:33 -06:00
parent 0b0f4ec9ed
commit f5703b9968
7 changed files with 253 additions and 53 deletions

View File

@ -32,6 +32,7 @@
use failure::Error;
use libc;
use log::warn;
use parking_lot::Mutex;
use std::mem;
use std::sync::{Arc, mpsc};

175
base/error.rs Normal file
View File

@ -0,0 +1,175 @@
// This file is part of Moonfire NVR, a security camera network video recorder.
// Copyright (C) 2018 Scott Lamb <slamb@slamb.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// In addition, as a special exception, the copyright holders give
// permission to link the code of portions of this program with the
// OpenSSL library under certain conditions as described in each
// individual source file, and distribute linked combinations including
// the two.
//
// You must obey the GNU General Public License in all respects for all
// of the code used other than OpenSSL. If you modify file(s) with this
// exception, you may extend this exception to your version of the
// file(s), but you are not obligated to do so. If you do not wish to do
// so, delete this exception statement from your version. If you delete
// this exception statement from all source files in the program, then
// also delete it here.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use failure::{Backtrace, Context, Fail};
use std::fmt;
#[derive(Debug)]
pub struct Error {
inner: Context<ErrorKind>,
}
impl Error {
pub fn kind(&self) -> ErrorKind {
*self.inner.get_context()
}
}
impl Fail for Error {
fn cause(&self) -> Option<&Fail> {
self.inner.cause()
}
fn backtrace(&self) -> Option<&Backtrace> {
self.inner.backtrace()
}
}
impl From<ErrorKind> for Error {
fn from(kind: ErrorKind) -> Error {
Error { inner: Context::new(kind) }
}
}
impl From<Context<ErrorKind>> for Error {
fn from(inner: Context<ErrorKind>) -> Error {
Error { inner }
}
}
/*impl From<failure::Error> for Error {
fn from(e: failure::Error) -> Error {
Error { inner: e.context(ErrorKind::Unknown) }
}
}
impl<E: std::error::Error + Send + Sync + 'static> From<E> for Error {
fn from(e: E) -> Error {
let f = e as Fail;
Error { inner: f.context(ErrorKind::Unknown) }
}
}*/
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.inner.cause() {
None => fmt::Display::fmt(&self.kind(), f),
Some(c) => write!(f, "{}: {}", self.kind(), c),
}
}
}
/// Error kind.
///
/// These codes are taken from
/// [grpc::StatusCode](https://github.com/grpc/grpc/blob/0e00c430827e81d61e1e7164ef04ca21ccbfaa77/include/grpcpp/impl/codegen/status_code_enum.h),
/// which is a nice general-purpose classification of errors. See that link for descriptions of
/// each error.
#[derive(Copy, Clone, Eq, PartialEq, Debug, Fail)]
pub enum ErrorKind {
#[fail(display = "Cancelled")] Cancelled,
#[fail(display = "Unknown")] Unknown,
#[fail(display = "Invalid argument")] InvalidArgument,
#[fail(display = "Deadline exceeded")] DeadlineExceeded,
#[fail(display = "Not found")] NotFound,
#[fail(display = "Already exists")] AlreadyExists,
#[fail(display = "Permission denied")] PermissionDenied,
#[fail(display = "Unauthenticated")] Unauthenticated,
#[fail(display = "Resource exhausted")] ResourceExhausted,
#[fail(display = "Failed precondition")] FailedPrecondition,
#[fail(display = "Aborted")] Aborted,
#[fail(display = "Out of range")] OutOfRange,
#[fail(display = "Unimplemented")] Unimplemented,
#[fail(display = "Internal")] Internal,
#[fail(display = "Unavailable")] Unavailable,
#[fail(display = "Data loss")] DataLoss,
#[doc(hidden)] #[fail(display = "__Nonexhaustive")] __Nonexhaustive,
}
/// Extension methods for `Result`.
pub trait ResultExt<T, E> {
/// Annotates an error with the given kind.
/// Example:
/// ```
/// use moonfire_base::{ErrorKind, ResultExt};
/// use std::io::Read;
/// let mut buf = [0u8; 1];
/// let r = std::io::Cursor::new("").read_exact(&mut buf[..]).err_kind(ErrorKind::Internal);
/// assert_eq!(r.unwrap_err().kind(), ErrorKind::Internal);
/// ```
fn err_kind(self, k: ErrorKind) -> Result<T, Error>;
}
impl<T, E> ResultExt<T, E> for Result<T, E> where E: Into<failure::Error> {
fn err_kind(self, k: ErrorKind) -> Result<T, Error> {
self.map_err(|e| e.into().context(k).into())
}
}
/// Like `failure::bail!`, but the first argument specifies a type as an `ErrorKind`.
///
/// Example:
/// ```
/// use moonfire_base::bail_t;
/// let e = || -> Result<(), moonfire_base::Error> {
/// bail_t!(Unauthenticated, "unknown user: {}", "slamb");
/// }().unwrap_err();
/// assert_eq!(e.kind(), moonfire_base::ErrorKind::Unauthenticated);
/// assert_eq!(e.to_string(), "Unauthenticated: unknown user: slamb");
/// ```
#[macro_export]
macro_rules! bail_t {
($t:ident, $e:expr) => {
return Err(failure::err_msg($e).context($crate::ErrorKind::$t).into());
};
($t:ident, $fmt:expr, $($arg:tt)+) => {
return Err(failure::err_msg(format!($fmt, $($arg)+)).context($crate::ErrorKind::$t).into());
};
}
/// Like `failure::format_err!`, but the first argument specifies a type as an `ErrorKind`.
///
/// Example:
/// ```
/// use moonfire_base::format_err_t;
/// let e = format_err_t!(Unauthenticated, "unknown user: {}", "slamb");
/// assert_eq!(e.kind(), moonfire_base::ErrorKind::Unauthenticated);
/// assert_eq!(e.to_string(), "Unauthenticated: unknown user: slamb");
/// ```
#[macro_export]
macro_rules! format_err_t {
($t:ident, $e:expr) => {
Into::<$crate::Error>::into(failure::err_msg($e).context($crate::ErrorKind::$t))
};
($t:ident, $fmt:expr, $($arg:tt)+) => {
Into::<$crate::Error>::into(failure::err_msg(format!($fmt, $($arg)+))
.context($crate::ErrorKind::$t))
};
}

View File

@ -28,11 +28,8 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
extern crate failure;
extern crate libc;
#[macro_use] extern crate log;
extern crate parking_lot;
extern crate time;
pub mod clock;
#[macro_use] mod error;
pub mod strutil;
pub use crate::error::{Error, ErrorKind, ResultExt};

View File

@ -30,7 +30,8 @@
//! Tools for implementing a `http_serve::Entity` body composed from many "slices".
use failure::Error;
use crate::base::Error;
use failure::Fail;
use futures::{Stream, stream};
use hyper::body::Payload;
use reffers::ARefs;

View File

@ -78,13 +78,12 @@
extern crate time;
use crate::base::strutil;
use crate::base::{strutil, Error, ErrorKind, ResultExt, bail_t, format_err_t};
use bytes::{Buf, BytesMut};
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
use crate::body::{Chunk, BoxedError, wrap_error};
use crate::db::recording::{self, TIME_UNITS_PER_SEC};
use crate::db::{self, dir};
use failure::Error;
use futures::Stream;
use futures::stream;
use http;
@ -372,7 +371,7 @@ impl Segment {
fn new(db: &db::LockedDatabase, row: &db::ListRecordingsRow, rel_range_90k: Range<i32>,
first_frame_num: u32) -> Result<Self, Error> {
Ok(Segment{
s: recording::Segment::new(db, row, rel_range_90k)?,
s: recording::Segment::new(db, row, rel_range_90k).err_kind(ErrorKind::Unknown)?,
index: UnsafeCell::new(Err(())),
index_once: ONCE_INIT,
first_frame_num,
@ -391,7 +390,7 @@ impl Segment {
let index: &'a _ = unsafe { &*self.index.get() };
match *index {
Ok(ref b) => return Ok(f(&b[..], self.lens())),
Err(()) => bail!("Unable to build index; see previous error."),
Err(()) => bail_t!(Unknown, "Unable to build index; see previous error."),
}
}
@ -407,7 +406,7 @@ impl Segment {
fn stsz(buf: &[u8], lens: SegmentLengths) -> &[u8] { &buf[lens.stts .. lens.stts + lens.stsz] }
fn stss(buf: &[u8], lens: SegmentLengths) -> &[u8] { &buf[lens.stts + lens.stsz ..] }
fn build_index(&self, playback: &db::RecordingPlayback) -> Result<Box<[u8]>, Error> {
fn build_index(&self, playback: &db::RecordingPlayback) -> Result<Box<[u8]>, failure::Error> {
let s = &self.s;
let lens = self.lens();
let len = lens.stts + lens.stsz + lens.stss;
@ -456,7 +455,7 @@ impl Segment {
// TrackRunBox / trun (8.8.8).
fn truns(&self, playback: &db::RecordingPlayback, initial_pos: u64, len: usize)
-> Result<Vec<u8>, Error> {
-> Result<Vec<u8>, failure::Error> {
let mut v = Vec::with_capacity(len);
struct RunInfo {
@ -521,7 +520,7 @@ impl Segment {
v.write_u32::<BigEndian>(it.bytes as u32)?;
data_pos += it.bytes as u64;
Ok(())
})?;
}).err_kind(ErrorKind::Internal)?;
if let Some(r) = run_info.take() {
// Finish the run as in the non-terminal case above.
let p = v.len();
@ -600,7 +599,7 @@ enum SliceType {
impl Slice {
fn new(end: u64, t: SliceType, p: usize) -> Result<Self, Error> {
if end >= (1<<40) || p >= (1<<20) {
bail!("end={} p={} too large for Slice", end, p);
bail_t!(InvalidArgument, "end={} p={} too large for {:?} Slice", end, p, t);
}
Ok(Slice(end | ((t as u64) << 40) | ((p as u64) << 44)))
@ -630,7 +629,8 @@ impl Slice {
}
let truns =
mp4.0.db.lock()
.with_recording_playback(s.s.id, &mut |playback| s.truns(playback, pos, len))?;
.with_recording_playback(s.s.id, &mut |playback| s.truns(playback, pos, len))
.err_kind(ErrorKind::Unknown)?;
let truns = ARefs::new(truns);
Ok(truns.map(|t| &t[r.start as usize .. r.end as usize]).into())
}
@ -672,7 +672,8 @@ impl slices::Slice for Slice {
.map_err(|e| wrap_error(e))
.and_then(move |c| {
if c.remaining() != (range.end - range.start) as usize {
return Err(wrap_error(format_err!(
return Err(wrap_error(format_err_t!(
Internal,
"Error producing {:?}: range {:?} produced incorrect len {}.",
self, range, c.remaining())));
}
@ -757,7 +758,8 @@ impl FileBuilder {
rel_range_90k: Range<i32>) -> Result<(), Error> {
if let Some(prev) = self.segments.last() {
if prev.s.have_trailing_zero() {
bail!("unable to append recording {} after recording {} with trailing zero",
bail_t!(InvalidArgument,
"unable to append recording {} after recording {} with trailing zero",
row.id, prev.s.id);
}
}
@ -777,15 +779,16 @@ impl FileBuilder {
dirs_by_stream_id: Arc<::fnv::FnvHashMap<i32, Arc<dir::SampleFileDir>>>)
-> Result<File, Error> {
let mut max_end = None;
let mut etag = hash::Hasher::new(hash::MessageDigest::sha1())?;
etag.update(&FORMAT_VERSION[..])?;
let mut etag = hash::Hasher::new(hash::MessageDigest::sha1())
.err_kind(ErrorKind::Internal)?;
etag.update(&FORMAT_VERSION[..]).err_kind(ErrorKind::Internal)?;
if self.include_timestamp_subtitle_track {
etag.update(b":ts:")?;
etag.update(b":ts:").err_kind(ErrorKind::Internal)?;
}
match self.type_ {
Type::Normal => {},
Type::InitSegment => etag.update(b":init:")?,
Type::MediaSegment => etag.update(b":media:")?,
Type::InitSegment => etag.update(b":init:").err_kind(ErrorKind::Internal)?,
Type::MediaSegment => etag.update(b":media:").err_kind(ErrorKind::Internal)?,
};
for s in &mut self.segments {
let d = &s.s.desired_range_90k;
@ -809,12 +812,12 @@ impl FileBuilder {
// Update the etag to reflect this segment.
let mut data = [0_u8; 28];
let mut cursor = io::Cursor::new(&mut data[..]);
cursor.write_i64::<BigEndian>(s.s.id.0)?;
cursor.write_i64::<BigEndian>(s.s.start.0)?;
cursor.write_u32::<BigEndian>(s.s.open_id)?;
cursor.write_i32::<BigEndian>(d.start)?;
cursor.write_i32::<BigEndian>(d.end)?;
etag.update(cursor.into_inner())?;
cursor.write_i64::<BigEndian>(s.s.id.0).err_kind(ErrorKind::Internal)?;
cursor.write_i64::<BigEndian>(s.s.start.0).err_kind(ErrorKind::Internal)?;
cursor.write_u32::<BigEndian>(s.s.open_id).err_kind(ErrorKind::Internal)?;
cursor.write_i32::<BigEndian>(d.start).err_kind(ErrorKind::Internal)?;
cursor.write_i32::<BigEndian>(d.end).err_kind(ErrorKind::Internal)?;
etag.update(cursor.into_inner()).err_kind(ErrorKind::Internal)?;
}
let max_end = match max_end {
None => 0,
@ -836,7 +839,8 @@ impl FileBuilder {
// If the segment is > 4 GiB, the 32-bit trun data offsets are untrustworthy.
// We'd need multiple moof+mdat sequences to support large media segments properly.
if self.body.slices.len() > u32::max_value() as u64 {
bail!("media segment has length {}, greater than allowed 4 GiB",
bail_t!(InvalidArgument,
"media segment has length {}, greater than allowed 4 GiB",
self.body.slices.len());
}
@ -871,6 +875,7 @@ impl FileBuilder {
debug!("slices: {:?}", self.body.slices);
let last_modified = ::std::time::UNIX_EPOCH +
::std::time::Duration::from_secs(max_end as u64);
let etag = etag.finish().err_kind(ErrorKind::Internal)?;
Ok(File(Arc::new(FileInner {
db,
dirs_by_stream_id,
@ -880,7 +885,7 @@ impl FileBuilder {
video_sample_entries: self.video_sample_entries,
initial_sample_byte_pos,
last_modified,
etag: HeaderValue::from_str(&format!("\"{}\"", &strutil::hex(&etag.finish()?)))
etag: HeaderValue::from_str(&format!("\"{}\"", &strutil::hex(&etag)))
.expect("hex string should be valid UTF-8"),
})))
}
@ -1051,7 +1056,7 @@ impl FileBuilder {
None => Some((e.width, e.height)),
Some((w, h)) => Some((cmp::max(w, e.width), cmp::max(h, e.height))),
}
}).ok_or_else(|| format_err!("No video_sample_entries"))?;
}).ok_or_else(|| format_err_t!(InvalidArgument, "no video_sample_entries"))?;
self.body.append_u32((width as u32) << 16);
self.body.append_u32((height as u32) << 16);
})
@ -1091,7 +1096,7 @@ impl FileBuilder {
let skip = s.s.desired_range_90k.start - actual_start_90k;
let keep = s.s.desired_range_90k.end - s.s.desired_range_90k.start;
if skip < 0 || keep < 0 {
bail!("skip={} keep={} on segment {:#?}", skip, keep, s);
bail_t!(Internal, "skip={} keep={} on segment {:#?}", skip, keep, s);
}
cur_media_time += skip as u64;
if unflushed.segment_duration + unflushed.media_time == cur_media_time {
@ -1408,7 +1413,7 @@ impl BodyState {
fn append_slice(&mut self, len: u64, t: SliceType, p: usize) -> Result<(), Error> {
let l = self.slices.len();
self.slices.append(Slice::new(l + len, t, p)?)
self.slices.append(Slice::new(l + len, t, p)?).err_kind(ErrorKind::Internal)
}
/// Appends a static bytestring, flushing the buffer if necessary.
@ -1436,7 +1441,7 @@ impl FileInner {
let mut v = Vec::with_capacity(l as usize);
let mut pos = self.initial_sample_byte_pos;
for s in &self.segments {
v.write_u64::<BigEndian>(pos)?;
v.write_u64::<BigEndian>(pos).err_kind(ErrorKind::Internal)?;
let r = s.s.sample_file_range();
pos += r.end - r.start;
}
@ -1456,14 +1461,14 @@ impl FileInner {
let s = &self.segments[i];
let f = self.dirs_by_stream_id
.get(&s.s.id.stream())
.ok_or_else(|| format_err!("{}: stream not found", s.s.id))?
.open_file(s.s.id)?;
.ok_or_else(|| format_err_t!(NotFound, "{}: stream not found", s.s.id))?
.open_file(s.s.id).err_kind(ErrorKind::Unknown)?;
let start = s.s.sample_file_range().start + r.start;
let mmap = Box::new(unsafe {
memmap::MmapOptions::new()
.offset(start)
.len((r.end - r.start) as usize)
.map(&f)?
.map(&f).err_kind(ErrorKind::Internal)?
});
use core::ops::Deref;
Ok(ARefs::new(mmap).map(|m| m.deref()).into())
@ -1477,10 +1482,11 @@ impl FileInner {
.unix_seconds();
let mut v = Vec::with_capacity(l as usize);
for ts in start_sec .. end_sec {
v.write_u16::<BigEndian>(SUBTITLE_LENGTH as u16)?;
v.write_u16::<BigEndian>(SUBTITLE_LENGTH as u16).expect("Vec write shouldn't fail");
let tm = time::at(time::Timespec{sec: ts, nsec: 0});
use std::io::Write;
write!(v, "{}", tm.strftime(SUBTITLE_TEMPLATE)?)?;
write!(v, "{}", tm.strftime(SUBTITLE_TEMPLATE).err_kind(ErrorKind::Internal)?)
.expect("Vec write shouldn't fail");
}
Ok(ARefs::new(v).map(|v| &v[r.start as usize .. r.end as usize]).into())
}
@ -2013,7 +2019,7 @@ mod tests {
testutil::init();
let db = TestDb::new(RealClocks {});
let e = make_mp4_from_encoders(Type::Normal, &db, vec![], 0 .. 0).err().unwrap();
assert_eq!(e.to_string(), "No video_sample_entries");
assert_eq!(e.to_string(), "Invalid argument: no video_sample_entries");
}
#[test]

View File

@ -30,6 +30,7 @@
//! Tools for implementing a `http_serve::Entity` body composed from many "slices".
use crate::base::format_err_t;
use crate::body::{BoxedError, wrap_error};
use failure::Error;
use futures::stream;
@ -113,8 +114,8 @@ impl<S> Slices<S> where S: Slice {
pub fn get_range(&self, ctx: &S::Ctx, range: Range<u64>)
-> Box<Stream<Item = S::Chunk, Error = BoxedError> + Send> {
if range.start > range.end || range.end > self.len {
return Box::new(stream::once(Err(wrap_error(format_err!(
"Bad range {:?} for slice of length {}", range, self.len)))));
return Box::new(stream::once(Err(wrap_error(format_err_t!(
Internal, "Bad range {:?} for slice of length {}", range, self.len)))));
}
// Binary search for the first slice of the range to write, determining its index and

View File

@ -31,7 +31,7 @@
extern crate hyper;
use crate::base::clock::Clocks;
use crate::base::strutil;
use crate::base::{ErrorKind, strutil};
use crate::body::{Body, BoxedError};
use base64;
use bytes::{BufMut, BytesMut};
@ -164,6 +164,15 @@ fn internal_server_err<E: Into<Error>>(err: E) -> Response<Body> {
plain_response(StatusCode::INTERNAL_SERVER_ERROR, err.into().to_string())
}
fn from_base_error(err: base::Error) -> Response<Body> {
let status_code = match err.kind() {
ErrorKind::InvalidArgument => StatusCode::BAD_REQUEST,
ErrorKind::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
plain_response(status_code, err.to_string())
}
#[derive(Debug, Eq, PartialEq)]
struct Segments {
ids: Range<i32>,
@ -347,7 +356,7 @@ impl ServiceInner {
if ent.sha1 == sha1 {
builder.append_video_sample_entry(ent.clone());
let mp4 = builder.build(self.db.clone(), self.dirs_by_stream_id.clone())
.map_err(internal_server_err)?;
.map_err(from_base_error)?;
return Ok(http_serve::serve(mp4, req));
}
}
@ -458,7 +467,7 @@ impl ServiceInner {
};
}
let mp4 = builder.build(self.db.clone(), self.dirs_by_stream_id.clone())
.map_err(internal_server_err)?;
.map_err(from_base_error)?;
Ok(http_serve::serve(mp4, req))
}
@ -859,11 +868,10 @@ mod tests {
}
impl Server {
fn new() -> Server {
fn new(require_auth: bool) -> Server {
let db = TestDb::new(crate::base::clock::RealClocks {});
let (shutdown_tx, shutdown_rx) = futures::sync::oneshot::channel::<()>();
let addr = "127.0.0.1:0".parse().unwrap();
let require_auth = true;
let service = super::Service::new(super::Config {
db: db.db.clone(),
ui_dir: None,
@ -961,7 +969,7 @@ mod tests {
#[test]
fn unauthorized_without_cookie() {
testutil::init();
let s = Server::new();
let s = Server::new(true);
let cli = reqwest::Client::new();
let resp = cli.get(&format!("{}/api/", &s.base_url)).send().unwrap();
assert_eq!(resp.status(), http::StatusCode::UNAUTHORIZED);
@ -970,7 +978,7 @@ mod tests {
#[test]
fn login() {
testutil::init();
let s = Server::new();
let s = Server::new(true);
let cli = reqwest::Client::new();
let login_url = format!("{}/api/login", &s.base_url);
@ -1003,7 +1011,7 @@ mod tests {
#[test]
fn logout() {
testutil::init();
let s = Server::new();
let s = Server::new(true);
let cli = reqwest::Client::new();
let mut p = HashMap::new();
p.insert("username", "slamb");
@ -1054,6 +1062,17 @@ mod tests {
.unwrap();
assert_eq!(resp.status(), http::StatusCode::UNAUTHORIZED);
}
#[test]
fn view_without_segments() {
testutil::init();
let s = Server::new(false);
let cli = reqwest::Client::new();
let resp = cli.get(
&format!("{}/api/cameras/{}/main/view.mp4", &s.base_url, s.db.test_camera_uuid))
.send().unwrap();
assert_eq!(resp.status(), http::StatusCode::BAD_REQUEST);
}
}
#[cfg(all(test, feature="nightly"))]