Skip to content

Commit

Permalink
qnap: Add update config feature
Browse files Browse the repository at this point in the history
  • Loading branch information
lcruz99 committed Dec 10, 2024
1 parent 133379c commit 0abc522
Show file tree
Hide file tree
Showing 3 changed files with 286 additions and 14 deletions.
52 changes: 44 additions & 8 deletions clis/teliod/src/config.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -22,7 +22,7 @@ impl std::ops::Mul<std::time::Duration> 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)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -115,6 +124,13 @@ where
Ok(Percentage(value))
}

fn serialize_percent<S>(percentage: &Percentage, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u8(percentage.0)
}

fn deserialize_log_level<'de, D>(deserializer: D) -> Result<LevelFilter, D::Error>
where
D: Deserializer<'de>,
Expand All @@ -126,6 +142,13 @@ where
})
}

fn serialize_log_level<S>(log_level: &LevelFilter, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&log_level.to_string())
}

fn deserialize_authentication_token<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<String, D::Error> {
Expand All @@ -138,7 +161,20 @@ fn deserialize_authentication_token<'de, D: Deserializer<'de>>(
}
}

#[derive(PartialEq, Eq, Deserialize, Debug)]
fn serialize_authentication_token<S>(auth_token: &str, serializer: S) -> Result<S::Ok, S::Error>
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,
Expand Down
5 changes: 3 additions & 2 deletions clis/teliod/src/configure_interface.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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,
Expand Down
243 changes: 239 additions & 4 deletions clis/teliod/src/qnap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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(),
Expand Down Expand Up @@ -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::<LevelFilter>() {
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::<InterfaceConfig>(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::<MqttConfig>(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() {
Expand Down Expand Up @@ -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
);
}
}

0 comments on commit 0abc522

Please sign in to comment.