switch from docopt to structopt

A couple reasons for this:

* the docopt crate is "unlikely to see significant future evolution",
  and the wider docopt project is "mostly unmaintained at this point".
  clap/structopt is more full-featured, has more natural subcommand
  support, etc.

* it may allow me to shrink the binary (#70). This change alone seems
  to be a slight regression, but it's a step toward getting rid of
  regex, which is pretty large. And I feel less ridiculous now that I
  don't have two parsing crates anyway; prettydiff was pulling in
  structopt.

There are some behavior changes here:

* misc --help output changes and such as you'd expect from switching
  argument-parsing libraries

* I properly used PathBuf and OsString for stuff that theoretically
  could be non-UTF-8. I haven't tested that it actually made any
  difference. I'm also still storing the sample file dirname as "text"
  in the database to avoid causing a diff when not doing a schema
  change.
This commit is contained in:
Scott Lamb 2020-04-17 22:41:55 -07:00
parent 066c086050
commit e8eb764b90
17 changed files with 383 additions and 417 deletions

81
Cargo.lock generated
View File

@ -275,6 +275,7 @@ dependencies = [
"atty", "atty",
"bitflags", "bitflags",
"strsim 0.8.0", "strsim 0.8.0",
"term_size",
"textwrap", "textwrap",
"unicode-width", "unicode-width",
"vec_map", "vec_map",
@ -509,18 +510,6 @@ dependencies = [
"winapi 0.3.8", "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]] [[package]]
name = "dtoa" name = "dtoa"
version = "0.4.4" version = "0.4.4"
@ -1282,7 +1271,6 @@ dependencies = [
"bytes", "bytes",
"cstr", "cstr",
"cursive", "cursive",
"docopt",
"failure", "failure",
"fnv", "fnv",
"futures", "futures",
@ -1310,6 +1298,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"smallvec 1.1.0", "smallvec 1.1.0",
"structopt 0.3.13",
"tempdir", "tempdir",
"time 0.1.42", "time 0.1.42",
"tokio", "tokio",
@ -1624,7 +1613,7 @@ checksum = "5240be0c9ea1bc7887819a36264cb9475eb71c58749808e5b989c8c1fdc67acf"
dependencies = [ dependencies = [
"ansi_term 0.9.0", "ansi_term 0.9.0",
"prettytable-rs", "prettytable-rs",
"structopt", "structopt 0.2.18",
] ]
[[package]] [[package]]
@ -1641,6 +1630,32 @@ dependencies = [
"unicode-width", "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]] [[package]]
name = "proc-macro-hack" name = "proc-macro-hack"
version = "0.5.11" version = "0.5.11"
@ -2302,7 +2317,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16c2cdbf9cc375f15d1b4141bc48aeef444806655cd0e904207edc8d68d86ed7" checksum = "16c2cdbf9cc375f15d1b4141bc48aeef444806655cd0e904207edc8d68d86ed7"
dependencies = [ dependencies = [
"clap", "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]] [[package]]
@ -2317,6 +2343,19 @@ dependencies = [
"syn 0.15.44", "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]] [[package]]
name = "subtle" name = "subtle"
version = "1.0.0" version = "1.0.0"
@ -2345,6 +2384,17 @@ dependencies = [
"unicode-xid 0.2.0", "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]] [[package]]
name = "synstructure" name = "synstructure"
version = "0.12.3" version = "0.12.3"
@ -2409,6 +2459,7 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [ dependencies = [
"term_size",
"unicode-width", "unicode-width",
] ]

View File

@ -25,7 +25,10 @@ byteorder = "1.0"
cstr = "0.1.7" cstr = "0.1.7"
cursive = "0.14.0" cursive = "0.14.0"
db = { package = "moonfire-db", path = "db" } db = { package = "moonfire-db", path = "db" }
docopt = "1.0" #structopt = "0.3.13"
structopt = { version = "0.3.13", features = ["default", "wrap_help"] }
# default = ["suggestions", "color", "vec_map", "derive", "std", "cargo"]
failure = "0.1.1" failure = "0.1.1"
ffmpeg = { package = "moonfire-ffmpeg", path = "ffmpeg" } ffmpeg = { package = "moonfire-ffmpeg", path = "ffmpeg" }
futures = "0.3" futures = "0.3"

View File

@ -42,6 +42,7 @@ use rusqlite::{Connection, Transaction, params};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::fmt; use std::fmt;
use std::net::IpAddr; use std::net::IpAddr;
use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
lazy_static! { lazy_static! {
@ -57,7 +58,7 @@ pub(crate) fn set_test_config() {
Arc::new(libpasta::Config::with_primitive(libpasta::primitives::Bcrypt::new(2))); Arc::new(libpasta::Config::with_primitive(libpasta::primitives::Bcrypt::new(2)));
} }
enum UserFlags { enum UserFlag {
Disabled = 1, Disabled = 1,
} }
@ -91,7 +92,7 @@ impl User {
} }
pub fn has_password(&self) -> bool { self.password_hash.is_some() } 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. /// A change to a user.
@ -132,7 +133,7 @@ impl UserChange {
} }
pub fn disable(&mut self) { 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, HttpOnly = 1,
Secure = 2, Secure = 2,
SameSite = 4, SameSite = 4,
SameSiteStrict = 8, SameSiteStrict = 8,
} }
impl FromStr for SessionFlag {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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)] #[derive(Copy, Clone)]
pub enum RevocationReason { pub enum RevocationReason {
LoggedOut = 1, LoggedOut = 1,
@ -209,7 +226,7 @@ pub enum RevocationReason {
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Session { pub struct Session {
user_id: i32, user_id: i32,
flags: i32, // bitmask of SessionFlags enum values flags: i32, // bitmask of SessionFlag enum values
domain: Option<Vec<u8>>, domain: Option<Vec<u8>>,
description: Option<String>, description: Option<String>,
seed: Seed, seed: Seed,

View File

@ -41,7 +41,7 @@ use log::warn;
use protobuf::Message; use protobuf::Message;
use nix::{NixPath, fcntl::{FlockArg, OFlag}, sys::stat::Mode}; use nix::{NixPath, fcntl::{FlockArg, OFlag}, sys::stat::Mode};
use nix::sys::statvfs::Statvfs; use nix::sys::statvfs::Statvfs;
use std::ffi::{CStr, CString}; use std::ffi::CStr;
use std::fs; use std::fs;
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::os::unix::io::{AsRawFd, RawFd}; use std::os::unix::io::{AsRawFd, RawFd};
@ -104,16 +104,14 @@ impl Drop for Fd {
impl Fd { impl Fd {
/// Opens the given path as a directory. /// Opens the given path as a directory.
pub fn open(path: &str, mkdir: bool) -> Result<Fd, nix::Error> { pub fn open<P: ?Sized + NixPath>(path: &P, mkdir: bool) -> Result<Fd, nix::Error> {
let cstring = CString::new(path).map_err(|_| nix::Error::InvalidPath)?;
if mkdir { 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)) => {}, Ok(()) | Err(nix::Error::Sys(nix::errno::Errno::EEXIST)) => {},
Err(e) => return Err(e), Err(e) => return Err(e),
} }
} }
let fd = nix::fcntl::open(cstring.as_c_str(), OFlag::O_DIRECTORY | OFlag::O_RDONLY, let fd = nix::fcntl::open(path, OFlag::O_DIRECTORY | OFlag::O_RDONLY, Mode::empty())?;
Mode::empty())?;
Ok(Fd(fd)) Ok(Fd(fd))
} }

View File

@ -52,9 +52,9 @@ const UPGRADE_NOTES: &'static str =
#[derive(Debug)] #[derive(Debug)]
pub struct Args<'a> { pub struct Args<'a> {
pub flag_sample_file_dir: Option<&'a str>, pub sample_file_dir: Option<&'a std::path::Path>,
pub flag_preset_journal: &'a str, pub preset_journal: &'a str,
pub flag_no_vacuum: bool, pub no_vacuum: bool,
} }
fn set_journal_mode(conn: &rusqlite::Connection, requested: &str) -> Result<(), Error> { fn set_journal_mode(conn: &rusqlite::Connection, requested: &str) -> Result<(), Error> {
@ -86,7 +86,7 @@ fn upgrade(args: &Args, target_ver: i32, conn: &mut rusqlite::Connection) -> Res
bail!("Database is at negative version {}!", old_ver); bail!("Database is at negative version {}!", old_ver);
} }
info!("Upgrading database from version {} to version {}...", old_ver, target_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 { for ver in old_ver .. target_ver {
info!("...from version {} to version {}", ver, ver + 1); info!("...from version {} to version {}", ver, ver + 1);
let tx = conn.transaction()?; let tx = conn.transaction()?;
@ -118,7 +118,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 // WAL is the preferred journal mode for normal operation; it reduces the number of syncs
// without compromising safety. // without compromising safety.
set_journal_mode(&conn, "wal")?; set_journal_mode(&conn, "wal")?;
if !args.flag_no_vacuum { if !args.no_vacuum {
info!("...vacuuming database after upgrade."); info!("...vacuuming database after upgrade.");
conn.execute_batch(r#" conn.execute_batch(r#"
pragma page_size = 16384; pragma page_size = 16384;
@ -157,7 +157,7 @@ impl NixPath for UuidPath {
mod tests { mod tests {
use crate::compare; use crate::compare;
use crate::testutil; use crate::testutil;
use failure::{ResultExt, format_err}; use failure::ResultExt;
use super::*; use super::*;
fn new_conn() -> Result<rusqlite::Connection, Error> { fn new_conn() -> Result<rusqlite::Connection, Error> {
@ -183,7 +183,7 @@ mod tests {
fn upgrade_and_compare() -> Result<(), Error> { fn upgrade_and_compare() -> Result<(), Error> {
testutil::init(); testutil::init();
let tmpdir = tempdir::TempDir::new("moonfire-nvr-test")?; 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()?; let mut upgraded = new_conn()?;
upgraded.execute_batch(include_str!("v0.sql"))?; upgraded.execute_batch(include_str!("v0.sql"))?;
upgraded.execute_batch(r#" upgraded.execute_batch(r#"
@ -218,9 +218,9 @@ mod tests {
(4, None), // transitional; don't compare schemas. (4, None), // transitional; don't compare schemas.
(5, Some(include_str!("../schema.sql")))] { (5, Some(include_str!("../schema.sql")))] {
upgrade(&Args { upgrade(&Args {
flag_sample_file_dir: Some(&path), sample_file_dir: Some(&tmpdir.path()),
flag_preset_journal: "delete", preset_journal: "delete",
flag_no_vacuum: false, no_vacuum: false,
}, *ver, &mut upgraded).context(format!("upgrading to version {}", ver))?; }, *ver, &mut upgraded).context(format!("upgrading to version {}", ver))?;
if let Some(f) = fresh_sql { if let Some(f) = fresh_sql {
compare(&upgraded, *ver, f)?; compare(&upgraded, *ver, f)?;

View File

@ -42,7 +42,7 @@ use uuid::Uuid;
pub fn run(args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error> { pub fn run(args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error> {
let sample_file_path = 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 \ .ok_or_else(|| format_err!("--sample-file-dir required when upgrading from \
schema version 1 to 2."))?; 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)?; 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#" tx.execute(r#"
insert into sample_file_dir (path, uuid, last_complete_open_id) insert into sample_file_dir (path, uuid, last_complete_open_id)
values (?, ?, ?) values (?, ?, ?)
@ -293,7 +296,7 @@ pub fn run(args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
/// * optional: reserved sample file uuids. /// * optional: reserved sample file uuids.
/// * optional: meta and meta-tmp from half-completed update attempts. /// * optional: meta and meta-tmp from half-completed update attempts.
/// * forbidden: anything else. /// * 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> { tx: &rusqlite::Transaction) -> Result<(), Error> {
// Build a hash of the uuids found in the directory. // Build a hash of the uuids found in the directory.
let n: i64 = tx.query_row(r#" 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()? { while let Some(row) = rows.next()? {
let uuid: crate::db::FromSqlUuid = row.get(0)?; let uuid: crate::db::FromSqlUuid = row.get(0)?;
if !files.remove(&uuid.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() { if !files.is_empty() {
bail!("{} unexpected sample file uuids in dir {}: {:?}!", bail!("{} unexpected sample file uuids in dir {}: {:?}!",
files.len(), sample_file_path, files); files.len(), sample_file_path.display(), files);
} }
Ok(()) Ok(())
} }

View File

@ -1,5 +1,5 @@
// This file is part of Moonfire NVR, a security camera network video recorder. // 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 // 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 // it under the terms of the GNU General Public License as published by
@ -32,36 +32,25 @@
use db::check; use db::check;
use failure::Error; use failure::Error;
use serde::Deserialize; use std::path::PathBuf;
use structopt::StructOpt;
static USAGE: &'static str = r#" #[derive(StructOpt)]
Checks database integrity. 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: /// Compare sample file lengths on disk to the database.
#[structopt(long)]
moonfire-nvr check [options] compare_lens: bool,
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,
} }
pub fn run() -> Result<(), Error> { pub fn run(args: &Args) -> Result<(), Error> {
let args: Args = super::parse_args(USAGE)?;
// TODO: ReadOnly should be sufficient but seems to fail. // 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 { check::run(&conn, &check::Options {
compare_lens: args.flag_compare_lens, compare_lens: args.compare_lens,
}) })
} }

View File

@ -38,36 +38,24 @@ use cursive::Cursive;
use cursive::views; use cursive::views;
use db; use db;
use failure::Error; use failure::Error;
use serde::Deserialize; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use structopt::StructOpt;
mod cameras; mod cameras;
mod dirs; mod dirs;
mod users; mod users;
static USAGE: &'static str = r#" #[derive(StructOpt)]
Interactive configuration editor. pub struct Args {
/// Directory holding the SQLite3 index database.
Usage: #[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path",
parse(from_os_str))]
moonfire-nvr config [options] db_dir: PathBuf,
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,
} }
pub fn run() -> Result<(), Error> { pub fn run(args: &Args) -> Result<(), Error> {
let args: Args = super::parse_args(USAGE)?; let (_db_dir, conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?;
let (_db_dir, conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::ReadWrite)?;
let clocks = clock::RealClocks {}; let clocks = clock::RealClocks {};
let db = Arc::new(db::Database::new(clocks, conn, true)?); let db = Arc::new(db::Database::new(clocks, conn, true)?);

View File

@ -1,5 +1,5 @@
// This file is part of Moonfire NVR, a security camera network video recorder. // 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 // 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 // it under the terms of the GNU General Public License as published by
@ -30,31 +30,19 @@
use failure::Error; use failure::Error;
use log::info; use log::info;
use serde::Deserialize; use structopt::StructOpt;
use std::path::PathBuf;
static USAGE: &'static str = r#" #[derive(StructOpt)]
Initializes a database. pub struct Args {
/// Directory holding the SQLite3 index database.
Usage: #[structopt(long, default_value = "/var/lib/moonfire-nvr/db", value_name="path",
parse(from_os_str))]
moonfire-nvr init [options] db_dir: PathBuf,
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,
} }
pub fn run() -> Result<(), Error> { pub fn run(args: &Args) -> Result<(), Error> {
let args: Args = super::parse_args(USAGE)?; let (_db_dir, mut conn) = super::open_conn(&args.db_dir, super::OpenMode::Create)?;
let (_db_dir, mut conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::Create)?;
// Check if the database has already been initialized. // Check if the database has already been initialized.
let cur_ver = db::get_schema_version(&conn)?; let cur_ver = db::get_schema_version(&conn)?;

View File

@ -1,5 +1,5 @@
// This file is part of Moonfire NVR, a security camera network video recorder. // 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 // 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 // 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). //! Subcommand to login a user (without requiring a password).
use base::clock::{self, Clocks}; use base::clock::{self, Clocks};
use db::auth::SessionFlags; use db::auth::SessionFlag;
use failure::{Error, ResultExt, bail, format_err}; use failure::{Error, format_err};
use serde::Deserialize;
use std::os::unix::fs::OpenOptionsExt as _; use std::os::unix::fs::OpenOptionsExt as _;
use std::io::Write as _; use std::io::Write as _;
use std::path::PathBuf; use std::path::PathBuf;
use structopt::StructOpt;
static USAGE: &'static str = r#" #[derive(Debug, Default, StructOpt)]
Logs in a user, returning the session cookie. 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 /// Create a session with the given permissions.
check the user's password and even can be used to create sessions with ///
permissions the user doesn't have. /// If unspecified, uses user's default permissions.
#[structopt(long, value_name="perms",
parse(try_from_str = protobuf::text_format::parse_from_str))]
permissions: Option<db::Permissions>,
Usage: /// Restrict this cookie to the given domain.
#[structopt(long)]
domain: Option<String>,
moonfire-nvr login [options] <username> /// Write the cookie to a new curl-compatible cookie-jar file.
moonfire-nvr login --help ///
/// ---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<PathBuf>,
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<SessionFlag>,
--db-dir=DIR Set the directory holding the SQLite3 index database. This /// Create the session for this username.
is typically on a flash device. username: String,
[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<String>,
flag_domain: Option<String>,
flag_curl_cookie_jar: Option<PathBuf>,
flag_session_flags: String,
arg_username: String,
} }
pub fn run() -> Result<(), Error> { pub fn run(args: &Args) -> Result<(), Error> {
let args: Args = super::parse_args(USAGE)?;
let clocks = clock::RealClocks {}; 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 db = std::sync::Arc::new(db::Database::new(clocks.clone(), conn, true).unwrap());
let mut l = db.lock(); let mut l = db.lock();
let u = l.get_user(&args.arg_username) let u = l.get_user(&args.username)
.ok_or_else(|| format_err!("no such user {:?}", &args.arg_username))?; .ok_or_else(|| format_err!("no such user {:?}", &args.username))?;
let permissions = match args.flag_permissions { let permissions = args.permissions.as_ref().unwrap_or(&u.permissions).clone();
None => u.permissions.clone(),
Some(s) => protobuf::text_format::parse_from_str(&s)
.context("unable to parse --permissions")?
};
let creation = db::auth::Request { let creation = db::auth::Request {
when_sec: Some(db.clocks().realtime().sec), when_sec: Some(db.clocks().realtime().sec),
user_agent: None, user_agent: None,
addr: None, addr: None,
}; };
let mut flags = 0; let mut flags = 0;
for f in args.flag_session_flags.split(',') { for f in &args.session_flags {
flags |= match f { flags |= *f as i32;
"http-only" => SessionFlags::HttpOnly,
"secure" => SessionFlags::Secure,
"same-site" => SessionFlags::SameSite,
"same-site-strict" => SessionFlags::SameSiteStrict,
_ => bail!("unknown session flag {:?}", f),
} as i32;
} }
let uid = u.id; let uid = u.id;
drop(u); drop(u);
let (sid, _) = l.make_session(creation, uid, 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)?; flags, permissions)?;
let mut encoded = [0u8; 64]; let mut encoded = [0u8; 64];
base64::encode_config_slice(&sid, base64::STANDARD_NO_PAD, &mut encoded); base64::encode_config_slice(&sid, base64::STANDARD_NO_PAD, &mut encoded);
let encoded = std::str::from_utf8(&encoded[..]).expect("base64 is valid UTF-8"); let encoded = std::str::from_utf8(&encoded[..]).expect("base64 is valid UTF-8");
if let Some(ref p) = args.flag_curl_cookie_jar { if let Some(ref p) = args.curl_cookie_jar {
let d = args.flag_domain.as_ref() let d = args.domain.as_ref()
.ok_or_else(|| format_err!("--cookiejar requires --domain"))?; .ok_or_else(|| format_err!("--cookiejar requires --domain"))?;
let mut f = std::fs::OpenOptions::new() let mut f = std::fs::OpenOptions::new()
.write(true) .write(true)
@ -139,11 +121,11 @@ pub fn run() -> Result<(), Error> {
fn curl_cookie(cookie: &str, flags: i32, domain: &str) -> String { 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}", 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, domain=domain,
tailmatch="FALSE", tailmatch="FALSE",
path="/", 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 expires="9223372036854775807", // 64-bit CURL_OFF_T_MAX, never expires
name="s", name="s",
value=cookie) value=cookie)
@ -153,24 +135,10 @@ fn curl_cookie(cookie: &str, flags: i32, domain: &str) -> String {
mod tests { mod tests {
use super::*; 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] #[test]
fn test_curl_cookie() { fn test_curl_cookie() {
assert_eq!(curl_cookie("o3mx3OntO7GzwwsD54OuyQ4IuipYrwPR2aiULPHSudAa+xIhwWjb+w1TnGRh8Z5Q", 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\ "#HttpOnly_localhost\tFALSE\t/\tFALSE\t9223372036854775807\ts\t\
o3mx3OntO7GzwwsD54OuyQ4IuipYrwPR2aiULPHSudAa+xIhwWjb+w1TnGRh8Z5Q"); o3mx3OntO7GzwwsD54OuyQ4IuipYrwPR2aiULPHSudAa+xIhwWjb+w1TnGRh8Z5Q");
} }

View File

@ -29,48 +29,19 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
use db::dir; use db::dir;
use docopt;
use failure::{Error, Fail}; use failure::{Error, Fail};
use nix::fcntl::FlockArg; use nix::fcntl::FlockArg;
use rusqlite; use rusqlite;
use serde::Deserialize;
use std::path::Path; use std::path::Path;
mod check; pub mod check;
mod config; pub mod config;
mod login; pub mod login;
mod init; pub mod init;
mod run; pub mod run;
mod sql; pub mod sql;
mod ts; pub mod ts;
mod upgrade; pub 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(),
}
}
}
#[derive(Copy, Clone, PartialEq, Eq)] #[derive(Copy, Clone, PartialEq, Eq)]
enum OpenMode { enum OpenMode {
@ -81,10 +52,10 @@ enum OpenMode {
/// Locks the directory without opening the database. /// 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. /// 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<dir::Fd, Error> { fn open_dir(db_dir: &Path, mode: OpenMode) -> Result<dir::Fd, Error> {
let dir = dir::Fd::open(db_dir, mode == OpenMode::Create)?; let dir = dir::Fd::open(db_dir, mode == OpenMode::Create)?;
let ro = mode == OpenMode::ReadOnly; 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", .map_err(|e| e.context(format!("db dir {:?} already in use; can't get {} lock",
db_dir, if ro { "shared" } else { "exclusive" })))?; db_dir, if ro { "shared" } else { "exclusive" })))?;
Ok(dir) Ok(dir)
@ -92,10 +63,10 @@ fn open_dir(db_dir: &str, mode: OpenMode) -> Result<dir::Fd, Error> {
/// Locks and opens the database. /// Locks and opens the database.
/// The returned `dir::Fd` holds the lock and should be kept open as long as the `Connection` is. /// 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 dir = open_dir(db_dir, mode)?;
let conn = rusqlite::Connection::open_with_flags( let conn = rusqlite::Connection::open_with_flags(
Path::new(&db_dir).join("db"), db_dir.join("db"),
match mode { match mode {
OpenMode::ReadOnly => rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY, OpenMode::ReadOnly => rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
OpenMode::ReadWrite => rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE, 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)?; rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX)?;
Ok((dir, conn)) Ok((dir, conn))
} }
fn parse_args<'a, T>(usage: &str) -> Result<T, Error> where T: ::serde::Deserialize<'a> {
Ok(docopt::Docopt::new(usage)
.and_then(|d| d.deserialize())
.unwrap_or_else(|e| e.exit()))
}

View File

@ -1,5 +1,5 @@
// This file is part of Moonfire NVR, a security camera network video recorder. // 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 // 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 // it under the terms of the GNU General Public License as published by
@ -33,19 +33,60 @@ use crate::stream;
use crate::streamer; use crate::streamer;
use crate::web; use crate::web;
use db::{dir, writer}; use db::{dir, writer};
use failure::{Error, ResultExt, bail}; use failure::{Error, bail};
use fnv::FnvHashMap; use fnv::FnvHashMap;
use futures::future::FutureExt; use futures::future::FutureExt;
use hyper::service::{make_service_fn, service_fn}; use hyper::service::{make_service_fn, service_fn};
use log::{info, warn}; use log::{info, warn};
use serde::Deserialize; use std::path::PathBuf;
use std::pin::Pin; use std::pin::Pin;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::thread; use std::thread;
use structopt::StructOpt;
use tokio; use tokio;
use tokio::signal::unix::{SignalKind, signal}; 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<db::Permissions>,
/// 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,
}
// These are used in a hack to get the name of the current time zone (e.g. America/Los_Angeles). // 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. // They seem to be correct for Linux and macOS at least.
const LOCALTIME_PATH: &'static str = "/etc/localtime"; const LOCALTIME_PATH: &'static str = "/etc/localtime";
@ -55,44 +96,6 @@ const ZONEINFO_PATHS: [&'static str; 2] = [
"/var/db/timezone/zoneinfo/" // macOS High Sierra "/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.
"#;
#[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<String>,
flag_trust_forward_hdrs: bool,
}
fn trim_zoneinfo(p: &str) -> &str { fn trim_zoneinfo(p: &str) -> &str {
for zp in &ZONEINFO_PATHS { for zp in &ZONEINFO_PATHS {
if p.starts_with(zp) { if p.starts_with(zp) {
@ -167,13 +170,12 @@ struct Syncer {
} }
#[tokio::main] #[tokio::main]
pub async fn run() -> Result<(), Error> { pub async fn run(args: &Args) -> Result<(), Error> {
let args: Args = super::parse_args(USAGE)?;
let clocks = clock::RealClocks {}; let clocks = clock::RealClocks {};
let (_db_dir, conn) = super::open_conn( let (_db_dir, conn) = super::open_conn(
&args.flag_db_dir, &args.db_dir,
if args.flag_read_only { super::OpenMode::ReadOnly } else { super::OpenMode::ReadWrite })?; if args.read_only { super::OpenMode::ReadOnly } else { super::OpenMode::ReadWrite })?;
let db = Arc::new(db::Database::new(clocks.clone(), conn, !args.flag_read_only).unwrap()); let db = Arc::new(db::Database::new(clocks.clone(), conn, !args.read_only).unwrap());
info!("Database is loaded."); info!("Database is loaded.");
{ {
@ -186,22 +188,18 @@ pub async fn run() -> Result<(), Error> {
let time_zone_name = resolve_zone()?; let time_zone_name = resolve_zone()?;
info!("Resolved timezone: {}", &time_zone_name); 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 { let s = web::Service::new(web::Config {
db: db.clone(), db: db.clone(),
ui_dir: Some(&args.flag_ui_dir), ui_dir: Some(&args.ui_dir),
allow_unauthenticated_permissions, allow_unauthenticated_permissions: args.allow_unauthenticated_permissions.clone(),
trust_forward_hdrs: args.flag_trust_forward_hdrs, trust_forward_hdrs: args.trust_forward_hdrs,
time_zone_name, time_zone_name,
})?; })?;
// Start a streamer for each stream. // Start a streamer for each stream.
let shutdown_streamers = Arc::new(AtomicBool::new(false)); let shutdown_streamers = Arc::new(AtomicBool::new(false));
let mut streamers = Vec::new(); let mut streamers = Vec::new();
let syncers = if !args.flag_read_only { let syncers = if !args.read_only {
let l = db.lock(); let l = db.lock();
let mut dirs = FnvHashMap::with_capacity_and_hasher( let mut dirs = FnvHashMap::with_capacity_and_hasher(
l.sample_file_dirs_by_id().len(), Default::default()); l.sample_file_dirs_by_id().len(), Default::default());
@ -267,14 +265,13 @@ pub async fn run() -> Result<(), Error> {
} else { None }; } else { None };
// Start the web interface. // Start the web interface.
let addr = args.flag_http_addr.parse().unwrap();
let make_svc = make_service_fn(move |_conn| { let make_svc = make_service_fn(move |_conn| {
futures::future::ok::<_, std::convert::Infallible>(service_fn({ futures::future::ok::<_, std::convert::Infallible>(service_fn({
let mut s = s.clone(); let mut s = s.clone();
move |req| Pin::from(s.serve(req)) 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) .tcp_nodelay(true)
.serve(make_svc); .serve(make_svc);

View File

@ -1,5 +1,5 @@
// This file is part of Moonfire NVR, a security camera network video recorder. // 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 // 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 // it under the terms of the GNU General Public License as published by
@ -31,45 +31,43 @@
//! Subcommand to run a SQLite shell. //! Subcommand to run a SQLite shell.
use failure::Error; use failure::Error;
use serde::Deserialize; use std::ffi::OsString;
use std::path::PathBuf;
use std::process::Command; use std::process::Command;
use super::OpenMode; use super::OpenMode;
use structopt::StructOpt;
static USAGE: &'static str = r#" #[derive(StructOpt)]
Runs a SQLite shell on the Moonfire NVR database with locking. 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] [--] [<arg>...] /// Arguments to pass to sqlite3.
moonfire-nvr sql --help ///
/// Use the -- separator to pass sqlite3 options, as in
Positional arguments will be passed to sqlite3. Use the -- separator to pass /// "moonfire-nvr sql -- -line 'select username from user'".
sqlite3 options, as in "moonfire-nvr sql -- -line 'select username from user'". #[structopt(parse(from_os_str))]
arg: Vec<OsString>,
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<String>,
} }
pub fn run() -> Result<(), Error> { pub fn run(args: &Args) -> Result<(), Error> {
let args: Args = super::parse_args(USAGE)?; let mode = if args.read_only { OpenMode::ReadOnly } else { OpenMode::ReadWrite };
let _db_dir = super::open_dir(&args.db_dir, mode)?;
let mode = if args.flag_read_only { OpenMode::ReadWrite } else { OpenMode::ReadOnly }; let mut db = OsString::new();
let _db_dir = super::open_dir(&args.flag_db_dir, mode)?; db.push("file:");
let mut db = format!("file:{}/db", &args.flag_db_dir); db.push(&args.db_dir);
if args.flag_read_only { db.push("/db");
db.push_str("?mode=ro"); 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(()) Ok(())
} }

View File

@ -1,5 +1,5 @@
// This file is part of Moonfire NVR, a security camera network video recorder. // 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 // 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 // it under the terms of the GNU General Public License as published by
@ -29,21 +29,22 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
use failure::Error; use failure::Error;
use serde::Deserialize; use structopt::StructOpt;
const USAGE: &'static str = r#" #[derive(StructOpt)]
Usage: moonfire-nvr ts <ts>... pub struct Args {
moonfire-nvr ts --help /// Timestamp(s) to translate.
"#; ///
/// May be either an integer or an RFC-3339-like string:
#[derive(Debug, Deserialize)] /// YYYY-mm-dd[THH:MM[:SS[:FFFFF]]][{Z,{+,-,}HH:MM}].
struct Args { ///
arg_ts: Vec<String>, /// Eg: 142913484000000, 2020-04-26, 2020-04-26T12:00:00:00000-07:00.
#[structopt(required = true)]
timestamps: Vec<String>,
} }
pub fn run() -> Result<(), Error> { pub fn run(args: &Args) -> Result<(), Error> {
let arg: Args = super::parse_args(&USAGE)?; for timestamp in &args.timestamps {
for timestamp in &arg.arg_ts {
let t = db::recording::Time::parse(timestamp)?; let t = db::recording::Time::parse(timestamp)?;
println!("{} == {}", t, t.0); println!("{} == {}", t, t.0);
} }

View File

@ -1,5 +1,5 @@
// This file is part of Moonfire NVR, a security camera network video recorder. // 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 // 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 // it under the terms of the GNU General Public License as published by
@ -33,45 +33,38 @@
/// See `guide/schema.md` for more information. /// See `guide/schema.md` for more information.
use failure::Error; use failure::Error;
use serde::Deserialize; use structopt::StructOpt;
const USAGE: &'static str = r#" #[derive(StructOpt)]
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)]
pub struct Args { pub struct Args {
flag_db_dir: String, #[structopt(long,
flag_sample_file_dir: Option<String>, help = "Directory holding the SQLite3 index database.",
flag_preset_journal: String, default_value = "/var/lib/moonfire-nvr/db",
flag_no_vacuum: bool, 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<std::path::PathBuf>,
#[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> { pub fn run(args: &Args) -> Result<(), Error> {
let args: Args = super::parse_args(USAGE)?; let (_db_dir, mut conn) = super::open_conn(&args.db_dir, super::OpenMode::ReadWrite)?;
let (_db_dir, mut conn) = super::open_conn(&args.flag_db_dir, super::OpenMode::ReadWrite)?;
db::upgrade::run(&db::upgrade::Args { db::upgrade::run(&db::upgrade::Args {
flag_sample_file_dir: args.flag_sample_file_dir.as_ref().map(|s| s.as_str()), sample_file_dir: args.sample_file_dir.as_ref().map(std::path::PathBuf::as_path),
flag_preset_journal: &args.flag_preset_journal, preset_journal: &args.preset_journal,
flag_no_vacuum: args.flag_no_vacuum, no_vacuum: args.no_vacuum,
}, &mut conn) }, &mut conn)
} }

View File

@ -1,5 +1,5 @@
// This file is part of Moonfire NVR, a security camera network video recorder. // 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 // 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 // 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))] #![cfg_attr(all(feature="nightly", test), feature(test))]
use log::{error, info}; use log::{error, info};
use serde::Deserialize; use structopt::StructOpt;
mod body; mod body;
mod cmds; mod cmds;
@ -43,39 +43,53 @@ mod stream;
mod streamer; mod streamer;
mod web; mod web;
/// Commandline usage string. This is in the particular format expected by the `docopt` crate. #[derive(StructOpt)]
/// Besides being printed on --help or argument parsing error, it's actually parsed to define the #[structopt(name="moonfire-nvr", about="security camera network video recorder")]
/// allowed commandline arguments and their defaults. enum Args {
const USAGE: &'static str = " /// Checks database integrity (like fsck).
Usage: moonfire-nvr <command> [<args>...] Check(cmds::check::Args),
moonfire-nvr (--help | --version)
Options: /// Interactively edits configuration.
-h, --help Show this message. Config(cmds::config::Args),
--version Show the version of moonfire-nvr.
Commands: /// Initializes a database.
check Check database integrity Init(cmds::init::Args),
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
";
/// Commandline arguments corresponding to `USAGE`; automatically filled by the `docopt` crate. /// Logs in a user, returning the session cookie.
#[derive(Debug, Deserialize)] ///
struct Args { /// This is a privileged command that directly accesses the database. It doesn't check the
arg_command: Option<cmds::Command>, /// 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 { impl Args {
let major = option_env!("CARGO_PKG_VERSION_MAJOR"); fn run(&self) -> Result<(), failure::Error> {
let minor = option_env!("CARGO_PKG_VERSION_MAJOR"); match self {
let patch = option_env!("CARGO_PKG_VERSION_MAJOR"); Args::Check(ref a) => cmds::check::run(a),
match (major, minor, patch) { Args::Config(ref a) => cmds::config::run(a),
(Some(major), Some(minor), Some(patch)) => format!("{}.{}.{}", major, minor, patch), Args::Init(ref a) => cmds::init::run(a),
_ => "".to_owned(), 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),
}
} }
} }
@ -88,14 +102,7 @@ fn parse_fmt<S: AsRef<str>>(fmt: S) -> Option<mylog::Format> {
} }
fn main() { fn main() {
// Parse commandline arguments. let args = Args::from_args();
// (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 mut h = mylog::Builder::new() let mut h = mylog::Builder::new()
.set_format(::std::env::var("MOONFIRE_FORMAT") .set_format(::std::env::var("MOONFIRE_FORMAT")
.ok() .ok()
@ -105,7 +112,7 @@ fn main() {
.build(); .build();
h.clone().install().unwrap(); 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); error!("{:?}", e);
::std::process::exit(1); ::std::process::exit(1);
} }

View File

@ -588,10 +588,10 @@ impl ServiceInner {
}.to_owned(); }.to_owned();
let mut l = self.db.lock(); let mut l = self.db.lock();
let is_secure = self.is_secure(req); let is_secure = self.is_secure(req);
let flags = (auth::SessionFlags::HttpOnly as i32) | let flags = (auth::SessionFlag::HttpOnly as i32) |
(auth::SessionFlags::SameSite as i32) | (auth::SessionFlag::SameSite as i32) |
(auth::SessionFlags::SameSiteStrict as i32) | (auth::SessionFlag::SameSiteStrict as i32) |
if is_secure { auth::SessionFlags::Secure as i32 } else { 0 }; if is_secure { auth::SessionFlag::Secure as i32 } else { 0 };
let (sid, _) = l.login_by_password(authreq, &r.username, r.password, Some(domain), let (sid, _) = l.login_by_password(authreq, &r.username, r.password, Some(domain),
flags) flags)
.map_err(|e| plain_response(StatusCode::UNAUTHORIZED, e.to_string()))?; .map_err(|e| plain_response(StatusCode::UNAUTHORIZED, e.to_string()))?;
@ -797,7 +797,7 @@ async fn with_json_body(mut req: Request<hyper::Body>)
pub struct Config<'a> { pub struct Config<'a> {
pub db: Arc<db::Database>, pub db: Arc<db::Database>,
pub ui_dir: Option<&'a str>, pub ui_dir: Option<&'a std::path::Path>,
pub trust_forward_hdrs: bool, pub trust_forward_hdrs: bool,
pub time_zone_name: String, pub time_zone_name: String,
pub allow_unauthenticated_permissions: Option<db::Permissions>, pub allow_unauthenticated_permissions: Option<db::Permissions>,
@ -840,12 +840,12 @@ impl Service {
}))) })))
} }
fn fill_ui_files(dir: &str, files: &mut HashMap<String, UiFile>) { fn fill_ui_files(dir: &std::path::Path, files: &mut HashMap<String, UiFile>) {
let r = match fs::read_dir(dir) { let r = match fs::read_dir(dir) {
Ok(r) => r, Ok(r) => r,
Err(e) => { Err(e) => {
warn!("Unable to search --ui-dir={}; will serve no static files. Error was: {}", warn!("Unable to search --ui-dir={}; will serve no static files. Error was: {}",
dir, e); dir.display(), e);
return; return;
} }
}; };