mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-11-09 21:49:46 -05:00
Merge branch 'master' into new-ui
This commit is contained in:
7
server/Cargo.lock
generated
7
server/Cargo.lock
generated
@@ -1204,7 +1204,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "moonfire-ffmpeg"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/scottlamb/moonfire-ffmpeg#d008cebe93e12dcdeeb341cc39b60ca6daf07226"
|
||||
source = "git+https://github.com/scottlamb/moonfire-ffmpeg#16d3b7c951a5d93900d3a6731f1ad79a610ad69a"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -1215,7 +1215,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "moonfire-nvr"
|
||||
version = "0.6.1"
|
||||
version = "0.6.2"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"blake3",
|
||||
@@ -1274,9 +1274,10 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "mylog"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/scottlamb/mylog#45cdec74e11b05d02b55d49cfb1a8391e0f281a5"
|
||||
source = "git+https://github.com/scottlamb/mylog#2b1085cfb3bd0b1f2afe7d8085045d81858c0050"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"libc",
|
||||
"log",
|
||||
"parking_lot",
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "moonfire-nvr"
|
||||
version = "0.6.1"
|
||||
version = "0.6.2"
|
||||
authors = ["Scott Lamb <slamb@slamb.org>"]
|
||||
edition = "2018"
|
||||
license-file = "../LICENSE.txt"
|
||||
|
||||
@@ -36,6 +36,15 @@ impl Error {
|
||||
pub fn compat(self) -> failure::Compat<Context<ErrorKind>> {
|
||||
self.inner.compat()
|
||||
}
|
||||
|
||||
pub fn map<F>(self, op: F) -> Self
|
||||
where
|
||||
F: FnOnce(ErrorKind) -> ErrorKind,
|
||||
{
|
||||
Self {
|
||||
inner: self.inner.map(op),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Fail for Error {
|
||||
@@ -149,10 +158,10 @@ where
|
||||
#[macro_export]
|
||||
macro_rules! bail_t {
|
||||
($t:ident, $e:expr) => {
|
||||
return Err(failure::err_msg($e).context($crate::ErrorKind::$t).into());
|
||||
return Err($crate::Error::from(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());
|
||||
return Err($crate::Error::from(failure::err_msg(format!($fmt, $($arg)+)).context($crate::ErrorKind::$t)).into());
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception.
|
||||
|
||||
use crate::schema::Permissions;
|
||||
use base::strutil;
|
||||
use base::{bail_t, format_err_t, strutil, ErrorKind, ResultExt};
|
||||
use failure::{bail, format_err, Error};
|
||||
use fnv::FnvHashMap;
|
||||
use lazy_static::lazy_static;
|
||||
@@ -678,23 +678,31 @@ impl State {
|
||||
conn: &Connection,
|
||||
req: Request,
|
||||
hash: &SessionHash,
|
||||
) -> Result<(&Session, &User), Error> {
|
||||
) -> Result<(&Session, &User), base::Error> {
|
||||
let s = match self.sessions.entry(*hash) {
|
||||
::std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
|
||||
::std::collections::hash_map::Entry::Vacant(e) => e.insert(lookup_session(conn, hash)?),
|
||||
::std::collections::hash_map::Entry::Vacant(e) => {
|
||||
let s = lookup_session(conn, hash).map_err(|e| {
|
||||
e.map(|k| match k {
|
||||
ErrorKind::NotFound => ErrorKind::Unauthenticated,
|
||||
e => e,
|
||||
})
|
||||
})?;
|
||||
e.insert(s)
|
||||
}
|
||||
};
|
||||
let u = match self.users_by_id.get(&s.user_id) {
|
||||
None => bail!("session references nonexistent user!"),
|
||||
None => bail_t!(Internal, "session references nonexistent user!"),
|
||||
Some(u) => u,
|
||||
};
|
||||
if let Some(r) = s.revocation_reason {
|
||||
bail!("session is no longer valid (reason={})", r);
|
||||
bail_t!(Unauthenticated, "session is no longer valid (reason={})", r);
|
||||
}
|
||||
s.last_use = req;
|
||||
s.use_count += 1;
|
||||
s.dirty = true;
|
||||
if u.disabled() {
|
||||
bail!("user {:?} is disabled", &u.username);
|
||||
bail_t!(Unauthenticated, "user {:?} is disabled", &u.username);
|
||||
}
|
||||
Ok((s, u))
|
||||
}
|
||||
@@ -811,9 +819,10 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
fn lookup_session(conn: &Connection, hash: &SessionHash) -> Result<Session, Error> {
|
||||
let mut stmt = conn.prepare_cached(
|
||||
r#"
|
||||
fn lookup_session(conn: &Connection, hash: &SessionHash) -> Result<Session, base::Error> {
|
||||
let mut stmt = conn
|
||||
.prepare_cached(
|
||||
r#"
|
||||
select
|
||||
user_id,
|
||||
seed,
|
||||
@@ -839,39 +848,52 @@ fn lookup_session(conn: &Connection, hash: &SessionHash) -> Result<Session, Erro
|
||||
where
|
||||
session_id_hash = ?
|
||||
"#,
|
||||
)?;
|
||||
let mut rows = stmt.query(params![&hash.0[..]])?;
|
||||
let row = rows.next()?.ok_or_else(|| format_err!("no such session"))?;
|
||||
let creation_addr: FromSqlIpAddr = row.get(8)?;
|
||||
let revocation_addr: FromSqlIpAddr = row.get(11)?;
|
||||
let last_use_addr: FromSqlIpAddr = row.get(16)?;
|
||||
)
|
||||
.err_kind(ErrorKind::Internal)?;
|
||||
let mut rows = stmt
|
||||
.query(params![&hash.0[..]])
|
||||
.err_kind(ErrorKind::Internal)?;
|
||||
let row = rows
|
||||
.next()
|
||||
.err_kind(ErrorKind::Internal)?
|
||||
.ok_or_else(|| format_err_t!(NotFound, "no such session"))?;
|
||||
let creation_addr: FromSqlIpAddr = row.get(8).err_kind(ErrorKind::Internal)?;
|
||||
let revocation_addr: FromSqlIpAddr = row.get(11).err_kind(ErrorKind::Internal)?;
|
||||
let last_use_addr: FromSqlIpAddr = row.get(16).err_kind(ErrorKind::Internal)?;
|
||||
let mut permissions = Permissions::new();
|
||||
permissions.merge_from_bytes(row.get_raw_checked(18)?.as_blob()?)?;
|
||||
permissions
|
||||
.merge_from_bytes(
|
||||
row.get_raw_checked(18)
|
||||
.err_kind(ErrorKind::Internal)?
|
||||
.as_blob()
|
||||
.err_kind(ErrorKind::Internal)?,
|
||||
)
|
||||
.err_kind(ErrorKind::Internal)?;
|
||||
Ok(Session {
|
||||
user_id: row.get(0)?,
|
||||
seed: row.get(1)?,
|
||||
flags: row.get(2)?,
|
||||
domain: row.get(3)?,
|
||||
description: row.get(4)?,
|
||||
creation_password_id: row.get(5)?,
|
||||
user_id: row.get(0).err_kind(ErrorKind::Internal)?,
|
||||
seed: row.get(1).err_kind(ErrorKind::Internal)?,
|
||||
flags: row.get(2).err_kind(ErrorKind::Internal)?,
|
||||
domain: row.get(3).err_kind(ErrorKind::Internal)?,
|
||||
description: row.get(4).err_kind(ErrorKind::Internal)?,
|
||||
creation_password_id: row.get(5).err_kind(ErrorKind::Internal)?,
|
||||
creation: Request {
|
||||
when_sec: row.get(6)?,
|
||||
user_agent: row.get(7)?,
|
||||
when_sec: row.get(6).err_kind(ErrorKind::Internal)?,
|
||||
user_agent: row.get(7).err_kind(ErrorKind::Internal)?,
|
||||
addr: creation_addr.0,
|
||||
},
|
||||
revocation: Request {
|
||||
when_sec: row.get(9)?,
|
||||
user_agent: row.get(10)?,
|
||||
when_sec: row.get(9).err_kind(ErrorKind::Internal)?,
|
||||
user_agent: row.get(10).err_kind(ErrorKind::Internal)?,
|
||||
addr: revocation_addr.0,
|
||||
},
|
||||
revocation_reason: row.get(12)?,
|
||||
revocation_reason_detail: row.get(13)?,
|
||||
revocation_reason: row.get(12).err_kind(ErrorKind::Internal)?,
|
||||
revocation_reason_detail: row.get(13).err_kind(ErrorKind::Internal)?,
|
||||
last_use: Request {
|
||||
when_sec: row.get(14)?,
|
||||
user_agent: row.get(15)?,
|
||||
when_sec: row.get(14).err_kind(ErrorKind::Internal)?,
|
||||
user_agent: row.get(15).err_kind(ErrorKind::Internal)?,
|
||||
addr: last_use_addr.0,
|
||||
},
|
||||
use_count: row.get(17)?,
|
||||
use_count: row.get(17).err_kind(ErrorKind::Internal)?,
|
||||
dirty: false,
|
||||
permissions,
|
||||
})
|
||||
@@ -969,7 +991,10 @@ mod tests {
|
||||
let e = state
|
||||
.authenticate_session(&conn, req.clone(), &sid.hash())
|
||||
.unwrap_err();
|
||||
assert_eq!(format!("{}", e), "session is no longer valid (reason=1)");
|
||||
assert_eq!(
|
||||
format!("{}", e),
|
||||
"Unauthenticated: session is no longer valid (reason=1)"
|
||||
);
|
||||
|
||||
// Everything should persist across reload.
|
||||
drop(state);
|
||||
@@ -977,7 +1002,10 @@ mod tests {
|
||||
let e = state
|
||||
.authenticate_session(&conn, req, &sid.hash())
|
||||
.unwrap_err();
|
||||
assert_eq!(format!("{}", e), "session is no longer valid (reason=1)");
|
||||
assert_eq!(
|
||||
format!("{}", e),
|
||||
"Unauthenticated: session is no longer valid (reason=1)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1028,7 +1056,10 @@ mod tests {
|
||||
let e = state
|
||||
.authenticate_session(&conn, req, &sid.hash())
|
||||
.unwrap_err();
|
||||
assert_eq!(format!("{}", e), "session is no longer valid (reason=1)");
|
||||
assert_eq!(
|
||||
format!("{}", e),
|
||||
"Unauthenticated: session is no longer valid (reason=1)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1159,7 +1190,10 @@ mod tests {
|
||||
let e = state
|
||||
.authenticate_session(&conn, req.clone(), &sid.hash())
|
||||
.unwrap_err();
|
||||
assert_eq!(format!("{}", e), "user \"slamb\" is disabled");
|
||||
assert_eq!(
|
||||
format!("{}", e),
|
||||
"Unauthenticated: user \"slamb\" is disabled"
|
||||
);
|
||||
|
||||
// The user should still be disabled after reload.
|
||||
drop(state);
|
||||
@@ -1167,7 +1201,10 @@ mod tests {
|
||||
let e = state
|
||||
.authenticate_session(&conn, req, &sid.hash())
|
||||
.unwrap_err();
|
||||
assert_eq!(format!("{}", e), "user \"slamb\" is disabled");
|
||||
assert_eq!(
|
||||
format!("{}", e),
|
||||
"Unauthenticated: user \"slamb\" is disabled"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1206,7 +1243,7 @@ mod tests {
|
||||
let e = state
|
||||
.authenticate_session(&conn, req.clone(), &sid.hash())
|
||||
.unwrap_err();
|
||||
assert_eq!(format!("{}", e), "no such session");
|
||||
assert_eq!(format!("{}", e), "Unauthenticated: no such session");
|
||||
|
||||
// The user should still be deleted after reload.
|
||||
drop(state);
|
||||
@@ -1215,7 +1252,7 @@ mod tests {
|
||||
let e = state
|
||||
.authenticate_session(&conn, req.clone(), &sid.hash())
|
||||
.unwrap_err();
|
||||
assert_eq!(format!("{}", e), "no such session");
|
||||
assert_eq!(format!("{}", e), "Unauthenticated: no such session");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -171,9 +171,9 @@ pub fn run(conn: &mut rusqlite::Connection, opts: &Options) -> Result<i32, Error
|
||||
let tx = conn.transaction()?;
|
||||
if !ctx.rows_to_delete.is_empty() {
|
||||
info!("Deleting {} recording rows", ctx.rows_to_delete.len());
|
||||
let mut d1 = tx.prepare("delete from recording where composite_id = ?")?;
|
||||
let mut d2 = tx.prepare("delete from recording_playback where composite_id = ?")?;
|
||||
let mut d3 = tx.prepare("delete from recording_integrity where composite_id = ?")?;
|
||||
let mut d1 = tx.prepare("delete from recording_playback where composite_id = ?")?;
|
||||
let mut d2 = tx.prepare("delete from recording_integrity where composite_id = ?")?;
|
||||
let mut d3 = tx.prepare("delete from recording where composite_id = ?")?;
|
||||
for &id in &ctx.rows_to_delete {
|
||||
d1.execute(params![id.0])?;
|
||||
d2.execute(params![id.0])?;
|
||||
|
||||
@@ -2173,7 +2173,7 @@ impl LockedDatabase {
|
||||
&mut self,
|
||||
req: auth::Request,
|
||||
sid: &auth::SessionHash,
|
||||
) -> Result<(&auth::Session, &User), Error> {
|
||||
) -> Result<(&auth::Session, &User), base::Error> {
|
||||
self.auth.authenticate_session(&self.conn, req, sid)
|
||||
}
|
||||
|
||||
|
||||
@@ -709,7 +709,7 @@ impl<'a, C: Clocks + Clone, D: DirWriter> Writer<'a, C, D> {
|
||||
// We must restore it on all success or error paths.
|
||||
|
||||
if let Some(unindexed) = w.unindexed_sample.take() {
|
||||
let duration = i32::try_from(pts_90k - i64::from(unindexed.pts_90k))?;
|
||||
let duration = pts_90k - unindexed.pts_90k;
|
||||
if duration <= 0 {
|
||||
w.unindexed_sample = Some(unindexed); // restore invariant.
|
||||
bail!(
|
||||
@@ -718,6 +718,17 @@ impl<'a, C: Clocks + Clone, D: DirWriter> Writer<'a, C, D> {
|
||||
pts_90k
|
||||
);
|
||||
}
|
||||
let duration = match i32::try_from(duration) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
w.unindexed_sample = Some(unindexed); // restore invariant.
|
||||
bail!(
|
||||
"excessive pts jump from {} to {}",
|
||||
unindexed.pts_90k,
|
||||
pts_90k
|
||||
)
|
||||
}
|
||||
};
|
||||
if let Err(e) = w.add_sample(
|
||||
duration,
|
||||
unindexed.len,
|
||||
@@ -1039,7 +1050,7 @@ mod tests {
|
||||
drop(tdb.syncer_channel);
|
||||
tdb.syncer_join.join().unwrap();
|
||||
|
||||
// Start a mocker syncer.
|
||||
// Start a mock syncer.
|
||||
let dir = MockDir::new();
|
||||
let syncer = super::Syncer {
|
||||
dir_id: *tdb
|
||||
@@ -1080,6 +1091,59 @@ mod tests {
|
||||
nix::Error::Sys(nix::errno::Errno::EIO)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn excessive_pts_jump() {
|
||||
testutil::init();
|
||||
let mut h = new_harness(0);
|
||||
let video_sample_entry_id =
|
||||
h.db.lock()
|
||||
.insert_video_sample_entry(VideoSampleEntryToInsert {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
pasp_h_spacing: 1,
|
||||
pasp_v_spacing: 1,
|
||||
data: [0u8; 100].to_vec(),
|
||||
rfc6381_codec: "avc1.000000".to_owned(),
|
||||
})
|
||||
.unwrap();
|
||||
let mut w = Writer::new(
|
||||
&h.dir,
|
||||
&h.db,
|
||||
&h.channel,
|
||||
testutil::TEST_STREAM_ID,
|
||||
video_sample_entry_id,
|
||||
);
|
||||
h.dir.expect(MockDirAction::Create(
|
||||
CompositeId::new(1, 0),
|
||||
Box::new(|_id| Err(nix_eio())),
|
||||
));
|
||||
let f = MockFile::new();
|
||||
h.dir.expect(MockDirAction::Create(
|
||||
CompositeId::new(1, 0),
|
||||
Box::new({
|
||||
let f = f.clone();
|
||||
move |_id| Ok(f.clone())
|
||||
}),
|
||||
));
|
||||
f.expect(MockFileAction::Write(Box::new(|_| Ok(1))));
|
||||
f.expect(MockFileAction::SyncAll(Box::new(|| Ok(()))));
|
||||
w.write(b"1", recording::Time(1), 0, true).unwrap();
|
||||
|
||||
let e = w
|
||||
.write(b"2", recording::Time(2), i32::max_value() as i64 + 1, true)
|
||||
.unwrap_err();
|
||||
assert!(e.to_string().contains("excessive pts jump"));
|
||||
h.dir.expect(MockDirAction::Sync(Box::new(|| Ok(()))));
|
||||
drop(w);
|
||||
assert!(h.syncer.iter(&h.syncer_rcv)); // AsyncSave
|
||||
assert_eq!(h.syncer.planned_flushes.len(), 1);
|
||||
assert!(h.syncer.iter(&h.syncer_rcv)); // planned flush
|
||||
assert_eq!(h.syncer.planned_flushes.len(), 0);
|
||||
assert!(h.syncer.iter(&h.syncer_rcv)); // DatabaseFlushed
|
||||
f.ensure_done();
|
||||
h.dir.ensure_done();
|
||||
}
|
||||
|
||||
/// Tests the database flushing while a syncer is still processing a previous flush event.
|
||||
#[test]
|
||||
fn double_flush() {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||
// Copyright (C) 2020 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
|
||||
// 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.
|
||||
|
||||
#![cfg_attr(all(feature = "nightly", test), feature(test))]
|
||||
|
||||
use log::{debug, error};
|
||||
use std::fmt::Write;
|
||||
use std::str::FromStr;
|
||||
use structopt::StructOpt;
|
||||
|
||||
@@ -107,6 +108,34 @@ impl Args {
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom panic hook that logs instead of directly writing to stderr.
|
||||
///
|
||||
/// This means it includes a timestamp and is more recognizable as a serious
|
||||
/// error (including console color coding by default, a format `lnav` will
|
||||
/// recognize, etc.).
|
||||
fn panic_hook(p: &std::panic::PanicInfo) {
|
||||
let mut msg;
|
||||
if let Some(l) = p.location() {
|
||||
msg = format!("panic at '{}'", l);
|
||||
} else {
|
||||
msg = "panic".to_owned();
|
||||
}
|
||||
if let Some(s) = p.payload().downcast_ref::<&str>() {
|
||||
write!(&mut msg, ": {}", s).unwrap();
|
||||
}
|
||||
let b = failure::Backtrace::new();
|
||||
if b.is_empty() {
|
||||
write!(
|
||||
&mut msg,
|
||||
"\n\n(set environment variable RUST_BACKTRACE=1 to see backtraces)"
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
write!(&mut msg, "\n\nBacktrace:\n{}", b).unwrap();
|
||||
}
|
||||
error!("{}", msg);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = Args::from_args();
|
||||
let mut h = mylog::Builder::new()
|
||||
@@ -116,10 +145,18 @@ fn main() {
|
||||
.and_then(|s| mylog::Format::from_str(&s))
|
||||
.unwrap_or(mylog::Format::Google),
|
||||
)
|
||||
.set_color(
|
||||
::std::env::var("MOONFIRE_COLOR")
|
||||
.map_err(|_| ())
|
||||
.and_then(|s| mylog::ColorMode::from_str(&s))
|
||||
.unwrap_or(mylog::ColorMode::Auto),
|
||||
)
|
||||
.set_spec(&::std::env::var("MOONFIRE_LOG").unwrap_or("info".to_owned()))
|
||||
.build();
|
||||
h.clone().install().unwrap();
|
||||
|
||||
std::panic::set_hook(Box::new(&panic_hook));
|
||||
|
||||
let r = {
|
||||
let _a = h.async_scope();
|
||||
args.run()
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
use crate::body::Body;
|
||||
use crate::json;
|
||||
use crate::mp4;
|
||||
use base::clock::Clocks;
|
||||
use base::{bail_t, ErrorKind};
|
||||
use base::{clock::Clocks, format_err_t};
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use core::borrow::Borrow;
|
||||
use core::str::FromStr;
|
||||
@@ -35,6 +35,26 @@ use tokio_tungstenite::tungstenite;
|
||||
use url::form_urlencoded;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// An HTTP error response.
|
||||
/// This is a thin wrapper over the hyper response type; it doesn't even verify
|
||||
/// that the response actually uses a non-2xx status code. Its purpose is to
|
||||
/// allow automatic conversion from `base::Error`. Rust's orphan rule prevents
|
||||
/// this crate from defining a direct conversion from `base::Error` to
|
||||
/// `hyper::Response`.
|
||||
struct HttpError(Response<Body>);
|
||||
|
||||
impl From<Response<Body>> for HttpError {
|
||||
fn from(response: Response<Body>) -> Self {
|
||||
HttpError(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<base::Error> for HttpError {
|
||||
fn from(err: base::Error) -> Self {
|
||||
HttpError(from_base_error(err))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
enum Path {
|
||||
TopLevel, // "/api/"
|
||||
@@ -141,23 +161,28 @@ fn plain_response<B: Into<Body>>(status: http::StatusCode, body: B) -> Response<
|
||||
.expect("hardcoded head should be valid")
|
||||
}
|
||||
|
||||
fn not_found<B: Into<Body>>(body: B) -> Response<Body> {
|
||||
plain_response(StatusCode::NOT_FOUND, body)
|
||||
fn not_found<B: Into<Body>>(body: B) -> HttpError {
|
||||
HttpError(plain_response(StatusCode::NOT_FOUND, body))
|
||||
}
|
||||
|
||||
fn bad_req<B: Into<Body>>(body: B) -> Response<Body> {
|
||||
plain_response(StatusCode::BAD_REQUEST, body)
|
||||
fn bad_req<B: Into<Body>>(body: B) -> HttpError {
|
||||
HttpError(plain_response(StatusCode::BAD_REQUEST, body))
|
||||
}
|
||||
|
||||
fn internal_server_err<E: Into<Error>>(err: E) -> Response<Body> {
|
||||
plain_response(StatusCode::INTERNAL_SERVER_ERROR, err.into().to_string())
|
||||
fn internal_server_err<E: Into<Error>>(err: E) -> HttpError {
|
||||
HttpError(plain_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
err.into().to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn from_base_error(err: base::Error) -> Response<Body> {
|
||||
use ErrorKind::*;
|
||||
let status_code = match err.kind() {
|
||||
ErrorKind::PermissionDenied | ErrorKind::Unauthenticated => StatusCode::UNAUTHORIZED,
|
||||
ErrorKind::InvalidArgument => StatusCode::BAD_REQUEST,
|
||||
ErrorKind::NotFound => StatusCode::NOT_FOUND,
|
||||
Unauthenticated => StatusCode::UNAUTHORIZED,
|
||||
PermissionDenied => StatusCode::FORBIDDEN,
|
||||
InvalidArgument | FailedPrecondition => StatusCode::BAD_REQUEST,
|
||||
NotFound => StatusCode::NOT_FOUND,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
plain_response(status_code, err.to_string())
|
||||
@@ -232,7 +257,7 @@ struct Caller {
|
||||
session: Option<json::Session>,
|
||||
}
|
||||
|
||||
type ResponseResult = Result<Response<Body>, Response<Body>>;
|
||||
type ResponseResult = Result<Response<Body>, HttpError>;
|
||||
|
||||
fn serve_json<T: serde::ser::Serialize>(req: &Request<hyper::Body>, out: &T) -> ResponseResult {
|
||||
let (mut resp, writer) = http_serve::streaming_body(&req).build();
|
||||
@@ -277,12 +302,9 @@ fn extract_sid(req: &Request<hyper::Body>) -> Option<auth::RawSessionId> {
|
||||
/// This returns the request body as bytes rather than performing
|
||||
/// deserialization. Keeping the bytes allows the caller to use a `Deserialize`
|
||||
/// that borrows from the bytes.
|
||||
async fn extract_json_body(req: &mut Request<hyper::Body>) -> Result<Bytes, Response<Body>> {
|
||||
async fn extract_json_body(req: &mut Request<hyper::Body>) -> Result<Bytes, HttpError> {
|
||||
if *req.method() != http::method::Method::POST {
|
||||
return Err(plain_response(
|
||||
StatusCode::METHOD_NOT_ALLOWED,
|
||||
"POST expected",
|
||||
));
|
||||
return Err(plain_response(StatusCode::METHOD_NOT_ALLOWED, "POST expected").into());
|
||||
}
|
||||
let correct_mime_type = match req.headers().get(header::CONTENT_TYPE) {
|
||||
Some(t) if t == "application/json" => true,
|
||||
@@ -376,10 +398,7 @@ impl Service {
|
||||
stream_type: db::StreamType,
|
||||
) -> ResponseResult {
|
||||
if !caller.permissions.view_video {
|
||||
return Err(plain_response(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"view_video required",
|
||||
));
|
||||
bail_t!(PermissionDenied, "view_video required");
|
||||
}
|
||||
|
||||
let stream_id;
|
||||
@@ -389,10 +408,10 @@ impl Service {
|
||||
let mut db = self.db.lock();
|
||||
open_id = match db.open {
|
||||
None => {
|
||||
return Err(plain_response(
|
||||
StatusCode::PRECONDITION_FAILED,
|
||||
"database is read-only; there are no live streams",
|
||||
))
|
||||
bail_t!(
|
||||
FailedPrecondition,
|
||||
"database is read-only; there are no live streams"
|
||||
);
|
||||
}
|
||||
Some(o) => o.id,
|
||||
};
|
||||
@@ -400,10 +419,7 @@ impl Service {
|
||||
plain_response(StatusCode::NOT_FOUND, format!("no such camera {}", uuid))
|
||||
})?;
|
||||
stream_id = camera.streams[stream_type.index()].ok_or_else(|| {
|
||||
plain_response(
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("no such stream {}/{}", uuid, stream_type),
|
||||
)
|
||||
format_err_t!(NotFound, "no such stream {}/{}", uuid, stream_type)
|
||||
})?;
|
||||
db.watch_live(
|
||||
stream_id,
|
||||
@@ -525,10 +541,15 @@ impl Service {
|
||||
_ => Err(plain_response(
|
||||
StatusCode::METHOD_NOT_ALLOWED,
|
||||
"POST, GET, or HEAD expected",
|
||||
)),
|
||||
)
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Serves an HTTP request.
|
||||
/// Note that the `serve` wrapper handles responses the same whether they
|
||||
/// are `Ok` or `Err`. But returning `Err` here with the `?` operator is
|
||||
/// convenient for error paths.
|
||||
async fn serve_inner(
|
||||
self: Arc<Self>,
|
||||
req: Request<::hyper::Body>,
|
||||
@@ -586,6 +607,12 @@ impl Service {
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Serves an HTTP request.
|
||||
/// An error return from this method causes hyper to abruptly drop the
|
||||
/// HTTP connection rather than respond. That's not terribly useful, so this
|
||||
/// method always returns `Ok`. It delegates to a `serve_inner` which is
|
||||
/// allowed to generate `Err` results with the `?` operator, but returns
|
||||
/// them to hyper as `Ok` results.
|
||||
pub async fn serve(
|
||||
self: Arc<Self>,
|
||||
req: Request<::hyper::Body>,
|
||||
@@ -600,7 +627,10 @@ impl Service {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Ok(from_base_error(e)),
|
||||
};
|
||||
Ok(self.serve_inner(req, p, caller).await.unwrap_or_else(|e| e))
|
||||
Ok(self
|
||||
.serve_inner(req, p, caller)
|
||||
.await
|
||||
.unwrap_or_else(|e| e.0))
|
||||
}
|
||||
|
||||
fn top_level(&self, req: &Request<::hyper::Body>, caller: Caller) -> ResponseResult {
|
||||
@@ -619,10 +649,7 @@ impl Service {
|
||||
|
||||
if camera_configs {
|
||||
if !caller.permissions.read_camera_configs {
|
||||
return Err(plain_response(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"read_camera_configs required",
|
||||
));
|
||||
bail_t!(PermissionDenied, "read_camera_configs required");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -756,10 +783,7 @@ impl Service {
|
||||
debug: bool,
|
||||
) -> ResponseResult {
|
||||
if !caller.permissions.view_video {
|
||||
return Err(plain_response(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"view_video required",
|
||||
));
|
||||
bail_t!(PermissionDenied, "view_video required");
|
||||
}
|
||||
let (stream_id, camera_name);
|
||||
{
|
||||
@@ -884,10 +908,11 @@ impl Service {
|
||||
};
|
||||
if let Some(end) = s.end_time {
|
||||
if end > cur_off {
|
||||
return Err(plain_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("end time {} is beyond specified recordings", end),
|
||||
));
|
||||
bail_t!(
|
||||
InvalidArgument,
|
||||
"end time {} is beyond specified recordings",
|
||||
end
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1019,6 +1044,10 @@ impl Service {
|
||||
))
|
||||
}
|
||||
|
||||
/// Returns true iff the client is connected over `https`.
|
||||
/// Moonfire NVR currently doesn't directly serve `https`, but it supports
|
||||
/// proxies which set the `X-Forwarded-Proto` header. See `guide/secure.md`
|
||||
/// for more information.
|
||||
fn is_secure(&self, req: &Request<::hyper::Body>) -> bool {
|
||||
self.trust_forward_hdrs
|
||||
&& req
|
||||
@@ -1124,10 +1153,7 @@ impl Service {
|
||||
|
||||
async fn post_signals(&self, mut req: Request<hyper::Body>, caller: Caller) -> ResponseResult {
|
||||
if !caller.permissions.update_signals {
|
||||
return Err(plain_response(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"update_signals required",
|
||||
));
|
||||
bail_t!(PermissionDenied, "update_signals required");
|
||||
}
|
||||
let r = extract_json_body(&mut req).await?;
|
||||
let r: json::PostSignalsRequest =
|
||||
@@ -1180,6 +1206,18 @@ impl Service {
|
||||
serve_json(req, &signals)
|
||||
}
|
||||
|
||||
/// Authenticates the session (if any) and returns a Caller.
|
||||
///
|
||||
/// If there's no session,
|
||||
/// 1. if `allow_unauthenticated_permissions` is configured, returns okay
|
||||
/// with those permissions.
|
||||
/// 2. if the caller specifies `unauth_path`, returns okay with no
|
||||
/// permissions.
|
||||
/// 3. returns `Unauthenticated` error otherwise.
|
||||
///
|
||||
/// Does no authorization. That is, this doesn't check that the returned
|
||||
/// permissions are sufficient for whatever operation the caller is
|
||||
/// performing.
|
||||
fn authenticate(
|
||||
&self,
|
||||
req: &Request<hyper::Body>,
|
||||
@@ -1188,22 +1226,28 @@ impl Service {
|
||||
if let Some(sid) = extract_sid(req) {
|
||||
let authreq = self.authreq(req);
|
||||
|
||||
// TODO: real error handling! this assumes all errors are due to lack of
|
||||
// authentication, when they could be logic errors in SQL or such.
|
||||
if let Ok((s, u)) = self
|
||||
match self
|
||||
.db
|
||||
.lock()
|
||||
.authenticate_session(authreq.clone(), &sid.hash())
|
||||
{
|
||||
return Ok(Caller {
|
||||
permissions: s.permissions.clone(),
|
||||
session: Some(json::Session {
|
||||
username: u.username.clone(),
|
||||
csrf: s.csrf(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
info!("authenticate_session failed");
|
||||
Ok((s, u)) => {
|
||||
return Ok(Caller {
|
||||
permissions: s.permissions.clone(),
|
||||
session: Some(json::Session {
|
||||
username: u.username.clone(),
|
||||
csrf: s.csrf(),
|
||||
}),
|
||||
})
|
||||
}
|
||||
Err(e) if e.kind() == base::ErrorKind::Unauthenticated => {
|
||||
// Log the specific reason this session is unauthenticated.
|
||||
// Don't let the API client see it, as it may have a
|
||||
// revocation reason that isn't for their eyes.
|
||||
warn!("Session authentication failed: {:?}", &e);
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(s) = self.allow_unauthenticated_permissions.as_ref() {
|
||||
|
||||
Reference in New Issue
Block a user