From a30f403dc452224b1348bd41a1cea516864ac7c8 Mon Sep 17 00:00:00 2001 From: Linus Date: Mon, 27 Jan 2025 22:59:53 +0100 Subject: [PATCH] Add support for managing users Support includes: - Listing users - Creating users - Deleting users - Changing the password of users --- crates/core/src/bc/model.rs | 4 + crates/core/src/bc/xml.rs | 45 +++++++ crates/core/src/bc_protocol.rs | 1 + crates/core/src/bc_protocol/users.rs | 179 +++++++++++++++++++++++++++ src/cmdline.rs | 1 + src/main.rs | 4 + src/users/cmdline.rs | 43 +++++++ src/users/mod.rs | 173 ++++++++++++++++++++++++++ 8 files changed, 450 insertions(+) create mode 100644 crates/core/src/bc_protocol/users.rs create mode 100644 src/users/cmdline.rs create mode 100644 src/users/mod.rs diff --git a/crates/core/src/bc/model.rs b/crates/core/src/bc/model.rs index 6449fb3c..a1e1cf27 100644 --- a/crates/core/src/bc/model.rs +++ b/crates/core/src/bc/model.rs @@ -40,6 +40,10 @@ pub const MSG_ID_GET_SERVICE_PORTS: u32 = 37; pub const MSG_ID_GET_EMAIL: u32 = 42; /// Set email settings pub const MSG_ID_SET_EMAIL: u32 = 43; +/// Get users and general system info +pub const MSG_ID_GET_ABILITY_SUPPORT: u32 = 58; +/// Update, create and remove users +pub const MSG_ID_UPDATE_USER_LIST: u32 = 59; /// Version messages have this ID pub const MSG_ID_VERSION: u32 = 80; /// Ping messages have this ID diff --git a/crates/core/src/bc/xml.rs b/crates/core/src/bc/xml.rs index c046d24f..b7cb8aee 100644 --- a/crates/core/src/bc/xml.rs +++ b/crates/core/src/bc/xml.rs @@ -138,6 +138,9 @@ pub struct BcXml { /// EmailTask for turning the email notifications on/off #[serde(rename = "EmailTask", skip_serializing_if = "Option::is_none")] pub email_task: Option, + /// Read and write users + #[serde(rename = "UserList", skip_serializing_if = "Option::is_none")] + pub user_list: Option, } impl BcXml { @@ -1566,6 +1569,48 @@ pub struct Schedule { pub time_block_list: TimeBlockList, } +/// List of users +#[derive(PartialEq, Eq, Default, Debug, Deserialize, Serialize)] +pub struct UserList { + /// XML Version + #[serde(rename = "@version")] + pub version: String, + /// The actual user-list + #[serde(rename = "User", skip_serializing_if = "Option::is_none")] + pub user_list: Option>, +} + +/// A struct for reading and writing camera user records +#[derive(PartialEq, Eq, Default, Debug, Deserialize, Serialize)] +pub struct User { + /// The user_name is used to identify the user in the API + #[serde(rename = "userName")] + pub user_name: String, + /// The password seems to only be included when creating or modifying a user + #[serde(rename = "password", skip_serializing_if = "Option::is_none")] + pub password: Option, + /// The user_id does not seem to have a purpose. It is not included when creating a user. + #[serde(rename = "userId", skip_serializing_if = "Option::is_none")] + pub user_id: Option, + /// User type, 0 is User and 1 is Administrator + #[serde(rename = "userLevel")] + pub user_level: u8, + /// Unknown, seems to be 1 for the current API user + #[serde(rename = "loginState", skip_serializing_if = "Option::is_none")] + pub login_state: Option, + /// The user_set_state states what will happen with a user-record. 4 different values have been + /// observed: none | add | delete | modify + /// + /// | Value | Description | + /// | --- | --- | + /// | none | This is the state set when reading Users. When writing this seems to indicate that the user should not be modified | + /// | add | Indicates that a new User should be created | + /// | delete | Indicates that the user should be removed | + /// | modify | Indicates that the user should be modified. It seems like only the password can be changed. | + #[serde(rename = "userSetState")] + pub user_set_state: String, +} + /// Convience function to return the xml version used throughout the library pub fn xml_ver() -> String { "1.1".to_string() diff --git a/crates/core/src/bc_protocol.rs b/crates/core/src/bc_protocol.rs index cf678094..b3ad9d90 100644 --- a/crates/core/src/bc_protocol.rs +++ b/crates/core/src/bc_protocol.rs @@ -40,6 +40,7 @@ mod support; mod talk; mod time; mod uid; +mod users; mod version; pub(crate) use connection::*; diff --git a/crates/core/src/bc_protocol/users.rs b/crates/core/src/bc_protocol/users.rs new file mode 100644 index 00000000..1b26f29f --- /dev/null +++ b/crates/core/src/bc_protocol/users.rs @@ -0,0 +1,179 @@ +use super::{BcCamera, Error, Result}; +use crate::bc::{model::*, xml::*}; + +impl BcCamera { + /// Returns all users configured in the camera + pub async fn get_users(&self) -> Result { + let connection = self.get_connection(); + + let msg_num = self.new_message_num(); + let mut sub_get = connection + .subscribe(MSG_ID_GET_ABILITY_SUPPORT, msg_num) + .await?; + let get = Bc { + meta: BcMeta { + msg_id: MSG_ID_GET_ABILITY_SUPPORT, + channel_id: self.channel_id, + msg_num, + response_code: 0, + stream_type: 0, + class: 0x6414, + }, + body: BcBody::ModernMsg(ModernMsg { + extension: Some(Extension { + user_name: Some("admin".to_owned()), + ..Default::default() + }), + payload: None, + }), + }; + + sub_get.send(get).await?; + let msg = sub_get.recv().await?; + if msg.meta.response_code != 200 { + return Err(Error::CameraServiceUnavailable { + id: msg.meta.msg_id, + code: msg.meta.response_code, + }); + } + + // Valid message with response_code == 200 + if let BcBody::ModernMsg(ModernMsg { + payload: + Some(BcPayloads::BcXml(BcXml { + user_list: Some(user_list), + .. + })), + .. + }) = msg.body + { + Ok(user_list) + } else { + Err(Error::UnintelligibleReply { + reply: std::sync::Arc::new(Box::new(msg)), + why: "Expected ModernMsg payload with a user_list but it was not recieved", + }) + } + } + + /// Add a new user + /// + /// This function does not check if the user exist and the API will likely throw an error if + /// that is the case + pub async fn add_user( + &self, + user_name: String, + password: String, + user_level: u8, + ) -> Result<()> { + let users = self.get_users().await; + + let mut users = users?.user_list.unwrap_or(Vec::new()); + if users.iter().any(|user| user.user_name == user_name) { + return Err(Error::Other("User already exists")); + } + + users.push(User { + user_set_state: "add".to_owned(), + user_name, + password: Some(password), + user_level, + user_id: None, + login_state: None, + }); + + self.set_users(users).await + } + + /// Modify a user. It seems the only property of a user that is modifiable is the password. + pub async fn modify_user(&self, user_name: String, password: String) -> Result<()> { + let users = self.get_users().await; + + let mut users = users?.user_list.unwrap_or(Vec::new()); + + if let Some(user) = users.iter_mut().find(|user| user.user_name == user_name) { + user.user_set_state = "modify".to_owned(); + user.password = Some(password.clone()); + } else { + return Err(Error::Other("User not found")); + } + + self.set_users(users).await + } + + /// Remove a user. This does not check for the existence of that user and will likely return an + /// error if the user doesn't exist. + pub async fn delete_user(&self, user_name: String) -> Result<()> { + let users = self.get_users().await; + + let mut users = users?.user_list.unwrap_or(Vec::new()); + + if let Some(user) = users.iter_mut().find(|user| user.user_name == user_name) { + user.user_set_state = "delete".to_owned(); + } else { + return Err(Error::Other("User not found")); + } + + self.set_users(users).await + } + + /// Helper method to send a UserList and wait for its success/failure. + async fn set_users(&self, users: Vec) -> Result<()> { + let bcxml = BcXml { + user_list: Some(UserList { + version: "1.1".to_owned(), + user_list: Some(users), + }), + ..Default::default() + }; + + let connection = self.get_connection(); + let msg_num = self.new_message_num(); + let mut sub_set = connection + .subscribe(MSG_ID_UPDATE_USER_LIST, msg_num) + .await?; + + let get = Bc { + meta: BcMeta { + msg_id: MSG_ID_UPDATE_USER_LIST, + channel_id: self.channel_id, + msg_num, + response_code: 0, + stream_type: 0, + class: 0x6414, + }, + body: BcBody::ModernMsg(ModernMsg { + extension: None, + payload: Some(BcPayloads::BcXml(bcxml)), + }), + }; + + sub_set.send(get).await?; + if let Ok(reply) = + tokio::time::timeout(tokio::time::Duration::from_millis(500), sub_set.recv()).await + { + let msg = reply?; + if msg.meta.response_code != 200 { + return Err(Error::CameraServiceUnavailable { + id: msg.meta.msg_id, + code: msg.meta.response_code, + }); + } + + if let BcMeta { + response_code: 200, .. + } = msg.meta + { + Ok(()) + } else { + Err(Error::UnintelligibleReply { + reply: std::sync::Arc::new(Box::new(msg)), + why: "The camera did not except the BcXmp with service data", + }) + } + } else { + // Some cameras seem to just not send a reply on success, so after 500ms we return Ok + Ok(()) + } + } +} diff --git a/src/cmdline.rs b/src/cmdline.rs index 8330252c..693c5b4e 100644 --- a/src/cmdline.rs +++ b/src/cmdline.rs @@ -32,4 +32,5 @@ pub enum Command { Image(super::image::Opt), Battery(super::battery::Opt), Services(super::services::Opt), + Users(super::users::Opt), } diff --git a/src/main.rs b/src/main.rs index 755ecc57..e143d05e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,6 +52,7 @@ mod services; mod statusled; #[cfg(feature = "gstreamer")] mod talk; +mod users; mod utils; use cmdline::{Command, Opt}; @@ -143,6 +144,9 @@ async fn main() -> Result<()> { Some(Command::Services(opts)) => { services::main(opts, neo_reactor.clone()).await?; } + Some(Command::Users(opts)) => { + users::main(opts, neo_reactor.clone()).await?; + } } Ok(()) diff --git a/src/users/cmdline.rs b/src/users/cmdline.rs new file mode 100644 index 00000000..f4f0f5e4 --- /dev/null +++ b/src/users/cmdline.rs @@ -0,0 +1,43 @@ +use clap::{Parser, ValueEnum}; + +#[derive(Parser, Debug)] +pub struct Opt { + /// The name of the camera. Must be a name in the config + pub camera: String, + /// The action to perform + #[command(subcommand)] + pub cmd: UserAction, +} + +#[derive(Parser, Debug)] +pub enum UserAction { + /// List users + List, + /// Create a new user + Add { + /// The username of the new user + user_name: String, + /// The password of the new user + password: String, + /// User type + user_type: UserType, + }, + Password { + /// The username of the new user + user_name: String, + /// The password of the new user + password: String, + }, + Delete { + /// The username of the user to delete + user_name: String, + }, +} + +#[derive(Parser, Debug, Clone, ValueEnum)] +pub enum UserType { + /// user_level = 0 + User, + /// user_level = 1 + Administrator, +} diff --git a/src/users/mod.rs b/src/users/mod.rs new file mode 100644 index 00000000..09415e87 --- /dev/null +++ b/src/users/mod.rs @@ -0,0 +1,173 @@ +/// +/// # Neolink Users +/// +/// This modules handles users +/// +/// +/// # Usage +/// +/// ```bash +/// # To list users +/// neolink --config=config.toml users CameraName list +/// # To create a a new administrator +/// neolink --config=config.toml users CameraName add newuser hunter2 administrator +/// # To delete a user +/// neolink --config=config.toml users CameraName delete newuser +/// ``` +/// +/// There are two types of users: +/// - administrator — Can access the device and change device settings +/// - user — Can access the device but not change device settings +/// +use anyhow::{anyhow, Context, Result}; + +mod cmdline; + +use crate::common::NeoReactor; +pub(crate) use cmdline::*; +use neolink_core::bc::xml::UserList; + +/// Entry point for the users subcommand +/// +/// Opt is the command line options +pub(crate) async fn main(opt: Opt, reactor: NeoReactor) -> Result<()> { + let camera = reactor.get(&opt.camera).await?; + + match opt.cmd { + UserAction::List => { + let user_list = camera + .run_task(|cam| { + Box::pin( + async move { cam.get_users().await.context("Unable to list camera users") }, + ) + }) + .await?; + match user_list { + UserList { + user_list: Some(users), + .. + } => { + println!("{:<12} {:>4} {:>16}", "Username", "ID", "User type"); + for user in users { + println!( + "{:<12} {:>4} {:>16}", + user.user_name, + user.user_id + .expect("user_id should exist when reading users"), + match user.user_level { + 0 => "User".to_owned(), + 1 => "Administrator".to_owned(), + n => format!("Unknown ({})", n), + }, + ); + } + } + _ => { + eprintln!("No users were included in response"); + std::process::exit(1); + } + } + } + UserAction::Add { + user_name, + password, + user_type, + } => { + camera + .run_task(|cam| { + Box::pin({ + let user_name = user_name.clone(); + let password = password.clone(); + let user_type = user_type.clone(); + async move { + let user_list = cam + .get_users() + .await + .context("Failed to list current users")?; + let user_exists = match user_list.user_list { + Some(users) => users.iter().any(|user| user.user_name == user_name), + _ => false, + }; + if user_exists { + return Err(anyhow!("The user '{}' already exists.", user_name)); + } + + cam.add_user( + user_name, + password, + match user_type { + UserType::User => 0, + UserType::Administrator => 1, + }, + ) + .await + .context("Unable to create user") + } + }) + }) + .await?; + println!("Successfully created user '{}'", user_name); + } + UserAction::Password { + user_name, + password, + } => { + camera + .run_task(|cam| { + Box::pin({ + let user_name = user_name.clone(); + let password = password.clone(); + async move { + let user_list = cam + .get_users() + .await + .context("Failed to list current users")?; + let user_exists = match user_list.user_list { + Some(users) => users.iter().any(|user| user.user_name == user_name), + _ => false, + }; + if !user_exists { + return Err(anyhow!("The user '{}' does not exist.", user_name)); + } + + cam.modify_user(user_name, password) + .await + .context("Unable to create user") + } + }) + }) + .await?; + println!("Successfully changed the password of '{}'", user_name); + } + UserAction::Delete { user_name } => { + camera + .run_task(|cam| { + Box::pin({ + let user_name = user_name.clone(); + async move { + let user_list = cam + .get_users() + .await + .context("Failed to list current users")?; + let user_exists = match user_list.user_list { + Some(users) => users.iter().any(|user| user.user_name == user_name), + _ => false, + }; + + if !user_exists { + return Err(anyhow!("The user '{}' does not exist.", user_name)); + } + + cam.delete_user(user_name) + .await + .context("Unable to delete user") + } + }) + }) + .await?; + println!("Successfully removed user '{}'", user_name); + } + } + + Ok(()) +}