From 1518ab24bb4138c92a67e964ff0ce317f907298d Mon Sep 17 00:00:00 2001 From: Linus Date: Mon, 27 Jan 2025 23:03:52 +0100 Subject: [PATCH 1/5] dissector: Deindent ConfigFileInfo It seemed to be a child of AbilitySupport, which is not the case --- dissector/messages.md | 90 +++++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/dissector/messages.md b/dissector/messages.md index b3a4e484..4c3844ed 100644 --- a/dissector/messages.md +++ b/dissector/messages.md @@ -1273,71 +1273,71 @@ Message have zero to two payloads. cameras only have weak encryption that is easily broken since the decryption key is fixed and well-known. - - 67: ` (FW Upgrade)` +- 67: ` (FW Upgrade)` - - Client + - Client - - Header + - Header - | Magic | Message ID | Message Length | Encryption Offset | Status Code | Message Class | Payload Offset | - | ----------- | ----------- | -------------- | ----------------- | ----------- | ------------- | -------------- | - | 0a bc de f0 | 00 00 00 43 | 00 00 01 00 | 00 00 00 00 | 00 00 | 64 14 | 00 00 00 00 | + | Magic | Message ID | Message Length | Encryption Offset | Status Code | Message Class | Payload Offset | + | ----------- | ----------- | -------------- | ----------------- | ----------- | ------------- | -------------- | + | 0a bc de f0 | 00 00 00 43 | 00 00 01 00 | 00 00 00 00 | 00 00 | 64 14 | 00 00 00 00 | - - Extension + - Extension - ```xml - - - - FIRMWAREFILE.pak - SIZE_IN_BYTES - 0 - - - ``` + ```xml + + + + FIRMWAREFILE.pak + SIZE_IN_BYTES + 0 + + + ``` - - **Notes:** updateParameter refers to updating the settings. If 1 it will restore factory settings. If 0 it will keep current. + - **Notes:** updateParameter refers to updating the settings. If 1 it will restore factory settings. If 0 it will keep current. - - Camera + - Camera - - Header + - Header - | Magic | Message ID | Message Length | Encryption Offset | Status Code | Message Class | Payload Offset | - | ----------- | ----------- | -------------- | ----------------- | ----------- | ------------- | -------------- | - | 0a bc de f0 | 00 00 00 43 | 00 00 00 00 | 00 00 00 00 | c8 00 | 00 00 | 00 00 00 00 | + | Magic | Message ID | Message Length | Encryption Offset | Status Code | Message Class | Payload Offset | + | ----------- | ----------- | -------------- | ----------------- | ----------- | ------------- | -------------- | + | 0a bc de f0 | 00 00 00 43 | 00 00 00 00 | 00 00 00 00 | c8 00 | 00 00 | 00 00 00 00 | - - Client + - Client - - Header + - Header - | Magic | Message ID | Message Length | Encryption Offset | Status Code | Message Class | Payload Offset | - | ----------- | ----------- | -------------- | ----------------- | ----------- | ------------- | -------------- | - | 0a bc de f0 | 00 00 00 43 | 00 00 94 58 | 00 00 00 00 | 00 00 | 64 14 | 00 00 00 6a | + | Magic | Message ID | Message Length | Encryption Offset | Status Code | Message Class | Payload Offset | + | ----------- | ----------- | -------------- | ----------------- | ----------- | ------------- | -------------- | + | 0a bc de f0 | 00 00 00 43 | 00 00 94 58 | 00 00 00 00 | 00 00 | 64 14 | 00 00 00 6a | - - Extension + - Extension - ```xml - - - 1 - - ``` + ```xml + + + 1 + + ``` - - Payload + - Payload - This contains binary data of the file but stops once the message size reaches - 38000 bytes and continues in another packet. There does not appear to - be a checksum or hash and this part contains only the raw bytes of the file. + This contains binary data of the file but stops once the message size reaches + 38000 bytes and continues in another packet. There does not appear to + be a checksum or hash and this part contains only the raw bytes of the file. - - Camera + - Camera - - Header + - Header - | Magic | Message ID | Message Length | Encryption Offset | Status Code | Message Class | Payload Offset | - | ----------- | ----------- | -------------- | ----------------- | ----------- | ------------- | -------------- | - | 0a bc de f0 | 00 00 00 43 | 00 00 00 00 | 00 00 00 00 | c8 00 | 00 00 | 00 00 00 00 | + | Magic | Message ID | Message Length | Encryption Offset | Status Code | Message Class | Payload Offset | + | ----------- | ----------- | -------------- | ----------------- | ----------- | ------------- | -------------- | + | 0a bc de f0 | 00 00 00 43 | 00 00 00 00 | 00 00 00 00 | c8 00 | 00 00 | 00 00 00 00 | - - **Notes:** Last two messages repeat until all data is sent + - **Notes:** Last two messages repeat until all data is sent - 76: `` From 76833e6242fa0eda12e0d2e35d376e4ba76b7b63 Mon Sep 17 00:00:00 2001 From: Linus Date: Mon, 27 Jan 2025 23:11:37 +0100 Subject: [PATCH 2/5] dissector: Document Message ID 59 It is used to manage users --- dissector/messages.md | 106 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 101 insertions(+), 5 deletions(-) diff --git a/dissector/messages.md b/dissector/messages.md index 4c3844ed..27b82f8c 100644 --- a/dissector/messages.md +++ b/dissector/messages.md @@ -1267,11 +1267,107 @@ Message have zero to two payloads. ``` - **Notes:** The passwords are not sent in some models of cameras namely - RLC-410 4mp, RLC-410 5mp, RLC-520 (fw 200710) in these cases the passwords - are blank. In some older cameras that do not use encryption at all these - passwords are completely visible to any network sniffers. Even the "encrypted" - cameras only have weak encryption that is easily broken since the - decryption key is fixed and well-known. + RLC-410 4mp, RLC-410 5mp, RLC-520 (fw 200710), RLC-811A in these cases the + passwords are blank. In some older cameras that do not use encryption at + all these passwords are completely visible to any network sniffers. Even + the "encrypted" cameras only have weak encryption that is easily broken + since the decryption key is fixed and well-known. + +- 59: `` + + - Client + + - Header: Standard header + + - Extension + + ```xml + + + admin + + ``` + + - Camera + + - Header: Standard header + + - Payload — Changing the password of testUser + + ```xml + + + + + admin + password12 + 1 + none + + + testUser + newPass + 0 + modify + + + + ``` + + - Payload — creating a user + + ```xml + + + + + admin + password12 + 1 + none + + + testUser + testPass + 0 + add + + + + ``` + + - Payload — deleting a user + + ```xml + + + + + admin + password12 + 1 + none + + + testUser + newPass + 0 + delete + + + + ``` + + - **Notes:** The passwords are not sent in some models of cameras namely + RLC-410 4mp, RLC-410 5mp, RLC-520 (fw 200710), RLC-811A in these cases the + passwords are blank. In some older cameras that do not use encryption at + all these passwords are completely visible to any network sniffers. Even + the "encrypted" cameras only have weak encryption that is easily broken + since the decryption key is fixed and well-known. + - **Notes:** The password field seems to only be needed when creating a user + or changing a users password. + - **Notes:** It does not appear like userLevel is modifiable (at least on + RLC-811A) - 67: ` (FW Upgrade)` From a30f403dc452224b1348bd41a1cea516864ac7c8 Mon Sep 17 00:00:00 2001 From: Linus Date: Mon, 27 Jan 2025 22:59:53 +0100 Subject: [PATCH 3/5] 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(()) +} From 7a66473ab9b5a5d468d1e72ff33e3fdbc3a2d84b Mon Sep 17 00:00:00 2001 From: Andrew King Date: Thu, 30 Jan 2025 11:21:08 +0700 Subject: [PATCH 4/5] Skip seser on the pass field Skip deser the pass field for camera's that try to sent it in the user list --- crates/core/src/bc/xml.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/core/src/bc/xml.rs b/crates/core/src/bc/xml.rs index b7cb8aee..87d5beeb 100644 --- a/crates/core/src/bc/xml.rs +++ b/crates/core/src/bc/xml.rs @@ -1587,7 +1587,7 @@ pub struct User { #[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")] + #[serde(rename = "password", skip_serializing_if = "Option::is_none", skip_deserializing)] 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")] From 43b62090c8bf1e34cd09b0f8accaab20fc602dfa Mon Sep 17 00:00:00 2001 From: Andrew King Date: Thu, 30 Jan 2025 11:24:17 +0700 Subject: [PATCH 5/5] Rust fmt --- crates/core/src/bc/xml.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/core/src/bc/xml.rs b/crates/core/src/bc/xml.rs index 87d5beeb..16048f58 100644 --- a/crates/core/src/bc/xml.rs +++ b/crates/core/src/bc/xml.rs @@ -1587,7 +1587,11 @@ pub struct User { #[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", skip_deserializing)] + #[serde( + rename = "password", + skip_serializing_if = "Option::is_none", + skip_deserializing + )] 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")]