diff --git a/.unreleased/LLT-5831 b/.unreleased/LLT-5831 new file mode 100644 index 000000000..e69de29bb diff --git a/Cargo.lock b/Cargo.lock index 9928e27fb..4e76ac58d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -741,6 +741,26 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const_format" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -3600,6 +3620,15 @@ dependencies = [ "tokio-rustls 0.25.0", ] +[[package]] +name = "rust-cgi" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f429f64b3929e733331d856e47ac00afb9dd8cda39f0a740c5622c886754acfa" +dependencies = [ + "http 1.1.0", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -4018,16 +4047,16 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.1.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d" +checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" dependencies = [ "futures", "log", "once_cell", "parking_lot", "scc", - "serial_test_derive 3.1.1", + "serial_test_derive 3.2.0", ] [[package]] @@ -4045,9 +4074,9 @@ dependencies = [ [[package]] name = "serial_test_derive" -version = "3.1.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" +checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" dependencies = [ "proc-macro2", "quote", @@ -4733,7 +4762,7 @@ dependencies = [ "network-framework-sys", "once_cell", "parking_lot", - "serial_test 3.1.1", + "serial_test 3.2.0", "telio-utils", "tokio", "tracing", @@ -5090,6 +5119,7 @@ dependencies = [ "anyhow", "base64 0.22.1", "clap 3.2.25", + "const_format", "dirs", "futures", "interprocess 2.2.1", @@ -5098,9 +5128,11 @@ dependencies = [ "regex", "reqwest 0.12.9", "rumqttc", + "rust-cgi", "rustls-native-certs 0.8.0", "serde", "serde_json", + "serial_test 3.2.0", "signal-hook", "signal-hook-tokio", "smart-default", @@ -5551,6 +5583,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "uniffi" version = "0.3.1+v0.25.0" @@ -5927,7 +5965,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/ci/build_libtelio.py b/ci/build_libtelio.py index b0feb98b6..38958ad32 100755 --- a/ci/build_libtelio.py +++ b/ci/build_libtelio.py @@ -296,6 +296,7 @@ def post_qnap_build_wrap_binary_on_qpkg(config, args): "packages": { "teliod": {"teliod": "teliod"}, }, + "build_args": ("--features", "qnap"), }, "macos": { "packages": { diff --git a/clis/teliod/Cargo.toml b/clis/teliod/Cargo.toml index 7bf49854f..57e4be793 100644 --- a/clis/teliod/Cargo.toml +++ b/clis/teliod/Cargo.toml @@ -34,6 +34,12 @@ anyhow.workspace = true smart-default = "0.7.1" base64 = "0.22.1" dirs = "4.0.0" +const_format = { version = "0.2.33", optional = true } +rust-cgi = { version = "0.7.1", optional = true } [dev-dependencies] rand = "0.8.5" +serial_test = "3.2.0" + +[features] +qnap = ["const_format", "rust-cgi"] diff --git a/clis/teliod/QNAP.md b/clis/teliod/QNAP.md new file mode 100644 index 000000000..6aed94ce1 --- /dev/null +++ b/clis/teliod/QNAP.md @@ -0,0 +1,128 @@ +# QNAP Build + +To build Teliod package for QNAP devices there are two alternatives: + +- Run cargo build directly: +```cargo build --verbose --target x86_64-unknown-linux-musl --package teliod --features qnap``` + +- Run build script [Recommended]: +```../../ci/build_libtelio.py build qnap x86_64 [--debug]``` + +The build script has an additional stage at the end of the build where it creates the QNAP package. + +## REST API + +This REST API allows interaction with the Teliod daemon. It provides endpoints for managing the daemon, updating its configuration, and retrieving logs and status information. + +### Endpoints + +#### 1. **Start the Daemon** +- **Endpoint**: `/` +- **Method**: `POST` +- **Description**: Starts the Teliod Daemon in the background. +- **Request Body**: None +- **Responses**: + - **201 OK**: Daemon started successfully. + - **400 Bad Request**: Daemon is already running. + - **500 Internal Server Error**: Failed to start the daemon. + +#### 2. **Stop the Daemon** +- **Endpoint**: `/` +- **Method**: `DELETE` +- **Description**: Stops the running Teliod Daemon. +- **Request Body**: None +- **Responses**: + - **200 OK**: Daemon stopped successfully. + - **410 Bad Request**: Daemon is not running. + +#### 3. **Update Configuration** +- **Endpoint**: `/` +- **Method**: `PATCH` +- **Description**: Updates the daemon configuration with provided settings. +- **Request Body**: JSON object containing the configuration updates. Only specified fields will be updated; others remain unchanged. +- **Example Request Body**: + ```json + { + "log_level": "info", + "log_file_path": "/new/path/to/log.log" + } + ``` + - **Responses**: + - **200 OK**: Configuration updated successfully + - **400 Bad Request**: Invalid JSON payload or configuration fields. + - **500 Internal Server Error**: Failed to update configuration. + +#### 4. **Get Meshnet Status** +- **Endpoint**: `/?info=get-status` +- **Method**: `GET` +- **Description**: Retrieves the current status of the Meshnet from Teliod daemon. +- **Request Body**: None +- **Responses**: + - **200 OK**: Status information in JSON format. + ```json + { + "telio_is_running": true, + "meshnet_ip": null, + "external_nodes": [] + ... + } + ``` + - **502 Internal Server Error**: Failed to retrieve status (Bad daemon response). + - **410 Gone**: Failed to communicate with the daemon (Couldn't send command/Daemon not accessible). + - **502 Gateway Timeout**: Failed to communicate with the daemon (Timeout while waiting daemon). + +#### 5. **Get Meshnet Logs** +- **Endpoint**: `/?info=get-meshnet-logs` +- **Method**: `GET` +- **Description**: Retrieves the latest logs of the Meshnet. +- **Request Body**: None +- **Responses**: + - **200 OK**: Log content in text format. + ``` + { + "Log line 1\nLog line 2\nLog line 3\n..." + } + ``` + - **502 Bad Gateway**: Error reading log file. + +#### 6. **Get Teliod Logs** +- **Endpoint**: `/?info=get-teliod-logs` +- **Method**: `GET` +- **Description**: Retrieves the latest logs of the Teliod Daemon. +- **Request Body**: None +- **Responses**: + - **200 OK**: Log content in text format. + ``` + { + "Log line 1\nLog line 2\nLog line 3\n..." + } + ``` + - **502 Bad Gateway**: Error reading log file. + +### Error Handling + +For all endpoints, the following error codes may be returned: +- **400 Bad Request**: The request was malformed or invalid. +- **404 Not Found**: Uri path is invalid. + +### Example usage with curl + +#### Start Teliod daemon: +```bash +curl -X POST http://:8080/ +``` + +#### Stop Teliod daemon: +```bash +curl -X DELETE http://:8080/ +``` + +#### Get Meshnet logs: +```bash +curl -X GET "http://:8080/?info=get-meshnet-logs" +``` + +#### Update Config: +```bash +curl -X PATCH -H "Content-Type: application/json" -d '{"log_level":"info", authentication_token": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}' http://:8080/cgi-bin/qpkg/teliod.cgi +``` diff --git a/clis/teliod/src/command_listener.rs b/clis/teliod/src/command_listener.rs index a27277ec0..fce865bc0 100644 --- a/clis/teliod/src/command_listener.rs +++ b/clis/teliod/src/command_listener.rs @@ -76,6 +76,19 @@ impl CommandListener { }) .await } + ClientCmd::QuitDaemon => + { + #[allow(mpsc_blocking_send)] + self.telio_task_tx + .send(TelioTaskCmd::Quit) + .await + .map(|_| CommandResponse::Ok) + .map_err(|e| { + error!("Error sending command: {}", e); + TeliodError::CommandFailed(ClientCmd::QuitDaemon) + }) + } + ClientCmd::IsAlive => Ok(CommandResponse::Ok), } } @@ -88,7 +101,6 @@ impl CommandListener { connection.respond(response.serialize()).await?; Ok(command) } else { - error!("Received invalid command from client: {}", command_str); connection .respond( CommandResponse::Err(format!("Invalid command: {}", command_str)).serialize(), diff --git a/clis/teliod/src/config.rs b/clis/teliod/src/config.rs index 315a1c4e0..5fc332e5e 100644 --- a/clis/teliod/src/config.rs +++ b/clis/teliod/src/config.rs @@ -1,6 +1,6 @@ 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}; @@ -12,7 +12,7 @@ use crate::configure_interface::InterfaceConfigurationProvider; #[derive(PartialEq, Eq, Clone, Copy, Debug, SmartDefault)] #[repr(transparent)] -pub struct Percentage(u8); +pub struct Percentage(pub u8); impl std::ops::Mul for Percentage { type Output = std::time::Duration; @@ -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 @@ -102,6 +111,33 @@ pub struct TeliodDaemonConfig { pub mqtt: MqttConfig, } +impl TeliodDaemonConfig { + #[allow(dead_code)] + pub fn update(&mut self, update: TeliodDaemonConfigPartial) { + if let Some(log_level) = update.log_level { + self.log_level = log_level; + } + if let Some(log_file_path) = update.log_file_path { + self.log_file_path = log_file_path; + } + if let Some(authentication_token) = update.authentication_token { + self.authentication_token = authentication_token; + } + if let Some(app_user_uid) = update.app_user_uid { + self.app_user_uid = app_user_uid; + } + if let Some(interface) = update.interface { + self.interface = interface; + } + if let Some(http_certificate_file_path) = update.http_certificate_file_path { + self.http_certificate_file_path = http_certificate_file_path; + } + if let Some(mqtt) = update.mqtt { + self.mqtt = mqtt; + } + } +} + fn deserialize_percent<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, @@ -115,6 +151,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 +169,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,12 +188,73 @@ 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, } +#[allow(dead_code)] +#[derive(Deserialize, Debug)] +pub struct TeliodDaemonConfigPartial { + #[serde(default, deserialize_with = "deserialize_partial_log_level")] + log_level: Option, + log_file_path: Option, + interface: Option, + app_user_uid: Option, + #[serde(default, deserialize_with = "deserialize_partial_authentication_token")] + authentication_token: Option, + http_certificate_file_path: Option>, + mqtt: Option, +} + +fn deserialize_partial_log_level<'de, D: Deserializer<'de>>( + deserializer: D, +) -> Result, D::Error> { + let deserialized_level_filter: Option = Option::deserialize(deserializer)?; + + match deserialized_level_filter { + Some(ref level_string) => LevelFilter::from_str(level_string).map(Some).map_err(|_| { + de::Error::unknown_variant( + level_string, + &["error", "warn", "info", "debug", "trace", "off"], + ) + }), + _ => Ok(None), + } +} + +fn deserialize_partial_authentication_token<'de, D: Deserializer<'de>>( + deserializer: D, +) -> Result, D::Error> { + let deserialized_auth_token: Option = Option::deserialize(deserializer)?; + + match deserialized_auth_token { + Some(ref raw_auth_token) => { + let re = regex::Regex::new("[0-9a-f]{64}").map_err(de::Error::custom)?; + if re.is_match(raw_auth_token) { + Ok(Some(raw_auth_token.to_owned())) + } else { + Err(de::Error::custom("Incorrect authentication token")) + } + } + None => Ok(None), + } +} + #[cfg(test)] mod tests { use std::time::Duration; @@ -191,7 +302,7 @@ mod tests { "authentication_token": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }"#; - assert_eq!(expected, serde_json::from_str(&json).unwrap()); + assert_eq!(expected, serde_json::from_str(json).unwrap()); } { @@ -207,7 +318,7 @@ mod tests { "mqtt": {} }"#; - assert_eq!(expected, serde_json::from_str(&json).unwrap()); + assert_eq!(expected, serde_json::from_str(json).unwrap()); } } } 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/daemon.rs b/clis/teliod/src/daemon.rs index 23dbb7200..d3065550d 100644 --- a/clis/teliod/src/daemon.rs +++ b/clis/teliod/src/daemon.rs @@ -15,6 +15,7 @@ use tokio::{sync::mpsc, sync::oneshot, time::Duration}; use tracing::{debug, error, info, trace}; use crate::core_api::{get_meshmap as get_meshmap_from_server, init_with_api}; +use crate::ClientCmd; use crate::{ command_listener::CommandListener, comms::DaemonSocket, @@ -254,9 +255,12 @@ pub async fn daemon_event_loop(config: TeliodDaemonConfig) -> Result<(), TeliodE match result { Ok(command) => { info!("Client command {:?} executed successfully", command); + if command == ClientCmd::QuitDaemon { + break Ok(()) + } } Err(err) => { - break Err(err); + error!("Received invalid command from client: {}", err); } } }, diff --git a/clis/teliod/src/main.rs b/clis/teliod/src/main.rs index ca3827270..e9021c5ba 100644 --- a/clis/teliod/src/main.rs +++ b/clis/teliod/src/main.rs @@ -19,6 +19,8 @@ mod configure_interface; mod core_api; mod daemon; mod nc; +#[cfg(feature = "qnap")] +mod qnap; use crate::{ command_listener::CommandResponse, @@ -35,6 +37,10 @@ const TIMEOUT_SEC: u64 = 1; enum ClientCmd { #[clap(about = "Retrieve the status report")] GetStatus, + #[clap(about = "Query if daemon is running")] + IsAlive, + #[clap(about = "Stop daemon execution")] + QuitDaemon, } #[derive(Parser, Debug)] @@ -44,6 +50,9 @@ enum Cmd { Daemon { config_path: String }, #[clap(flatten)] Client(ClientCmd), + #[cfg(feature = "qnap")] + #[clap(about = "Receive and parse http requests")] + QnapCgi, } #[derive(Debug, ThisError)] @@ -97,12 +106,18 @@ async fn main() -> Result<(), TeliodError> { Err(TeliodError::DaemonIsRunning) } else { let file = File::open(&config_path)?; + let mut config: TeliodDaemonConfig = serde_json::from_reader(file)?; - let token = std::env::var("NORD_TOKEN").ok(); - if let Some(t) = token { + + if let Ok(token) = std::env::var("NORD_TOKEN") { debug!("Overriding token from env"); - config.authentication_token = t; + if token.len() == 64 && token.chars().all(|c| c.is_ascii_hexdigit()) { + config.authentication_token = token; + } else { + error!("Token from env not valid") + } } + Box::pin(daemon::daemon_event_loop(config)).await } } @@ -134,5 +149,10 @@ async fn main() -> Result<(), TeliodError> { Err(TeliodError::DaemonIsNotRunning) } } + #[cfg(feature = "qnap")] + Cmd::QnapCgi => { + rust_cgi::handle(qnap::handle_request); + Ok(()) + } } } diff --git a/clis/teliod/src/qnap.rs b/clis/teliod/src/qnap.rs new file mode 100644 index 000000000..a93dc6810 --- /dev/null +++ b/clis/teliod/src/qnap.rs @@ -0,0 +1,408 @@ +use core::str; +use std::{ + fs, + io::Write, + process::{Command, Stdio}, +}; + +use const_format::concatcp; +use rust_cgi::{http::Method, http::StatusCode, text_response, Request, Response}; + +use crate::{ + command_listener::CommandResponse, + config::{TeliodDaemonConfig, TeliodDaemonConfigPartial}, + ClientCmd, DaemonSocket, TeliodError, TIMEOUT_SEC, +}; + +const QPKG_DIR: &str = "/share/CACHEDEV1_DATA/.qpkg/NordSecurityMeshnet"; +const TELIOD_BIN: &str = concatcp!(QPKG_DIR, "/teliod"); +const MESHNET_LOG: &str = concatcp!(QPKG_DIR, "/meshnet.log"); +const TELIOD_LOG: &str = "/var/log/teliod.log"; + +#[cfg(not(test))] +const TELIOD_CFG: &str = concatcp!(QPKG_DIR, "/teliod.cfg"); +#[cfg(test)] +use tests::TELIOD_CFG; + +macro_rules! teliod_blocking_query { + ($command:expr) => {{ + let socket_path = DaemonSocket::get_ipc_socket_path(); + futures::executor::block_on(tokio::time::timeout( + std::time::Duration::from_secs(TIMEOUT_SEC), + DaemonSocket::send_command( + &socket_path.unwrap_or_default(), + &serde_json::to_string(&$command).unwrap_or_default(), + ), + )) + }}; +} + +pub(crate) fn handle_request(request: Request) -> Response { + match (request.method(), request.uri().query()) { + (&Method::POST, _) => start_daemon(), + (&Method::DELETE, _) => stop_daemon(), + (&Method::PATCH, _) => { + let body = match str::from_utf8(request.body()) { + Ok(body) => body, + Err(error) => { + return text_response( + StatusCode::BAD_REQUEST, + format!("Invalid UTF-8 in request body: {}", error), + ) + } + }; + update_config(body) + } + (&Method::GET, Some("info=get-status")) => get_status(), + (&Method::GET, Some("info=get-teliod-logs")) => get_teliod_logs(), + (&Method::GET, Some("info=get-meshnet-logs")) => get_meshnet_logs(), + (_, _) => text_response(StatusCode::BAD_REQUEST, "Invalid request."), + } +} + +fn is_teliod_running() -> bool { + matches!(teliod_blocking_query!(ClientCmd::IsAlive), Ok(Ok(_))) +} + +fn shutdown_teliod() -> Result<(), TeliodError> { + if let Ok(Ok(daemon_reply)) = teliod_blocking_query!(ClientCmd::QuitDaemon) { + if CommandResponse::deserialize(&daemon_reply) + .is_ok_and(|response| response == CommandResponse::Ok) + { + return Ok(()); + } + } + Err(TeliodError::ClientTimeoutError) +} + +fn start_daemon() -> Response { + if is_teliod_running() { + return text_response(StatusCode::BAD_REQUEST, "Application is already running."); + } + + let mut teliod_log_file = match fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(TELIOD_LOG) + { + Ok(file) => file, + Err(_) => { + return text_response( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to open teliod log file.", + ); + } + }; + let stdout = match teliod_log_file.try_clone() { + Ok(file) => Stdio::from(file), + Err(error) => { + return text_response( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Failed to start the application: {error}"), + ) + } + }; + let stderr = match teliod_log_file.try_clone() { + Ok(file) => Stdio::from(file), + Err(error) => { + return text_response( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Failed to start the application: {error}"), + ) + } + }; + match Command::new("setsid") + .arg(TELIOD_BIN) + .arg("daemon") + .arg(TELIOD_CFG) + .stdout(stdout) + .stderr(stderr) + .spawn() + { + Ok(process) => { + let _ = teliod_log_file.write_all(format!("Process ID: {}\n", process.id()).as_bytes()); + text_response(StatusCode::CREATED, "Application started successfully.") + } + Err(error) => text_response( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Failed to start the application: {error}"), + ), + } +} + +fn stop_daemon() -> Response { + match shutdown_teliod() { + Ok(_) => text_response(StatusCode::OK, "Application stopped successfully."), + Err(error) => text_response( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Unable to stop application: {error}"), + ), + } +} + +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( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to read existing config", + ); + } + }; + let updated_config = match serde_json::from_str::(body) { + Ok(updates) => updates, + Err(e) => { + return text_response( + StatusCode::BAD_REQUEST, + format!("Invalid JSON payload: {e}"), + ); + } + }; + + config.update(updated_config); + + match fs::write(TELIOD_CFG, serde_json::to_string_pretty(&config).unwrap()) { + Ok(_) => text_response(StatusCode::OK, "Configuration updated successfully"), + Err(_) => text_response( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to write updated config", + ), + } +} + +fn get_status() -> Response { + if !is_teliod_running() { + return text_response(StatusCode::GONE, "Application is not running."); + } + + match teliod_blocking_query!(ClientCmd::GetStatus) { + Ok(Ok(daemon_reply)) => match CommandResponse::deserialize(&daemon_reply) { + Ok(CommandResponse::StatusReport(status)) => text_response( + StatusCode::OK, + serde_json::to_string_pretty(&status).unwrap_or_default(), + ), + Ok(cmd_response) => text_response( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Unexpected Teliod response: {}", cmd_response.serialize()), + ), + Err(error) => text_response( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Failed to retrieve status: {}", error), + ), + }, + Ok(Err(error)) => text_response( + StatusCode::GONE, + format!("Failed to communicate with the daemon: {}", error), + ), + Err(error) => text_response( + StatusCode::GATEWAY_TIMEOUT, + format!("Failed to communicate with the daemon: {}", error), + ), + } +} + +fn get_teliod_logs() -> Response { + match fs::read_to_string(TELIOD_LOG) { + Ok(logs) => text_response(StatusCode::OK, logs), + Err(error) => text_response( + StatusCode::BAD_GATEWAY, + format!("Error reading teliod log file: {}", error), + ), + } +} + +fn get_meshnet_logs() -> Response { + match fs::read_to_string(MESHNET_LOG) { + Ok(logs) => text_response(StatusCode::OK, logs), + Err(error) => text_response( + StatusCode::BAD_GATEWAY, + format!("Error reading meshnet log file: {}", error), + ), + } +} + +#[cfg(test)] +mod tests { + use std::{fs, num::NonZeroU64, path::PathBuf}; + + use reqwest::StatusCode; + use serial_test::serial; + use tracing::level_filters::LevelFilter; + use uuid::Uuid; + + use super::{update_config, TeliodDaemonConfig}; + use crate::{ + config::{InterfaceConfig, MqttConfig, Percentage}, + configure_interface::InterfaceConfigurationProvider, + }; + + pub const TELIOD_CFG: &str = "/tmp/teliod_config.json"; + + #[test] + #[serial] + fn test_update_config() { + let mut expected_config = TeliodDaemonConfig { + log_level: LevelFilter::DEBUG, + log_file_path: "/path/to/log".to_owned(), + interface: InterfaceConfig { + name: "eth0".to_owned(), + config_provider: InterfaceConfigurationProvider::Manual, + }, + app_user_uid: Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap(), + authentication_token: + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_owned(), + http_certificate_file_path: Some(PathBuf::from("/http/certificate/path/")), + mqtt: MqttConfig { + backoff_initial: NonZeroU64::new(5).unwrap(), + backoff_maximal: NonZeroU64::new(600).unwrap(), + reconnect_after_expiry: Percentage(100), + certificate_file_path: Some(PathBuf::from("some/certificate/path/")), + }, + }; + 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/", + "mqtt": { + "backoff_initial": 5, + "backoff_maximal": 600, + "reconnect_after_expiry": 100, + "certificate_file_path": "some/certificate/path" + } + } + "#; + fs::write(TELIOD_CFG, initial_config).unwrap(); + + let read_config = + serde_json::from_str::(&fs::read_to_string(TELIOD_CFG).unwrap()) + .unwrap(); + assert_eq!(read_config, expected_config); + + expected_config.log_level = LevelFilter::INFO; + expected_config.log_file_path = "/new/path/to/log".to_owned(); + expected_config.authentication_token = + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_owned(); + expected_config.app_user_uid = + Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap(); + expected_config.interface = InterfaceConfig { + name: "eth1".to_owned(), + config_provider: InterfaceConfigurationProvider::Ifconfig, + }; + expected_config.http_certificate_file_path = + Some(PathBuf::from("new/http/certificate/path/")); + expected_config.mqtt = MqttConfig { + certificate_file_path: Some(PathBuf::from("new/certificate/path/")), + ..Default::default() + }; + 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" + }, + "http_certificate_file_path": "new/http/certificate/path/", + "mqtt": { + "backoff_initial": 1, + "backoff_maximal": 300, + "reconnect_after_expiry": 90, + "certificate_file_path": "new/certificate/path" + } + } + "#; + assert_eq!(update_config(update_body).status(), StatusCode::OK); + + let updated_config: TeliodDaemonConfig = + serde_json::from_str(&fs::read_to_string(TELIOD_CFG).unwrap()).unwrap(); + assert_eq!(updated_config, expected_config); + } + + #[test] + #[serial] + fn test_update_partial_config() { + let mut expected_config = TeliodDaemonConfig { + log_level: LevelFilter::DEBUG, + log_file_path: "/path/to/log".to_owned(), + interface: InterfaceConfig { + name: "eth0".to_owned(), + config_provider: InterfaceConfigurationProvider::Manual, + }, + app_user_uid: Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap(), + authentication_token: + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_owned(), + http_certificate_file_path: Some(PathBuf::from("/http/certificate/path/")), + mqtt: MqttConfig::default(), + }; + 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/", + "mqtt": { + "backoff_initial": 1, + "backoff_maximal": 300, + "reconnect_after_expiry": 90, + "certificate_file_path": null + } + } + "#; + fs::write(TELIOD_CFG, initial_config).unwrap(); + + let read_config = + serde_json::from_str::(&fs::read_to_string(TELIOD_CFG).unwrap()) + .unwrap(); + assert_eq!(read_config, expected_config); + + expected_config.interface.name = "eth1".to_owned(); + expected_config.interface.config_provider = InterfaceConfigurationProvider::Ifconfig; + let update_body = r#" + { + "interface": { + "name": "eth1", + "config_provider": "ifconfig" + } + } + "#; + assert_eq!(update_config(update_body).status(), StatusCode::OK); + + let updated_config = + serde_json::from_str::(&fs::read_to_string(TELIOD_CFG).unwrap()) + .unwrap(); + assert_eq!(updated_config, expected_config); + + expected_config.authentication_token = + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_owned(); + let update_body = r#" + { + "authentication_token": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + } + "#; + assert_eq!(update_config(update_body).status(), StatusCode::OK); + + let updated_config = + serde_json::from_str::(&fs::read_to_string(TELIOD_CFG).unwrap()) + .unwrap(); + assert_eq!(updated_config, expected_config); + } +} diff --git a/qnap/package_routines b/qnap/package_routines index 7ace22297..04dd53e1b 100644 --- a/qnap/package_routines +++ b/qnap/package_routines @@ -110,10 +110,10 @@ #}" # PKG_POST_REMOVE="{ - rm -rf /tmp/nordsecmeshnet - rm -f /home/Qhttpd/Web/NordSecurityMeshnet - rm -f /home/httpd/cgi-bin/qpkg/teliod.cgi - rm -f /var/log/teliod.log + $CMD_RM -rf /tmp/nordsecuritymeshnet + $CMD_RM -f /home/Qhttpd/Web/NordSecurityMeshnet + $CMD_RM -f /home/httpd/cgi-bin/qpkg/teliod.cgi + $CMD_RM -f /var/log/teliod.log }" # ###################################################################### diff --git a/qnap/shared/NordSecMeshnet.sh b/qnap/shared/NordSecMeshnet.sh index 74a6dc704..90377e8ed 100755 --- a/qnap/shared/NordSecMeshnet.sh +++ b/qnap/shared/NordSecMeshnet.sh @@ -1,8 +1,9 @@ #!/bin/sh +set -eu + CONF=/etc/config/qpkg.conf QPKG_NAME="NordSecurityMeshnet" - QPKG_ROOT=`/sbin/getcfg $QPKG_NAME Install_Path -f ${CONF}` APACHE_ROOT=`/sbin/getcfg SHARE_DEF defWeb -d Qweb -f /etc/config/def_share.info` export QNAP_QPKG=$QPKG_NAME diff --git a/qnap/shared/teliod.cfg b/qnap/shared/teliod.cfg index 7ab7db276..15c3ffe42 100644 --- a/qnap/shared/teliod.cfg +++ b/qnap/shared/teliod.cfg @@ -8,4 +8,3 @@ "config_provider": "manual" } } - diff --git a/qnap/shared/teliod.cgi b/qnap/shared/teliod.cgi index f6a9b4d9c..260e2959e 100755 --- a/qnap/shared/teliod.cgi +++ b/qnap/shared/teliod.cgi @@ -1,131 +1,8 @@ -#!/bin/bash +#!/bin/sh +set -eu -echo "Content-Type: application/json" -echo "" +CONF=/etc/config/qpkg.conf +QPKG_NAME="NordSecurityMeshnet" +QPKG_ROOT=`/sbin/getcfg $QPKG_NAME Install_Path -f ${CONF}` -# Varibles set by the HTTP web server -# -# $REQUEST_METHOD -# $QUERY_STRING -# $CONTENT_LENGTH - -POST_DATA="" - -if [[ "$REQUEST_METHOD" == "POST" || "$REQUEST_METHOD" == "PATCH" ]]; then - read -n $CONTENT_LENGTH POST_DATA -fi - -QPKG_DIR="/share/CACHEDEV1_DATA/.qpkg/NordSecurityMeshnet" -APP_TMP_DIR="/tmp/nordsecuritymeshnet" -APP_CMD="$QPKG_DIR/teliod" -CONFIG_FILE="$QPKG_DIR/teliod.cfg" -LOG_FILE="/var/log/teliod.log" -PID_FILE="$APP_TMP_DIR/teliod.pid" - -is_application_running() { - if [ -f ${PID_FILE} ]; then - PID=$(cat ${PID_FILE}) - if [ -d /proc/${PID}/ ]; then - # application is running - return 0 - fi - fi - # application is NOT running - return 1 -} - -send_response() { - local code=$1 - local message=$2 - echo "{\"code\": $code, \"message\": \"$message\"}" - exit $code -} - -start_daemon() { - if is_application_running; then - send_response 400 "Application is already running." - fi - $APP_CMD daemon $CONFIG_FILE > $LOG_FILE 2>&1 & - if [[ $? -eq 0 ]]; then - echo $! > ${PID_FILE} - send_response 200 "Application started successfully." - else - echo $! > ${PID_FILE} - send_response 500 "Failed to start the application." - fi -} - -stop_daemon() { - if [ -f ${PID_FILE} ]; then - PID=$(cat ${PID_FILE}) - kill ${PID} || true - rm -f ${PID_FILE} - send_response 200 "Application stopped successfully." - else - send_response 400 "Application PID not found." - fi -} - -update_config() { - NEW_CONFIG=$(echo "$POST_DATA" | jq -r '.config') - if [[ -z "$NEW_CONFIG" ]]; then - send_response 400 "No configuration provided." - fi - echo "$NEW_CONFIG" > $CONFIG_FILE - if [[ $? -eq 0 ]]; then - send_response 200 "Configuration updated successfully." - else - send_response 500 "Failed to update configuration." - fi -} - -get_status() { - if is_application_running; then - STATUS=$($APP_CMD get-status) - echo "{\"code\": 200, \"status-report\": $STATUS}" - exit 0 - else - send_response 400 "Application is not running." - fi -} - -get_logs() { - if [[ -f $LOG_FILE ]]; then - LOGS=$(tail -n 100 $LOG_FILE | jq -R -s '.') - echo "{\"code\": 200, \"logs\": $LOGS}" - exit 0 - else - send_response 404 "Log file not found." - fi -} - -case "$REQUEST_METHOD" in - POST) - if [[ "$QUERY_STRING" == "action=start" ]]; then - start_daemon - elif [[ "$QUERY_STRING" == "action=stop" ]]; then - stop_daemon - else - send_response 400 "Invalid action for POST." - fi - ;; - PATCH) - if [[ "$QUERY_STRING" == "action=update-config" ]]; then - update_config - else - send_response 400 "Invalid action for PATCH." - fi - ;; - GET) - if [[ "$QUERY_STRING" == "action=get-status" ]]; then - get_status - elif [[ "$QUERY_STRING" == "action=get-logs" ]]; then - get_logs - else - send_response 400 "Invalid action for GET." - fi - ;; - *) - send_response 405 "Method not allowed." - ;; -esac \ No newline at end of file +exec "${QPKG_ROOT}/teliod" qnap-cgi