diff --git a/CHANGELOG.md b/CHANGELOG.md index c5dbeaf..6a2cc8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/guide/build.md b/guide/build.md index 644f42a..af37317 100644 --- a/guide/build.md +++ b/guide/build.md @@ -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: diff --git a/guide/install.md b/guide/install.md index d8a026e..a23633b 100644 --- a/guide/install.md +++ b/guide/install.md @@ -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 diff --git a/guide/secure.md b/guide/secure.md index dd2ea3e..1cab011 100644 --- a/guide/secure.md +++ b/guide/secure.md @@ -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: diff --git a/server/db/lib.rs b/server/db/lib.rs index d16e889..b4c50fe 100644 --- a/server/db/lib.rs +++ b/server/db/lib.rs @@ -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; diff --git a/server/src/cmds/run/config.rs b/server/src/cmds/run/config.rs new file mode 100644 index 0000000..6d0c405 --- /dev/null +++ b/server/src/cmds/run/config.rs @@ -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, + + /// 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, + + /// 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, + + /// 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() + } + } +} diff --git a/server/src/cmds/run.rs b/server/src/cmds/run/mod.rs similarity index 73% rename from server/src/cmds/run.rs rename to server/src/cmds/run/mod.rs index 26041e6..3afae69 100644 --- a/server/src/cmds/run.rs +++ b/server/src/cmds/run/mod.rs @@ -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, - - /// 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, - - /// 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 { + let config = std::fs::read(path)?; + let config = serde_json::from_slice(&config)?; + Ok(config) +} + pub fn run(args: Args) -> Result { + 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 { r } -async fn async_run(args: Args) -> Result { +async fn async_run(read_only: bool, config: &ConfigFile) -> Result { 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 { } } -async fn inner(args: Args, shutdown_rx: base::shutdown::Receiver) -> Result { +async fn inner( + read_only: bool, + config: &ConfigFile, + shutdown_rx: base::shutdown::Receiver, +) -> Result { 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> = 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 Result(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, 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 Self { + RtspLibrary::Retina + } +} + impl std::str::FromStr for RtspLibrary { type Err = Error; diff --git a/server/src/streamer.rs b/server/src/streamer.rs index 2f4490c..dc0a741 100644 --- a/server/src/streamer.rs +++ b/server/src/streamer.rs @@ -20,7 +20,6 @@ where C: Clocks + Clone, { pub opener: &'a dyn stream::Opener, - pub default_transport: retina::client::Transport, pub db: &'tmp Arc>, 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; {