diff --git a/Cargo.lock b/Cargo.lock index 3b24c3d..1b9e8a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,15 +15,6 @@ dependencies = [ "const-random", ] -[[package]] -name = "aho-corasick" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fb5e95d83b38284460a5fda7d6470aa0b8844d283a0b614b8535e880800d2d" -dependencies = [ - "memchr", -] - [[package]] name = "ansi_term" version = "0.9.0" @@ -299,6 +290,7 @@ dependencies = [ "atty", "bitflags", "strsim 0.8.0", + "term_size", "textwrap", "unicode-width", "vec_map", @@ -533,18 +525,6 @@ dependencies = [ "winapi 0.3.8", ] -[[package]] -name = "docopt" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f525a586d310c87df72ebcd98009e57f1cc030c8c268305287a476beb653969" -dependencies = [ - "lazy_static", - "regex", - "serde", - "strsim 0.9.3", -] - [[package]] name = "dtoa" version = "0.4.4" @@ -1075,6 +1055,19 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lexical-core" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7043aa5c05dd34fb73b47acb8c3708eac428de4545ea3682ed2f11293ebd890" +dependencies = [ + "arrayvec 0.4.12", + "cfg-if", + "rustc_version", + "ryu", + "static_assertions", +] + [[package]] name = "libc" version = "0.2.66" @@ -1258,8 +1251,8 @@ dependencies = [ "lazy_static", "libc", "log", + "nom 5.1.1", "parking_lot", - "regex", "time 0.1.42", ] @@ -1288,7 +1281,6 @@ dependencies = [ "prettydiff", "protobuf", "protobuf-codegen-pure", - "regex", "ring", "rusqlite", "smallvec 1.1.0", @@ -1319,7 +1311,6 @@ dependencies = [ "bytes", "cstr", "cursive", - "docopt", "failure", "fnv", "futures", @@ -1338,16 +1329,17 @@ dependencies = [ "moonfire-tflite", "mylog", "nix", + "nom 5.1.1", "parking_lot", "protobuf", "reffers", - "regex", "reqwest", "ring", "rusqlite", "serde", "serde_json", "smallvec 1.1.0", + "structopt 0.3.13", "tempdir", "time 0.1.42", "tokio", @@ -1445,6 +1437,17 @@ dependencies = [ "version_check 0.1.5", ] +[[package]] +name = "nom" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b471253da97532da4b61552249c521e01e736071f71c1a4f7ebbfbf0a06aad6" +dependencies = [ + "lexical-core", + "memchr", + "version_check 0.9.1", +] + [[package]] name = "num" version = "0.2.1" @@ -1672,7 +1675,7 @@ checksum = "5240be0c9ea1bc7887819a36264cb9475eb71c58749808e5b989c8c1fdc67acf" dependencies = [ "ansi_term 0.9.0", "prettytable-rs", - "structopt", + "structopt 0.2.18", ] [[package]] @@ -1689,6 +1692,32 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "proc-macro-error" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98e9e4b82e0ef281812565ea4751049f1bdcdfccda7d3f459f2e138a40c08678" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2 1.0.8", + "quote 1.0.2", + "syn 1.0.14", + "version_check 0.9.1", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f5444ead4e9935abd7f27dc51f7e852a0569ac888096d5ec2499470794e2e53" +dependencies = [ + "proc-macro2 1.0.8", + "quote 1.0.2", + "syn 1.0.14", + "syn-mid", + "version_check 0.9.1", +] + [[package]] name = "proc-macro-hack" version = "0.5.11" @@ -1912,18 +1941,6 @@ dependencies = [ "stable_deref_trait", ] -[[package]] -name = "regex" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5508c1941e4e7cb19965abef075d35a9a8b5cdf0846f30b4050e9b55dc55e87" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", - "thread_local", -] - [[package]] name = "regex-automata" version = "0.1.8" @@ -1933,12 +1950,6 @@ dependencies = [ "byteorder", ] -[[package]] -name = "regex-syntax" -version = "0.6.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e734e891f5b408a29efbf8309e656876276f49ab6a6ac208600b4419bd893d90" - [[package]] name = "remove_dir_all" version = "0.5.2" @@ -2331,6 +2342,12 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8" +[[package]] +name = "static_assertions" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f3eb36b47e512f8f1c9e3d10c2c1965bc992bd9cdb024fa581e2194501c83d3" + [[package]] name = "strsim" version = "0.8.0" @@ -2350,7 +2367,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16c2cdbf9cc375f15d1b4141bc48aeef444806655cd0e904207edc8d68d86ed7" dependencies = [ "clap", - "structopt-derive", + "structopt-derive 0.2.18", +] + +[[package]] +name = "structopt" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6da2e8d107dfd7b74df5ef4d205c6aebee0706c647f6bc6a2d5789905c00fb" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive 0.4.6", ] [[package]] @@ -2365,6 +2393,19 @@ dependencies = [ "syn 0.15.44", ] +[[package]] +name = "structopt-derive" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a489c87c08fbaf12e386665109dd13470dcc9c4583ea3e10dd2b4523e5ebd9ac" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2 1.0.8", + "quote 1.0.2", + "syn 1.0.14", +] + [[package]] name = "subtle" version = "1.0.0" @@ -2393,6 +2434,17 @@ dependencies = [ "unicode-xid 0.2.0", ] +[[package]] +name = "syn-mid" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" +dependencies = [ + "proc-macro2 1.0.8", + "quote 1.0.2", + "syn 1.0.14", +] + [[package]] name = "synstructure" version = "0.12.3" @@ -2457,18 +2509,10 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" dependencies = [ + "term_size", "unicode-width", ] -[[package]] -name = "thread_local" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" -dependencies = [ - "lazy_static", -] - [[package]] name = "time" version = "0.1.42" @@ -2866,7 +2910,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bb43f70885151e629e2a19ce9e50bd730fd436cfd4b666894c9ce4de9141164" dependencies = [ - "nom", + "nom 4.2.3", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 4200e62..8f8da7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,6 @@ byteorder = "1.0" cstr = "0.1.7" cursive = "0.14.0" db = { package = "moonfire-db", path = "db" } -docopt = "1.0" failure = "0.1.1" ffmpeg = { package = "moonfire-ffmpeg", git = "https://github.com/scottlamb/moonfire-ffmpeg" } futures = "0.3" @@ -42,18 +41,19 @@ libc = "0.2" log = { version = "0.4", features = ["release_max_level_info"] } memchr = "2.0.2" memmap = "0.7" +moonfire-tflite = { git = "https://github.com/scottlamb/moonfire-tflite", features = ["edgetpu"], optional = true } mylog = { git = "https://github.com/scottlamb/mylog" } nix = "0.16.1" +nom = "5.1.1" parking_lot = { version = "0.9", features = [] } protobuf = { git = "https://github.com/stepancheg/rust-protobuf" } reffers = "0.6.0" -regex = "1.0" ring = "0.14.6" rusqlite = "0.21.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" smallvec = "1.0" -moonfire-tflite = { git = "https://github.com/scottlamb/moonfire-tflite", features = ["edgetpu"], optional = true } +structopt = { version = "0.3.13", features = ["default", "wrap_help"] } time = "0.1" tokio = { version = "0.2.0", features = ["blocking", "macros", "rt-threaded", "signal"] } tokio-tungstenite = "0.10.1" diff --git a/base/Cargo.toml b/base/Cargo.toml index 790a6cc..c759c39 100644 --- a/base/Cargo.toml +++ b/base/Cargo.toml @@ -17,5 +17,5 @@ lazy_static = "1.0" libc = "0.2" log = "0.4" parking_lot = { version = "0.9", features = [] } -regex = "1.0" +nom = "5.1.1" time = "0.1" diff --git a/base/lib.rs b/base/lib.rs index be3e65e..c5c38f9 100644 --- a/base/lib.rs +++ b/base/lib.rs @@ -29,6 +29,7 @@ // along with this program. If not, see . pub mod clock; +pub mod time; mod error; pub mod strutil; diff --git a/base/strutil.rs b/base/strutil.rs index de48c0b..24ddb90 100644 --- a/base/strutil.rs +++ b/base/strutil.rs @@ -28,10 +28,13 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use lazy_static::lazy_static; -use regex::Regex; +use nom::IResult; +use nom::branch::alt; +use nom::bytes::complete::{tag, take_while1}; +use nom::character::complete::space0; +use nom::combinator::{map, map_res, opt}; +use nom::sequence::{delimited, tuple}; use std::fmt::Write as _; -use std::str::FromStr as _; static MULTIPLIERS: [(char, u64); 4] = [ // (suffix character, power of 2) @@ -58,32 +61,33 @@ pub fn encode_size(mut raw: i64) -> String { encoded } +fn decode_sizepart(input: &str) -> IResult<&str, i64> { + map( + tuple(( + map_res(take_while1(|c: char| c.is_ascii_digit()), + |input: &str| i64::from_str_radix(input, 10)), + opt(alt(( + nom::combinator::value(1<<40, tag("T")), + nom::combinator::value(1<<30, tag("G")), + nom::combinator::value(1<<20, tag("M")), + nom::combinator::value(1<<10, tag("K")) + ))) + )), + |(n, opt_unit)| n * opt_unit.unwrap_or(1) + )(input) +} + +fn decode_size_internal(input: &str) -> IResult<&str, i64> { + nom::multi::fold_many1( + delimited(space0, decode_sizepart, space0), + 0, + |sum, i| sum + i)(input) +} + /// Decodes a human-readable size as output by encode_size. pub fn decode_size(encoded: &str) -> Result { - let mut decoded = 0i64; - lazy_static! { - static ref RE: Regex = Regex::new(r"\s*([0-9]+)([TGMK])?,?\s*").unwrap(); - } - let mut last_pos = 0; - for cap in RE.captures_iter(encoded) { - let whole_cap = cap.get(0).unwrap(); - if whole_cap.start() > last_pos { - return Err(()); - } - last_pos = whole_cap.end(); - let mut piece = i64::from_str(&cap[1]).map_err(|_| ())?; - if let Some(m) = cap.get(2) { - let m = m.as_str().as_bytes()[0] as char; - for &(some_m, n) in &MULTIPLIERS { - if some_m == m { - piece *= 1i64<. + +//! Time and durations for Moonfire NVR's internal format. + +use failure::{Error, bail, format_err}; +use nom::branch::alt; +use nom::bytes::complete::{tag, take_while_m_n}; +use nom::combinator::{map, map_res, opt}; +use nom::sequence::{preceded, tuple}; +use std::ops; +use std::fmt; +use std::str::FromStr; +use time; + +type IResult<'a, I, O> = nom::IResult>; + +pub const TIME_UNITS_PER_SEC: i64 = 90_000; + +/// A time specified as 90,000ths of a second since 1970-01-01 00:00:00 UTC. +#[derive(Clone, Copy, Default, Eq, Ord, PartialEq, PartialOrd)] +pub struct Time(pub i64); + +/// Returns a parser for a `len`-digit non-negative number which fits into an i32. +fn fixed_len_num<'a>(len: usize) -> impl Fn(&'a str) -> IResult<&'a str, i32> { + map_res(take_while_m_n(len, len, |c: char| c.is_ascii_digit()), + |input: &str| i32::from_str_radix(input, 10)) +} + +/// Parses `YYYY-mm-dd` into pieces. +fn parse_datepart(input: &str) -> IResult<&str, (i32, i32, i32)> { + tuple(( + fixed_len_num(4), + preceded(tag("-"), fixed_len_num(2)), + preceded(tag("-"), fixed_len_num(2)) + ))(input) +} + +/// Parses `HH:MM[:SS[:FFFFF]]` into pieces. +fn parse_timepart(input: &str) -> IResult<&str, (i32, i32, i32, i32)> { + let (input, (hr, _, min)) = tuple((fixed_len_num(2), tag(":"), fixed_len_num(2)))(input)?; + let (input, stuff) = opt(tuple(( + preceded(tag(":"), fixed_len_num(2)), + opt(preceded(tag(":"), fixed_len_num(5))) + )))(input)?; + let (sec, opt_subsec) = stuff.unwrap_or((0, None)); + Ok((input, (hr, min, sec, opt_subsec.unwrap_or(0)))) +} + +/// Parses `Z` (UTC) or `{+,-,}HH:MM` into a time zone offset in seconds. +fn parse_zone(input: &str) -> IResult<&str, i32> { + alt(( + nom::combinator::value(0, tag("Z")), + map( + tuple(( + opt(nom::character::complete::one_of(&b"+-"[..])), + fixed_len_num(2), + tag(":"), + fixed_len_num(2) + )), + |(sign, hr, _, min)| { + let off = hr * 3600 + min * 60; + if sign == Some('-') { off } else { -off } + }) + ))(input) +} + +impl Time { + pub fn new(tm: time::Timespec) -> Self { + Time(tm.sec * TIME_UNITS_PER_SEC + tm.nsec as i64 * TIME_UNITS_PER_SEC / 1_000_000_000) + } + + pub const fn min_value() -> Self { Time(i64::min_value()) } + pub const fn max_value() -> Self { Time(i64::max_value()) } + + /// Parses a time as either 90,000ths of a second since epoch or a RFC 3339-like string. + /// + /// The former is 90,000ths of a second since 1970-01-01T00:00:00 UTC, excluding leap seconds. + /// + /// The latter is a date such as `2006-01-02T15:04:05`, followed by an optional 90,000ths of + /// a second such as `:00001`, followed by an optional time zone offset such as `Z` or + /// `-07:00`. A missing fraction is assumed to be 0. A missing time zone offset implies the + /// local time zone. + pub fn parse(input: &str) -> Result { + // First try parsing as 90,000ths of a second since epoch. + match i64::from_str(input) { + Ok(i) => return Ok(Time(i)), + Err(_) => {}, + } + + // If that failed, parse as a time string or bust. + let (remaining, ((tm_year, tm_mon, tm_mday), opt_time, opt_zone)) = + tuple((parse_datepart, + opt(preceded(tag("T"), parse_timepart)), + opt(parse_zone)))(input) + .map_err(|e| match e { + nom::Err::Incomplete(_) => format_err!("incomplete"), + nom::Err::Error(e) | nom::Err::Failure(e) => { + format_err!("{}", nom::error::convert_error(input, e)) + } + })?; + if remaining != "" { + bail!("unexpected suffix {:?} following time string", remaining); + } + let (tm_hour, tm_min, tm_sec, subsec) = opt_time.unwrap_or((0, 0, 0, 0)); + let mut tm = time::Tm { + tm_sec, + tm_min, + tm_hour, + tm_mday, + tm_mon, + tm_year, + tm_wday: 0, + tm_yday: 0, + tm_isdst: -1, + tm_utcoff: 0, + tm_nsec: 0, + }; + if tm.tm_mon == 0 { + bail!("time {:?} has month 0", input); + } + tm.tm_mon -= 1; + if tm.tm_year < 1900 { + bail!("time {:?} has year before 1900", input); + } + tm.tm_year -= 1900; + + // The time crate doesn't use tm_utcoff properly; it just calls timegm() if tm_utcoff == 0, + // mktime() otherwise. If a zone is specified, use the timegm path and a manual offset. + // If no zone is specified, use the tm_utcoff path. This is pretty lame, but follow the + // chrono crate's lead and just use 0 or 1 to choose between these functions. + let sec = if let Some(off) = opt_zone { + tm.to_timespec().sec + i64::from(off) + } else { + tm.tm_utcoff = 1; + tm.to_timespec().sec + }; + Ok(Time(sec * TIME_UNITS_PER_SEC + i64::from(subsec))) + } + + /// Convert to unix seconds by floor method (rounding down). + pub fn unix_seconds(&self) -> i64 { self.0 / TIME_UNITS_PER_SEC } +} + +impl std::str::FromStr for Time { + type Err = Error; + + fn from_str(s: &str) -> Result { Self::parse(s) } +} + +impl ops::Sub for Time { + type Output = Duration; + fn sub(self, rhs: Time) -> Duration { Duration(self.0 - rhs.0) } +} + +impl ops::AddAssign for Time { + fn add_assign(&mut self, rhs: Duration) { self.0 += rhs.0 } +} + +impl ops::Add for Time { + type Output = Time; + fn add(self, rhs: Duration) -> Time { Time(self.0 + rhs.0) } +} + +impl ops::Sub for Time { + type Output = Time; + fn sub(self, rhs: Duration) -> Time { Time(self.0 - rhs.0) } +} + +impl fmt::Debug for Time { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Write both the raw and display forms. + write!(f, "{} /* {} */", self.0, self) + } +} + +impl fmt::Display for Time { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let tm = time::at(time::Timespec{sec: self.0 / TIME_UNITS_PER_SEC, nsec: 0}); + let zone_minutes = tm.tm_utcoff.abs() / 60; + write!(f, "{}:{:05}{}{:02}:{:02}", tm.strftime("%FT%T").or_else(|_| Err(fmt::Error))?, + self.0 % TIME_UNITS_PER_SEC, + if tm.tm_utcoff > 0 { '+' } else { '-' }, zone_minutes / 60, zone_minutes % 60) + } +} + +/// A duration specified in 1/90,000ths of a second. +/// Durations are typically non-negative, but a `moonfire_db::db::CameraDayValue::duration` may be +/// negative. +#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)] +pub struct Duration(pub i64); + +impl Duration { + pub fn to_tm_duration(&self) -> time::Duration { + time::Duration::nanoseconds(self.0 * 100000 / 9) + } +} + +impl fmt::Display for Duration { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut seconds = self.0 / TIME_UNITS_PER_SEC; + const MINUTE_IN_SECONDS: i64 = 60; + const HOUR_IN_SECONDS: i64 = 60 * MINUTE_IN_SECONDS; + const DAY_IN_SECONDS: i64 = 24 * HOUR_IN_SECONDS; + let days = seconds / DAY_IN_SECONDS; + seconds %= DAY_IN_SECONDS; + let hours = seconds / HOUR_IN_SECONDS; + seconds %= HOUR_IN_SECONDS; + let minutes = seconds / MINUTE_IN_SECONDS; + seconds %= MINUTE_IN_SECONDS; + let mut have_written = if days > 0 { + write!(f, "{} day{}", days, if days == 1 { "" } else { "s" })?; + true + } else { + false + }; + if hours > 0 { + write!(f, "{}{} hour{}", if have_written { " " } else { "" }, + hours, if hours == 1 { "" } else { "s" })?; + have_written = true; + } + if minutes > 0 { + write!(f, "{}{} minute{}", if have_written { " " } else { "" }, + minutes, if minutes == 1 { "" } else { "s" })?; + have_written = true; + } + if seconds > 0 || !have_written { + write!(f, "{}{} second{}", if have_written { " " } else { "" }, + seconds, if seconds == 1 { "" } else { "s" })?; + } + Ok(()) + } +} + +impl ops::Add for Duration { + type Output = Duration; + fn add(self, rhs: Duration) -> Duration { Duration(self.0 + rhs.0) } +} + +impl ops::AddAssign for Duration { + fn add_assign(&mut self, rhs: Duration) { self.0 += rhs.0 } +} + +impl ops::SubAssign for Duration { + fn sub_assign(&mut self, rhs: Duration) { self.0 -= rhs.0 } +} + +#[cfg(test)] +mod tests { + use super::{Duration, Time, TIME_UNITS_PER_SEC}; + + #[test] + fn test_parse_time() { + std::env::set_var("TZ", "America/Los_Angeles"); + time::tzset(); + let tests = &[ + ("2006-01-02T15:04:05-07:00", 102261550050000), + ("2006-01-02T15:04:05:00001-07:00", 102261550050001), + ("2006-01-02T15:04:05-08:00", 102261874050000), + ("2006-01-02T15:04:05", 102261874050000), // implied -08:00 + ("2006-01-02T15:04", 102261873600000), // implied -08:00 + ("2006-01-02T15:04:05:00001", 102261874050001), // implied -08:00 + ("2006-01-02T15:04:05-00:00", 102259282050000), + ("2006-01-02T15:04:05Z", 102259282050000), + ("2006-01-02-08:00", 102256992000000), // implied -08:00 + ("2006-01-02", 102256992000000), // implied -08:00 + ("2006-01-02Z", 102254400000000), + ("102261550050000", 102261550050000), + ]; + for test in tests { + assert_eq!(test.1, Time::parse(test.0).unwrap().0, "parsing {}", test.0); + } + } + + #[test] + fn test_format_time() { + std::env::set_var("TZ", "America/Los_Angeles"); + time::tzset(); + assert_eq!("2006-01-02T15:04:05:00000-08:00", format!("{}", Time(102261874050000))); + } + + #[test] + fn test_display_duration() { + let tests = &[ + // (output, seconds) + ("0 seconds", 0), + ("1 second", 1), + ("1 minute", 60), + ("1 minute 1 second", 61), + ("2 minutes", 120), + ("1 hour", 3600), + ("1 hour 1 minute", 3660), + ("2 hours", 7200), + ("1 day", 86400), + ("1 day 1 hour", 86400 + 3600), + ("2 days", 2 * 86400), + ]; + for test in tests { + assert_eq!(test.0, format!("{}", Duration(test.1 * TIME_UNITS_PER_SEC))); + } + } +} diff --git a/db/Cargo.toml b/db/Cargo.toml index 3f39976..df7ac26 100644 --- a/db/Cargo.toml +++ b/db/Cargo.toml @@ -31,7 +31,6 @@ odds = { version = "0.3.1", features = ["std-vec"] } parking_lot = { version = "0.9", features = [] } prettydiff = "0.3.1" protobuf = { git = "https://github.com/stepancheg/rust-protobuf" } -regex = "1.0" ring = "0.14.6" rusqlite = "0.21.0" smallvec = "1.0" diff --git a/db/auth.rs b/db/auth.rs index 555e2a3..2970674 100644 --- a/db/auth.rs +++ b/db/auth.rs @@ -42,6 +42,7 @@ use rusqlite::{Connection, Transaction, params}; use std::collections::BTreeMap; use std::fmt; use std::net::IpAddr; +use std::str::FromStr; use std::sync::Arc; lazy_static! { @@ -57,7 +58,7 @@ pub(crate) fn set_test_config() { Arc::new(libpasta::Config::with_primitive(libpasta::primitives::Bcrypt::new(2))); } -enum UserFlags { +enum UserFlag { Disabled = 1, } @@ -91,7 +92,7 @@ impl User { } pub fn has_password(&self) -> bool { self.password_hash.is_some() } - fn disabled(&self) -> bool { (self.flags & UserFlags::Disabled as i32) != 0 } + fn disabled(&self) -> bool { (self.flags & UserFlag::Disabled as i32) != 0 } } /// A change to a user. @@ -132,7 +133,7 @@ impl UserChange { } pub fn disable(&mut self) { - self.flags |= UserFlags::Disabled as i32; + self.flags |= UserFlag::Disabled as i32; } } @@ -194,13 +195,29 @@ impl rusqlite::types::FromSql for FromSqlIpAddr { } } -pub enum SessionFlags { +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(i32)] +pub enum SessionFlag { HttpOnly = 1, Secure = 2, SameSite = 4, SameSiteStrict = 8, } +impl FromStr for SessionFlag { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "http-only" => Ok(Self::HttpOnly), + "secure" => Ok(Self::Secure), + "same-site" => Ok(Self::SameSite), + "same-site-strict" => Ok(Self::SameSiteStrict), + _ => bail!("No such session flag {:?}", s), + } + } +} + #[derive(Copy, Clone)] pub enum RevocationReason { LoggedOut = 1, @@ -210,7 +227,7 @@ pub enum RevocationReason { #[derive(Debug, Default)] pub struct Session { user_id: i32, - flags: i32, // bitmask of SessionFlags enum values + flags: i32, // bitmask of SessionFlag enum values domain: Option>, description: Option, seed: Seed, diff --git a/db/dir.rs b/db/dir.rs index 0772001..c905e8b 100644 --- a/db/dir.rs +++ b/db/dir.rs @@ -41,7 +41,7 @@ use log::warn; use protobuf::Message; use nix::{NixPath, fcntl::{FlockArg, OFlag}, sys::stat::Mode}; use nix::sys::statvfs::Statvfs; -use std::ffi::{CStr, CString}; +use std::ffi::CStr; use std::fs; use std::io::{Read, Write}; use std::os::unix::io::{AsRawFd, RawFd}; @@ -104,16 +104,14 @@ impl Drop for Fd { impl Fd { /// Opens the given path as a directory. - pub fn open(path: &str, mkdir: bool) -> Result { - let cstring = CString::new(path).map_err(|_| nix::Error::InvalidPath)?; + pub fn open(path: &P, mkdir: bool) -> Result { if mkdir { - match nix::unistd::mkdir(cstring.as_c_str(), nix::sys::stat::Mode::S_IRWXU) { + match nix::unistd::mkdir(path, nix::sys::stat::Mode::S_IRWXU) { Ok(()) | Err(nix::Error::Sys(nix::errno::Errno::EEXIST)) => {}, Err(e) => return Err(e), } } - let fd = nix::fcntl::open(cstring.as_c_str(), OFlag::O_DIRECTORY | OFlag::O_RDONLY, - Mode::empty())?; + let fd = nix::fcntl::open(path, OFlag::O_DIRECTORY | OFlag::O_RDONLY, Mode::empty())?; Ok(Fd(fd)) } diff --git a/db/recording.rs b/db/recording.rs index efc55bd..c73cafe 100644 --- a/db/recording.rs +++ b/db/recording.rs @@ -30,199 +30,17 @@ use crate::coding::{append_varint32, decode_varint32, unzigzag32, zigzag32}; use crate::db; -use failure::{Error, bail, format_err}; -use lazy_static::lazy_static; +use failure::{Error, bail}; use log::trace; -use regex::Regex; -use std::ops; -use std::fmt; use std::ops::Range; -use std::str::FromStr; -use time; -pub const TIME_UNITS_PER_SEC: i64 = 90000; +pub use base::time::TIME_UNITS_PER_SEC; + pub const DESIRED_RECORDING_DURATION: i64 = 60 * TIME_UNITS_PER_SEC; pub const MAX_RECORDING_DURATION: i64 = 5 * 60 * TIME_UNITS_PER_SEC; -/// A time specified as 90,000ths of a second since 1970-01-01 00:00:00 UTC. -#[derive(Clone, Copy, Default, Eq, Ord, PartialEq, PartialOrd)] -pub struct Time(pub i64); - -impl Time { - pub fn new(tm: time::Timespec) -> Self { - Time(tm.sec * TIME_UNITS_PER_SEC + tm.nsec as i64 * TIME_UNITS_PER_SEC / 1_000_000_000) - } - - pub const fn min_value() -> Self { Time(i64::min_value()) } - pub const fn max_value() -> Self { Time(i64::max_value()) } - - /// Parses a time as either 90,000ths of a second since epoch or a RFC 3339-like string. - /// - /// The former is 90,000ths of a second since 1970-01-01T00:00:00 UTC, excluding leap seconds. - /// - /// The latter is a string such as `2006-01-02T15:04:05`, followed by an optional 90,000ths of - /// a second such as `:00001`, followed by an optional time zone offset such as `Z` or - /// `-07:00`. A missing fraction is assumed to be 0. A missing time zone offset implies the - /// local time zone. - pub fn parse(s: &str) -> Result { - lazy_static! { - static ref RE: Regex = Regex::new(r#"(?x) - ^ - ([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2}) - (?::([0-9]{5}))? - (Z|[+-]([0-9]{2}):([0-9]{2}))? - $"#).unwrap(); - } - - // First try parsing as 90,000ths of a second since epoch. - match i64::from_str(s) { - Ok(i) => return Ok(Time(i)), - Err(_) => {}, - } - - // If that failed, parse as a time string or bust. - let c = RE.captures(s).ok_or_else(|| format_err!("unparseable time {:?}", s))?; - let mut tm = time::Tm{ - tm_sec: i32::from_str(c.get(6).unwrap().as_str()).unwrap(), - tm_min: i32::from_str(c.get(5).unwrap().as_str()).unwrap(), - tm_hour: i32::from_str(c.get(4).unwrap().as_str()).unwrap(), - tm_mday: i32::from_str(c.get(3).unwrap().as_str()).unwrap(), - tm_mon: i32::from_str(c.get(2).unwrap().as_str()).unwrap(), - tm_year: i32::from_str(c.get(1).unwrap().as_str()).unwrap(), - tm_wday: 0, - tm_yday: 0, - tm_isdst: -1, - tm_utcoff: 0, - tm_nsec: 0, - }; - if tm.tm_mon == 0 { - bail!("time {:?} has month 0", s); - } - tm.tm_mon -= 1; - if tm.tm_year < 1900 { - bail!("time {:?} has year before 1900", s); - } - tm.tm_year -= 1900; - - // The time crate doesn't use tm_utcoff properly; it just calls timegm() if tm_utcoff == 0, - // mktime() otherwise. If a zone is specified, use the timegm path and a manual offset. - // If no zone is specified, use the tm_utcoff path. This is pretty lame, but follow the - // chrono crate's lead and just use 0 or 1 to choose between these functions. - let sec = if let Some(zone) = c.get(8) { - tm.to_timespec().sec + if zone.as_str() == "Z" { - 0 - } else { - let off = i64::from_str(c.get(9).unwrap().as_str()).unwrap() * 3600 + - i64::from_str(c.get(10).unwrap().as_str()).unwrap() * 60; - if zone.as_str().as_bytes()[0] == b'-' { off } else { -off } - } - } else { - tm.tm_utcoff = 1; - tm.to_timespec().sec - }; - let fraction = if let Some(f) = c.get(7) { i64::from_str(f.as_str()).unwrap() } else { 0 }; - Ok(Time(sec * TIME_UNITS_PER_SEC + fraction)) - } - - /// Convert to unix seconds by floor method (rounding down). - pub fn unix_seconds(&self) -> i64 { self.0 / TIME_UNITS_PER_SEC } -} - -impl ops::Sub for Time { - type Output = Duration; - fn sub(self, rhs: Time) -> Duration { Duration(self.0 - rhs.0) } -} - -impl ops::AddAssign for Time { - fn add_assign(&mut self, rhs: Duration) { self.0 += rhs.0 } -} - -impl ops::Add for Time { - type Output = Time; - fn add(self, rhs: Duration) -> Time { Time(self.0 + rhs.0) } -} - -impl ops::Sub for Time { - type Output = Time; - fn sub(self, rhs: Duration) -> Time { Time(self.0 - rhs.0) } -} - -impl fmt::Debug for Time { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // Write both the raw and display forms. - write!(f, "{} /* {} */", self.0, self) - } -} - -impl fmt::Display for Time { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let tm = time::at(time::Timespec{sec: self.0 / TIME_UNITS_PER_SEC, nsec: 0}); - let zone_minutes = tm.tm_utcoff.abs() / 60; - write!(f, "{}:{:05}{}{:02}:{:02}", tm.strftime("%FT%T").or_else(|_| Err(fmt::Error))?, - self.0 % TIME_UNITS_PER_SEC, - if tm.tm_utcoff > 0 { '+' } else { '-' }, zone_minutes / 60, zone_minutes % 60) - } -} - -/// A duration specified in 1/90,000ths of a second. -/// Durations are typically non-negative, but a `db::CameraDayValue::duration` may be negative. -#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)] -pub struct Duration(pub i64); - -impl Duration { - pub fn to_tm_duration(&self) -> time::Duration { - time::Duration::nanoseconds(self.0 * 100000 / 9) - } -} - -impl fmt::Display for Duration { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let mut seconds = self.0 / TIME_UNITS_PER_SEC; - const MINUTE_IN_SECONDS: i64 = 60; - const HOUR_IN_SECONDS: i64 = 60 * MINUTE_IN_SECONDS; - const DAY_IN_SECONDS: i64 = 24 * HOUR_IN_SECONDS; - let days = seconds / DAY_IN_SECONDS; - seconds %= DAY_IN_SECONDS; - let hours = seconds / HOUR_IN_SECONDS; - seconds %= HOUR_IN_SECONDS; - let minutes = seconds / MINUTE_IN_SECONDS; - seconds %= MINUTE_IN_SECONDS; - let mut have_written = if days > 0 { - write!(f, "{} day{}", days, if days == 1 { "" } else { "s" })?; - true - } else { - false - }; - if hours > 0 { - write!(f, "{}{} hour{}", if have_written { " " } else { "" }, - hours, if hours == 1 { "" } else { "s" })?; - have_written = true; - } - if minutes > 0 { - write!(f, "{}{} minute{}", if have_written { " " } else { "" }, - minutes, if minutes == 1 { "" } else { "s" })?; - have_written = true; - } - if seconds > 0 || !have_written { - write!(f, "{}{} second{}", if have_written { " " } else { "" }, - seconds, if seconds == 1 { "" } else { "s" })?; - } - Ok(()) - } -} - -impl ops::Add for Duration { - type Output = Duration; - fn add(self, rhs: Duration) -> Duration { Duration(self.0 + rhs.0) } -} - -impl ops::AddAssign for Duration { - fn add_assign(&mut self, rhs: Duration) { self.0 += rhs.0 } -} - -impl ops::SubAssign for Duration { - fn sub_assign(&mut self, rhs: Duration) { self.0 -= rhs.0 } -} +pub use base::time::Time; +pub use base::time::Duration; /// An iterator through a sample index. /// Initially invalid; call `next()` before each read. @@ -533,52 +351,6 @@ mod tests { use super::*; use crate::testutil::{self, TestDb}; - #[test] - fn test_parse_time() { - testutil::init(); - let tests = &[ - ("2006-01-02T15:04:05-07:00", 102261550050000), - ("2006-01-02T15:04:05:00001-07:00", 102261550050001), - ("2006-01-02T15:04:05-08:00", 102261874050000), - ("2006-01-02T15:04:05", 102261874050000), // implied -08:00 - ("2006-01-02T15:04:05:00001", 102261874050001), // implied -08:00 - ("2006-01-02T15:04:05-00:00", 102259282050000), - ("2006-01-02T15:04:05Z", 102259282050000), - ("102261550050000", 102261550050000), - ]; - for test in tests { - assert_eq!(test.1, Time::parse(test.0).unwrap().0, "parsing {}", test.0); - } - } - - #[test] - fn test_format_time() { - testutil::init(); - assert_eq!("2006-01-02T15:04:05:00000-08:00", format!("{}", Time(102261874050000))); - } - - #[test] - fn test_display_duration() { - testutil::init(); - let tests = &[ - // (output, seconds) - ("0 seconds", 0), - ("1 second", 1), - ("1 minute", 60), - ("1 minute 1 second", 61), - ("2 minutes", 120), - ("1 hour", 3600), - ("1 hour 1 minute", 3660), - ("2 hours", 7200), - ("1 day", 86400), - ("1 day 1 hour", 86400 + 3600), - ("2 days", 2 * 86400), - ]; - for test in tests { - assert_eq!(test.0, format!("{}", Duration(test.1 * TIME_UNITS_PER_SEC))); - } - } - /// Tests encoding the example from design/schema.md. #[test] fn test_encode_example() { diff --git a/db/upgrade/mod.rs b/db/upgrade/mod.rs index 2a916cd..22cd8b7 100644 --- a/db/upgrade/mod.rs +++ b/db/upgrade/mod.rs @@ -53,9 +53,9 @@ const UPGRADE_NOTES: &'static str = #[derive(Debug)] pub struct Args<'a> { - pub flag_sample_file_dir: Option<&'a str>, - pub flag_preset_journal: &'a str, - pub flag_no_vacuum: bool, + pub sample_file_dir: Option<&'a std::path::Path>, + pub preset_journal: &'a str, + pub no_vacuum: bool, } fn set_journal_mode(conn: &rusqlite::Connection, requested: &str) -> Result<(), Error> { @@ -88,7 +88,7 @@ fn upgrade(args: &Args, target_ver: i32, conn: &mut rusqlite::Connection) -> Res bail!("Database is at negative version {}!", old_ver); } info!("Upgrading database from version {} to version {}...", old_ver, target_ver); - set_journal_mode(&conn, args.flag_preset_journal)?; + set_journal_mode(&conn, args.preset_journal)?; for ver in old_ver .. target_ver { info!("...from version {} to version {}", ver, ver + 1); let tx = conn.transaction()?; @@ -120,7 +120,7 @@ pub fn run(args: &Args, conn: &mut rusqlite::Connection) -> Result<(), Error> { // WAL is the preferred journal mode for normal operation; it reduces the number of syncs // without compromising safety. set_journal_mode(&conn, "wal")?; - if !args.flag_no_vacuum { + if !args.no_vacuum { info!("...vacuuming database after upgrade."); conn.execute_batch(r#" pragma page_size = 16384; @@ -159,7 +159,7 @@ impl NixPath for UuidPath { mod tests { use crate::compare; use crate::testutil; - use failure::{ResultExt, format_err}; + use failure::ResultExt; use fnv::FnvHashMap; use super::*; @@ -209,7 +209,7 @@ mod tests { fn upgrade_and_compare() -> Result<(), Error> { testutil::init(); let tmpdir = tempdir::TempDir::new("moonfire-nvr-test")?; - let path = tmpdir.path().to_str().ok_or_else(|| format_err!("invalid UTF-8"))?.to_owned(); + //let path = tmpdir.path().to_str().ok_or_else(|| format_err!("invalid UTF-8"))?.to_owned(); let mut upgraded = new_conn()?; upgraded.execute_batch(include_str!("v0.sql"))?; upgraded.execute_batch(r#" @@ -252,9 +252,9 @@ mod tests { (5, Some(include_str!("v5.sql"))), (6, Some(include_str!("../schema.sql")))] { upgrade(&Args { - flag_sample_file_dir: Some(&path), - flag_preset_journal: "delete", - flag_no_vacuum: false, + sample_file_dir: Some(&tmpdir.path()), + preset_journal: "delete", + no_vacuum: false, }, *ver, &mut upgraded).context(format!("upgrading to version {}", ver))?; if let Some(f) = fresh_sql { compare(&upgraded, *ver, f)?; diff --git a/db/upgrade/v1_to_v2.rs b/db/upgrade/v1_to_v2.rs index 9cc2649..cc59923 100644 --- a/db/upgrade/v1_to_v2.rs +++ b/db/upgrade/v1_to_v2.rs @@ -42,7 +42,7 @@ use uuid::Uuid; pub fn run(args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error> { let sample_file_path = - args.flag_sample_file_dir + args.sample_file_dir .ok_or_else(|| format_err!("--sample-file-dir required when upgrading from \ schema version 1 to 2."))?; @@ -122,6 +122,9 @@ pub fn run(args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error> } dir::write_meta(d.as_raw_fd(), &meta)?; + let sample_file_path = sample_file_path.to_str() + .ok_or_else(|| format_err!("sample file dir {} is not a valid string", + sample_file_path.display()))?; tx.execute(r#" insert into sample_file_dir (path, uuid, last_complete_open_id) values (?, ?, ?) @@ -293,7 +296,7 @@ pub fn run(args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error> /// * optional: reserved sample file uuids. /// * optional: meta and meta-tmp from half-completed update attempts. /// * forbidden: anything else. -fn verify_dir_contents(sample_file_path: &str, dir: &mut nix::dir::Dir, +fn verify_dir_contents(sample_file_path: &std::path::Path, dir: &mut nix::dir::Dir, tx: &rusqlite::Transaction) -> Result<(), Error> { // Build a hash of the uuids found in the directory. let n: i64 = tx.query_row(r#" @@ -337,7 +340,7 @@ fn verify_dir_contents(sample_file_path: &str, dir: &mut nix::dir::Dir, while let Some(row) = rows.next()? { let uuid: crate::db::FromSqlUuid = row.get(0)?; if !files.remove(&uuid.0) { - bail!("{} is missing from dir {}!", uuid.0, sample_file_path); + bail!("{} is missing from dir {}!", uuid.0, sample_file_path.display()); } } } @@ -360,7 +363,7 @@ fn verify_dir_contents(sample_file_path: &str, dir: &mut nix::dir::Dir, if !files.is_empty() { bail!("{} unexpected sample file uuids in dir {}: {:?}!", - files.len(), sample_file_path, files); + files.len(), sample_file_path.display(), files); } Ok(()) } diff --git a/src/cmds/check.rs b/src/cmds/check.rs index df64276..3619cba 100644 --- a/src/cmds/check.rs +++ b/src/cmds/check.rs @@ -1,5 +1,5 @@ // This file is part of Moonfire NVR, a security camera network video recorder. -// Copyright (C) 2018 The Moonfire NVR Authors +// Copyright (C) 2018-2020 The Moonfire NVR Authors // // 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 @@ -32,36 +32,25 @@ use db::check; use failure::Error; -use serde::Deserialize; +use std::path::PathBuf; +use structopt::StructOpt; -static USAGE: &'static str = r#" -Checks database integrity. +#[derive(StructOpt)] +pub struct Args { + /// Directory holding the SQLite3 index database. + #[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path", + parse(from_os_str))] + db_dir: PathBuf, -Usage: - - moonfire-nvr check [options] - moonfire-nvr check --help - -Options: - - --db-dir=DIR Set the directory holding the SQLite3 index database. - This is typically on a flash device. - [default: /var/lib/moonfire-nvr/db] - --compare-lens Compare sample file lengths on disk to the database. -"#; - -#[derive(Debug, Deserialize)] -struct Args { - flag_db_dir: String, - flag_compare_lens: bool, + /// Compare sample file lengths on disk to the database. + #[structopt(long)] + compare_lens: bool, } -pub fn run() -> Result<(), Error> { - let args: Args = super::parse_args(USAGE)?; - +pub fn run(args: &Args) -> Result<(), Error> { // TODO: ReadOnly should be sufficient but seems to fail. - let (_db_dir, conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::ReadWrite)?; + let (_db_dir, conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?; check::run(&conn, &check::Options { - compare_lens: args.flag_compare_lens, + compare_lens: args.compare_lens, }) } diff --git a/src/cmds/config/mod.rs b/src/cmds/config/mod.rs index 1ca54c9..d2296ae 100644 --- a/src/cmds/config/mod.rs +++ b/src/cmds/config/mod.rs @@ -38,36 +38,24 @@ use cursive::Cursive; use cursive::views; use db; use failure::Error; -use serde::Deserialize; +use std::path::PathBuf; use std::sync::Arc; +use structopt::StructOpt; mod cameras; mod dirs; mod users; -static USAGE: &'static str = r#" -Interactive configuration editor. - -Usage: - - moonfire-nvr config [options] - moonfire-nvr config --help - -Options: - - --db-dir=DIR Set the directory holding the SQLite3 index database. - This is typically on a flash device. - [default: /var/lib/moonfire-nvr/db] -"#; - -#[derive(Debug, Deserialize)] -struct Args { - flag_db_dir: String, +#[derive(StructOpt)] +pub struct Args { + /// Directory holding the SQLite3 index database. + #[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path", + parse(from_os_str))] + db_dir: PathBuf, } -pub fn run() -> Result<(), Error> { - let args: Args = super::parse_args(USAGE)?; - let (_db_dir, conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::ReadWrite)?; +pub fn run(args: &Args) -> Result<(), Error> { + let (_db_dir, conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?; let clocks = clock::RealClocks {}; let db = Arc::new(db::Database::new(clocks, conn, true)?); diff --git a/src/cmds/init.rs b/src/cmds/init.rs index 1099a47..bc791da 100644 --- a/src/cmds/init.rs +++ b/src/cmds/init.rs @@ -1,5 +1,5 @@ // This file is part of Moonfire NVR, a security camera network video recorder. -// Copyright (C) 2016 The Moonfire NVR Authors +// Copyright (C) 2016-2020 The Moonfire NVR Authors // // 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 @@ -30,31 +30,19 @@ use failure::Error; use log::info; -use serde::Deserialize; +use structopt::StructOpt; +use std::path::PathBuf; -static USAGE: &'static str = r#" -Initializes a database. - -Usage: - - moonfire-nvr init [options] - moonfire-nvr init --help - -Options: - - --db-dir=DIR Set the directory holding the SQLite3 index database. - This is typically on a flash device. - [default: /var/lib/moonfire-nvr/db] -"#; - -#[derive(Debug, Deserialize)] -struct Args { - flag_db_dir: String, +#[derive(StructOpt)] +pub struct Args { + /// Directory holding the SQLite3 index database. + #[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path", + parse(from_os_str))] + db_dir: PathBuf, } -pub fn run() -> Result<(), Error> { - let args: Args = super::parse_args(USAGE)?; - let (_db_dir, mut conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::Create)?; +pub fn run(args: &Args) -> Result<(), Error> { + let (_db_dir, mut conn) = super::open_conn(&args.db_dir, super::OpenMode::Create)?; // Check if the database has already been initialized. let cur_ver = db::get_schema_version(&conn)?; diff --git a/src/cmds/login.rs b/src/cmds/login.rs index 93b6d41..46462a0 100644 --- a/src/cmds/login.rs +++ b/src/cmds/login.rs @@ -1,5 +1,5 @@ // This file is part of Moonfire NVR, a security camera network video recorder. -// Copyright (C) 2019 The Moonfire NVR Authors +// Copyright (C) 2019-2020 The Moonfire NVR Authors // // 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 @@ -31,92 +31,74 @@ //! Subcommand to login a user (without requiring a password). use base::clock::{self, Clocks}; -use db::auth::SessionFlags; -use failure::{Error, ResultExt, bail, format_err}; -use serde::Deserialize; +use db::auth::SessionFlag; +use failure::{Error, format_err}; use std::os::unix::fs::OpenOptionsExt as _; use std::io::Write as _; use std::path::PathBuf; +use structopt::StructOpt; -static USAGE: &'static str = r#" -Logs in a user, returning the session cookie. +#[derive(Debug, Default, StructOpt)] +pub struct Args { + /// Directory holding the SQLite3 index database. + #[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path", + parse(from_os_str))] + db_dir: PathBuf, -This is a privileged command that directly accesses the database. It doesn't -check the user's password and even can be used to create sessions with -permissions the user doesn't have. + /// Create a session with the given permissions. + /// + /// If unspecified, uses user's default permissions. + #[structopt(long, value_name="perms", + parse(try_from_str = protobuf::text_format::parse_from_str))] + permissions: Option, -Usage: + /// Restrict this cookie to the given domain. + #[structopt(long)] + domain: Option, - moonfire-nvr login [options] - moonfire-nvr login --help + /// Write the cookie to a new curl-compatible cookie-jar file. + /// + /// ---domain must be specified. This file can be used later with curl's --cookie flag. + #[structopt(long, requires("domain"), value_name="path")] + curl_cookie_jar: Option, -Options: + /// Set the given db::auth::SessionFlags. + #[structopt(long, default_value="http-only,secure,same-site,same-site-strict", + value_name="flags", use_delimiter=true)] + session_flags: Vec, - --db-dir=DIR Set the directory holding the SQLite3 index database. This - is typically on a flash device. - [default: /var/lib/moonfire-nvr/db] - --permissions=PERMISSIONS - Create a session with the given permissions. If - unspecified, uses user's default permissions. - --domain=DOMAIN The domain this cookie lives on. Optional. - --curl-cookie-jar=FILE - Writes the cookie to a new curl-compatible cookie-jar - file. --domain must be specified. This can be used later - with curl's --cookie flag. - --session-flags=FLAGS - Set the given db::auth::SessionFlags. - [default: http-only,secure,same-site,same-site-strict] -"#; - -#[derive(Debug, Default, Deserialize, Eq, PartialEq)] -struct Args { - flag_db_dir: String, - flag_permissions: Option, - flag_domain: Option, - flag_curl_cookie_jar: Option, - flag_session_flags: String, - arg_username: String, + /// Create the session for this username. + username: String, } -pub fn run() -> Result<(), Error> { - let args: Args = super::parse_args(USAGE)?; +pub fn run(args: &Args) -> Result<(), Error> { let clocks = clock::RealClocks {}; - let (_db_dir, conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::ReadWrite)?; + let (_db_dir, conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?; let db = std::sync::Arc::new(db::Database::new(clocks.clone(), conn, true).unwrap()); let mut l = db.lock(); - let u = l.get_user(&args.arg_username) - .ok_or_else(|| format_err!("no such user {:?}", &args.arg_username))?; - let permissions = match args.flag_permissions { - None => u.permissions.clone(), - Some(s) => protobuf::text_format::parse_from_str(&s) - .context("unable to parse --permissions")? - }; + let u = l.get_user(&args.username) + .ok_or_else(|| format_err!("no such user {:?}", &args.username))?; + let permissions = args.permissions.as_ref().unwrap_or(&u.permissions).clone(); let creation = db::auth::Request { when_sec: Some(db.clocks().realtime().sec), user_agent: None, addr: None, }; let mut flags = 0; - for f in args.flag_session_flags.split(',') { - flags |= match f { - "http-only" => SessionFlags::HttpOnly, - "secure" => SessionFlags::Secure, - "same-site" => SessionFlags::SameSite, - "same-site-strict" => SessionFlags::SameSiteStrict, - _ => bail!("unknown session flag {:?}", f), - } as i32; + for f in &args.session_flags { + flags |= *f as i32; } let uid = u.id; drop(u); let (sid, _) = l.make_session(creation, uid, - args.flag_domain.as_ref().map(|d| d.as_bytes().to_owned()), + args.domain.as_ref().map(|d| d.as_bytes().to_owned()), flags, permissions)?; let mut encoded = [0u8; 64]; base64::encode_config_slice(&sid, base64::STANDARD_NO_PAD, &mut encoded); let encoded = std::str::from_utf8(&encoded[..]).expect("base64 is valid UTF-8"); - if let Some(ref p) = args.flag_curl_cookie_jar { - let d = args.flag_domain.as_ref() + if let Some(ref p) = args.curl_cookie_jar { + let d = args.domain.as_ref() .ok_or_else(|| format_err!("--cookiejar requires --domain"))?; let mut f = std::fs::OpenOptions::new() .write(true) @@ -139,11 +121,11 @@ pub fn run() -> Result<(), Error> { fn curl_cookie(cookie: &str, flags: i32, domain: &str) -> String { format!("{httponly}{domain}\t{tailmatch}\t{path}\t{secure}\t{expires}\t{name}\t{value}", - httponly=if (flags & SessionFlags::HttpOnly as i32) != 0 { "#HttpOnly_" } else { "" }, + httponly=if (flags & SessionFlag::HttpOnly as i32) != 0 { "#HttpOnly_" } else { "" }, domain=domain, tailmatch="FALSE", path="/", - secure=if (flags & SessionFlags::Secure as i32) != 0 { "TRUE" } else { "FALSE" }, + secure=if (flags & SessionFlag::Secure as i32) != 0 { "TRUE" } else { "FALSE" }, expires="9223372036854775807", // 64-bit CURL_OFF_T_MAX, never expires name="s", value=cookie) @@ -153,24 +135,10 @@ fn curl_cookie(cookie: &str, flags: i32, domain: &str) -> String { mod tests { use super::*; - #[test] - fn test_args() { - let args: Args = docopt::Docopt::new(USAGE).unwrap() - .argv(&["nvr", "login", "--curl-cookie-jar=foo.txt", "slamb"]) - .deserialize().unwrap(); - assert_eq!(args, Args { - flag_db_dir: "/var/lib/moonfire-nvr/db".to_owned(), - flag_curl_cookie_jar: Some(PathBuf::from("foo.txt")), - flag_session_flags: "http-only,secure,same-site,same-site-strict".to_owned(), - arg_username: "slamb".to_owned(), - ..Default::default() - }); - } - #[test] fn test_curl_cookie() { assert_eq!(curl_cookie("o3mx3OntO7GzwwsD54OuyQ4IuipYrwPR2aiULPHSudAa+xIhwWjb+w1TnGRh8Z5Q", - SessionFlags::HttpOnly as i32, "localhost"), + SessionFlag::HttpOnly as i32, "localhost"), "#HttpOnly_localhost\tFALSE\t/\tFALSE\t9223372036854775807\ts\t\ o3mx3OntO7GzwwsD54OuyQ4IuipYrwPR2aiULPHSudAa+xIhwWjb+w1TnGRh8Z5Q"); } diff --git a/src/cmds/mod.rs b/src/cmds/mod.rs index c2d3840..e635bc5 100644 --- a/src/cmds/mod.rs +++ b/src/cmds/mod.rs @@ -29,48 +29,19 @@ // along with this program. If not, see . use db::dir; -use docopt; use failure::{Error, Fail}; use nix::fcntl::FlockArg; use rusqlite; -use serde::Deserialize; use std::path::Path; -mod check; -mod config; -mod login; -mod init; -mod run; -mod sql; -mod ts; -mod upgrade; - -#[derive(Debug, Deserialize)] -pub enum Command { - Check, - Config, - Login, - Init, - Run, - Sql, - Ts, - Upgrade, -} - -impl Command { - pub fn run(&self) -> Result<(), Error> { - match *self { - Command::Check => check::run(), - Command::Config => config::run(), - Command::Login => login::run(), - Command::Init => init::run(), - Command::Run => run::run(), - Command::Sql => sql::run(), - Command::Ts => ts::run(), - Command::Upgrade => upgrade::run(), - } - } -} +pub mod check; +pub mod config; +pub mod login; +pub mod init; +pub mod run; +pub mod sql; +pub mod ts; +pub mod upgrade; #[derive(Copy, Clone, PartialEq, Eq)] enum OpenMode { @@ -81,10 +52,10 @@ enum OpenMode { /// Locks the directory without opening the database. /// The returned `dir::Fd` holds the lock and should be kept open as long as the `Connection` is. -fn open_dir(db_dir: &str, mode: OpenMode) -> Result { +fn open_dir(db_dir: &Path, mode: OpenMode) -> Result { let dir = dir::Fd::open(db_dir, mode == OpenMode::Create)?; let ro = mode == OpenMode::ReadOnly; - dir.lock(if ro { FlockArg::LockExclusiveNonblock } else { FlockArg::LockSharedNonblock }) + dir.lock(if ro { FlockArg::LockSharedNonblock } else { FlockArg::LockExclusiveNonblock }) .map_err(|e| e.context(format!("db dir {:?} already in use; can't get {} lock", db_dir, if ro { "shared" } else { "exclusive" })))?; Ok(dir) @@ -92,10 +63,10 @@ fn open_dir(db_dir: &str, mode: OpenMode) -> Result { /// Locks and opens the database. /// The returned `dir::Fd` holds the lock and should be kept open as long as the `Connection` is. -fn open_conn(db_dir: &str, mode: OpenMode) -> Result<(dir::Fd, rusqlite::Connection), Error> { +fn open_conn(db_dir: &Path, mode: OpenMode) -> Result<(dir::Fd, rusqlite::Connection), Error> { let dir = open_dir(db_dir, mode)?; let conn = rusqlite::Connection::open_with_flags( - Path::new(&db_dir).join("db"), + db_dir.join("db"), match mode { OpenMode::ReadOnly => rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY, OpenMode::ReadWrite => rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE, @@ -108,9 +79,3 @@ fn open_conn(db_dir: &str, mode: OpenMode) -> Result<(dir::Fd, rusqlite::Connect rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX)?; Ok((dir, conn)) } - -fn parse_args<'a, T>(usage: &str) -> Result where T: ::serde::Deserialize<'a> { - Ok(docopt::Docopt::new(usage) - .and_then(|d| d.deserialize()) - .unwrap_or_else(|e| e.exit())) -} diff --git a/src/cmds/run.rs b/src/cmds/run.rs index 22a3b4f..8907d4d 100644 --- a/src/cmds/run.rs +++ b/src/cmds/run.rs @@ -1,5 +1,5 @@ // This file is part of Moonfire NVR, a security camera network video recorder. -// Copyright (C) 2016 The Moonfire NVR Authors +// Copyright (C) 2016-2020 The Moonfire NVR Authors // // 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 @@ -33,19 +33,66 @@ use crate::stream; use crate::streamer; use crate::web; use db::{dir, writer}; -use failure::{Error, ResultExt, bail}; +use failure::{Error, bail}; use fnv::FnvHashMap; use futures::future::FutureExt; use hyper::service::{make_service_fn, service_fn}; use log::{info, warn}; -use serde::Deserialize; +use std::path::PathBuf; use std::pin::Pin; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::thread; +use structopt::StructOpt; use tokio; use tokio::signal::unix::{SignalKind, signal}; +#[derive(StructOpt)] +pub struct Args { + /// Directory holding the SQLite3 index database. + #[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path", + parse(from_os_str))] + db_dir: PathBuf, + + /// Directory holding user interface files (.html, .js, etc). + #[structopt(default_value = "/usr/local/lib/moonfire-nvr/ui", value_name="path", + parse(from_os_str))] + ui_dir: std::path::PathBuf, + + /// Bind address for unencrypted HTTP server. + #[structopt(long, default_value = "0.0.0.0:8080", parse(try_from_str))] + http_addr: std::net::SocketAddr, + + /// Open the database in read-only mode and disables recording. + /// + /// Note this is incompatible with authentication, so you'll likely want to specify + /// --allow_unauthenticated_permissions. + #[structopt(long)] + read_only: bool, + + /// Allow unauthenticated access to the web interface, with the given permissions (may be + /// empty). Should be a text Permissions protobuf such as "view_videos: true". + /// + /// Note that even an empty string allows some basic access that would be rejected if the + /// argument were omitted. + #[structopt(long, parse(try_from_str = protobuf::text_format::parse_from_str))] + allow_unauthenticated_permissions: Option, + + /// Trust X-Real-IP: and X-Forwarded-Proto: headers on the incoming request. + /// + /// Set this only after ensuring your proxy server is configured to set them and that no + /// untrusted requests bypass the proxy server. You may want to specify + /// --http-addr=127.0.0.1:8080. + #[structopt(long)] + trust_forward_hdrs: bool, + + /// Perform object detection on SUB streams. + /// + /// Note: requires compilation with --feature=analytics. + #[structopt(long)] + object_detection: bool, +} + // These are used in a hack to get the name of the current time zone (e.g. America/Los_Angeles). // They seem to be correct for Linux and macOS at least. const LOCALTIME_PATH: &'static str = "/etc/localtime"; @@ -55,47 +102,6 @@ const ZONEINFO_PATHS: [&'static str; 2] = [ "/var/db/timezone/zoneinfo/" // macOS High Sierra ]; -const USAGE: &'static str = r#" -Usage: moonfire-nvr run [options] - -Options: - -h, --help Show this message. - --db-dir=DIR Set the directory holding the SQLite3 index database. - This is typically on a flash device. - [default: /var/lib/moonfire-nvr/db] - --ui-dir=DIR Set the directory with the user interface files - (.html, .js, etc). - [default: /usr/local/lib/moonfire-nvr/ui] - --http-addr=ADDR Set the bind address for the unencrypted HTTP server. - [default: 0.0.0.0:8080] - --read-only Forces read-only mode / disables recording. - --allow-unauthenticated-permissions=PERMISSIONS - Allow unauthenticated access to the web interface, - with the given permissions (may be empty). - PERMISSIONS should be a text Permissions protobuf - such as "view_videos: true". NOTE: even an empty - string allows some basic access that would be - rejected if the argument were omitted. - --trust-forward-hdrs Trust X-Real-IP: and X-Forwarded-Proto: headers on - the incoming request. Set this only after ensuring - your proxy server is configured to set them and that - no untrusted requests bypass the proxy server. - You may want to specify --http-addr=127.0.0.1:8080. - --object-detection Perform object detection on SUB streams. - Note: requires compilation with --feature=analytics. -"#; - -#[derive(Debug, Deserialize)] -struct Args { - flag_db_dir: String, - flag_http_addr: String, - flag_ui_dir: String, - flag_read_only: bool, - flag_allow_unauthenticated_permissions: Option, - flag_trust_forward_hdrs: bool, - flag_object_detection: bool, -} - fn trim_zoneinfo(p: &str) -> &str { for zp in &ZONEINFO_PATHS { if p.starts_with(zp) { @@ -170,16 +176,15 @@ struct Syncer { } #[tokio::main] -pub async fn run() -> Result<(), Error> { - let args: Args = super::parse_args(USAGE)?; +pub async fn run(args: &Args) -> Result<(), Error> { let clocks = clock::RealClocks {}; let (_db_dir, conn) = super::open_conn( - &args.flag_db_dir, - if args.flag_read_only { super::OpenMode::ReadOnly } else { super::OpenMode::ReadWrite })?; - let db = Arc::new(db::Database::new(clocks.clone(), conn, !args.flag_read_only).unwrap()); + &args.db_dir, + if args.read_only { super::OpenMode::ReadOnly } else { super::OpenMode::ReadWrite })?; + let db = Arc::new(db::Database::new(clocks.clone(), conn, !args.read_only).unwrap()); info!("Database is loaded."); - let object_detector = match args.flag_object_detection { + let object_detector = match args.object_detection { false => None, true => Some(crate::analytics::ObjectDetector::new()?), }; @@ -194,22 +199,18 @@ pub async fn run() -> Result<(), Error> { let time_zone_name = resolve_zone()?; info!("Resolved timezone: {}", &time_zone_name); - let allow_unauthenticated_permissions = args.flag_allow_unauthenticated_permissions - .map(|s| protobuf::text_format::parse_from_str(&s)) - .transpose() - .context("Unable to parse --allow-unauthenticated-permissions")?; let s = web::Service::new(web::Config { db: db.clone(), - ui_dir: Some(&args.flag_ui_dir), - allow_unauthenticated_permissions, - trust_forward_hdrs: args.flag_trust_forward_hdrs, + ui_dir: Some(&args.ui_dir), + allow_unauthenticated_permissions: args.allow_unauthenticated_permissions.clone(), + trust_forward_hdrs: args.trust_forward_hdrs, time_zone_name, })?; // Start a streamer for each stream. let shutdown_streamers = Arc::new(AtomicBool::new(false)); let mut streamers = Vec::new(); - let syncers = if !args.flag_read_only { + let syncers = if !args.read_only { let l = db.lock(); let mut dirs = FnvHashMap::with_capacity_and_hasher( l.sample_file_dirs_by_id().len(), Default::default()); @@ -280,14 +281,13 @@ pub async fn run() -> Result<(), Error> { } else { None }; // Start the web interface. - let addr = args.flag_http_addr.parse().unwrap(); let make_svc = make_service_fn(move |_conn| { futures::future::ok::<_, std::convert::Infallible>(service_fn({ let mut s = s.clone(); move |req| Pin::from(s.serve(req)) })) }); - let server = ::hyper::server::Server::bind(&addr) + let server = ::hyper::server::Server::bind(&args.http_addr) .tcp_nodelay(true) .serve(make_svc); diff --git a/src/cmds/sql.rs b/src/cmds/sql.rs index 8e5cc2c..74e59e5 100644 --- a/src/cmds/sql.rs +++ b/src/cmds/sql.rs @@ -1,5 +1,5 @@ // This file is part of Moonfire NVR, a security camera network video recorder. -// Copyright (C) 2019 The Moonfire NVR Authors +// Copyright (C) 2019-2020 The Moonfire NVR Authors // // 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 @@ -31,45 +31,43 @@ //! Subcommand to run a SQLite shell. use failure::Error; -use serde::Deserialize; +use std::ffi::OsString; +use std::path::PathBuf; use std::process::Command; use super::OpenMode; +use structopt::StructOpt; -static USAGE: &'static str = r#" -Runs a SQLite shell on the Moonfire NVR database with locking. +#[derive(StructOpt)] +pub struct Args { + /// Directory holding the SQLite3 index database. + #[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path", + parse(from_os_str))] + db_dir: PathBuf, -Usage: + /// Opens the database in read-only mode and locks it only for shared access. + /// + /// This can be run simultaneously with "moonfire-nvr run --read-only". + #[structopt(long)] + read_only: bool, - moonfire-nvr sql [options] [--] [...] - moonfire-nvr sql --help - -Positional arguments will be passed to sqlite3. Use the -- separator to pass -sqlite3 options, as in "moonfire-nvr sql -- -line 'select username from user'". - -Options: - - --db-dir=DIR Set the directory holding the SQLite3 index database. - This is typically on a flash device. - [default: /var/lib/moonfire-nvr/db] - --read-only Accesses the database in read-only mode. -"#; - -#[derive(Debug, Deserialize)] -struct Args { - flag_db_dir: String, - flag_read_only: bool, - arg_arg: Vec, + /// Arguments to pass to sqlite3. + /// + /// Use the -- separator to pass sqlite3 options, as in + /// "moonfire-nvr sql -- -line 'select username from user'". + #[structopt(parse(from_os_str))] + arg: Vec, } -pub fn run() -> Result<(), Error> { - let args: Args = super::parse_args(USAGE)?; - - let mode = if args.flag_read_only { OpenMode::ReadWrite } else { OpenMode::ReadOnly }; - let _db_dir = super::open_dir(&args.flag_db_dir, mode)?; - let mut db = format!("file:{}/db", &args.flag_db_dir); - if args.flag_read_only { - db.push_str("?mode=ro"); +pub fn run(args: &Args) -> Result<(), Error> { + let mode = if args.read_only { OpenMode::ReadOnly } else { OpenMode::ReadWrite }; + let _db_dir = super::open_dir(&args.db_dir, mode)?; + let mut db = OsString::new(); + db.push("file:"); + db.push(&args.db_dir); + db.push("/db"); + if args.read_only { + db.push("?mode=ro"); } - Command::new("sqlite3").arg(&db).args(&args.arg_arg).status()?; + Command::new("sqlite3").arg(&db).args(&args.arg).status()?; Ok(()) } diff --git a/src/cmds/ts.rs b/src/cmds/ts.rs index a637b86..9625c39 100644 --- a/src/cmds/ts.rs +++ b/src/cmds/ts.rs @@ -1,5 +1,5 @@ // This file is part of Moonfire NVR, a security camera network video recorder. -// Copyright (C) 2016 The Moonfire NVR Authors +// Copyright (C) 2016-2020 The Moonfire NVR Authors // // 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 @@ -29,21 +29,22 @@ // along with this program. If not, see . use failure::Error; -use serde::Deserialize; +use structopt::StructOpt; -const USAGE: &'static str = r#" -Usage: moonfire-nvr ts ... - moonfire-nvr ts --help -"#; - -#[derive(Debug, Deserialize)] -struct Args { - arg_ts: Vec, +#[derive(StructOpt)] +pub struct Args { + /// Timestamp(s) to translate. + /// + /// May be either an integer or an RFC-3339-like string: + /// YYYY-mm-dd[THH:MM[:SS[:FFFFF]]][{Z,{+,-,}HH:MM}]. + /// + /// Eg: 142913484000000, 2020-04-26, 2020-04-26T12:00:00:00000-07:00. + #[structopt(required = true)] + timestamps: Vec, } -pub fn run() -> Result<(), Error> { - let arg: Args = super::parse_args(&USAGE)?; - for timestamp in &arg.arg_ts { +pub fn run(args: &Args) -> Result<(), Error> { + for timestamp in &args.timestamps { let t = db::recording::Time::parse(timestamp)?; println!("{} == {}", t, t.0); } diff --git a/src/cmds/upgrade/mod.rs b/src/cmds/upgrade/mod.rs index 615ea66..c1505e4 100644 --- a/src/cmds/upgrade/mod.rs +++ b/src/cmds/upgrade/mod.rs @@ -1,5 +1,5 @@ // This file is part of Moonfire NVR, a security camera network video recorder. -// Copyright (C) 2016 The Moonfire NVR Authors +// Copyright (C) 2016-2020 The Moonfire NVR Authors // // 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 @@ -33,45 +33,38 @@ /// See `guide/schema.md` for more information. use failure::Error; -use serde::Deserialize; +use structopt::StructOpt; -const USAGE: &'static str = r#" -Upgrade to the latest database schema. - -Usage: moonfire-nvr upgrade [options] - -Options: - -h, --help Show this message. - --db-dir=DIR Set the directory holding the SQLite3 index database. - This is typically on a flash device. - [default: /var/lib/moonfire-nvr/db] - --sample-file-dir=DIR When upgrading from schema version 1 to 2, the sample file directory. - This is typically on a hard drive. - --preset-journal=MODE Resets the SQLite journal_mode to the specified mode - prior to the upgrade. The default, delete, is - recommended. off is very dangerous but may be - desirable in some circumstances. See guide/schema.md - for more information. The journal mode will be reset - to wal after the upgrade. - [default: delete] - --no-vacuum Skips the normal post-upgrade vacuum operation. -"#; - -#[derive(Debug, Deserialize)] +#[derive(StructOpt)] pub struct Args { - flag_db_dir: String, - flag_sample_file_dir: Option, - flag_preset_journal: String, - flag_no_vacuum: bool, + #[structopt(long, + help = "Directory holding the SQLite3 index database.", + default_value = "/var/lib/moonfire-nvr/db", + parse(from_os_str))] + db_dir: std::path::PathBuf, + + #[structopt(help = "When upgrading from schema version 1 to 2, the sample file directory.", + long, parse(from_os_str))] + sample_file_dir: Option, + + #[structopt(help = "Resets the SQLite journal_mode to the specified mode prior to the \ + upgrade. The default, delete, is recommended. off is very dangerous \ + but may be desirable in some circumstances. See guide/schema.md for \ + more information. The journal mode will be reset to wal after the \ + upgrade.", + long, default_value = "delete")] + preset_journal: String, + + #[structopt(help = "Skips the normal post-upgrade vacuum operation.", long)] + no_vacuum: bool, } -pub fn run() -> Result<(), Error> { - let args: Args = super::parse_args(USAGE)?; - let (_db_dir, mut conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::ReadWrite)?; +pub fn run(args: &Args) -> Result<(), Error> { + let (_db_dir, mut conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?; db::upgrade::run(&db::upgrade::Args { - flag_sample_file_dir: args.flag_sample_file_dir.as_ref().map(|s| s.as_str()), - flag_preset_journal: &args.flag_preset_journal, - flag_no_vacuum: args.flag_no_vacuum, + sample_file_dir: args.sample_file_dir.as_ref().map(std::path::PathBuf::as_path), + preset_journal: &args.preset_journal, + no_vacuum: args.no_vacuum, }, &mut conn) } diff --git a/src/h264.rs b/src/h264.rs index 538c62f..7e1ce52 100644 --- a/src/h264.rs +++ b/src/h264.rs @@ -42,8 +42,6 @@ use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; use failure::{Error, bail, format_err}; -use lazy_static::lazy_static; -use regex::bytes::Regex; use std::convert::TryFrom; // See ISO/IEC 14496-10 table 7-1 - NAL unit type codes, syntax element categories, and NAL unit @@ -91,15 +89,28 @@ fn default_pixel_aspect_ratio(width: u16, height: u16) -> (u16, u16) { /// /// TODO: detect invalid byte streams. For example, several 0x00s not followed by a 0x01, a stream /// stream not starting with 0x00 0x00 0x00 0x01, or an empty NAL unit. -fn decode_h264_annex_b<'a, F>(data: &'a [u8], mut f: F) -> Result<(), Error> +fn decode_h264_annex_b<'a, F>(mut data: &'a [u8], mut f: F) -> Result<(), Error> where F: FnMut(&'a [u8]) -> Result<(), Error> { - lazy_static! { - static ref START_CODE: Regex = Regex::new(r"(\x00{2,}\x01)").unwrap(); - } - for unit in START_CODE.split(data) { - if !unit.is_empty() { - f(unit)?; + let start_code = &b"\x00\x00\x01"[..]; + use nom::FindSubstring; + 'outer: while let Some(pos) = data.find_substring(start_code) { + let mut unit = &data[0..pos]; + data = &data[pos + start_code.len() ..]; + // Have zero or more bytes that end in a start code. Strip out any trailing 0x00s and + // process the unit if there's anything left. + loop { + match unit.last() { + None => continue 'outer, + Some(b) if *b == 0 => { unit = &unit[..unit.len()-1]; }, + Some(_) => break, + } } + f(unit)?; + } + + // No remaining start codes; likely a unit left. + if !data.is_empty() { + f(data)?; } Ok(()) } diff --git a/src/main.rs b/src/main.rs index b2ba361..7493b3b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ // This file is part of Moonfire NVR, a security camera network video recorder. -// Copyright (C) 2016 The Moonfire NVR Authors +// Copyright (C) 2016-2020 The Moonfire NVR Authors // // 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 @@ -31,7 +31,7 @@ #![cfg_attr(all(feature="nightly", test), feature(test))] use log::{error, info}; -use serde::Deserialize; +use structopt::StructOpt; #[cfg(feature = "analytics")] mod analytics; @@ -74,39 +74,53 @@ mod stream; mod streamer; mod web; -/// Commandline usage string. This is in the particular format expected by the `docopt` crate. -/// Besides being printed on --help or argument parsing error, it's actually parsed to define the -/// allowed commandline arguments and their defaults. -const USAGE: &'static str = " -Usage: moonfire-nvr [...] - moonfire-nvr (--help | --version) +#[derive(StructOpt)] +#[structopt(name="moonfire-nvr", about="security camera network video recorder")] +enum Args { + /// Checks database integrity (like fsck). + Check(cmds::check::Args), -Options: - -h, --help Show this message. - --version Show the version of moonfire-nvr. + /// Interactively edits configuration. + Config(cmds::config::Args), -Commands: - check Check database integrity - init Initialize a database - run Run the daemon: record from cameras and serve HTTP - shell Start an interactive shell to modify the database - ts Translate human-readable and numeric timestamps - upgrade Upgrade the database to the latest schema -"; + /// Initializes a database. + Init(cmds::init::Args), -/// Commandline arguments corresponding to `USAGE`; automatically filled by the `docopt` crate. -#[derive(Debug, Deserialize)] -struct Args { - arg_command: Option, + /// Logs in a user, returning the session cookie. + /// + /// This is a privileged command that directly accesses the database. It doesn't check the + /// user's password and even can be used to create sessions with permissions the user doesn't + /// have. + Login(cmds::login::Args), + + /// Runs the server, saving recordings and allowing web access. + Run(cmds::run::Args), + + /// Runs a SQLite3 shell on Moonfire NVR's index database. + /// + /// Note this locks the database to prevent simultaneous access with a running server. The + /// server maintains cached state which could be invalidated otherwise. + Sql(cmds::sql::Args), + + /// Translates between integer and human-readable timestamps. + Ts(cmds::ts::Args), + + /// Upgrades to the latest database schema. + Upgrade(cmds::upgrade::Args), } -fn version() -> String { - let major = option_env!("CARGO_PKG_VERSION_MAJOR"); - let minor = option_env!("CARGO_PKG_VERSION_MAJOR"); - let patch = option_env!("CARGO_PKG_VERSION_MAJOR"); - match (major, minor, patch) { - (Some(major), Some(minor), Some(patch)) => format!("{}.{}.{}", major, minor, patch), - _ => "".to_owned(), +impl Args { + fn run(&self) -> Result<(), failure::Error> { + match self { + Args::Check(ref a) => cmds::check::run(a), + Args::Config(ref a) => cmds::config::run(a), + Args::Init(ref a) => cmds::init::run(a), + Args::Login(ref a) => cmds::login::run(a), + Args::Run(ref a) => cmds::run::run(a), + Args::Sql(ref a) => cmds::sql::run(a), + Args::Ts(ref a) => cmds::ts::run(a), + Args::Upgrade(ref a) => cmds::upgrade::run(a), + } } } @@ -119,14 +133,7 @@ fn parse_fmt>(fmt: S) -> Option { } fn main() { - // Parse commandline arguments. - // (Note this differs from cmds::parse_args in that it specifies options_first.) - let args: Args = docopt::Docopt::new(USAGE) - .and_then(|d| d.options_first(true) - .version(Some(version())) - .deserialize()) - .unwrap_or_else(|e| e.exit()); - + let args = Args::from_args(); let mut h = mylog::Builder::new() .set_format(::std::env::var("MOONFIRE_FORMAT") .ok() @@ -136,7 +143,7 @@ fn main() { .build(); h.clone().install().unwrap(); - if let Err(e) = { let _a = h.r#async(); args.arg_command.unwrap().run() } { + if let Err(e) = { let _a = h.r#async(); args.run() } { error!("{:?}", e); ::std::process::exit(1); } diff --git a/src/web.rs b/src/web.rs index 288f246..f668074 100644 --- a/src/web.rs +++ b/src/web.rs @@ -34,7 +34,6 @@ use bytes::Bytes; use crate::body::{Body, BoxedError}; use crate::json; use crate::mp4; -use base64; use bytes::{BufMut, BytesMut}; use core::borrow::Borrow; use core::str::FromStr; @@ -46,12 +45,12 @@ use futures::sink::SinkExt; use futures::future::{self, Future, TryFutureExt}; use futures::stream::StreamExt; use http::{Request, Response, status::StatusCode}; -use http_serve; use http::header::{self, HeaderValue}; -use lazy_static::lazy_static; use log::{debug, info, warn}; -use regex::Regex; -use serde_json; +use nom::IResult; +use nom::bytes::complete::{take_while1, tag}; +use nom::combinator::{all_consuming, map, map_res, opt}; +use nom::sequence::{preceded, tuple}; use std::collections::HashMap; use std::cmp; use std::fs; @@ -64,14 +63,6 @@ use tokio_tungstenite::tungstenite; use url::form_urlencoded; use uuid::Uuid; -lazy_static! { - /// Regex used to parse the `s` query parameter to `view.mp4`. - /// As described in `design/api.md`, this is of the form - /// `START_ID[-END_ID][@OPEN_ID][.[REL_START_TIME]-[REL_END_TIME]]`. - static ref SEGMENTS_RE: Regex = - Regex::new(r"^(\d+)(-\d+)?(@\d+)?(?:\.(\d+)?-(\d+)?)?$").unwrap(); -} - type BoxedFuture = Box, BoxedError>> + Sync + Send + 'static>; @@ -204,41 +195,48 @@ struct Segments { end_time: Option, } +fn num<'a, T: FromStr>() -> impl Fn(&'a str) -> IResult<&'a str, T> { + map_res(take_while1(|c: char| c.is_ascii_digit()), FromStr::from_str) +} + impl Segments { - pub fn parse(input: &str) -> Result { - let caps = SEGMENTS_RE.captures(input).ok_or(())?; - let ids_start = i32::from_str(caps.get(1).unwrap().as_str()).map_err(|_| ())?; - let ids_end = match caps.get(2) { - Some(m) => i32::from_str(&m.as_str()[1..]).map_err(|_| ())?, - None => ids_start, - } + 1; - let open_id = match caps.get(3) { - Some(m) => Some(u32::from_str(&m.as_str()[1..]).map_err(|_| ())?), - None => None, - }; - if ids_start < 0 || ids_end <= ids_start { + /// Parses the `s` query parameter to `view.mp4` as described in `design/api.md`. + /// Doesn't do any validation. + fn parse(i: &str) -> IResult<&str, Segments> { + // Parse START_ID[-END_ID] into Range. + // Note that END_ID is inclusive, but Ranges are half-open. + let (i, ids) = map(tuple((num::(), opt(preceded(tag("-"), num::())))), + |(start, end)| start .. end.unwrap_or(start) + 1)(i)?; + + // Parse [@OPEN_ID] into Option. + let (i, open_id) = opt(preceded(tag("@"), num::()))(i)?; + + // Parse [.[REL_START_TIME]-[REL_END_TIME]] into (i64, Option). + let (i, (start_time, end_time)) = map( + opt(preceded(tag("."), tuple((opt(num::()), tag("-"), opt(num::()))))), + |t| { + t.map(|(s, _, e)| (s.unwrap_or(0), e)) + .unwrap_or((0, None)) + })(i)?; + + Ok((i, Segments { ids, open_id, start_time, end_time, })) + } +} + +impl FromStr for Segments { + type Err = (); + + fn from_str(s: &str) -> Result { + let (_, s) = all_consuming(Segments::parse)(s).map_err(|_| ())?; + if s.ids.end <= s.ids.start { return Err(()); } - let start_time = caps.get(4).map_or(Ok(0), |m| i64::from_str(m.as_str())).map_err(|_| ())?; - if start_time < 0 { - return Err(()); + if let Some(e) = s.end_time { + if e < s.start_time { + return Err(()); + } } - let end_time = match caps.get(5) { - Some(v) => { - let e = i64::from_str(v.as_str()).map_err(|_| ())?; - if e <= start_time { - return Err(()); - } - Some(e) - }, - None => None - }; - Ok(Segments { - ids: ids_start .. ids_end, - open_id, - start_time, - end_time, - }) + Ok(s) } } @@ -421,7 +419,7 @@ impl ServiceInner { let (key, value) = (key.borrow(), value.borrow()); match key { "s" => { - let s = Segments::parse(value).map_err( + let s = Segments::from_str(value).map_err( |()| plain_response(StatusCode::BAD_REQUEST, format!("invalid s parameter: {}", value)))?; debug!("stream_view_mp4: appending s={:?}", s); @@ -587,10 +585,10 @@ impl ServiceInner { }.to_owned(); let mut l = self.db.lock(); let is_secure = self.is_secure(req); - let flags = (auth::SessionFlags::HttpOnly as i32) | - (auth::SessionFlags::SameSite as i32) | - (auth::SessionFlags::SameSiteStrict as i32) | - if is_secure { auth::SessionFlags::Secure as i32 } else { 0 }; + let flags = (auth::SessionFlag::HttpOnly as i32) | + (auth::SessionFlag::SameSite as i32) | + (auth::SessionFlag::SameSiteStrict as i32) | + if is_secure { auth::SessionFlag::Secure as i32 } else { 0 }; let (sid, _) = l.login_by_password(authreq, &r.username, r.password, Some(domain), flags) .map_err(|e| plain_response(StatusCode::UNAUTHORIZED, e.to_string()))?; @@ -796,7 +794,7 @@ async fn with_json_body(mut req: Request) pub struct Config<'a> { pub db: Arc, - pub ui_dir: Option<&'a str>, + pub ui_dir: Option<&'a std::path::Path>, pub trust_forward_hdrs: bool, pub time_zone_name: String, pub allow_unauthenticated_permissions: Option, @@ -839,12 +837,12 @@ impl Service { }))) } - fn fill_ui_files(dir: &str, files: &mut HashMap) { + fn fill_ui_files(dir: &std::path::Path, files: &mut HashMap) { let r = match fs::read_dir(dir) { Ok(r) => r, Err(e) => { warn!("Unable to search --ui-dir={}; will serve no static files. Error was: {}", - dir, e); + dir.display(), e); return; } }; @@ -1075,6 +1073,7 @@ mod tests { use futures::future::FutureExt; use log::info; use std::collections::HashMap; + use std::str::FromStr; use super::Segments; struct Server { @@ -1221,25 +1220,25 @@ mod tests { fn test_segments() { testutil::init(); assert_eq!(Segments{ids: 1..2, open_id: None, start_time: 0, end_time: None}, - Segments::parse("1").unwrap()); + Segments::from_str("1").unwrap()); assert_eq!(Segments{ids: 1..2, open_id: Some(42), start_time: 0, end_time: None}, - Segments::parse("1@42").unwrap()); + Segments::from_str("1@42").unwrap()); assert_eq!(Segments{ids: 1..2, open_id: None, start_time: 26, end_time: None}, - Segments::parse("1.26-").unwrap()); + Segments::from_str("1.26-").unwrap()); assert_eq!(Segments{ids: 1..2, open_id: Some(42), start_time: 26, end_time: None}, - Segments::parse("1@42.26-").unwrap()); + Segments::from_str("1@42.26-").unwrap()); assert_eq!(Segments{ids: 1..2, open_id: None, start_time: 0, end_time: Some(42)}, - Segments::parse("1.-42").unwrap()); + Segments::from_str("1.-42").unwrap()); assert_eq!(Segments{ids: 1..2, open_id: None, start_time: 26, end_time: Some(42)}, - Segments::parse("1.26-42").unwrap()); + Segments::from_str("1.26-42").unwrap()); assert_eq!(Segments{ids: 1..6, open_id: None, start_time: 0, end_time: None}, - Segments::parse("1-5").unwrap()); + Segments::from_str("1-5").unwrap()); assert_eq!(Segments{ids: 1..6, open_id: None, start_time: 26, end_time: None}, - Segments::parse("1-5.26-").unwrap()); + Segments::from_str("1-5.26-").unwrap()); assert_eq!(Segments{ids: 1..6, open_id: None, start_time: 0, end_time: Some(42)}, - Segments::parse("1-5.-42").unwrap()); + Segments::from_str("1-5.-42").unwrap()); assert_eq!(Segments{ids: 1..6, open_id: None, start_time: 26, end_time: Some(42)}, - Segments::parse("1-5.26-42").unwrap()); + Segments::from_str("1-5.26-42").unwrap()); } #[tokio::test]