mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-01-13 07:53:22 -05:00
add concept of user/session permissions
(I also considered the names "capabilities" and "scopes", but I think "permissions" is the most widely understood.) This is increasingly necessary as the web API becomes more capable. Among other things, it allows: * non-administrator users who can view but not access camera passwords or change any state * workers that update signal state based on cameras' built-in motion detection or a security system's events but don't need to view videos * control over what can be done without authenticating Currently session permissions are just copied from user permissions, but you can also imagine admin sessions vs not, as a checkbox when signing in. This would match the standard Unix workflow of using a non-administrative session most of the time. Relevant to my current signals work (#28) and to the addition of an administrative API (#35, including #66).
This commit is contained in:
parent
d8b8d5d5e0
commit
fda7e4ca2b
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,6 +7,7 @@ cameras.sql
|
|||||||
node_modules
|
node_modules
|
||||||
prep.config
|
prep.config
|
||||||
settings-nvr-local.js
|
settings-nvr-local.js
|
||||||
|
db/schema.rs
|
||||||
target
|
target
|
||||||
ui-dist
|
ui-dist
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
|
29
Cargo.lock
generated
29
Cargo.lock
generated
@ -954,7 +954,8 @@ dependencies = [
|
|||||||
"odds 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"odds 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"openssl 0.10.23 (registry+https://github.com/rust-lang/crates.io-index)",
|
"openssl 0.10.23 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"parking_lot 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"parking_lot 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"protobuf 2.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"protobuf 3.0.0-pre (git+https://github.com/stepancheg/rust-protobuf)",
|
||||||
|
"protobuf-codegen-pure 3.0.0-pre (git+https://github.com/stepancheg/rust-protobuf)",
|
||||||
"regex 1.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"regex 1.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rusqlite 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rusqlite 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"smallvec 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
"smallvec 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
@ -1000,6 +1001,7 @@ dependencies = [
|
|||||||
"mylog 0.1.0 (git+https://github.com/scottlamb/mylog)",
|
"mylog 0.1.0 (git+https://github.com/scottlamb/mylog)",
|
||||||
"openssl 0.10.23 (registry+https://github.com/rust-lang/crates.io-index)",
|
"openssl 0.10.23 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"parking_lot 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"parking_lot 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"protobuf 3.0.0-pre (git+https://github.com/stepancheg/rust-protobuf)",
|
||||||
"reffers 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"reffers 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"regex 1.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"regex 1.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"reqwest 0.9.17 (registry+https://github.com/rust-lang/crates.io-index)",
|
"reqwest 0.9.17 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
@ -1296,8 +1298,25 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "protobuf"
|
name = "protobuf"
|
||||||
version = "2.6.1"
|
version = "3.0.0-pre"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/stepancheg/rust-protobuf#76c8892a410fa7a3d74041332c20fb2b1a74f71f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protobuf-codegen"
|
||||||
|
version = "3.0.0-pre"
|
||||||
|
source = "git+https://github.com/stepancheg/rust-protobuf#76c8892a410fa7a3d74041332c20fb2b1a74f71f"
|
||||||
|
dependencies = [
|
||||||
|
"protobuf 3.0.0-pre (git+https://github.com/stepancheg/rust-protobuf)",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protobuf-codegen-pure"
|
||||||
|
version = "3.0.0-pre"
|
||||||
|
source = "git+https://github.com/stepancheg/rust-protobuf#76c8892a410fa7a3d74041332c20fb2b1a74f71f"
|
||||||
|
dependencies = [
|
||||||
|
"protobuf 3.0.0-pre (git+https://github.com/stepancheg/rust-protobuf)",
|
||||||
|
"protobuf-codegen 3.0.0-pre (git+https://github.com/stepancheg/rust-protobuf)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "publicsuffix"
|
name = "publicsuffix"
|
||||||
@ -2453,7 +2472,9 @@ dependencies = [
|
|||||||
"checksum phf_shared 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0"
|
"checksum phf_shared 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0"
|
||||||
"checksum pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c"
|
"checksum pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c"
|
||||||
"checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
|
"checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
|
||||||
"checksum protobuf 2.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a151c11a92df0059d6ab446fafa3b21a1210aad4bc2293e1c946e8132b10db01"
|
"checksum protobuf 3.0.0-pre (git+https://github.com/stepancheg/rust-protobuf)" = "<none>"
|
||||||
|
"checksum protobuf-codegen 3.0.0-pre (git+https://github.com/stepancheg/rust-protobuf)" = "<none>"
|
||||||
|
"checksum protobuf-codegen-pure 3.0.0-pre (git+https://github.com/stepancheg/rust-protobuf)" = "<none>"
|
||||||
"checksum publicsuffix 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5afecba86dcf1e4fd610246f89899d1924fe12e1e89f555eb7c7f710f3c5ad1d"
|
"checksum publicsuffix 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5afecba86dcf1e4fd610246f89899d1924fe12e1e89f555eb7c7f710f3c5ad1d"
|
||||||
"checksum quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "faf4799c5d274f3868a4aae320a0a182cbd2baee377b378f080e16a23e9d80db"
|
"checksum quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "faf4799c5d274f3868a4aae320a0a182cbd2baee377b378f080e16a23e9d80db"
|
||||||
"checksum rand 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)" = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c"
|
"checksum rand 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)" = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c"
|
||||||
|
@ -41,6 +41,7 @@ memmap = "0.7"
|
|||||||
mylog = { git = "https://github.com/scottlamb/mylog" }
|
mylog = { git = "https://github.com/scottlamb/mylog" }
|
||||||
openssl = "0.10"
|
openssl = "0.10"
|
||||||
parking_lot = { version = "0.8", features = [] }
|
parking_lot = { version = "0.8", features = [] }
|
||||||
|
protobuf = { git = "https://github.com/stepancheg/rust-protobuf" }
|
||||||
reffers = "0.5.1"
|
reffers = "0.5.1"
|
||||||
regex = "1.0"
|
regex = "1.0"
|
||||||
ring = "0.14.6"
|
ring = "0.14.6"
|
||||||
|
@ -26,7 +26,7 @@ mylog = { git = "https://github.com/scottlamb/mylog" }
|
|||||||
odds = { version = "0.3.1", features = ["std-vec"] }
|
odds = { version = "0.3.1", features = ["std-vec"] }
|
||||||
openssl = "0.10"
|
openssl = "0.10"
|
||||||
parking_lot = { version = "0.8", features = [] }
|
parking_lot = { version = "0.8", features = [] }
|
||||||
protobuf = "2.0"
|
protobuf = { git = "https://github.com/stepancheg/rust-protobuf" }
|
||||||
regex = "1.0"
|
regex = "1.0"
|
||||||
rusqlite = "0.18"
|
rusqlite = "0.18"
|
||||||
smallvec = "0.6"
|
smallvec = "0.6"
|
||||||
@ -34,3 +34,6 @@ tempdir = "0.3"
|
|||||||
time = "0.1"
|
time = "0.1"
|
||||||
uuid = { version = "0.7", features = ["std", "v4"] }
|
uuid = { version = "0.7", features = ["std", "v4"] }
|
||||||
itertools = "0.8.0"
|
itertools = "0.8.0"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
protobuf-codegen-pure = { git = "https://github.com/stepancheg/rust-protobuf" }
|
||||||
|
80
db/auth.rs
80
db/auth.rs
@ -31,11 +31,13 @@
|
|||||||
use log::info;
|
use log::info;
|
||||||
use base::strutil;
|
use base::strutil;
|
||||||
use blake2_rfc::blake2b::blake2b;
|
use blake2_rfc::blake2b::blake2b;
|
||||||
|
use crate::schema::Permissions;
|
||||||
use failure::{Error, bail, format_err};
|
use failure::{Error, bail, format_err};
|
||||||
use fnv::FnvHashMap;
|
use fnv::FnvHashMap;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use libpasta;
|
use libpasta;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use protobuf::Message;
|
||||||
use rusqlite::{Connection, Transaction, types::ToSql};
|
use rusqlite::{Connection, Transaction, types::ToSql};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@ -68,6 +70,7 @@ pub struct User {
|
|||||||
pub password_id: i32,
|
pub password_id: i32,
|
||||||
pub password_failure_count: i64,
|
pub password_failure_count: i64,
|
||||||
pub unix_uid: Option<i32>,
|
pub unix_uid: Option<i32>,
|
||||||
|
pub permissions: Permissions,
|
||||||
|
|
||||||
/// True iff this `User` has changed since the last flush.
|
/// True iff this `User` has changed since the last flush.
|
||||||
/// Only a couple things are flushed lazily: `password_failure_count` and (on upgrade to a new
|
/// Only a couple things are flushed lazily: `password_failure_count` and (on upgrade to a new
|
||||||
@ -79,10 +82,11 @@ impl User {
|
|||||||
pub fn change(&self) -> UserChange {
|
pub fn change(&self) -> UserChange {
|
||||||
UserChange {
|
UserChange {
|
||||||
id: Some(self.id),
|
id: Some(self.id),
|
||||||
username: self.username.to_string(),
|
username: self.username.clone(),
|
||||||
flags: self.flags,
|
flags: self.flags,
|
||||||
set_password_hash: None,
|
set_password_hash: None,
|
||||||
unix_uid: self.unix_uid,
|
unix_uid: self.unix_uid,
|
||||||
|
permissions: self.permissions.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,6 +107,7 @@ pub struct UserChange {
|
|||||||
pub flags: i32,
|
pub flags: i32,
|
||||||
set_password_hash: Option<Option<String>>,
|
set_password_hash: Option<Option<String>>,
|
||||||
pub unix_uid: Option<i32>,
|
pub unix_uid: Option<i32>,
|
||||||
|
pub permissions: Permissions,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserChange {
|
impl UserChange {
|
||||||
@ -113,6 +118,7 @@ impl UserChange {
|
|||||||
flags: 0,
|
flags: 0,
|
||||||
set_password_hash: None,
|
set_password_hash: None,
|
||||||
unix_uid: None,
|
unix_uid: None,
|
||||||
|
permissions: Permissions::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,6 +221,8 @@ pub struct Session {
|
|||||||
revocation_reason: Option<i32>, // see RevocationReason enum
|
revocation_reason: Option<i32>, // see RevocationReason enum
|
||||||
revocation_reason_detail: Option<String>,
|
revocation_reason_detail: Option<String>,
|
||||||
|
|
||||||
|
pub permissions: Permissions,
|
||||||
|
|
||||||
last_use: Request,
|
last_use: Request,
|
||||||
use_count: i32,
|
use_count: i32,
|
||||||
dirty: bool,
|
dirty: bool,
|
||||||
@ -342,7 +350,8 @@ impl State {
|
|||||||
password_hash,
|
password_hash,
|
||||||
password_id,
|
password_id,
|
||||||
password_failure_count,
|
password_failure_count,
|
||||||
unix_uid
|
unix_uid,
|
||||||
|
permissions
|
||||||
from
|
from
|
||||||
user
|
user
|
||||||
"#)?;
|
"#)?;
|
||||||
@ -350,6 +359,8 @@ impl State {
|
|||||||
while let Some(row) = rows.next()? {
|
while let Some(row) = rows.next()? {
|
||||||
let id = row.get(0)?;
|
let id = row.get(0)?;
|
||||||
let name: String = row.get(1)?;
|
let name: String = row.get(1)?;
|
||||||
|
let mut permissions = Permissions::new();
|
||||||
|
permissions.merge_from_bytes(row.get_raw_checked(7)?.as_blob()?)?;
|
||||||
state.users_by_id.insert(id, User {
|
state.users_by_id.insert(id, User {
|
||||||
id,
|
id,
|
||||||
username: name.clone(),
|
username: name.clone(),
|
||||||
@ -359,6 +370,7 @@ impl State {
|
|||||||
password_failure_count: row.get(5)?,
|
password_failure_count: row.get(5)?,
|
||||||
unix_uid: row.get(6)?,
|
unix_uid: row.get(6)?,
|
||||||
dirty: false,
|
dirty: false,
|
||||||
|
permissions,
|
||||||
});
|
});
|
||||||
state.users_by_name.insert(name, id);
|
state.users_by_name.insert(name, id);
|
||||||
}
|
}
|
||||||
@ -385,7 +397,8 @@ impl State {
|
|||||||
password_id = :password_id,
|
password_id = :password_id,
|
||||||
password_failure_count = :password_failure_count,
|
password_failure_count = :password_failure_count,
|
||||||
flags = :flags,
|
flags = :flags,
|
||||||
unix_uid = :unix_uid
|
unix_uid = :unix_uid,
|
||||||
|
permissions = :permissions
|
||||||
where
|
where
|
||||||
id = :id
|
id = :id
|
||||||
"#)?;
|
"#)?;
|
||||||
@ -402,6 +415,7 @@ impl State {
|
|||||||
},
|
},
|
||||||
Some(h) => (h, e.get().password_id + 1, 0),
|
Some(h) => (h, e.get().password_id + 1, 0),
|
||||||
};
|
};
|
||||||
|
let permissions = change.permissions.write_to_bytes().expect("proto3->vec is infallible");
|
||||||
stmt.execute_named(&[
|
stmt.execute_named(&[
|
||||||
(":username", &&change.username[..]),
|
(":username", &&change.username[..]),
|
||||||
(":password_hash", phash),
|
(":password_hash", phash),
|
||||||
@ -410,6 +424,7 @@ impl State {
|
|||||||
(":flags", &change.flags),
|
(":flags", &change.flags),
|
||||||
(":unix_uid", &change.unix_uid),
|
(":unix_uid", &change.unix_uid),
|
||||||
(":id", &id),
|
(":id", &id),
|
||||||
|
(":permissions", &permissions),
|
||||||
])?;
|
])?;
|
||||||
}
|
}
|
||||||
let u = e.into_mut();
|
let u = e.into_mut();
|
||||||
@ -421,20 +436,23 @@ impl State {
|
|||||||
}
|
}
|
||||||
u.flags = change.flags;
|
u.flags = change.flags;
|
||||||
u.unix_uid = change.unix_uid;
|
u.unix_uid = change.unix_uid;
|
||||||
|
u.permissions = change.permissions;
|
||||||
Ok(u)
|
Ok(u)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_user(&mut self, conn: &Connection, change: UserChange) -> Result<&User, Error> {
|
fn add_user(&mut self, conn: &Connection, change: UserChange) -> Result<&User, Error> {
|
||||||
let mut stmt = conn.prepare_cached(r#"
|
let mut stmt = conn.prepare_cached(r#"
|
||||||
insert into user (username, password_hash, flags, unix_uid)
|
insert into user (username, password_hash, flags, unix_uid, permissions)
|
||||||
values (:username, :password_hash, :flags, :unix_uid)
|
values (:username, :password_hash, :flags, :unix_uid, :permissions)
|
||||||
"#)?;
|
"#)?;
|
||||||
let password_hash = change.set_password_hash.unwrap_or(None);
|
let password_hash = change.set_password_hash.unwrap_or(None);
|
||||||
|
let permissions = change.permissions.write_to_bytes().expect("proto3->vec is infallible");
|
||||||
stmt.execute_named(&[
|
stmt.execute_named(&[
|
||||||
(":username", &&change.username[..]),
|
(":username", &&change.username[..]),
|
||||||
(":password_hash", &password_hash),
|
(":password_hash", &password_hash),
|
||||||
(":flags", &change.flags),
|
(":flags", &change.flags),
|
||||||
(":unix_uid", &change.unix_uid),
|
(":unix_uid", &change.unix_uid),
|
||||||
|
(":permissions", &permissions),
|
||||||
])?;
|
])?;
|
||||||
let id = conn.last_insert_rowid() as i32;
|
let id = conn.last_insert_rowid() as i32;
|
||||||
self.users_by_name.insert(change.username.clone(), id);
|
self.users_by_name.insert(change.username.clone(), id);
|
||||||
@ -452,6 +470,7 @@ impl State {
|
|||||||
password_failure_count: 0,
|
password_failure_count: 0,
|
||||||
unix_uid: change.unix_uid,
|
unix_uid: change.unix_uid,
|
||||||
dirty: false,
|
dirty: false,
|
||||||
|
permissions: change.permissions,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -503,12 +522,13 @@ impl State {
|
|||||||
}
|
}
|
||||||
let password_id = u.password_id;
|
let password_id = u.password_id;
|
||||||
State::make_session(conn, req, u, domain, Some(password_id), session_flags,
|
State::make_session(conn, req, u, domain, Some(password_id), session_flags,
|
||||||
&mut self.sessions)
|
&mut self.sessions, u.permissions.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_session<'s>(conn: &Connection, creation: Request, user: &mut User, domain: Vec<u8>,
|
fn make_session<'s>(conn: &Connection, creation: Request, user: &mut User, domain: Vec<u8>,
|
||||||
creation_password_id: Option<i32>, flags: i32,
|
creation_password_id: Option<i32>, flags: i32,
|
||||||
sessions: &'s mut FnvHashMap<SessionHash, Session>)
|
sessions: &'s mut FnvHashMap<SessionHash, Session>,
|
||||||
|
permissions: Permissions)
|
||||||
-> Result<(RawSessionId, &'s Session), Error> {
|
-> Result<(RawSessionId, &'s Session), Error> {
|
||||||
let mut session_id = RawSessionId::new();
|
let mut session_id = RawSessionId::new();
|
||||||
::openssl::rand::rand_bytes(&mut session_id.0).unwrap();
|
::openssl::rand::rand_bytes(&mut session_id.0).unwrap();
|
||||||
@ -518,13 +538,16 @@ impl State {
|
|||||||
let mut stmt = conn.prepare_cached(r#"
|
let mut stmt = conn.prepare_cached(r#"
|
||||||
insert into user_session (session_id_hash, user_id, seed, flags, domain,
|
insert into user_session (session_id_hash, user_id, seed, flags, domain,
|
||||||
creation_password_id, creation_time_sec,
|
creation_password_id, creation_time_sec,
|
||||||
creation_user_agent, creation_peer_addr)
|
creation_user_agent, creation_peer_addr,
|
||||||
|
permissions)
|
||||||
values (:session_id_hash, :user_id, :seed, :flags, :domain,
|
values (:session_id_hash, :user_id, :seed, :flags, :domain,
|
||||||
:creation_password_id, :creation_time_sec,
|
:creation_password_id, :creation_time_sec,
|
||||||
:creation_user_agent, :creation_peer_addr)
|
:creation_user_agent, :creation_peer_addr,
|
||||||
|
:permissions)
|
||||||
"#)?;
|
"#)?;
|
||||||
let addr = creation.addr_buf();
|
let addr = creation.addr_buf();
|
||||||
let addr: Option<&[u8]> = addr.as_ref().map(|a| a.as_ref());
|
let addr: Option<&[u8]> = addr.as_ref().map(|a| a.as_ref());
|
||||||
|
let permissions_blob = permissions.write_to_bytes().expect("proto3->vec is infallible");
|
||||||
stmt.execute_named(&[
|
stmt.execute_named(&[
|
||||||
(":session_id_hash", &&hash.0[..]),
|
(":session_id_hash", &&hash.0[..]),
|
||||||
(":user_id", &user.id),
|
(":user_id", &user.id),
|
||||||
@ -535,6 +558,7 @@ impl State {
|
|||||||
(":creation_time_sec", &creation.when_sec),
|
(":creation_time_sec", &creation.when_sec),
|
||||||
(":creation_user_agent", &creation.user_agent),
|
(":creation_user_agent", &creation.user_agent),
|
||||||
(":creation_peer_addr", &addr),
|
(":creation_peer_addr", &addr),
|
||||||
|
(":permissions", &permissions_blob),
|
||||||
])?;
|
])?;
|
||||||
let e = match sessions.entry(hash) {
|
let e = match sessions.entry(hash) {
|
||||||
::std::collections::hash_map::Entry::Occupied(_) => panic!("duplicate session hash!"),
|
::std::collections::hash_map::Entry::Occupied(_) => panic!("duplicate session hash!"),
|
||||||
@ -547,6 +571,7 @@ impl State {
|
|||||||
creation_password_id,
|
creation_password_id,
|
||||||
creation,
|
creation,
|
||||||
seed: Seed(seed),
|
seed: Seed(seed),
|
||||||
|
permissions,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
Ok((session_id, session))
|
Ok((session_id, session))
|
||||||
@ -692,7 +717,8 @@ fn lookup_session(conn: &Connection, hash: &SessionHash) -> Result<Session, Erro
|
|||||||
last_use_time_sec,
|
last_use_time_sec,
|
||||||
last_use_user_agent,
|
last_use_user_agent,
|
||||||
last_use_peer_addr,
|
last_use_peer_addr,
|
||||||
use_count
|
use_count,
|
||||||
|
permissions
|
||||||
from
|
from
|
||||||
user_session
|
user_session
|
||||||
where
|
where
|
||||||
@ -703,6 +729,8 @@ fn lookup_session(conn: &Connection, hash: &SessionHash) -> Result<Session, Erro
|
|||||||
let creation_addr: FromSqlIpAddr = row.get(8)?;
|
let creation_addr: FromSqlIpAddr = row.get(8)?;
|
||||||
let revocation_addr: FromSqlIpAddr = row.get(11)?;
|
let revocation_addr: FromSqlIpAddr = row.get(11)?;
|
||||||
let last_use_addr: FromSqlIpAddr = row.get(16)?;
|
let last_use_addr: FromSqlIpAddr = row.get(16)?;
|
||||||
|
let mut permissions = Permissions::new();
|
||||||
|
permissions.merge_from_bytes(row.get_raw_checked(18)?.as_blob()?)?;
|
||||||
Ok(Session {
|
Ok(Session {
|
||||||
user_id: row.get(0)?,
|
user_id: row.get(0)?,
|
||||||
seed: row.get(1)?,
|
seed: row.get(1)?,
|
||||||
@ -729,6 +757,7 @@ fn lookup_session(conn: &Connection, hash: &SessionHash) -> Result<Session, Erro
|
|||||||
},
|
},
|
||||||
use_count: row.get(17)?,
|
use_count: row.get(17)?,
|
||||||
dirty: false,
|
dirty: false,
|
||||||
|
permissions,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -961,4 +990,35 @@ mod tests {
|
|||||||
let e = state.authenticate_session(&conn, req.clone(), &sid.hash()).unwrap_err();
|
let e = state.authenticate_session(&conn, req.clone(), &sid.hash()).unwrap_err();
|
||||||
assert_eq!(format!("{}", e), "no such session");
|
assert_eq!(format!("{}", e), "no such session");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn permissions() {
|
||||||
|
testutil::init();
|
||||||
|
let mut conn = Connection::open_in_memory().unwrap();
|
||||||
|
db::init(&mut conn).unwrap();
|
||||||
|
let mut state = State::init(&conn).unwrap();
|
||||||
|
let mut change = UserChange::add_user("slamb".to_owned());
|
||||||
|
change.permissions.view_video = true;
|
||||||
|
let u = state.apply(&conn, change).unwrap();
|
||||||
|
assert!(u.permissions.view_video);
|
||||||
|
assert!(!u.permissions.update_signals);
|
||||||
|
let mut change = u.change();
|
||||||
|
assert!(change.permissions.view_video);
|
||||||
|
assert!(!change.permissions.update_signals);
|
||||||
|
change.permissions.update_signals = true;
|
||||||
|
let u = state.apply(&conn, change).unwrap();
|
||||||
|
assert!(u.permissions.view_video);
|
||||||
|
assert!(u.permissions.update_signals);
|
||||||
|
let uid = u.id;
|
||||||
|
|
||||||
|
{
|
||||||
|
let tx = conn.transaction().unwrap();
|
||||||
|
state.flush(&tx).unwrap();
|
||||||
|
tx.commit().unwrap();
|
||||||
|
}
|
||||||
|
let state = State::init(&conn).unwrap();
|
||||||
|
let u = state.users_by_id().get(&uid).unwrap();
|
||||||
|
assert!(u.permissions.view_video);
|
||||||
|
assert!(u.permissions.update_signals);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,7 @@ use crate::recording;
|
|||||||
use failure::Error;
|
use failure::Error;
|
||||||
use fnv::FnvHashMap;
|
use fnv::FnvHashMap;
|
||||||
use log::error;
|
use log::error;
|
||||||
|
use protobuf::prelude::MessageField;
|
||||||
use rusqlite::types::ToSql;
|
use rusqlite::types::ToSql;
|
||||||
use crate::schema;
|
use crate::schema;
|
||||||
use std::os::unix::ffi::OsStrExt;
|
use std::os::unix::ffi::OsStrExt;
|
||||||
@ -69,7 +70,7 @@ pub fn run(conn: &rusqlite::Connection, opts: &Options) -> Result<(), Error> {
|
|||||||
meta.db_uuid.extend_from_slice(&db_uuid.as_bytes()[..]);
|
meta.db_uuid.extend_from_slice(&db_uuid.as_bytes()[..]);
|
||||||
meta.dir_uuid.extend_from_slice(&dir_uuid.0.as_bytes()[..]);
|
meta.dir_uuid.extend_from_slice(&dir_uuid.0.as_bytes()[..]);
|
||||||
{
|
{
|
||||||
let o = meta.mut_last_complete_open();
|
let o = meta.last_complete_open.mut_message();
|
||||||
o.id = open_id;
|
o.id = open_id;
|
||||||
o.uuid.extend_from_slice(&open_uuid.0.as_bytes()[..]);
|
o.uuid.extend_from_slice(&open_uuid.0.as_bytes()[..]);
|
||||||
}
|
}
|
||||||
|
7
db/db.rs
7
db/db.rs
@ -66,6 +66,7 @@ use log::{error, info, trace};
|
|||||||
use lru_cache::LruCache;
|
use lru_cache::LruCache;
|
||||||
use openssl::hash;
|
use openssl::hash;
|
||||||
use parking_lot::{Mutex,MutexGuard};
|
use parking_lot::{Mutex,MutexGuard};
|
||||||
|
use protobuf::prelude::MessageField;
|
||||||
use rusqlite::types::ToSql;
|
use rusqlite::types::ToSql;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::collections::{BTreeMap, VecDeque};
|
use std::collections::{BTreeMap, VecDeque};
|
||||||
@ -326,7 +327,7 @@ impl SampleFileDir {
|
|||||||
meta.db_uuid.extend_from_slice(&db_uuid.as_bytes()[..]);
|
meta.db_uuid.extend_from_slice(&db_uuid.as_bytes()[..]);
|
||||||
meta.dir_uuid.extend_from_slice(&self.uuid.as_bytes()[..]);
|
meta.dir_uuid.extend_from_slice(&self.uuid.as_bytes()[..]);
|
||||||
if let Some(o) = self.last_complete_open {
|
if let Some(o) = self.last_complete_open {
|
||||||
let open = meta.mut_last_complete_open();
|
let open = meta.last_complete_open.mut_message();
|
||||||
open.id = o.id;
|
open.id = o.id;
|
||||||
open.uuid.extend_from_slice(&o.uuid.as_bytes()[..]);
|
open.uuid.extend_from_slice(&o.uuid.as_bytes()[..]);
|
||||||
}
|
}
|
||||||
@ -1061,7 +1062,7 @@ impl LockedDatabase {
|
|||||||
if dir.dir.is_some() { continue }
|
if dir.dir.is_some() { continue }
|
||||||
let mut meta = dir.meta(&self.uuid);
|
let mut meta = dir.meta(&self.uuid);
|
||||||
if let Some(o) = self.open.as_ref() {
|
if let Some(o) = self.open.as_ref() {
|
||||||
let open = meta.mut_in_progress_open();
|
let open = meta.in_progress_open.mut_message();
|
||||||
open.id = o.id;
|
open.id = o.id;
|
||||||
open.uuid.extend_from_slice(&o.uuid.as_bytes()[..]);
|
open.uuid.extend_from_slice(&o.uuid.as_bytes()[..]);
|
||||||
}
|
}
|
||||||
@ -1540,7 +1541,7 @@ impl LockedDatabase {
|
|||||||
{
|
{
|
||||||
meta.db_uuid.extend_from_slice(&self.uuid.as_bytes()[..]);
|
meta.db_uuid.extend_from_slice(&self.uuid.as_bytes()[..]);
|
||||||
meta.dir_uuid.extend_from_slice(uuid_bytes);
|
meta.dir_uuid.extend_from_slice(uuid_bytes);
|
||||||
let open = meta.mut_in_progress_open();
|
let open = meta.in_progress_open.mut_message();
|
||||||
open.id = o.id;
|
open.id = o.id;
|
||||||
open.uuid.extend_from_slice(&o.uuid.as_bytes()[..]);
|
open.uuid.extend_from_slice(&o.uuid.as_bytes()[..]);
|
||||||
}
|
}
|
||||||
|
@ -47,4 +47,5 @@ pub mod writer;
|
|||||||
pub mod testutil;
|
pub mod testutil;
|
||||||
|
|
||||||
pub use crate::db::*;
|
pub use crate::db::*;
|
||||||
|
pub use crate::schema::Permissions;
|
||||||
pub use crate::signal::Signal;
|
pub use crate::signal::Signal;
|
||||||
|
@ -60,3 +60,22 @@ message DirMeta {
|
|||||||
// guaranteed that no data has yet been written by this open.
|
// guaranteed that no data has yet been written by this open.
|
||||||
Open in_progress_open = 4;
|
Open in_progress_open = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Permissions to perform actions, currently all simple bools.
|
||||||
|
//
|
||||||
|
// These indicate actions which may be unnecessary in some contexts. Some
|
||||||
|
// basic access - like listing the cameras - is currently always allowed.
|
||||||
|
// See design/api.md for a description of what requires these permissions.
|
||||||
|
//
|
||||||
|
// These are used in a few contexts:
|
||||||
|
// * a session - affects what can be done when using that session to
|
||||||
|
// authenticate.
|
||||||
|
// * a user - when a new session is created, it inherits these permissions.
|
||||||
|
// * on the commandline - to specify what permissions are available for
|
||||||
|
// unauthenticated access.
|
||||||
|
message Permissions {
|
||||||
|
bool view_video = 1;
|
||||||
|
bool read_camera_configs = 2;
|
||||||
|
|
||||||
|
bool update_signals = 3;
|
||||||
|
}
|
642
db/schema.rs
642
db/schema.rs
@ -1,642 +0,0 @@
|
|||||||
// This file is generated by rust-protobuf 2.0.4. Do not edit
|
|
||||||
// @generated
|
|
||||||
|
|
||||||
// https://github.com/Manishearth/rust-clippy/issues/702
|
|
||||||
#![allow(unknown_lints)]
|
|
||||||
#![allow(clippy)]
|
|
||||||
|
|
||||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
|
||||||
|
|
||||||
#![allow(box_pointers)]
|
|
||||||
#![allow(dead_code)]
|
|
||||||
#![allow(missing_docs)]
|
|
||||||
#![allow(non_camel_case_types)]
|
|
||||||
#![allow(non_snake_case)]
|
|
||||||
#![allow(non_upper_case_globals)]
|
|
||||||
#![allow(trivial_casts)]
|
|
||||||
#![allow(unsafe_code)]
|
|
||||||
#![allow(unused_imports)]
|
|
||||||
#![allow(unused_results)]
|
|
||||||
|
|
||||||
use protobuf::Message as Message_imported_for_functions;
|
|
||||||
use protobuf::ProtobufEnum as ProtobufEnum_imported_for_functions;
|
|
||||||
|
|
||||||
#[derive(PartialEq,Clone,Default)]
|
|
||||||
pub struct DirMeta {
|
|
||||||
// message fields
|
|
||||||
pub db_uuid: ::std::vec::Vec<u8>,
|
|
||||||
pub dir_uuid: ::std::vec::Vec<u8>,
|
|
||||||
pub last_complete_open: ::protobuf::SingularPtrField<DirMeta_Open>,
|
|
||||||
pub in_progress_open: ::protobuf::SingularPtrField<DirMeta_Open>,
|
|
||||||
// special fields
|
|
||||||
unknown_fields: ::protobuf::UnknownFields,
|
|
||||||
cached_size: ::protobuf::CachedSize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DirMeta {
|
|
||||||
pub fn new() -> DirMeta {
|
|
||||||
::std::default::Default::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
// bytes db_uuid = 1;
|
|
||||||
|
|
||||||
pub fn clear_db_uuid(&mut self) {
|
|
||||||
self.db_uuid.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Param is passed by value, moved
|
|
||||||
pub fn set_db_uuid(&mut self, v: ::std::vec::Vec<u8>) {
|
|
||||||
self.db_uuid = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mutable pointer to the field.
|
|
||||||
// If field is not initialized, it is initialized with default value first.
|
|
||||||
pub fn mut_db_uuid(&mut self) -> &mut ::std::vec::Vec<u8> {
|
|
||||||
&mut self.db_uuid
|
|
||||||
}
|
|
||||||
|
|
||||||
// Take field
|
|
||||||
pub fn take_db_uuid(&mut self) -> ::std::vec::Vec<u8> {
|
|
||||||
::std::mem::replace(&mut self.db_uuid, ::std::vec::Vec::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_db_uuid(&self) -> &[u8] {
|
|
||||||
&self.db_uuid
|
|
||||||
}
|
|
||||||
|
|
||||||
// bytes dir_uuid = 2;
|
|
||||||
|
|
||||||
pub fn clear_dir_uuid(&mut self) {
|
|
||||||
self.dir_uuid.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Param is passed by value, moved
|
|
||||||
pub fn set_dir_uuid(&mut self, v: ::std::vec::Vec<u8>) {
|
|
||||||
self.dir_uuid = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mutable pointer to the field.
|
|
||||||
// If field is not initialized, it is initialized with default value first.
|
|
||||||
pub fn mut_dir_uuid(&mut self) -> &mut ::std::vec::Vec<u8> {
|
|
||||||
&mut self.dir_uuid
|
|
||||||
}
|
|
||||||
|
|
||||||
// Take field
|
|
||||||
pub fn take_dir_uuid(&mut self) -> ::std::vec::Vec<u8> {
|
|
||||||
::std::mem::replace(&mut self.dir_uuid, ::std::vec::Vec::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_dir_uuid(&self) -> &[u8] {
|
|
||||||
&self.dir_uuid
|
|
||||||
}
|
|
||||||
|
|
||||||
// .DirMeta.Open last_complete_open = 3;
|
|
||||||
|
|
||||||
pub fn clear_last_complete_open(&mut self) {
|
|
||||||
self.last_complete_open.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_last_complete_open(&self) -> bool {
|
|
||||||
self.last_complete_open.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Param is passed by value, moved
|
|
||||||
pub fn set_last_complete_open(&mut self, v: DirMeta_Open) {
|
|
||||||
self.last_complete_open = ::protobuf::SingularPtrField::some(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mutable pointer to the field.
|
|
||||||
// If field is not initialized, it is initialized with default value first.
|
|
||||||
pub fn mut_last_complete_open(&mut self) -> &mut DirMeta_Open {
|
|
||||||
if self.last_complete_open.is_none() {
|
|
||||||
self.last_complete_open.set_default();
|
|
||||||
}
|
|
||||||
self.last_complete_open.as_mut().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Take field
|
|
||||||
pub fn take_last_complete_open(&mut self) -> DirMeta_Open {
|
|
||||||
self.last_complete_open.take().unwrap_or_else(|| DirMeta_Open::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_last_complete_open(&self) -> &DirMeta_Open {
|
|
||||||
self.last_complete_open.as_ref().unwrap_or_else(|| DirMeta_Open::default_instance())
|
|
||||||
}
|
|
||||||
|
|
||||||
// .DirMeta.Open in_progress_open = 4;
|
|
||||||
|
|
||||||
pub fn clear_in_progress_open(&mut self) {
|
|
||||||
self.in_progress_open.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_in_progress_open(&self) -> bool {
|
|
||||||
self.in_progress_open.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Param is passed by value, moved
|
|
||||||
pub fn set_in_progress_open(&mut self, v: DirMeta_Open) {
|
|
||||||
self.in_progress_open = ::protobuf::SingularPtrField::some(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mutable pointer to the field.
|
|
||||||
// If field is not initialized, it is initialized with default value first.
|
|
||||||
pub fn mut_in_progress_open(&mut self) -> &mut DirMeta_Open {
|
|
||||||
if self.in_progress_open.is_none() {
|
|
||||||
self.in_progress_open.set_default();
|
|
||||||
}
|
|
||||||
self.in_progress_open.as_mut().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Take field
|
|
||||||
pub fn take_in_progress_open(&mut self) -> DirMeta_Open {
|
|
||||||
self.in_progress_open.take().unwrap_or_else(|| DirMeta_Open::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_in_progress_open(&self) -> &DirMeta_Open {
|
|
||||||
self.in_progress_open.as_ref().unwrap_or_else(|| DirMeta_Open::default_instance())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ::protobuf::Message for DirMeta {
|
|
||||||
fn is_initialized(&self) -> bool {
|
|
||||||
for v in &self.last_complete_open {
|
|
||||||
if !v.is_initialized() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
for v in &self.in_progress_open {
|
|
||||||
if !v.is_initialized() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream) -> ::protobuf::ProtobufResult<()> {
|
|
||||||
while !is.eof()? {
|
|
||||||
let (field_number, wire_type) = is.read_tag_unpack()?;
|
|
||||||
match field_number {
|
|
||||||
1 => {
|
|
||||||
::protobuf::rt::read_singular_proto3_bytes_into(wire_type, is, &mut self.db_uuid)?;
|
|
||||||
},
|
|
||||||
2 => {
|
|
||||||
::protobuf::rt::read_singular_proto3_bytes_into(wire_type, is, &mut self.dir_uuid)?;
|
|
||||||
},
|
|
||||||
3 => {
|
|
||||||
::protobuf::rt::read_singular_message_into(wire_type, is, &mut self.last_complete_open)?;
|
|
||||||
},
|
|
||||||
4 => {
|
|
||||||
::protobuf::rt::read_singular_message_into(wire_type, is, &mut self.in_progress_open)?;
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
::std::result::Result::Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute sizes of nested messages
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
fn compute_size(&self) -> u32 {
|
|
||||||
let mut my_size = 0;
|
|
||||||
if !self.db_uuid.is_empty() {
|
|
||||||
my_size += ::protobuf::rt::bytes_size(1, &self.db_uuid);
|
|
||||||
}
|
|
||||||
if !self.dir_uuid.is_empty() {
|
|
||||||
my_size += ::protobuf::rt::bytes_size(2, &self.dir_uuid);
|
|
||||||
}
|
|
||||||
if let Some(ref v) = self.last_complete_open.as_ref() {
|
|
||||||
let len = v.compute_size();
|
|
||||||
my_size += 1 + ::protobuf::rt::compute_raw_varint32_size(len) + len;
|
|
||||||
}
|
|
||||||
if let Some(ref v) = self.in_progress_open.as_ref() {
|
|
||||||
let len = v.compute_size();
|
|
||||||
my_size += 1 + ::protobuf::rt::compute_raw_varint32_size(len) + len;
|
|
||||||
}
|
|
||||||
my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
|
|
||||||
self.cached_size.set(my_size);
|
|
||||||
my_size
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream) -> ::protobuf::ProtobufResult<()> {
|
|
||||||
if !self.db_uuid.is_empty() {
|
|
||||||
os.write_bytes(1, &self.db_uuid)?;
|
|
||||||
}
|
|
||||||
if !self.dir_uuid.is_empty() {
|
|
||||||
os.write_bytes(2, &self.dir_uuid)?;
|
|
||||||
}
|
|
||||||
if let Some(ref v) = self.last_complete_open.as_ref() {
|
|
||||||
os.write_tag(3, ::protobuf::wire_format::WireTypeLengthDelimited)?;
|
|
||||||
os.write_raw_varint32(v.get_cached_size())?;
|
|
||||||
v.write_to_with_cached_sizes(os)?;
|
|
||||||
}
|
|
||||||
if let Some(ref v) = self.in_progress_open.as_ref() {
|
|
||||||
os.write_tag(4, ::protobuf::wire_format::WireTypeLengthDelimited)?;
|
|
||||||
os.write_raw_varint32(v.get_cached_size())?;
|
|
||||||
v.write_to_with_cached_sizes(os)?;
|
|
||||||
}
|
|
||||||
os.write_unknown_fields(self.get_unknown_fields())?;
|
|
||||||
::std::result::Result::Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_cached_size(&self) -> u32 {
|
|
||||||
self.cached_size.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_unknown_fields(&self) -> &::protobuf::UnknownFields {
|
|
||||||
&self.unknown_fields
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields {
|
|
||||||
&mut self.unknown_fields
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn (::std::any::Any) {
|
|
||||||
self as &dyn (::std::any::Any)
|
|
||||||
}
|
|
||||||
fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) {
|
|
||||||
self as &mut dyn (::std::any::Any)
|
|
||||||
}
|
|
||||||
fn into_any(self: Box<Self>) -> ::std::boxed::Box<dyn (::std::any::Any)> {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor {
|
|
||||||
Self::descriptor_static()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new() -> DirMeta {
|
|
||||||
DirMeta::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
|
|
||||||
static mut descriptor: ::protobuf::lazy::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::lazy::Lazy {
|
|
||||||
lock: ::protobuf::lazy::ONCE_INIT,
|
|
||||||
ptr: 0 as *const ::protobuf::reflect::MessageDescriptor,
|
|
||||||
};
|
|
||||||
unsafe {
|
|
||||||
descriptor.get(|| {
|
|
||||||
let mut fields = ::std::vec::Vec::new();
|
|
||||||
fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeBytes>(
|
|
||||||
"db_uuid",
|
|
||||||
|m: &DirMeta| { &m.db_uuid },
|
|
||||||
|m: &mut DirMeta| { &mut m.db_uuid },
|
|
||||||
));
|
|
||||||
fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeBytes>(
|
|
||||||
"dir_uuid",
|
|
||||||
|m: &DirMeta| { &m.dir_uuid },
|
|
||||||
|m: &mut DirMeta| { &mut m.dir_uuid },
|
|
||||||
));
|
|
||||||
fields.push(::protobuf::reflect::accessor::make_singular_ptr_field_accessor::<_, ::protobuf::types::ProtobufTypeMessage<DirMeta_Open>>(
|
|
||||||
"last_complete_open",
|
|
||||||
|m: &DirMeta| { &m.last_complete_open },
|
|
||||||
|m: &mut DirMeta| { &mut m.last_complete_open },
|
|
||||||
));
|
|
||||||
fields.push(::protobuf::reflect::accessor::make_singular_ptr_field_accessor::<_, ::protobuf::types::ProtobufTypeMessage<DirMeta_Open>>(
|
|
||||||
"in_progress_open",
|
|
||||||
|m: &DirMeta| { &m.in_progress_open },
|
|
||||||
|m: &mut DirMeta| { &mut m.in_progress_open },
|
|
||||||
));
|
|
||||||
::protobuf::reflect::MessageDescriptor::new::<DirMeta>(
|
|
||||||
"DirMeta",
|
|
||||||
fields,
|
|
||||||
file_descriptor_proto()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_instance() -> &'static DirMeta {
|
|
||||||
static mut instance: ::protobuf::lazy::Lazy<DirMeta> = ::protobuf::lazy::Lazy {
|
|
||||||
lock: ::protobuf::lazy::ONCE_INIT,
|
|
||||||
ptr: 0 as *const DirMeta,
|
|
||||||
};
|
|
||||||
unsafe {
|
|
||||||
instance.get(DirMeta::new)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ::protobuf::Clear for DirMeta {
|
|
||||||
fn clear(&mut self) {
|
|
||||||
self.clear_db_uuid();
|
|
||||||
self.clear_dir_uuid();
|
|
||||||
self.clear_last_complete_open();
|
|
||||||
self.clear_in_progress_open();
|
|
||||||
self.unknown_fields.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ::std::fmt::Debug for DirMeta {
|
|
||||||
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
|
|
||||||
::protobuf::text_format::fmt(self, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ::protobuf::reflect::ProtobufValue for DirMeta {
|
|
||||||
fn as_ref(&self) -> ::protobuf::reflect::ProtobufValueRef {
|
|
||||||
::protobuf::reflect::ProtobufValueRef::Message(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(PartialEq,Clone,Default)]
|
|
||||||
pub struct DirMeta_Open {
|
|
||||||
// message fields
|
|
||||||
pub id: u32,
|
|
||||||
pub uuid: ::std::vec::Vec<u8>,
|
|
||||||
// special fields
|
|
||||||
unknown_fields: ::protobuf::UnknownFields,
|
|
||||||
cached_size: ::protobuf::CachedSize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DirMeta_Open {
|
|
||||||
pub fn new() -> DirMeta_Open {
|
|
||||||
::std::default::Default::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
// uint32 id = 1;
|
|
||||||
|
|
||||||
pub fn clear_id(&mut self) {
|
|
||||||
self.id = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Param is passed by value, moved
|
|
||||||
pub fn set_id(&mut self, v: u32) {
|
|
||||||
self.id = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_id(&self) -> u32 {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
// bytes uuid = 2;
|
|
||||||
|
|
||||||
pub fn clear_uuid(&mut self) {
|
|
||||||
self.uuid.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Param is passed by value, moved
|
|
||||||
pub fn set_uuid(&mut self, v: ::std::vec::Vec<u8>) {
|
|
||||||
self.uuid = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mutable pointer to the field.
|
|
||||||
// If field is not initialized, it is initialized with default value first.
|
|
||||||
pub fn mut_uuid(&mut self) -> &mut ::std::vec::Vec<u8> {
|
|
||||||
&mut self.uuid
|
|
||||||
}
|
|
||||||
|
|
||||||
// Take field
|
|
||||||
pub fn take_uuid(&mut self) -> ::std::vec::Vec<u8> {
|
|
||||||
::std::mem::replace(&mut self.uuid, ::std::vec::Vec::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_uuid(&self) -> &[u8] {
|
|
||||||
&self.uuid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ::protobuf::Message for DirMeta_Open {
|
|
||||||
fn is_initialized(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream) -> ::protobuf::ProtobufResult<()> {
|
|
||||||
while !is.eof()? {
|
|
||||||
let (field_number, wire_type) = is.read_tag_unpack()?;
|
|
||||||
match field_number {
|
|
||||||
1 => {
|
|
||||||
if wire_type != ::protobuf::wire_format::WireTypeVarint {
|
|
||||||
return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type));
|
|
||||||
}
|
|
||||||
let tmp = is.read_uint32()?;
|
|
||||||
self.id = tmp;
|
|
||||||
},
|
|
||||||
2 => {
|
|
||||||
::protobuf::rt::read_singular_proto3_bytes_into(wire_type, is, &mut self.uuid)?;
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
::std::result::Result::Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute sizes of nested messages
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
fn compute_size(&self) -> u32 {
|
|
||||||
let mut my_size = 0;
|
|
||||||
if self.id != 0 {
|
|
||||||
my_size += ::protobuf::rt::value_size(1, self.id, ::protobuf::wire_format::WireTypeVarint);
|
|
||||||
}
|
|
||||||
if !self.uuid.is_empty() {
|
|
||||||
my_size += ::protobuf::rt::bytes_size(2, &self.uuid);
|
|
||||||
}
|
|
||||||
my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
|
|
||||||
self.cached_size.set(my_size);
|
|
||||||
my_size
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream) -> ::protobuf::ProtobufResult<()> {
|
|
||||||
if self.id != 0 {
|
|
||||||
os.write_uint32(1, self.id)?;
|
|
||||||
}
|
|
||||||
if !self.uuid.is_empty() {
|
|
||||||
os.write_bytes(2, &self.uuid)?;
|
|
||||||
}
|
|
||||||
os.write_unknown_fields(self.get_unknown_fields())?;
|
|
||||||
::std::result::Result::Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_cached_size(&self) -> u32 {
|
|
||||||
self.cached_size.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_unknown_fields(&self) -> &::protobuf::UnknownFields {
|
|
||||||
&self.unknown_fields
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields {
|
|
||||||
&mut self.unknown_fields
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn (::std::any::Any) {
|
|
||||||
self as &dyn (::std::any::Any)
|
|
||||||
}
|
|
||||||
fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) {
|
|
||||||
self as &mut dyn (::std::any::Any)
|
|
||||||
}
|
|
||||||
fn into_any(self: Box<Self>) -> ::std::boxed::Box<dyn (::std::any::Any)> {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor {
|
|
||||||
Self::descriptor_static()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new() -> DirMeta_Open {
|
|
||||||
DirMeta_Open::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
|
|
||||||
static mut descriptor: ::protobuf::lazy::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::lazy::Lazy {
|
|
||||||
lock: ::protobuf::lazy::ONCE_INIT,
|
|
||||||
ptr: 0 as *const ::protobuf::reflect::MessageDescriptor,
|
|
||||||
};
|
|
||||||
unsafe {
|
|
||||||
descriptor.get(|| {
|
|
||||||
let mut fields = ::std::vec::Vec::new();
|
|
||||||
fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeUint32>(
|
|
||||||
"id",
|
|
||||||
|m: &DirMeta_Open| { &m.id },
|
|
||||||
|m: &mut DirMeta_Open| { &mut m.id },
|
|
||||||
));
|
|
||||||
fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeBytes>(
|
|
||||||
"uuid",
|
|
||||||
|m: &DirMeta_Open| { &m.uuid },
|
|
||||||
|m: &mut DirMeta_Open| { &mut m.uuid },
|
|
||||||
));
|
|
||||||
::protobuf::reflect::MessageDescriptor::new::<DirMeta_Open>(
|
|
||||||
"DirMeta_Open",
|
|
||||||
fields,
|
|
||||||
file_descriptor_proto()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_instance() -> &'static DirMeta_Open {
|
|
||||||
static mut instance: ::protobuf::lazy::Lazy<DirMeta_Open> = ::protobuf::lazy::Lazy {
|
|
||||||
lock: ::protobuf::lazy::ONCE_INIT,
|
|
||||||
ptr: 0 as *const DirMeta_Open,
|
|
||||||
};
|
|
||||||
unsafe {
|
|
||||||
instance.get(DirMeta_Open::new)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ::protobuf::Clear for DirMeta_Open {
|
|
||||||
fn clear(&mut self) {
|
|
||||||
self.clear_id();
|
|
||||||
self.clear_uuid();
|
|
||||||
self.unknown_fields.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ::std::fmt::Debug for DirMeta_Open {
|
|
||||||
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
|
|
||||||
::protobuf::text_format::fmt(self, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ::protobuf::reflect::ProtobufValue for DirMeta_Open {
|
|
||||||
fn as_ref(&self) -> ::protobuf::reflect::ProtobufValueRef {
|
|
||||||
::protobuf::reflect::ProtobufValueRef::Message(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static file_descriptor_proto_data: &'static [u8] = b"\
|
|
||||||
\n\x0cschema.proto\"\xdf\x01\n\x07DirMeta\x12\x17\n\x07db_uuid\x18\x01\
|
|
||||||
\x20\x01(\x0cR\x06dbUuid\x12\x19\n\x08dir_uuid\x18\x02\x20\x01(\x0cR\x07\
|
|
||||||
dirUuid\x12;\n\x12last_complete_open\x18\x03\x20\x01(\x0b2\r.DirMeta.Ope\
|
|
||||||
nR\x10lastCompleteOpen\x127\n\x10in_progress_open\x18\x04\x20\x01(\x0b2\
|
|
||||||
\r.DirMeta.OpenR\x0einProgressOpen\x1a*\n\x04Open\x12\x0e\n\x02id\x18\
|
|
||||||
\x01\x20\x01(\rR\x02id\x12\x12\n\x04uuid\x18\x02\x20\x01(\x0cR\x04uuidJ\
|
|
||||||
\xc1\x17\n\x06\x12\x04\x1e\0=\x01\n\xc2\x0b\n\x01\x0c\x12\x03\x1e\0\x122\
|
|
||||||
\xb7\x0b\x20This\x20file\x20is\x20part\x20of\x20Moonfire\x20NVR,\x20a\
|
|
||||||
\x20security\x20camera\x20digital\x20video\x20recorder.\n\x20Copyright\
|
|
||||||
\x20(C)\x202018\x20Scott\x20Lamb\x20<slamb@slamb.org>\n\n\x20This\x20pro\
|
|
||||||
gram\x20is\x20free\x20software:\x20you\x20can\x20redistribute\x20it\x20a\
|
|
||||||
nd/or\x20modify\n\x20it\x20under\x20the\x20terms\x20of\x20the\x20GNU\x20\
|
|
||||||
General\x20Public\x20License\x20as\x20published\x20by\n\x20the\x20Free\
|
|
||||||
\x20Software\x20Foundation,\x20either\x20version\x203\x20of\x20the\x20Li\
|
|
||||||
cense,\x20or\n\x20(at\x20your\x20option)\x20any\x20later\x20version.\n\n\
|
|
||||||
\x20In\x20addition,\x20as\x20a\x20special\x20exception,\x20the\x20copyri\
|
|
||||||
ght\x20holders\x20give\n\x20permission\x20to\x20link\x20the\x20code\x20o\
|
|
||||||
f\x20portions\x20of\x20this\x20program\x20with\x20the\n\x20OpenSSL\x20li\
|
|
||||||
brary\x20under\x20certain\x20conditions\x20as\x20described\x20in\x20each\
|
|
||||||
\n\x20individual\x20source\x20file,\x20and\x20distribute\x20linked\x20co\
|
|
||||||
mbinations\x20including\n\x20the\x20two.\n\n\x20You\x20must\x20obey\x20t\
|
|
||||||
he\x20GNU\x20General\x20Public\x20License\x20in\x20all\x20respects\x20fo\
|
|
||||||
r\x20all\n\x20of\x20the\x20code\x20used\x20other\x20than\x20OpenSSL.\x20\
|
|
||||||
If\x20you\x20modify\x20file(s)\x20with\x20this\n\x20exception,\x20you\
|
|
||||||
\x20may\x20extend\x20this\x20exception\x20to\x20your\x20version\x20of\
|
|
||||||
\x20the\n\x20file(s),\x20but\x20you\x20are\x20not\x20obligated\x20to\x20\
|
|
||||||
do\x20so.\x20If\x20you\x20do\x20not\x20wish\x20to\x20do\n\x20so,\x20dele\
|
|
||||||
te\x20this\x20exception\x20statement\x20from\x20your\x20version.\x20If\
|
|
||||||
\x20you\x20delete\n\x20this\x20exception\x20statement\x20from\x20all\x20\
|
|
||||||
source\x20files\x20in\x20the\x20program,\x20then\n\x20also\x20delete\x20\
|
|
||||||
it\x20here.\n\n\x20This\x20program\x20is\x20distributed\x20in\x20the\x20\
|
|
||||||
hope\x20that\x20it\x20will\x20be\x20useful,\n\x20but\x20WITHOUT\x20ANY\
|
|
||||||
\x20WARRANTY;\x20without\x20even\x20the\x20implied\x20warranty\x20of\n\
|
|
||||||
\x20MERCHANTABILITY\x20or\x20FITNESS\x20FOR\x20A\x20PARTICULAR\x20PURPOS\
|
|
||||||
E.\x20\x20See\x20the\n\x20GNU\x20General\x20Public\x20License\x20for\x20\
|
|
||||||
more\x20details.\n\n\x20You\x20should\x20have\x20received\x20a\x20copy\
|
|
||||||
\x20of\x20the\x20GNU\x20General\x20Public\x20License\n\x20along\x20with\
|
|
||||||
\x20this\x20program.\x20\x20If\x20not,\x20see\x20<http://www.gnu.org/lic\
|
|
||||||
enses/>.\n\n\xf1\x01\n\x02\x04\0\x12\x04$\0=\x01\x1a\xe4\x01\x20Metadata\
|
|
||||||
\x20stored\x20in\x20sample\x20file\x20dirs\x20as\x20\"<dir>/meta\".\x20T\
|
|
||||||
his\x20is\x20checked\n\x20against\x20the\x20metadata\x20stored\x20within\
|
|
||||||
\x20the\x20database\x20to\x20detect\x20inconsistencies\n\x20between\x20t\
|
|
||||||
he\x20directory\x20and\x20database,\x20such\x20as\x20those\x20described\
|
|
||||||
\x20in\n\x20design/schema.md.\n\n\n\n\x03\x04\0\x01\x12\x03$\x08\x0f\n\
|
|
||||||
\xcf\x01\n\x04\x04\0\x02\0\x12\x03(\x02\x14\x1a\xc1\x01\x20A\x20uuid\x20\
|
|
||||||
associated\x20with\x20the\x20database,\x20in\x20binary\x20form.\x20dir_u\
|
|
||||||
uid\x20is\x20strictly\n\x20more\x20powerful,\x20but\x20it\x20improves\
|
|
||||||
\x20diagnostics\x20to\x20know\x20if\x20the\x20directory\n\x20belongs\x20\
|
|
||||||
to\x20the\x20expected\x20database\x20at\x20all\x20or\x20not.\n\n\r\n\x05\
|
|
||||||
\x04\0\x02\0\x04\x12\x04(\x02$\x11\n\x0c\n\x05\x04\0\x02\0\x05\x12\x03(\
|
|
||||||
\x02\x07\n\x0c\n\x05\x04\0\x02\0\x01\x12\x03(\x08\x0f\n\x0c\n\x05\x04\0\
|
|
||||||
\x02\0\x03\x12\x03(\x12\x13\n;\n\x04\x04\0\x02\x01\x12\x03+\x02\x15\x1a.\
|
|
||||||
\x20A\x20uuid\x20associated\x20with\x20the\x20directory\x20itself.\n\n\r\
|
|
||||||
\n\x05\x04\0\x02\x01\x04\x12\x04+\x02(\x14\n\x0c\n\x05\x04\0\x02\x01\x05\
|
|
||||||
\x12\x03+\x02\x07\n\x0c\n\x05\x04\0\x02\x01\x01\x12\x03+\x08\x10\n\x0c\n\
|
|
||||||
\x05\x04\0\x02\x01\x03\x12\x03+\x13\x14\nE\n\x04\x04\0\x03\0\x12\x04.\
|
|
||||||
\x021\x03\x1a7\x20Corresponds\x20to\x20an\x20entry\x20in\x20the\x20`open\
|
|
||||||
`\x20database\x20table.\n\n\x0c\n\x05\x04\0\x03\0\x01\x12\x03.\n\x0e\n\r\
|
|
||||||
\n\x06\x04\0\x03\0\x02\0\x12\x03/\x04\x12\n\x0f\n\x07\x04\0\x03\0\x02\0\
|
|
||||||
\x04\x12\x04/\x04.\x10\n\x0e\n\x07\x04\0\x03\0\x02\0\x05\x12\x03/\x04\n\
|
|
||||||
\n\x0e\n\x07\x04\0\x03\0\x02\0\x01\x12\x03/\x0b\r\n\x0e\n\x07\x04\0\x03\
|
|
||||||
\0\x02\0\x03\x12\x03/\x10\x11\n\r\n\x06\x04\0\x03\0\x02\x01\x12\x030\x04\
|
|
||||||
\x13\n\x0f\n\x07\x04\0\x03\0\x02\x01\x04\x12\x040\x04/\x12\n\x0e\n\x07\
|
|
||||||
\x04\0\x03\0\x02\x01\x05\x12\x030\x04\t\n\x0e\n\x07\x04\0\x03\0\x02\x01\
|
|
||||||
\x01\x12\x030\n\x0e\n\x0e\n\x07\x04\0\x03\0\x02\x01\x03\x12\x030\x11\x12\
|
|
||||||
\n\xb0\x02\n\x04\x04\0\x02\x02\x12\x037\x02\x1e\x1a\xa2\x02\x20The\x20la\
|
|
||||||
st\x20open\x20that\x20was\x20known\x20to\x20be\x20recorded\x20in\x20the\
|
|
||||||
\x20database\x20as\x20completed.\n\x20Absent\x20if\x20this\x20has\x20nev\
|
|
||||||
er\x20happened.\x20Note\x20this\x20can\x20backtrack\x20in\x20exactly\x20\
|
|
||||||
one\n\x20scenario:\x20when\x20deleting\x20the\x20directory,\x20after\x20\
|
|
||||||
all\x20associated\x20files\x20have\n\x20been\x20deleted,\x20last_complet\
|
|
||||||
e_open\x20can\x20be\x20moved\x20to\x20in_progress_open.\n\n\r\n\x05\x04\
|
|
||||||
\0\x02\x02\x04\x12\x047\x021\x03\n\x0c\n\x05\x04\0\x02\x02\x06\x12\x037\
|
|
||||||
\x02\x06\n\x0c\n\x05\x04\0\x02\x02\x01\x12\x037\x07\x19\n\x0c\n\x05\x04\
|
|
||||||
\0\x02\x02\x03\x12\x037\x1c\x1d\n\xd6\x01\n\x04\x04\0\x02\x03\x12\x03<\
|
|
||||||
\x02\x1c\x1a\xc8\x01\x20The\x20last\x20run\x20which\x20is\x20in\x20progr\
|
|
||||||
ess,\x20if\x20different\x20from\x20last_complete_open.\n\x20This\x20may\
|
|
||||||
\x20or\x20may\x20not\x20have\x20been\x20recorded\x20in\x20the\x20databas\
|
|
||||||
e,\x20but\x20it's\n\x20guaranteed\x20that\x20no\x20data\x20has\x20yet\
|
|
||||||
\x20been\x20written\x20by\x20this\x20open.\n\n\r\n\x05\x04\0\x02\x03\x04\
|
|
||||||
\x12\x04<\x027\x1e\n\x0c\n\x05\x04\0\x02\x03\x06\x12\x03<\x02\x06\n\x0c\
|
|
||||||
\n\x05\x04\0\x02\x03\x01\x12\x03<\x07\x17\n\x0c\n\x05\x04\0\x02\x03\x03\
|
|
||||||
\x12\x03<\x1a\x1bb\x06proto3\
|
|
||||||
";
|
|
||||||
|
|
||||||
static mut file_descriptor_proto_lazy: ::protobuf::lazy::Lazy<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::lazy::Lazy {
|
|
||||||
lock: ::protobuf::lazy::ONCE_INIT,
|
|
||||||
ptr: 0 as *const ::protobuf::descriptor::FileDescriptorProto,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn parse_descriptor_proto() -> ::protobuf::descriptor::FileDescriptorProto {
|
|
||||||
::protobuf::parse_from_bytes(file_descriptor_proto_data).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn file_descriptor_proto() -> &'static ::protobuf::descriptor::FileDescriptorProto {
|
|
||||||
unsafe {
|
|
||||||
file_descriptor_proto_lazy.get(|| {
|
|
||||||
parse_descriptor_proto()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -328,7 +328,11 @@ create table user (
|
|||||||
-- a Unix domain socket. (Additionally, the UID running Moonfire NVR can authenticate
|
-- a Unix domain socket. (Additionally, the UID running Moonfire NVR can authenticate
|
||||||
-- as anyone; there's no point in trying to do otherwise.) This might be an easy
|
-- as anyone; there's no point in trying to do otherwise.) This might be an easy
|
||||||
-- bootstrap method once configuration happens through a web UI rather than text UI.
|
-- bootstrap method once configuration happens through a web UI rather than text UI.
|
||||||
unix_uid integer
|
unix_uid integer,
|
||||||
|
|
||||||
|
-- Permissions available for newly created tokens or when authenticating via
|
||||||
|
-- unix_uid above. A serialized "Permissions" protobuf.
|
||||||
|
permissions blob
|
||||||
);
|
);
|
||||||
|
|
||||||
-- A single session, whether for browser or robot use.
|
-- A single session, whether for browser or robot use.
|
||||||
@ -391,7 +395,10 @@ create table user_session (
|
|||||||
last_use_time_sec integer, -- sec since epoch
|
last_use_time_sec integer, -- sec since epoch
|
||||||
last_use_user_agent text, -- User-Agent header from inbound HTTP request.
|
last_use_user_agent text, -- User-Agent header from inbound HTTP request.
|
||||||
last_use_peer_addr blob, -- IPv4 or IPv6 address, or null for Unix socket.
|
last_use_peer_addr blob, -- IPv4 or IPv6 address, or null for Unix socket.
|
||||||
use_count not null default 0
|
use_count not null default 0,
|
||||||
|
|
||||||
|
-- Permissions associated with this token; a serialized "Permissions" protobuf.
|
||||||
|
permissions blob
|
||||||
) without rowid;
|
) without rowid;
|
||||||
|
|
||||||
create index user_session_uid on user_session (user_id);
|
create index user_session_uid on user_session (user_id);
|
||||||
|
@ -33,6 +33,7 @@
|
|||||||
use crate::dir;
|
use crate::dir;
|
||||||
use failure::{Error, bail, format_err};
|
use failure::{Error, bail, format_err};
|
||||||
use libc;
|
use libc;
|
||||||
|
use protobuf::prelude::MessageField;
|
||||||
use rusqlite::types::ToSql;
|
use rusqlite::types::ToSql;
|
||||||
use crate::schema::DirMeta;
|
use crate::schema::DirMeta;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@ -113,7 +114,7 @@ pub fn run(args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
|
|||||||
{
|
{
|
||||||
meta.db_uuid.extend_from_slice(db_uuid_bytes);
|
meta.db_uuid.extend_from_slice(db_uuid_bytes);
|
||||||
meta.dir_uuid.extend_from_slice(dir_uuid_bytes);
|
meta.dir_uuid.extend_from_slice(dir_uuid_bytes);
|
||||||
let open = meta.mut_last_complete_open();
|
let open = meta.last_complete_open.mut_message();
|
||||||
open.id = open_id;
|
open.id = open_id;
|
||||||
open.uuid.extend_from_slice(&open_uuid_bytes);
|
open.uuid.extend_from_slice(&open_uuid_bytes);
|
||||||
}
|
}
|
||||||
|
@ -37,10 +37,11 @@ use crate::dir;
|
|||||||
use failure::Error;
|
use failure::Error;
|
||||||
use libc;
|
use libc;
|
||||||
use crate::schema;
|
use crate::schema;
|
||||||
|
use protobuf::prelude::MessageField;
|
||||||
|
use rusqlite::types::ToSql;
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::mem;
|
use std::mem;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use rusqlite::types::ToSql;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// Opens the sample file dir.
|
/// Opens the sample file dir.
|
||||||
@ -68,7 +69,7 @@ fn open_sample_file_dir(tx: &rusqlite::Transaction) -> Result<Arc<dir::SampleFil
|
|||||||
meta.db_uuid.extend_from_slice(&db_uuid.0.as_bytes()[..]);
|
meta.db_uuid.extend_from_slice(&db_uuid.0.as_bytes()[..]);
|
||||||
meta.dir_uuid.extend_from_slice(&s_uuid.0.as_bytes()[..]);
|
meta.dir_uuid.extend_from_slice(&s_uuid.0.as_bytes()[..]);
|
||||||
{
|
{
|
||||||
let open = meta.mut_last_complete_open();
|
let open = meta.last_complete_open.mut_message();
|
||||||
open.id = o_id as u32;
|
open.id = o_id as u32;
|
||||||
open.uuid.extend_from_slice(&o_uuid.0.as_bytes()[..]);
|
open.uuid.extend_from_slice(&o_uuid.0.as_bytes()[..]);
|
||||||
}
|
}
|
||||||
|
@ -62,6 +62,9 @@ pub fn run(_args: &super::Args, tx: &rusqlite::Transaction) -> Result<(), Error>
|
|||||||
time_90k integer primary key,
|
time_90k integer primary key,
|
||||||
changes blob
|
changes blob
|
||||||
);
|
);
|
||||||
|
|
||||||
|
alter table user add column permissions blob;
|
||||||
|
alter table user_session add column permissions blob;
|
||||||
"#)?;
|
"#)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -53,11 +53,15 @@ request parameters:
|
|||||||
|
|
||||||
* `days`: a boolean indicating if the days parameter described below
|
* `days`: a boolean indicating if the days parameter described below
|
||||||
should be included.
|
should be included.
|
||||||
|
* `cameraConfigs`: a boolean indicating if the `camera.config` parameter
|
||||||
|
described below should be included. This requires the
|
||||||
|
`read_camera_configs` permission as described in `schema.proto`.
|
||||||
|
|
||||||
Example request URI:
|
Example request URI (with added whitespace between parameters):
|
||||||
|
|
||||||
```
|
```
|
||||||
/api/?days=true
|
/api/?days=true
|
||||||
|
&cameraConfigs=true
|
||||||
```
|
```
|
||||||
|
|
||||||
The `application/json` response will have a dict as follows:
|
The `application/json` response will have a dict as follows:
|
||||||
@ -68,6 +72,11 @@ The `application/json` response will have a dict as follows:
|
|||||||
* `uuid`: in text format
|
* `uuid`: in text format
|
||||||
* `shortName`: a short name (typically one or two words)
|
* `shortName`: a short name (typically one or two words)
|
||||||
* `description`: a longer description (typically a phrase or paragraph)
|
* `description`: a longer description (typically a phrase or paragraph)
|
||||||
|
* `config`: (only included if request parameter `cameraConfigs` is true)
|
||||||
|
a dictionary describing the configuration of the camera:
|
||||||
|
* `username`
|
||||||
|
* `password`
|
||||||
|
* `host`
|
||||||
* `streams`: a dict of stream type ("main" or "sub") to a dictionary
|
* `streams`: a dict of stream type ("main" or "sub") to a dictionary
|
||||||
describing the stream:
|
describing the stream:
|
||||||
* `retainBytes`: the configured total number of bytes of completed
|
* `retainBytes`: the configured total number of bytes of completed
|
||||||
@ -81,9 +90,10 @@ The `application/json` response will have a dict as follows:
|
|||||||
be lesser if there are gaps in the recorded data.
|
be lesser if there are gaps in the recorded data.
|
||||||
* `totalSampleFileBytes`: the total number of bytes of sample data
|
* `totalSampleFileBytes`: the total number of bytes of sample data
|
||||||
(the `mdat` portion of a `.mp4` file).
|
(the `mdat` portion of a `.mp4` file).
|
||||||
* `days`: object representing calendar days (in the server's time
|
* `days`: (only included if request pararameter `days` is true)
|
||||||
zone) with non-zero total duration of recordings for that day. The
|
dictionary representing calendar days (in the server's time zone)
|
||||||
keys are of the form `YYYY-mm-dd`; the values are objects with the
|
with non-zero total duration of recordings for that day. The keys
|
||||||
|
are of the form `YYYY-mm-dd`; the values are objects with the
|
||||||
following attributes:
|
following attributes:
|
||||||
* `totalDuration90k` is the total duration recorded during that
|
* `totalDuration90k` is the total duration recorded during that
|
||||||
day. If a recording spans a day boundary, some portion of it
|
day. If a recording spans a day boundary, some portion of it
|
||||||
@ -124,6 +134,11 @@ Example response:
|
|||||||
"uuid": "fd20f7a2-9d69-4cb3-94ed-d51a20c3edfe",
|
"uuid": "fd20f7a2-9d69-4cb3-94ed-d51a20c3edfe",
|
||||||
"shortName": "driveway",
|
"shortName": "driveway",
|
||||||
"description": "Hikvision DS-2CD2032 overlooking the driveway from east",
|
"description": "Hikvision DS-2CD2032 overlooking the driveway from east",
|
||||||
|
"config": {
|
||||||
|
"host": "192.168.1.100",
|
||||||
|
"user": "admin",
|
||||||
|
"password": "12345",
|
||||||
|
},
|
||||||
"streams": {
|
"streams": {
|
||||||
"main": {
|
"main": {
|
||||||
"retainBytes": 536870912000,
|
"retainBytes": 536870912000,
|
||||||
@ -193,7 +208,9 @@ Example response:
|
|||||||
|
|
||||||
### `GET /api/cameras/<uuid>/`
|
### `GET /api/cameras/<uuid>/`
|
||||||
|
|
||||||
Returns information for the camera with the given URL.
|
Returns information for the camera with the given URL. As in the like section
|
||||||
|
of `GET /api/` with the `days` parameter set and the `cameraConfigs` parameter
|
||||||
|
unset.
|
||||||
|
|
||||||
Example response:
|
Example response:
|
||||||
|
|
||||||
@ -311,6 +328,8 @@ Example response:
|
|||||||
|
|
||||||
### `GET /api/cameras/<uuid>/<stream>/view.mp4`
|
### `GET /api/cameras/<uuid>/<stream>/view.mp4`
|
||||||
|
|
||||||
|
Requires the `view_video` permission.
|
||||||
|
|
||||||
Returns a `.mp4` file, with an etag and support for range requests. The MIME
|
Returns a `.mp4` file, with an etag and support for range requests. The MIME
|
||||||
type will be `video/mp4`, with a `codecs` parameter as specified in
|
type will be `video/mp4`, with a `codecs` parameter as specified in
|
||||||
[RFC 6381][rfc-6381].
|
[RFC 6381][rfc-6381].
|
||||||
@ -525,6 +544,8 @@ This represents the following observations:
|
|||||||
|
|
||||||
### `POST /api/signals`
|
### `POST /api/signals`
|
||||||
|
|
||||||
|
Requires the `update_signals` permission.
|
||||||
|
|
||||||
Alters the state of a signal.
|
Alters the state of a signal.
|
||||||
|
|
||||||
A typical client might be a subscriber of a camera's built-in motion
|
A typical client might be a subscriber of a camera's built-in motion
|
||||||
|
@ -84,7 +84,8 @@ Moonfire NVR can be run as a systemd service. Create
|
|||||||
[Service]
|
[Service]
|
||||||
ExecStart=/usr/local/bin/moonfire-nvr run \
|
ExecStart=/usr/local/bin/moonfire-nvr run \
|
||||||
--db-dir=/var/lib/moonfire-nvr/db \
|
--db-dir=/var/lib/moonfire-nvr/db \
|
||||||
--http-addr=0.0.0.0:8080
|
--http-addr=0.0.0.0:8080 \
|
||||||
|
--allow_unauthenticated_scopes='view_video: true'
|
||||||
Environment=TZ=:/etc/localtime
|
Environment=TZ=:/etc/localtime
|
||||||
Environment=MOONFIRE_FORMAT=google-systemd
|
Environment=MOONFIRE_FORMAT=google-systemd
|
||||||
Environment=MOONFIRE_LOG=info
|
Environment=MOONFIRE_LOG=info
|
||||||
|
@ -154,12 +154,14 @@ In your `/etc/systemd/system/moonfire-nvr.service` file, look for these lines:
|
|||||||
```
|
```
|
||||||
ExecStart=/usr/local/bin/moonfire-nvr run \
|
ExecStart=/usr/local/bin/moonfire-nvr run \
|
||||||
--db-dir=/var/lib/moonfire-nvr/db \
|
--db-dir=/var/lib/moonfire-nvr/db \
|
||||||
--http-addr=0.0.0.0:8080
|
--http-addr=0.0.0.0:8080 \
|
||||||
|
--allow-unauthenticated-permissions='view_video: true'
|
||||||
```
|
```
|
||||||
|
|
||||||
Add `--require-auth --trust-forward-hdrs`. This change has two effects:
|
Replace the last line with `--trust-forward-hdrs`. This change has two effects:
|
||||||
|
|
||||||
* `--require-auth` means that web users must authenticate.
|
* No `--allow-unauthenticated-permissions` means that web users must
|
||||||
|
authenticate.
|
||||||
* `--trust-forward-hdrs` means that Moonfire NVR will look for `X-Real-IP`
|
* `--trust-forward-hdrs` means that Moonfire NVR will look for `X-Real-IP`
|
||||||
and `X-Forwarded-Proto` headers as added by the webserver configuration
|
and `X-Forwarded-Proto` headers as added by the webserver configuration
|
||||||
in the next section.
|
in the next section.
|
||||||
|
@ -106,7 +106,8 @@ After=network-online.target
|
|||||||
ExecStart=${SERVICE_BIN} run \\
|
ExecStart=${SERVICE_BIN} run \\
|
||||||
--db-dir=${DB_DIR} \\
|
--db-dir=${DB_DIR} \\
|
||||||
--ui-dir=${LIB_DIR}/ui \\
|
--ui-dir=${LIB_DIR}/ui \\
|
||||||
--http-addr=0.0.0.0:${NVR_PORT}
|
--http-addr=0.0.0.0:${NVR_PORT} \\
|
||||||
|
--allow-unauthenticated-permissions='view_video: true'
|
||||||
Environment=TZ=:/etc/localtime
|
Environment=TZ=:/etc/localtime
|
||||||
Environment=MOONFIRE_FORMAT=google-systemd
|
Environment=MOONFIRE_FORMAT=google-systemd
|
||||||
Environment=MOONFIRE_LOG=info
|
Environment=MOONFIRE_LOG=info
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
use cursive::Cursive;
|
use cursive::Cursive;
|
||||||
use cursive::traits::{Boxable, Identifiable};
|
use cursive::traits::{Boxable, Identifiable};
|
||||||
use cursive::views;
|
use cursive::views;
|
||||||
|
use log::info;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// Builds a `UserChange` from an active `edit_user_dialog`.
|
/// Builds a `UserChange` from an active `edit_user_dialog`.
|
||||||
@ -50,6 +51,13 @@ fn get_change(siv: &mut Cursive, db: &db::LockedDatabase, id: Option<i32>,
|
|||||||
},
|
},
|
||||||
PasswordChange::Clear => change.clear_password(),
|
PasswordChange::Clear => change.clear_password(),
|
||||||
};
|
};
|
||||||
|
for (id, ref mut b) in &mut [
|
||||||
|
("perm_view_video", &mut change.permissions.view_video),
|
||||||
|
("perm_read_camera_configs", &mut change.permissions.read_camera_configs),
|
||||||
|
("perm_update_signals", &mut change.permissions.update_signals)] {
|
||||||
|
**b = siv.find_id::<views::Checkbox>(id).unwrap().is_checked();
|
||||||
|
info!("{}: {}", id, **b);
|
||||||
|
}
|
||||||
change
|
change
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,9 +120,7 @@ fn select_set(siv: &mut Cursive) {
|
|||||||
/// Adds or updates a user.
|
/// Adds or updates a user.
|
||||||
/// (The former if `item` is None; the latter otherwise.)
|
/// (The former if `item` is None; the latter otherwise.)
|
||||||
fn edit_user_dialog(db: &Arc<db::Database>, siv: &mut Cursive, item: Option<i32>) {
|
fn edit_user_dialog(db: &Arc<db::Database>, siv: &mut Cursive, item: Option<i32>) {
|
||||||
let username;
|
let (username, id_str, has_password, permissions);
|
||||||
let id_str;
|
|
||||||
let has_password;
|
|
||||||
let mut pw_group = views::RadioGroup::new();
|
let mut pw_group = views::RadioGroup::new();
|
||||||
{
|
{
|
||||||
let l = db.lock();
|
let l = db.lock();
|
||||||
@ -122,6 +128,7 @@ fn edit_user_dialog(db: &Arc<db::Database>, siv: &mut Cursive, item: Option<i32>
|
|||||||
username = u.map(|u| u.username.clone()).unwrap_or(String::new());
|
username = u.map(|u| u.username.clone()).unwrap_or(String::new());
|
||||||
id_str = item.map(|id| id.to_string()).unwrap_or("<new>".to_string());
|
id_str = item.map(|id| id.to_string()).unwrap_or("<new>".to_string());
|
||||||
has_password = u.map(|u| u.has_password()).unwrap_or(false);
|
has_password = u.map(|u| u.has_password()).unwrap_or(false);
|
||||||
|
permissions = u.map(|u| u.permissions.clone()).unwrap_or(db::Permissions::default());
|
||||||
}
|
}
|
||||||
let top_list = views::ListView::new()
|
let top_list = views::ListView::new()
|
||||||
.child("id", views::TextView::new(id_str))
|
.child("id", views::TextView::new(id_str))
|
||||||
@ -156,6 +163,18 @@ fn edit_user_dialog(db: &Arc<db::Database>, siv: &mut Cursive, item: Option<i32>
|
|||||||
.full_width()));
|
.full_width()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
layout.add_child(views::DummyView);
|
||||||
|
layout.add_child(views::TextView::new("permissions"));
|
||||||
|
let mut perms = views::ListView::new();
|
||||||
|
for (name, b) in &[("view_video", permissions.view_video),
|
||||||
|
("read_camera_configs", permissions.read_camera_configs),
|
||||||
|
("update_signals", permissions.update_signals)] {
|
||||||
|
let mut checkbox = views::Checkbox::new();
|
||||||
|
checkbox.set_checked(*b);
|
||||||
|
perms.add_child(name, checkbox.with_id(format!("perm_{}", name)));
|
||||||
|
}
|
||||||
|
layout.add_child(perms);
|
||||||
|
|
||||||
let dialog = views::Dialog::around(layout);
|
let dialog = views::Dialog::around(layout);
|
||||||
let dialog = if let Some(id) = item {
|
let dialog = if let Some(id) = item {
|
||||||
dialog.title("Edit user")
|
dialog.title("Edit user")
|
||||||
|
@ -33,7 +33,7 @@ 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, bail};
|
use failure::{Error, bail, format_err};
|
||||||
use fnv::FnvHashMap;
|
use fnv::FnvHashMap;
|
||||||
use futures::{Future, Stream};
|
use futures::{Future, Stream};
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
@ -68,7 +68,13 @@ Options:
|
|||||||
--http-addr=ADDR Set the bind address for the unencrypted HTTP server.
|
--http-addr=ADDR Set the bind address for the unencrypted HTTP server.
|
||||||
[default: 0.0.0.0:8080]
|
[default: 0.0.0.0:8080]
|
||||||
--read-only Forces read-only mode / disables recording.
|
--read-only Forces read-only mode / disables recording.
|
||||||
--require-auth Requires authentication to access the web interface.
|
--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
|
--trust-forward-hdrs Trust X-Real-IP: and X-Forwarded-Proto: headers on
|
||||||
the incoming request. Set this only after ensuring
|
the incoming request. Set this only after ensuring
|
||||||
your proxy server is configured to set them and that
|
your proxy server is configured to set them and that
|
||||||
@ -82,7 +88,7 @@ struct Args {
|
|||||||
flag_http_addr: String,
|
flag_http_addr: String,
|
||||||
flag_ui_dir: String,
|
flag_ui_dir: String,
|
||||||
flag_read_only: bool,
|
flag_read_only: bool,
|
||||||
flag_require_auth: bool,
|
flag_allow_unauthenticated_permissions: Option<String>,
|
||||||
flag_trust_forward_hdrs: bool,
|
flag_trust_forward_hdrs: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,10 +192,14 @@ pub 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()
|
||||||
|
.map_err(|_| format_err!("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.flag_ui_dir),
|
||||||
require_auth: args.flag_require_auth,
|
allow_unauthenticated_permissions,
|
||||||
trust_forward_hdrs: args.flag_trust_forward_hdrs,
|
trust_forward_hdrs: args.flag_trust_forward_hdrs,
|
||||||
time_zone_name,
|
time_zone_name,
|
||||||
})?;
|
})?;
|
||||||
|
36
src/json.rs
36
src/json.rs
@ -42,9 +42,9 @@ pub struct TopLevel<'a> {
|
|||||||
pub time_zone_name: &'a str,
|
pub time_zone_name: &'a str,
|
||||||
|
|
||||||
// Use a custom serializer which presents the map's values as a sequence and includes the
|
// Use a custom serializer which presents the map's values as a sequence and includes the
|
||||||
// "days" attribute or not, according to the bool in the tuple.
|
// "days" and "camera_configs" attributes or not, according to the respective bools.
|
||||||
#[serde(serialize_with = "TopLevel::serialize_cameras")]
|
#[serde(serialize_with = "TopLevel::serialize_cameras")]
|
||||||
pub cameras: (&'a db::LockedDatabase, bool),
|
pub cameras: (&'a db::LockedDatabase, bool, bool),
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub session: Option<Session>,
|
pub session: Option<Session>,
|
||||||
@ -83,10 +83,21 @@ pub struct Camera<'a> {
|
|||||||
pub short_name: &'a str,
|
pub short_name: &'a str,
|
||||||
pub description: &'a str,
|
pub description: &'a str,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub config: Option<CameraConfig<'a>>,
|
||||||
|
|
||||||
#[serde(serialize_with = "Camera::serialize_streams")]
|
#[serde(serialize_with = "Camera::serialize_streams")]
|
||||||
pub streams: [Option<Stream<'a>>; 2],
|
pub streams: [Option<Stream<'a>>; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all="camelCase")]
|
||||||
|
pub struct CameraConfig<'a> {
|
||||||
|
pub host: &'a str,
|
||||||
|
pub username: &'a str,
|
||||||
|
pub password: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all="camelCase")]
|
#[serde(rename_all="camelCase")]
|
||||||
pub struct Stream<'a> {
|
pub struct Stream<'a> {
|
||||||
@ -163,11 +174,20 @@ pub struct SignalTypeState<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Camera<'a> {
|
impl<'a> Camera<'a> {
|
||||||
pub fn wrap(c: &'a db::Camera, db: &'a db::LockedDatabase, include_days: bool) -> Result<Self, Error> {
|
pub fn wrap(c: &'a db::Camera, db: &'a db::LockedDatabase, include_days: bool,
|
||||||
|
include_config: bool) -> Result<Self, Error> {
|
||||||
Ok(Camera {
|
Ok(Camera {
|
||||||
uuid: c.uuid,
|
uuid: c.uuid,
|
||||||
short_name: &c.short_name,
|
short_name: &c.short_name,
|
||||||
description: &c.description,
|
description: &c.description,
|
||||||
|
config: match include_config {
|
||||||
|
false => None,
|
||||||
|
true => Some(CameraConfig {
|
||||||
|
host: &c.host,
|
||||||
|
username: &c.username,
|
||||||
|
password: &c.password,
|
||||||
|
}),
|
||||||
|
},
|
||||||
streams: [
|
streams: [
|
||||||
Stream::wrap(db, c.streams[0], include_days)?,
|
Stream::wrap(db, c.streams[0], include_days)?,
|
||||||
Stream::wrap(db, c.streams[1], include_days)?,
|
Stream::wrap(db, c.streams[1], include_days)?,
|
||||||
@ -295,16 +315,18 @@ struct StreamDayValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TopLevel<'a> {
|
impl<'a> TopLevel<'a> {
|
||||||
/// Serializes cameras as a list (rather than a map), optionally including the `days` field.
|
/// Serializes cameras as a list (rather than a map), optionally including the `days` and
|
||||||
fn serialize_cameras<S>(cameras: &(&db::LockedDatabase, bool),
|
/// `cameras` fields.
|
||||||
|
fn serialize_cameras<S>(cameras: &(&db::LockedDatabase, bool, bool),
|
||||||
serializer: S) -> Result<S::Ok, S::Error>
|
serializer: S) -> Result<S::Ok, S::Error>
|
||||||
where S: Serializer {
|
where S: Serializer {
|
||||||
let (db, include_days) = *cameras;
|
let (db, include_days, include_config) = *cameras;
|
||||||
let cs = db.cameras_by_id();
|
let cs = db.cameras_by_id();
|
||||||
let mut seq = serializer.serialize_seq(Some(cs.len()))?;
|
let mut seq = serializer.serialize_seq(Some(cs.len()))?;
|
||||||
for (_, c) in cs {
|
for (_, c) in cs {
|
||||||
seq.serialize_element(
|
seq.serialize_element(
|
||||||
&Camera::wrap(c, db, include_days).map_err(|e| S::Error::custom(e))?)?;
|
&Camera::wrap(c, db, include_days, include_config)
|
||||||
|
.map_err(|e| S::Error::custom(e))?)?;
|
||||||
}
|
}
|
||||||
seq.end()
|
seq.end()
|
||||||
}
|
}
|
||||||
|
139
src/web.rs
139
src/web.rs
@ -179,6 +179,7 @@ fn internal_server_err<E: Into<Error>>(err: E) -> Response<Body> {
|
|||||||
|
|
||||||
fn from_base_error(err: base::Error) -> Response<Body> {
|
fn from_base_error(err: base::Error) -> Response<Body> {
|
||||||
let status_code = match err.kind() {
|
let status_code = match err.kind() {
|
||||||
|
ErrorKind::PermissionDenied | ErrorKind::Unauthenticated => StatusCode::UNAUTHORIZED,
|
||||||
ErrorKind::InvalidArgument => StatusCode::BAD_REQUEST,
|
ErrorKind::InvalidArgument => StatusCode::BAD_REQUEST,
|
||||||
ErrorKind::NotFound => StatusCode::NOT_FOUND,
|
ErrorKind::NotFound => StatusCode::NOT_FOUND,
|
||||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
@ -241,13 +242,21 @@ struct UiFile {
|
|||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct Caller {
|
||||||
|
permissions: db::Permissions,
|
||||||
|
session: Option<json::Session>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Caller {
|
||||||
|
}
|
||||||
|
|
||||||
struct ServiceInner {
|
struct ServiceInner {
|
||||||
db: Arc<db::Database>,
|
db: Arc<db::Database>,
|
||||||
dirs_by_stream_id: Arc<FnvHashMap<i32, Arc<SampleFileDir>>>,
|
dirs_by_stream_id: Arc<FnvHashMap<i32, Arc<SampleFileDir>>>,
|
||||||
ui_files: HashMap<String, UiFile>,
|
ui_files: HashMap<String, UiFile>,
|
||||||
pool: futures_cpupool::CpuPool,
|
pool: futures_cpupool::CpuPool,
|
||||||
time_zone_name: String,
|
time_zone_name: String,
|
||||||
require_auth: bool,
|
allow_unauthenticated_permissions: Option<db::Permissions>,
|
||||||
trust_forward_hdrs: bool,
|
trust_forward_hdrs: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,24 +273,32 @@ fn serve_json<T: serde::ser::Serialize>(req: &Request<hyper::Body>, out: &T) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ServiceInner {
|
impl ServiceInner {
|
||||||
fn top_level(&self, req: &Request<::hyper::Body>, session: Option<json::Session>)
|
fn top_level(&self, req: &Request<::hyper::Body>, caller: Caller) -> ResponseResult {
|
||||||
-> ResponseResult {
|
|
||||||
let mut days = false;
|
let mut days = false;
|
||||||
|
let mut camera_configs = false;
|
||||||
if let Some(q) = req.uri().query() {
|
if let Some(q) = req.uri().query() {
|
||||||
for (key, value) in form_urlencoded::parse(q.as_bytes()) {
|
for (key, value) in form_urlencoded::parse(q.as_bytes()) {
|
||||||
let (key, value): (_, &str) = (key.borrow(), value.borrow());
|
let (key, value): (_, &str) = (key.borrow(), value.borrow());
|
||||||
match key {
|
match key {
|
||||||
"days" => days = value == "true",
|
"days" => days = value == "true",
|
||||||
|
"cameraConfigs" => camera_configs = value == "true",
|
||||||
_ => {},
|
_ => {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if camera_configs {
|
||||||
|
if !caller.permissions.read_camera_configs {
|
||||||
|
return Err(plain_response(StatusCode::UNAUTHORIZED,
|
||||||
|
"read_camera_configs required"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let db = self.db.lock();
|
let db = self.db.lock();
|
||||||
serve_json(req, &json::TopLevel {
|
serve_json(req, &json::TopLevel {
|
||||||
time_zone_name: &self.time_zone_name,
|
time_zone_name: &self.time_zone_name,
|
||||||
cameras: (&db, days),
|
cameras: (&db, days, camera_configs),
|
||||||
session,
|
session: caller.session,
|
||||||
signals: (&db, days),
|
signals: (&db, days),
|
||||||
signal_types: &db,
|
signal_types: &db,
|
||||||
})
|
})
|
||||||
@ -291,7 +308,7 @@ impl ServiceInner {
|
|||||||
let db = self.db.lock();
|
let db = self.db.lock();
|
||||||
let camera = db.get_camera(uuid)
|
let camera = db.get_camera(uuid)
|
||||||
.ok_or_else(|| not_found(format!("no such camera {}", uuid)))?;
|
.ok_or_else(|| not_found(format!("no such camera {}", uuid)))?;
|
||||||
serve_json(req, &json::Camera::wrap(camera, &db, true).map_err(internal_server_err)?)
|
serve_json(req, &json::Camera::wrap(camera, &db, true, false).map_err(internal_server_err)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stream_recordings(&self, req: &Request<::hyper::Body>, uuid: Uuid, type_: db::StreamType)
|
fn stream_recordings(&self, req: &Request<::hyper::Body>, uuid: Uuid, type_: db::StreamType)
|
||||||
@ -372,9 +389,12 @@ impl ServiceInner {
|
|||||||
Err(not_found("no such init segment"))
|
Err(not_found("no such init segment"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stream_view_mp4(&self, req: &Request<::hyper::Body>, uuid: Uuid,
|
fn stream_view_mp4(&self, req: &Request<::hyper::Body>, caller: Caller, uuid: Uuid,
|
||||||
stream_type: db::StreamType, mp4_type: mp4::Type, debug: bool)
|
stream_type: db::StreamType, mp4_type: mp4::Type, debug: bool)
|
||||||
-> ResponseResult {
|
-> ResponseResult {
|
||||||
|
if !caller.permissions.view_video {
|
||||||
|
return Err(plain_response(StatusCode::UNAUTHORIZED, "view_video required"));
|
||||||
|
}
|
||||||
let stream_id = {
|
let stream_id = {
|
||||||
let db = self.db.lock();
|
let db = self.db.lock();
|
||||||
let camera = db.get_camera(uuid)
|
let camera = db.get_camera(uuid)
|
||||||
@ -629,7 +649,11 @@ impl ServiceInner {
|
|||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn post_signals(&self, req: &Request<hyper::Body>, body: hyper::Chunk) -> ResponseResult {
|
fn post_signals(&self, req: &Request<hyper::Body>, caller: Caller, body: hyper::Chunk)
|
||||||
|
-> ResponseResult {
|
||||||
|
if !caller.permissions.update_signals {
|
||||||
|
return Err(plain_response(StatusCode::UNAUTHORIZED, "update_signals required"));
|
||||||
|
}
|
||||||
let r: json::PostSignalsRequest = serde_json::from_slice(&body)
|
let r: json::PostSignalsRequest = serde_json::from_slice(&body)
|
||||||
.map_err(|e| bad_req(e.to_string()))?;
|
.map_err(|e| bad_req(e.to_string()))?;
|
||||||
let mut l = self.db.lock();
|
let mut l = self.db.lock();
|
||||||
@ -676,24 +700,39 @@ impl ServiceInner {
|
|||||||
serve_json(req, &signals)
|
serve_json(req, &signals)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn authenticated(&self, req: &Request<hyper::Body>) -> Result<Option<json::Session>, Error> {
|
fn authenticate(&self, req: &Request<hyper::Body>, unauth_path: bool)
|
||||||
|
-> Result<Caller, base::Error> {
|
||||||
if let Some(sid) = extract_sid(req) {
|
if let Some(sid) = extract_sid(req) {
|
||||||
let authreq = self.authreq(req);
|
let authreq = self.authreq(req);
|
||||||
match self.db.lock().authenticate_session(authreq.clone(), &sid.hash()) {
|
|
||||||
Ok((s, u)) => {
|
// TODO: real error handling! this assumes all errors are due to lack of
|
||||||
return Ok(Some(json::Session {
|
// authentication, when they could be logic errors in SQL or such.
|
||||||
|
if let Ok((s, u)) = self.db.lock().authenticate_session(authreq.clone(), &sid.hash()) {
|
||||||
|
return Ok(Caller {
|
||||||
|
permissions: s.permissions.clone(),
|
||||||
|
session: Some(json::Session {
|
||||||
username: u.username.clone(),
|
username: u.username.clone(),
|
||||||
csrf: s.csrf(),
|
csrf: s.csrf(),
|
||||||
}))
|
}),
|
||||||
},
|
});
|
||||||
Err(_) => {
|
|
||||||
// TODO: real error handling! this assumes all errors are due to lack of
|
|
||||||
// authentication, when they could be logic errors in SQL or such.
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(None)
|
|
||||||
|
if let Some(s) = self.allow_unauthenticated_permissions.as_ref() {
|
||||||
|
return Ok(Caller {
|
||||||
|
permissions: s.clone(),
|
||||||
|
session: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if unauth_path {
|
||||||
|
return Ok(Caller {
|
||||||
|
permissions: db::Permissions::default(),
|
||||||
|
session: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
bail_t!(Unauthenticated, "unauthenticated");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -783,9 +822,9 @@ 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 str>,
|
||||||
pub require_auth: bool,
|
|
||||||
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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -820,7 +859,7 @@ impl Service {
|
|||||||
dirs_by_stream_id,
|
dirs_by_stream_id,
|
||||||
ui_files,
|
ui_files,
|
||||||
pool: futures_cpupool::Builder::new().pool_size(1).name_prefix("static").create(),
|
pool: futures_cpupool::Builder::new().pool_size(1).name_prefix("static").create(),
|
||||||
require_auth: config.require_auth,
|
allow_unauthenticated_permissions: config.allow_unauthenticated_permissions,
|
||||||
trust_forward_hdrs: config.trust_forward_hdrs,
|
trust_forward_hdrs: config.trust_forward_hdrs,
|
||||||
time_zone_name: config.time_zone_name,
|
time_zone_name: config.time_zone_name,
|
||||||
})))
|
})))
|
||||||
@ -867,8 +906,11 @@ impl Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stream_live_m4s(&self, _req: &Request<::hyper::Body>, uuid: Uuid,
|
fn stream_live_m4s(&self, _req: &Request<::hyper::Body>, caller: Caller, uuid: Uuid,
|
||||||
stream_type: db::StreamType) -> ResponseResult {
|
stream_type: db::StreamType) -> ResponseResult {
|
||||||
|
if !caller.permissions.view_video {
|
||||||
|
return Err(plain_response(StatusCode::UNAUTHORIZED, "view_video required"));
|
||||||
|
}
|
||||||
let stream_id;
|
let stream_id;
|
||||||
let open_id;
|
let open_id;
|
||||||
let (sub_tx, sub_rx) = futures::sync::mpsc::unbounded();
|
let (sub_tx, sub_rx) = futures::sync::mpsc::unbounded();
|
||||||
@ -952,14 +994,14 @@ impl Service {
|
|||||||
.unwrap())
|
.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn signals(&self, req: Request<hyper::Body>)
|
fn signals(&self, req: Request<hyper::Body>, caller: Caller)
|
||||||
-> Box<dyn Future<Item = Response<Body>, Error = Response<Body>> + Send + 'static> {
|
-> Box<dyn Future<Item = Response<Body>, Error = Response<Body>> + Send + 'static> {
|
||||||
use http::method::Method;
|
use http::method::Method;
|
||||||
match *req.method() {
|
match *req.method() {
|
||||||
Method::POST => Box::new(with_json_body(req)
|
Method::POST => Box::new(with_json_body(req)
|
||||||
.and_then({
|
.and_then({
|
||||||
let s = self.0.clone();
|
let s = self.0.clone();
|
||||||
move |(req, b)| s.post_signals(&req, b)
|
move |(req, b)| s.post_signals(&req, caller, b)
|
||||||
})),
|
})),
|
||||||
Method::GET | Method::HEAD => Box::new(future::result(self.0.get_signals(&req))),
|
Method::GET | Method::HEAD => Box::new(future::result(self.0.get_signals(&req))),
|
||||||
_ => Box::new(future::err(plain_response(StatusCode::METHOD_NOT_ALLOWED,
|
_ => Box::new(future::err(plain_response(StatusCode::METHOD_NOT_ALLOWED,
|
||||||
@ -992,36 +1034,33 @@ impl ::hyper::service::Service for Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let p = Path::decode(req.uri().path());
|
let p = Path::decode(req.uri().path());
|
||||||
let require_auth = self.0.require_auth && match p {
|
let always_allow_unauthenticated = match p {
|
||||||
Path::NotFound | Path::Request | Path::Login | Path::Logout | Path::Static => false,
|
Path::NotFound | Path::Request | Path::Login | Path::Logout | Path::Static => true,
|
||||||
_ => true,
|
_ => false,
|
||||||
};
|
};
|
||||||
debug!("request on: {}: {:?}, require_auth={}", req.uri(), p, require_auth);
|
debug!("request on: {}: {:?}", req.uri(), p);
|
||||||
let session = match self.0.authenticated(&req) {
|
let caller = match self.0.authenticate(&req, always_allow_unauthenticated) {
|
||||||
Ok(s) => s,
|
Ok(c) => c,
|
||||||
Err(e) => return Box::new(future::ok(internal_server_err(e))),
|
Err(e) => return Box::new(future::ok(from_base_error(e))),
|
||||||
};
|
};
|
||||||
if require_auth && session.is_none() {
|
|
||||||
return Box::new(future::ok(
|
|
||||||
plain_response(StatusCode::UNAUTHORIZED, "unauthorized")));
|
|
||||||
}
|
|
||||||
match p {
|
match p {
|
||||||
Path::InitSegment(sha1, debug) => wrap_r(true, self.0.init_segment(sha1, debug, &req)),
|
Path::InitSegment(sha1, debug) => wrap_r(true, self.0.init_segment(sha1, debug, &req)),
|
||||||
Path::TopLevel => wrap_r(true, self.0.top_level(&req, session)),
|
Path::TopLevel => wrap_r(true, self.0.top_level(&req, caller)),
|
||||||
Path::Request => wrap_r(true, self.0.request(&req)),
|
Path::Request => wrap_r(true, self.0.request(&req)),
|
||||||
Path::Camera(uuid) => wrap_r(true, self.0.camera(&req, uuid)),
|
Path::Camera(uuid) => wrap_r(true, self.0.camera(&req, uuid)),
|
||||||
Path::StreamRecordings(uuid, type_) => {
|
Path::StreamRecordings(uuid, type_) => {
|
||||||
wrap_r(true, self.0.stream_recordings(&req, uuid, type_))
|
wrap_r(true, self.0.stream_recordings(&req, uuid, type_))
|
||||||
},
|
},
|
||||||
Path::StreamViewMp4(uuid, type_, debug) => {
|
Path::StreamViewMp4(uuid, type_, debug) => {
|
||||||
wrap_r(true, self.0.stream_view_mp4(&req, uuid, type_, mp4::Type::Normal, debug))
|
wrap_r(true, self.0.stream_view_mp4(&req, caller, uuid, type_, mp4::Type::Normal,
|
||||||
},
|
|
||||||
Path::StreamViewMp4Segment(uuid, type_, debug) => {
|
|
||||||
wrap_r(true, self.0.stream_view_mp4(&req, uuid, type_, mp4::Type::MediaSegment,
|
|
||||||
debug))
|
debug))
|
||||||
},
|
},
|
||||||
|
Path::StreamViewMp4Segment(uuid, type_, debug) => {
|
||||||
|
wrap_r(true, self.0.stream_view_mp4(&req, caller, uuid, type_,
|
||||||
|
mp4::Type::MediaSegment, debug))
|
||||||
|
},
|
||||||
Path::StreamLiveMp4Segments(uuid, type_) => {
|
Path::StreamLiveMp4Segments(uuid, type_) => {
|
||||||
wrap_r(true, self.stream_live_m4s(&req, uuid, type_))
|
wrap_r(true, self.stream_live_m4s(&req, caller, uuid, type_))
|
||||||
},
|
},
|
||||||
Path::NotFound => wrap(true, future::err(not_found("path not understood"))),
|
Path::NotFound => wrap(true, future::err(not_found("path not understood"))),
|
||||||
Path::Login => wrap(true, with_form_body(req).and_then({
|
Path::Login => wrap(true, with_form_body(req).and_then({
|
||||||
@ -1032,7 +1071,7 @@ impl ::hyper::service::Service for Service {
|
|||||||
let s = self.clone();
|
let s = self.clone();
|
||||||
move |(req, b)| { s.0.logout(&req, b) }
|
move |(req, b)| { s.0.logout(&req, b) }
|
||||||
})),
|
})),
|
||||||
Path::Signals => wrap(true, self.signals(req)),
|
Path::Signals => wrap(true, self.signals(req, caller)),
|
||||||
Path::Static => wrap_r(false, self.0.static_file(&req, req.uri().path())),
|
Path::Static => wrap_r(false, self.0.static_file(&req, req.uri().path())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1057,14 +1096,14 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Server {
|
impl Server {
|
||||||
fn new(require_auth: bool) -> Server {
|
fn new(allow_unauthenticated_permissions: Option<db::Permissions>) -> Server {
|
||||||
let db = TestDb::new(base::clock::RealClocks {});
|
let db = TestDb::new(base::clock::RealClocks {});
|
||||||
let (shutdown_tx, shutdown_rx) = futures::sync::oneshot::channel::<()>();
|
let (shutdown_tx, shutdown_rx) = futures::sync::oneshot::channel::<()>();
|
||||||
let addr = "127.0.0.1:0".parse().unwrap();
|
let addr = "127.0.0.1:0".parse().unwrap();
|
||||||
let service = super::Service::new(super::Config {
|
let service = super::Service::new(super::Config {
|
||||||
db: db.db.clone(),
|
db: db.db.clone(),
|
||||||
ui_dir: None,
|
ui_dir: None,
|
||||||
require_auth,
|
allow_unauthenticated_permissions,
|
||||||
trust_forward_hdrs: true,
|
trust_forward_hdrs: true,
|
||||||
time_zone_name: "".to_owned(),
|
time_zone_name: "".to_owned(),
|
||||||
}).unwrap();
|
}).unwrap();
|
||||||
@ -1213,7 +1252,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn unauthorized_without_cookie() {
|
fn unauthorized_without_cookie() {
|
||||||
testutil::init();
|
testutil::init();
|
||||||
let s = Server::new(true);
|
let s = Server::new(None);
|
||||||
let cli = reqwest::Client::new();
|
let cli = reqwest::Client::new();
|
||||||
let resp = cli.get(&format!("{}/api/", &s.base_url)).send().unwrap();
|
let resp = cli.get(&format!("{}/api/", &s.base_url)).send().unwrap();
|
||||||
assert_eq!(resp.status(), http::StatusCode::UNAUTHORIZED);
|
assert_eq!(resp.status(), http::StatusCode::UNAUTHORIZED);
|
||||||
@ -1222,7 +1261,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn login() {
|
fn login() {
|
||||||
testutil::init();
|
testutil::init();
|
||||||
let s = Server::new(true);
|
let s = Server::new(None);
|
||||||
let cli = reqwest::Client::new();
|
let cli = reqwest::Client::new();
|
||||||
let login_url = format!("{}/api/login", &s.base_url);
|
let login_url = format!("{}/api/login", &s.base_url);
|
||||||
|
|
||||||
@ -1255,7 +1294,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn logout() {
|
fn logout() {
|
||||||
testutil::init();
|
testutil::init();
|
||||||
let s = Server::new(true);
|
let s = Server::new(None);
|
||||||
let cli = reqwest::Client::new();
|
let cli = reqwest::Client::new();
|
||||||
let mut p = HashMap::new();
|
let mut p = HashMap::new();
|
||||||
p.insert("username", "slamb");
|
p.insert("username", "slamb");
|
||||||
@ -1310,7 +1349,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn view_without_segments() {
|
fn view_without_segments() {
|
||||||
testutil::init();
|
testutil::init();
|
||||||
let s = Server::new(false);
|
let mut permissions = db::Permissions::new();
|
||||||
|
permissions.view_video = true;
|
||||||
|
let s = Server::new(Some(permissions));
|
||||||
let cli = reqwest::Client::new();
|
let cli = reqwest::Client::new();
|
||||||
let resp = cli.get(
|
let resp = cli.get(
|
||||||
&format!("{}/api/cameras/{}/main/view.mp4", &s.base_url, s.db.test_camera_uuid))
|
&format!("{}/api/cameras/{}/main/view.mp4", &s.base_url, s.db.test_camera_uuid))
|
||||||
|
@ -215,7 +215,7 @@ function fetch(selectedRange, videoLength) {
|
|||||||
function updateSession(session) {
|
function updateSession(session) {
|
||||||
let sessionBar = $('#session');
|
let sessionBar = $('#session');
|
||||||
sessionBar.empty();
|
sessionBar.empty();
|
||||||
if (session === null) {
|
if (session === null || session === undefined) {
|
||||||
sessionBar.hide();
|
sessionBar.hide();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -247,7 +247,7 @@ function updateSession(session) {
|
|||||||
*/
|
*/
|
||||||
function onReceivedTopLevel(data) {
|
function onReceivedTopLevel(data) {
|
||||||
if (data === null) {
|
if (data === null) {
|
||||||
data = {cameras: [], session: null, timeZoneName: null};
|
data = {cameras: [], timeZoneName: null};
|
||||||
}
|
}
|
||||||
|
|
||||||
newTimeZone(data.timeZoneName);
|
newTimeZone(data.timeZoneName);
|
||||||
|
Loading…
Reference in New Issue
Block a user