Skip to content

Commit

Permalink
Add support for managing users
Browse files Browse the repository at this point in the history
Support includes:
- Listing users
- Creating users
- Deleting users
- Changing the password of users
  • Loading branch information
ecksun committed Jan 29, 2025
1 parent 76833e6 commit a30f403
Show file tree
Hide file tree
Showing 8 changed files with 450 additions and 0 deletions.
4 changes: 4 additions & 0 deletions crates/core/src/bc/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions crates/core/src/bc/xml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<EmailTask>,
/// Read and write users
#[serde(rename = "UserList", skip_serializing_if = "Option::is_none")]
pub user_list: Option<UserList>,
}

impl BcXml {
Expand Down Expand Up @@ -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<Vec<User>>,
}

/// 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<String>,
/// 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<u32>,
/// 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<u8>,
/// 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()
Expand Down
1 change: 1 addition & 0 deletions crates/core/src/bc_protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ mod support;
mod talk;
mod time;
mod uid;
mod users;
mod version;

pub(crate) use connection::*;
Expand Down
179 changes: 179 additions & 0 deletions crates/core/src/bc_protocol/users.rs
Original file line number Diff line number Diff line change
@@ -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<UserList> {
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<User>) -> 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(())
}
}
}
1 change: 1 addition & 0 deletions src/cmdline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ pub enum Command {
Image(super::image::Opt),
Battery(super::battery::Opt),
Services(super::services::Opt),
Users(super::users::Opt),
}
4 changes: 4 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ mod services;
mod statusled;
#[cfg(feature = "gstreamer")]
mod talk;
mod users;
mod utils;

use cmdline::{Command, Opt};
Expand Down Expand Up @@ -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(())
Expand Down
43 changes: 43 additions & 0 deletions src/users/cmdline.rs
Original file line number Diff line number Diff line change
@@ -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,
}
Loading

0 comments on commit a30f403

Please sign in to comment.