introduce /etc/moonfire-nvr.json (#133)

This commit is contained in:
Scott Lamb 2022-03-09 13:12:33 -08:00
parent 1a51b53b54
commit ceaef46ea9
9 changed files with 239 additions and 122 deletions

View File

@ -8,6 +8,8 @@ Each release is tagged in Git and on the Docker repository
## unreleased
* introduce a configuration file `/etc/moonfire-nvr.json`; you will need
to create one when upgrading.
* bump minimum Rust version from 1.53 to 1.56.
* fix [#187](https://github.com/scottlamb/moonfire-nvr/issues/187):
incompatibility with cameras that (incorrectly) omit the SDP origin line.

View File

@ -277,23 +277,19 @@ some of the shell script's subcommands that wrap Docker (`start`, `stop`, and
If you want to deploy a non-Docker build on Linux, you may want to use
`systemd`. Create `/etc/systemd/system/moonfire-nvr.service`:
```
```ini
[Unit]
Description=Moonfire NVR
After=network-online.target
[Service]
ExecStart=/usr/local/bin/moonfire-nvr run \
--db-dir=/var/lib/moonfire-nvr/db \
--http-addr=0.0.0.0:8080 \
--allow-unauthenticated-permissions='view_video: true'
ExecStart=/usr/local/bin/moonfire-nvr run
Environment=TZ=:/etc/localtime
Environment=MOONFIRE_FORMAT=google-systemd
Environment=MOONFIRE_LOG=info
Environment=RUST_BACKTRACE=1
Type=simple
User=moonfire-nvr
Nice=-20
Restart=on-failure
CPUAccounting=true
MemoryAccounting=true
@ -303,10 +299,24 @@ BlockIOAccounting=true
WantedBy=multi-user.target
```
Note that the arguments used here are insecure. You can change that via
replacing the `--allow-unauthenticated-permissions` argument here as
described in [Securing Moonfire NVR and exposing it to the
Internet](secure.md).
You'll also need a `/etc/moonfire-nvr.json`:
```json
{
"binds": [
{
"ipv4": "0.0.0.0:8080",
"allowUnauthenticatedPermissions": {
"viewVideo": true
}
}
]
}
```
Note this configuration is insecure. You can change that via replacing the
`allowUnauthenticatedPermissions` here as described in [Securing Moonfire NVR
and exposing it to the Internet](secure.md).
Some handy commands:

View File

@ -71,6 +71,7 @@ image_name="scottlamb/moonfire-nvr:latest"
container_name="moonfire-nvr"
common_docker_run_args=(
--mount=type=bind,source=/var/lib/moonfire-nvr,destination=/var/lib/moonfire-nvr
--mount=type=bind,source=/etc/moonfire-nvr.json,destination=/etc/moonfire-nvr.json
# Add additional mount lines here for each sample file directory
# outside of /var/lib/moonfire-nvr, eg:
@ -107,12 +108,6 @@ run)
--name="${container_name}" \
"${image_name}" \
run \
# Add any additional `moonfire-nvr run` arguments here, eg
# "--rtsp-library=ffmpeg" if the default "--rtsp-library=retina"
# isn't working.
--allow-unauthenticated-permissions='view_video: true' \
"$@"
;;
start|stop|logs|rm)
@ -263,6 +258,21 @@ In the user interface,
### Starting it up
You'll need to create the runtime configuration file, `/etc/moonfire-nvr.json`:
```json
{
"binds": [
{
"ipv4": "0.0.0.0:8080",
"allowUnauthenticatedPermissions": {
"viewVideo": true
}
}
]
}
```
Note that at this stage, Moonfire NVR's web interface is **insecure**: it
doesn't use `https` and doesn't require you to authenticate
to it. You might be comfortable starting it in this configuration to try it

View File

@ -161,31 +161,33 @@ your browser. See [How to secure Nginx with Let's Encrypt on Ubuntu
## 6. Reconfigure Moonfire NVR
If you follow the recommended Docker setup, your `/usr/local/bin/nvr` script
will contain this line:
If you follow the recommended Docker setup, your `/etc/moonfire-nvr.json`
will contain these lines:
```
--allow-unauthenticated-permissions='view_video: true'
```json
"allowUnauthenticatedPermissions": {
"viewVideo": true
}
```
Replace it with the following:
Replace them with the following:
```
--trust-forward-hdrs
```json
"trustForwardHdrs": true
```
This change has two effects:
* No `--allow-unauthenticated-permissions` means that web users must
authenticate.
* `--trust-forward-hdrs` means that Moonfire NVR will look for `X-Real-IP`
* No `allowUnauthenticatePermissions` means that web users must authenticate.
* `trustForwardHdrs` means that Moonfire NVR will look for `X-Real-IP`
and `X-Forwarded-Proto` headers as added by the webserver configuration
in the next section.
If the webserver is running on the same machine as Moonfire NVR, you might
also change `--publish=8080:8080` to `--publish=127.0.0.1:8080:8080`, which
prevents other machines on the network from impersonating the proxy,
effectively allowing them to lie about the client's IP and protocol.
also change `--publish=8080:8080` to `--publish=127.0.0.1:8080:8080` in your
`/usr/local/bin/nvr` script, preventing other machines on the network from
impersonating the proxy, effectively allowing them to lie about the client's IP
and protocol.
To make this take effect, you'll need to stop the running Docker container,
delete it, and create/run a new one:

View File

@ -27,7 +27,7 @@ mod proto {
}
mod raw;
pub mod recording;
use proto::schema;
pub use proto::schema;
pub mod signal;
pub mod upgrade;
pub mod writer;

View File

@ -0,0 +1,105 @@
// This file is part of Moonfire NVR, a security camera network video recorder.
// Copyright (C) 2022 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception.
//! Runtime configuration file (`/etc/moonfire-nvr.conf`).
use std::path::PathBuf;
use serde::Deserialize;
fn default_db_dir() -> PathBuf {
"/var/lib/moonfire-nvr/db".into()
}
fn default_ui_dir() -> PathBuf {
"/usr/local/lib/moonfire-nvr/ui".into()
}
/// Top-level configuration file object.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConfigFile {
pub binds: Vec<BindConfig>,
/// Directory holding the SQLite3 index database.
#[serde(default = "default_db_dir")]
pub db_dir: PathBuf,
/// Directory holding user interface files (`.html`, `.js`, etc).
#[serde(default = "default_ui_dir")]
pub ui_dir: PathBuf,
/// The number of worker threads used by the asynchronous runtime.
///
/// Defaults to the number of cores on the system.
#[serde(default)]
pub worker_threads: Option<usize>,
/// RTSP library to use for fetching the cameras' video stream.
/// Moonfire NVR is in the process of switching from `ffmpeg` (used since
/// the beginning of the project) to `retina` (a pure-Rust RTSP library
/// developed by Moonfire NVR's author).
#[serde(default)]
pub rtsp_library: crate::stream::RtspLibrary,
}
/// Per-bind configuration.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BindConfig {
/// The address to bind to.
#[serde(flatten)]
pub address: AddressConfig,
/// Allow unauthenticated API access on this bind, with the given
/// permissions (defaults to empty).
///
/// Note that even an empty string allows some basic access that would be rejected if the
/// argument were omitted.
#[serde(default)]
pub allow_unauthenticated_permissions: Option<Permissions>,
/// Trusts `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 a localhost bind address.
#[serde(default)]
pub trust_forward_hdrs: bool,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum AddressConfig {
/// IPv4 address such as `0.0.0.0:8080` or `127.0.0.1:8080`.
Ipv4(std::net::SocketAddrV4),
/// IPv6 address such as `[::]:8080` or `[::1]:8080`.
Ipv6(std::net::SocketAddrV6),
// TODO: /// Unix socket path such as `/var/lib/moonfire-nvr/sock`.
// Unix(PathBuf),
// TODO: SystemdFileDescriptorName(String), see
// https://www.freedesktop.org/software/systemd/man/systemd.socket.html
}
/// JSON analog of `Permissions` defined in `db/proto/schema.proto`.
#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Permissions {
view_video: bool,
read_camera_configs: bool,
update_signals: bool,
}
impl Permissions {
pub fn as_proto(&self) -> db::schema::Permissions {
db::schema::Permissions {
view_video: self.view_video,
read_camera_configs: self.read_camera_configs,
update_signals: self.update_signals,
..Default::default()
}
}
}

View File

@ -1,7 +1,8 @@
// This file is part of Moonfire NVR, a security camera network video recorder.
// Copyright (C) 2021 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
// Copyright (C) 2022 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception.
use crate::cmds::run::config::Permissions;
use crate::streamer;
use crate::web;
use base::clock;
@ -11,75 +12,28 @@ use fnv::FnvHashMap;
use hyper::service::{make_service_fn, service_fn};
use log::error;
use log::{info, warn};
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::thread;
use structopt::StructOpt;
use tokio::signal::unix::{signal, SignalKind};
use self::config::ConfigFile;
mod config;
#[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,
/// The number of worker threads used by the asynchronous runtime.
/// Defaults to the number of cores on the system.
#[structopt(long, value_name = "worker_threads")]
worker_threads: Option<usize>,
/// Directory holding user interface files (.html, .js, etc).
#[structopt(
long,
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,
#[structopt(short, long, default_value = "/etc/moonfire-nvr.json")]
config: PathBuf,
/// 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.
/// Note this is incompatible with session authentication; consider adding
/// a bind with `allowUnauthenticatedPermissions` your config.
#[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,
/// RTSP library to use for fetching the cameras' video stream.
/// Moonfire NVR is in the process of switching from `ffmpeg` (used since
/// the beginning of the project) to `retina` (a pure-Rust RTSP library
/// developed by Moonfire NVR's author).
#[structopt(long, default_value = "retina", parse(try_from_str))]
rtsp_library: crate::stream::RtspLibrary,
/// The RTSP transport (`tcp` or `udp`) to use when none is specified in the
/// per-stream configuration.
#[structopt(long, default_value)]
rtsp_transport: retina::client::Transport,
}
// These are used in a hack to get the name of the current time zone (e.g. America/Los_Angeles).
@ -171,14 +125,23 @@ struct Syncer {
join: thread::JoinHandle<()>,
}
fn read_config(path: &Path) -> Result<ConfigFile, Error> {
let config = std::fs::read(path)?;
let config = serde_json::from_slice(&config)?;
Ok(config)
}
pub fn run(args: Args) -> Result<i32, Error> {
let config = read_config(&args.config)
.with_context(|_| format!("unable to read {}", &args.config.display()))?;
let mut builder = tokio::runtime::Builder::new_multi_thread();
builder.enable_all();
if let Some(worker_threads) = args.worker_threads {
if let Some(worker_threads) = config.worker_threads {
builder.worker_threads(worker_threads);
}
let rt = builder.build()?;
let r = rt.block_on(async_run(args));
let r = rt.block_on(async_run(args.read_only, &config));
// tokio normally waits for all spawned tasks to complete, but:
// * in the graceful shutdown path, we wait for specific tasks with logging.
@ -188,14 +151,14 @@ pub fn run(args: Args) -> Result<i32, Error> {
r
}
async fn async_run(args: Args) -> Result<i32, Error> {
async fn async_run(read_only: bool, config: &ConfigFile) -> Result<i32, Error> {
let (shutdown_tx, shutdown_rx) = base::shutdown::channel();
let mut shutdown_tx = Some(shutdown_tx);
tokio::pin! {
let int = signal(SignalKind::interrupt())?;
let term = signal(SignalKind::terminate())?;
let inner = inner(args, shutdown_rx);
let inner = inner(read_only, config, shutdown_rx);
}
tokio::select! {
@ -219,17 +182,21 @@ async fn async_run(args: Args) -> Result<i32, Error> {
}
}
async fn inner(args: Args, shutdown_rx: base::shutdown::Receiver) -> Result<i32, Error> {
async fn inner(
read_only: bool,
config: &ConfigFile,
shutdown_rx: base::shutdown::Receiver,
) -> Result<i32, Error> {
let clocks = clock::RealClocks {};
let (_db_dir, conn) = super::open_conn(
&args.db_dir,
if args.read_only {
&config.db_dir,
if read_only {
super::OpenMode::ReadOnly
} else {
super::OpenMode::ReadWrite
},
)?;
let db = Arc::new(db::Database::new(clocks, conn, !args.read_only)?);
let db = Arc::new(db::Database::new(clocks, conn, !read_only)?);
info!("Database is loaded.");
{
@ -245,19 +212,12 @@ async fn inner(args: Args, shutdown_rx: base::shutdown::Receiver) -> Result<i32,
let time_zone_name = resolve_zone()?;
info!("Resolved timezone: {}", &time_zone_name);
let svc = Arc::new(web::Service::new(web::Config {
db: db.clone(),
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 mut streamers = Vec::new();
let mut session_groups_by_camera: FnvHashMap<i32, Arc<retina::client::SessionGroup>> =
FnvHashMap::default();
let syncers = if !args.read_only {
let syncers = if !read_only {
let l = db.lock();
let mut dirs = FnvHashMap::with_capacity_and_hasher(
l.sample_file_dirs_by_id().len(),
@ -266,8 +226,7 @@ async fn inner(args: Args, shutdown_rx: base::shutdown::Receiver) -> Result<i32,
let streams = l.streams_by_id().len();
let env = streamer::Environment {
db: &db,
opener: args.rtsp_library.opener(),
default_transport: args.rtsp_transport,
opener: config.rtsp_library.opener(),
shutdown_rx: &shutdown_rx,
};
@ -354,19 +313,40 @@ async fn inner(args: Args, shutdown_rx: base::shutdown::Receiver) -> Result<i32,
None
};
// Start the web interface.
let make_svc = make_service_fn(move |_conn| {
futures::future::ok::<_, std::convert::Infallible>(service_fn({
let svc = Arc::clone(&svc);
move |req| Arc::clone(&svc).serve(req)
}))
});
let server = ::hyper::Server::try_bind(&args.http_addr)
.with_context(|_| format!("unable to bind --http-addr={}", &args.http_addr))?
.tcp_nodelay(true)
.serve(make_svc);
let server = server.with_graceful_shutdown(shutdown_rx.future());
let server_handle = tokio::spawn(server);
// Start the web interface(s).
let web_handles: Result<Vec<_>, Error> = config
.binds
.iter()
.map(|b| {
let svc = Arc::new(web::Service::new(web::Config {
db: db.clone(),
ui_dir: Some(&config.ui_dir),
allow_unauthenticated_permissions: b
.allow_unauthenticated_permissions
.as_ref()
.map(Permissions::as_proto),
trust_forward_hdrs: b.trust_forward_hdrs,
time_zone_name: time_zone_name.clone(),
})?);
let make_svc = make_service_fn(move |_conn| {
futures::future::ok::<_, std::convert::Infallible>(service_fn({
let svc = Arc::clone(&svc);
move |req| Arc::clone(&svc).serve(req)
}))
});
let socket_addr = match b.address {
config::AddressConfig::Ipv4(a) => a.into(),
config::AddressConfig::Ipv6(a) => a.into(),
};
let server = ::hyper::Server::try_bind(&socket_addr)
.with_context(|_| format!("unable to bind to {}", &socket_addr))?
.tcp_nodelay(true)
.serve(make_svc);
let server = server.with_graceful_shutdown(shutdown_rx.future());
Ok(tokio::spawn(server))
})
.collect();
let web_handles = web_handles?;
info!("Ready to serve HTTP requests");
let _ = shutdown_rx.as_future().await;
@ -394,7 +374,9 @@ async fn inner(args: Args, shutdown_rx: base::shutdown::Receiver) -> Result<i32,
db.lock().clear_watches();
info!("Waiting for HTTP requests to finish.");
server_handle.await??;
for h in web_handles {
h.await??;
}
info!("Waiting for TEARDOWN requests to complete.");
for g in session_groups_by_camera.values() {

View File

@ -11,6 +11,7 @@ use lazy_static::lazy_static;
use log::warn;
use retina::client::{Credentials, Transport};
use retina::codec::{CodecItem, VideoParameters};
use serde::Deserialize;
use std::convert::TryFrom;
use std::ffi::CString;
use std::pin::Pin;
@ -26,11 +27,18 @@ lazy_static! {
pub static ref FFMPEG: Ffmpeg = Ffmpeg::new();
}
#[derive(Copy, Clone, Debug, Deserialize)]
pub enum RtspLibrary {
Ffmpeg,
Retina,
}
impl Default for RtspLibrary {
fn default() -> Self {
RtspLibrary::Retina
}
}
impl std::str::FromStr for RtspLibrary {
type Err = Error;

View File

@ -20,7 +20,6 @@ where
C: Clocks + Clone,
{
pub opener: &'a dyn stream::Opener,
pub default_transport: retina::client::Transport,
pub db: &'tmp Arc<Database<C>>,
pub shutdown_rx: &'tmp base::shutdown::Receiver,
}
@ -96,7 +95,7 @@ where
dir,
syncer_channel,
opener: env.opener,
transport: stream_transport.unwrap_or(env.default_transport),
transport: stream_transport.unwrap_or_default(),
stream_id,
session_group,
short_name: format!("{}-{}", c.short_name, s.type_.as_str()),
@ -434,7 +433,6 @@ mod tests {
opener: &opener,
db: &db.db,
shutdown_rx: &shutdown_rx,
default_transport: retina::client::Transport::Tcp,
};
let mut stream;
{