Merge branch 'master' into new-ui

This commit is contained in:
Scott Lamb
2021-03-12 14:53:15 -08:00
24 changed files with 862 additions and 174 deletions

7
server/Cargo.lock generated
View File

@@ -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",
]

View File

@@ -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"

View File

@@ -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());
};
}

View File

@@ -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]

View File

@@ -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])?;

View File

@@ -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)
}

View File

@@ -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() {

View File

@@ -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()

View File

@@ -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() {