From 0abc522856c0cb443242ecf0c2ee9d38c3e61415 Mon Sep 17 00:00:00 2001 From: Luis Cruz Date: Tue, 10 Dec 2024 18:34:01 +0000 Subject: [PATCH] qnap: Add update config feature --- clis/teliod/src/config.rs | 52 +++++- clis/teliod/src/configure_interface.rs | 5 +- clis/teliod/src/qnap.rs | 243 ++++++++++++++++++++++++- 3 files changed, 286 insertions(+), 14 deletions(-) diff --git a/clis/teliod/src/config.rs b/clis/teliod/src/config.rs index 315a1c4e0..6793111cd 100644 --- a/clis/teliod/src/config.rs +++ b/clis/teliod/src/config.rs @@ -1,9 +1,9 @@ use std::{num::NonZeroU64, path::PathBuf, str::FromStr}; -use serde::{de, Deserialize, Deserializer, Serialize}; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use smart_default::SmartDefault; use std::fs; -use tracing::{debug, info, level_filters::LevelFilter}; +use tracing::{debug, info, level_filters::LevelFilter, Level}; use uuid::Uuid; use telio::crypto::SecretKey; @@ -22,7 +22,7 @@ impl std::ops::Mul for Percentage { } } -#[derive(PartialEq, Eq, Clone, Debug, Deserialize, SmartDefault)] +#[derive(PartialEq, Eq, Clone, Debug, Deserialize, Serialize, SmartDefault)] #[serde(default)] pub struct MqttConfig { /// Starting backoff time for mqtt retry, has to be at least one. (in seconds) @@ -34,7 +34,10 @@ pub struct MqttConfig { /// Percentage of the expiry period after which new mqtt token will be requested #[default(reconnect_after_expiry_default())] - #[serde(deserialize_with = "deserialize_percent")] + #[serde( + deserialize_with = "deserialize_percent", + serialize_with = "serialize_percent" + )] pub reconnect_after_expiry: Percentage, /// Path to a mqtt pem certificate to be used when connecting to Notification Center @@ -83,16 +86,22 @@ impl DeviceIdentity { } } -#[derive(PartialEq, Eq, Deserialize, Debug)] +#[derive(PartialEq, Eq, Serialize, Deserialize, Debug)] pub struct TeliodDaemonConfig { - #[serde(deserialize_with = "deserialize_log_level")] + #[serde( + deserialize_with = "deserialize_log_level", + serialize_with = "serialize_log_level" + )] pub log_level: LevelFilter, pub log_file_path: String, pub interface: InterfaceConfig, pub app_user_uid: Uuid, - #[serde(deserialize_with = "deserialize_authentication_token")] + #[serde( + deserialize_with = "deserialize_authentication_token", + serialize_with = "serialize_authentication_token" + )] pub authentication_token: String, /// Path to a http pem certificate to be used when connecting to CoreApi @@ -115,6 +124,13 @@ where Ok(Percentage(value)) } +fn serialize_percent(percentage: &Percentage, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_u8(percentage.0) +} + fn deserialize_log_level<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, @@ -126,6 +142,13 @@ where }) } +fn serialize_log_level(log_level: &LevelFilter, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str(&log_level.to_string()) +} + fn deserialize_authentication_token<'de, D: Deserializer<'de>>( deserializer: D, ) -> Result { @@ -138,7 +161,20 @@ fn deserialize_authentication_token<'de, D: Deserializer<'de>>( } } -#[derive(PartialEq, Eq, Deserialize, Debug)] +fn serialize_authentication_token(auth_token: &str, serializer: S) -> Result +where + S: Serializer, +{ + if auth_token.len() == 64 && auth_token.chars().all(|c| c.is_ascii_hexdigit()) { + serializer.serialize_str(auth_token) + } else { + Err(serde::ser::Error::custom( + "Invalid authentication token format", + )) + } +} + +#[derive(Default, PartialEq, Eq, Deserialize, Serialize, Debug)] pub struct InterfaceConfig { pub name: String, pub config_provider: InterfaceConfigurationProvider, diff --git a/clis/teliod/src/configure_interface.rs b/clis/teliod/src/configure_interface.rs index ed4181adc..c15b974c9 100644 --- a/clis/teliod/src/configure_interface.rs +++ b/clis/teliod/src/configure_interface.rs @@ -1,5 +1,5 @@ use crate::TeliodError; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use std::net::IpAddr; use std::process::Command; use tracing::{error, info}; @@ -24,9 +24,10 @@ fn execute(command: &mut Command) -> Result<(), TeliodError> { } } -#[derive(Debug, Deserialize, PartialEq, Eq)] +#[derive(Default, Debug, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum InterfaceConfigurationProvider { + #[default] Manual, Ifconfig, Iproute, diff --git a/clis/teliod/src/qnap.rs b/clis/teliod/src/qnap.rs index 498a06fd1..5cedbe715 100644 --- a/clis/teliod/src/qnap.rs +++ b/clis/teliod/src/qnap.rs @@ -20,7 +20,10 @@ const TELIOD_TMP_DIR: &str = "/tmp/nordsecuritymeshnet"; const PID_FILE: &str = concatcp!(TELIOD_TMP_DIR, "/teliod.pid"); const QPKG_DIR: &str = "/share/CACHEDEV1_DATA/.qpkg/NordSecurityMeshnet"; const TELIOD_BIN: &str = concatcp!(QPKG_DIR, "/teliod"); +#[cfg(not(test))] const TELIOD_CFG: &str = concatcp!(QPKG_DIR, "/teliod.cfg"); +#[cfg(test)] +use tests::TELIOD_CFG; const MESHNET_LOG: &str = concatcp!(QPKG_DIR, "/meshnet.log"); const TELIOD_LOG: &str = "/var/log/teliod.log"; @@ -29,7 +32,15 @@ pub(crate) fn handle_request(request: Request) -> Response { (&Method::POST, Some("action=start")) => start_daemon(), (&Method::POST, Some("action=stop")) => stop_daemon(), (&Method::PATCH, Some(action)) => { - text_response(400, "Invalid request.") + if !action.starts_with("action=update-config") { + text_response(400, "Invalid request.") + } else { + let body = match String::from_utf8(request.into_body()) { + Ok(body) => body, + Err(_) => return text_response(400, "Invalid UTF-8 in request body."), + }; + update_config(&body) + } } (&Method::GET, Some("info=get-status")) => get_status(), (&Method::GET, Some("info=get-teliod-logs")) => get_teliod_logs(), @@ -121,9 +132,81 @@ fn stop_daemon() -> Response { } } -// TODO: LLT-5712 -// fn update_config(_post_data: &str) { -// } +fn update_config(body: &str) -> Response { + let mut config: TeliodDaemonConfig = match fs::read_to_string(TELIOD_CFG) + .and_then(|content| serde_json::from_str(&content).map_err(|e| e.into())) + { + Ok(config) => config, + Err(e) => { + eprintln!("Error reading config file: {}", e); + return text_response(500, "Failed to read existing config"); + } + }; + + let update_values: serde_json::Value = match serde_json::from_str(body) { + Ok(updates) => updates, + Err(e) => { + eprintln!("Error parsing config json: {}", e); + return text_response(400, "Invalid JSON payload"); + } + }; + + if let serde_json::Value::Object(update_map) = update_values { + for (key, value) in update_map { + match key.as_str() { + "log_level" => { + if let Some(log_level_str) = value.as_str() { + if let Ok(log_level) = log_level_str.parse::() { + config.log_level = log_level; + } + } + } + "log_file_path" => { + if let Some(log_file_path) = value.as_str() { + config.log_file_path = log_file_path.to_string(); + } + } + "authentication_token" => { + if let Some(auth_token) = value.as_str() { + config.authentication_token = auth_token.to_string(); + } + } + "app_user_uid" => { + if let Some(uid_str) = value.as_str() { + if let Ok(uid) = Uuid::parse_str(uid_str) { + config.app_user_uid = uid; + } + } + } + "interface" => { + if let Ok(interface) = serde_json::from_value::(value) { + config.interface = interface; + } + } + "http_certificate_file_path" => { + if value.is_null() { + config.http_certificate_file_path = None; + } else if let Some(path_str) = value.as_str() { + config.http_certificate_file_path = Some(PathBuf::from(path_str)); + } + } + "mqtt" => { + if let Ok(mqtt_config) = serde_json::from_value::(value) { + config.mqtt = mqtt_config; + } + } + _ => {} + } + } + } else { + return text_response(400, "Invalid JSON format"); + } + + match fs::write(TELIOD_CFG, serde_json::to_string_pretty(&config).unwrap()) { + Ok(_) => text_response(200, "Configuration updated successfully"), + Err(_) => text_response(500, "Failed to write updated config"), + } +} fn get_status() -> Response { if is_teliod_running() && teliod_socket_exists() { @@ -172,3 +255,155 @@ fn get_meshnet_logs() -> Response { Err(_) => text_response(404, "Log file not found."), } } + +#[cfg(test)] +mod tests { + use std::{fs, path::PathBuf}; + use tracing::level_filters::LevelFilter; + use uuid::Uuid; + + use super::{update_config, MqttConfig, TeliodDaemonConfig}; + use crate::configure_interface::InterfaceConfigurationProvider; + + pub const TELIOD_CFG: &str = "/tmp/teliod_config.json"; + + #[test] + fn test_update_config() { + let initial_config = r#" + { + "log_level": "debug", + "log_file_path": "/path/to/log", + "authentication_token": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "app_user_uid": "00000000-0000-0000-0000-000000000000", + "interface": { + "name": "eth0", + "config_provider": "manual" + } + } + "#; + fs::write(TELIOD_CFG, initial_config).unwrap(); + + let update_body = r#" + { + "log_level": "info", + "log_file_path": "/new/path/to/log", + "authentication_token": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "app_user_uid": "11111111-1111-1111-1111-111111111111", + "interface": { + "name": "eth1", + "config_provider": "ifconfig" + } + } + "#; + + let response = update_config(update_body); + assert_eq!(response.status(), 200); + + let updated_config: TeliodDaemonConfig = + serde_json::from_str(&fs::read_to_string(TELIOD_CFG).unwrap()).unwrap(); + + assert_eq!( + updated_config.log_level, + tracing::level_filters::LevelFilter::INFO + ); + assert_eq!(updated_config.log_file_path, "/new/path/to/log"); + assert_eq!( + updated_config.authentication_token, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ); + assert_eq!( + updated_config.app_user_uid, + Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap() + ); + assert_eq!(updated_config.interface.name, "eth1"); + assert_eq!( + updated_config.interface.config_provider, + InterfaceConfigurationProvider::Ifconfig + ); + assert_eq!(updated_config.http_certificate_file_path, None); + let mqtt_default_cfg = MqttConfig::default(); + assert_eq!( + updated_config.mqtt.backoff_initial, + mqtt_default_cfg.backoff_initial + ); + assert_eq!( + updated_config.mqtt.backoff_maximal, + mqtt_default_cfg.backoff_maximal + ); + assert_eq!( + updated_config.mqtt.reconnect_after_expiry, + mqtt_default_cfg.reconnect_after_expiry + ); + assert_eq!( + updated_config.mqtt.certificate_file_path, + mqtt_default_cfg.certificate_file_path + ); + } + + #[test] + fn test_update_config_auth_token_only() { + let initial_config = r#" + { + "log_level": "debug", + "log_file_path": "/path/to/log", + "authentication_token": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "app_user_uid": "00000000-0000-0000-0000-000000000000", + "interface": { + "name": "eth0", + "config_provider": "manual" + }, + "http_certificate_file_path": "/http/certificate/path/" + } + "#; + fs::write(TELIOD_CFG, initial_config).unwrap(); + + let update_body = r#" + { + "authentication_token": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + } + "#; + + let response = update_config(update_body); + assert_eq!(response.status(), 200); + + let updated_config: TeliodDaemonConfig = + serde_json::from_str(&fs::read_to_string(TELIOD_CFG).unwrap()).unwrap(); + + assert_eq!(updated_config.log_level, LevelFilter::DEBUG); + assert_eq!(updated_config.log_file_path, "/path/to/log"); + assert_eq!( + updated_config.authentication_token, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ); + assert_eq!( + updated_config.app_user_uid, + Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap() + ); + assert_eq!(updated_config.interface.name, "eth0"); + assert_eq!( + updated_config.interface.config_provider, + InterfaceConfigurationProvider::Manual + ); + assert_eq!( + updated_config.http_certificate_file_path, + Some(PathBuf::from("/http/certificate/path/")) + ); + let mqtt_default_cfg = MqttConfig::default(); + assert_eq!( + updated_config.mqtt.backoff_initial, + mqtt_default_cfg.backoff_initial + ); + assert_eq!( + updated_config.mqtt.backoff_maximal, + mqtt_default_cfg.backoff_maximal + ); + assert_eq!( + updated_config.mqtt.reconnect_after_expiry, + mqtt_default_cfg.reconnect_after_expiry + ); + assert_eq!( + updated_config.mqtt.certificate_file_path, + mqtt_default_cfg.certificate_file_path + ); + } +}