diff --git a/.gitignore b/.gitignore index af14613e6..04ad9d8ec 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ nat-lab/**/report.html events-moose.log qnap/**/teliod report.json +/contrib/http_root/cgi-bin/builder diff --git a/Cargo.lock b/Cargo.lock index e6c47e48f..0b2f746c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2481,6 +2481,28 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" +[[package]] +name = "maud" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df518b75016b4289cdddffa1b01f2122f4a49802c93191f3133f6dc2472ebcaa" +dependencies = [ + "itoa", + "maud_macros", +] + +[[package]] +name = "maud_macros" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa453238ec218da0af6b11fc5978d3b5c3a45ed97b722391a2a11f3306274e18" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.95", +] + [[package]] name = "md5" version = "0.7.0" @@ -5189,8 +5211,11 @@ dependencies = [ "clap 3.2.25", "const_format", "dirs", + "form_urlencoded", "futures", "interprocess 2.2.2", + "lazy_static", + "maud", "nix 0.28.0", "rand", "regex", diff --git a/Justfile b/Justfile index 37529d1d7..3f8493587 100644 --- a/Justfile +++ b/Justfile @@ -69,8 +69,15 @@ pylint: # Start a dev web cgi server, for local teliod cgi development web: - @echo "Go to http://127.0.0.1:8080/cgi-bin/teliod.cgi" - python3 -m http.server --cgi -d $(pwd)/contrib/http_root/ -b 127.0.0.1 8080 + if ! [ -e 'busybox' ]; then \ + echo "install mini_httpd:\n$ apt install busybox"; \ + fi + + @echo "Go to http://127.0.0.1:8080/cgi-bin/teliod.cgi/" + + cd $(pwd)/contrib/http_root; \ + echo -n $(whoami) > cgi-bin/builder; \ + sudo busybox httpd -f -p 127.0.0.1:8080 -vv -u root -h $(pwd) _udeps-install: _nightly-install cargo +{{ nightly }} install cargo-udeps@0.1.47 --locked diff --git a/clis/teliod/Cargo.toml b/clis/teliod/Cargo.toml index b793ab955..f7e3a9a03 100644 --- a/clis/teliod/Cargo.toml +++ b/clis/teliod/Cargo.toml @@ -34,6 +34,10 @@ anyhow.workspace = true smart-default = "0.7.1" base64 = "0.22.1" dirs = "4.0.0" + +form_urlencoded = { version = "1.2.1", optional = true } +maud = { version = "0.26.0", optional = true } +lazy_static = { workspace = true, optional = true} const_format = { version = "0.2.33", optional = true } rust-cgi = { version = "0.7.1", optional = true } @@ -42,6 +46,9 @@ rand = "0.8.5" serial_test = "3.2.0" [features] -cgi = ["const_format", "rust-cgi"] +# TODO: remove +default = ["cgi"] + +cgi = ["const_format", "rust-cgi", "lazy_static", "maud", "form_urlencoded"] qnap = ["cgi"] diff --git a/clis/teliod/assets/index.html b/clis/teliod/assets/index.html new file mode 100644 index 000000000..c9c25f68d --- /dev/null +++ b/clis/teliod/assets/index.html @@ -0,0 +1,64 @@ + + + + + + + + Nord Security Meshnet + + + + + + + + + + + +
+
+
+
+ + + +
+

Nord Security Meshnet

+
+ Docs > +
+ +
+ + diff --git a/qnap/shared/web/script.js b/clis/teliod/assets/script.js similarity index 100% rename from qnap/shared/web/script.js rename to clis/teliod/assets/script.js diff --git a/clis/teliod/assets/spinner.svg b/clis/teliod/assets/spinner.svg new file mode 100644 index 000000000..9d839f348 --- /dev/null +++ b/clis/teliod/assets/spinner.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/clis/teliod/assets/style.css b/clis/teliod/assets/style.css new file mode 100644 index 000000000..992641579 --- /dev/null +++ b/clis/teliod/assets/style.css @@ -0,0 +1,111 @@ +/* General Styles */ +body { + margin: 0; + padding: 0; + font-family: 'Arial', sans-serif; + background-color: #f9f9f9; + color: #2a2b32; + display: flex; + flex-direction: column; + justify-content: space-between; + + align-items: center; + +} + +.container { + margin: 0; + padding: 0; + /* display: flex; */ + flex-direction: column; + justify-content: flex-start; + /* align-items: center; */ + min-height: 100vh; + max-width: 600px +} + +/* Container */ +.pannel { + /* width: 90%; */ + /* max-width: 600px; */ + background: #ffffff; + /* height: auto; */ + box-shadow: + 0 0 0 1px rgba(56,60,67,.05), + 0 1px 3px 0 rgba(56,60,67,.15); + padding: 24px; + margin: 40px 24px 0px; + + /* box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); */ + border-radius: 12px; + overflow: hidden; + text-align: left; + + /* displa */ +} + +/* Header */ +/* .container header { */ + /* padding: 20px; */ + /* background: #0078d7; */ + /* color: white; */ +/* } */ + +/* Main Section */ +/* main { */ + /* padding: 20px; */ +/* } */ + +/* .pannel-body { */ + /* font-size: 20px; */ + /* margin-bottom: 10px; */ +/* } */ + +/* form { */ + /* display: flex; */ + /* flex-direction: column; */ + /* align-items: center; */ +/* } */ + +/* form label { */ + /* margin-bottom: 5px; */ + /* font-weight: bold; */ +/* } */ + +/* form input { */ + /* padding: 8px; */ + /* width: 80%; */ + /* margin-bottom: 10px; */ + /* border: 1px solid #ccc; */ + /* border-radius: 4px; */ + /* font-size: 16px; */ +/* } */ + +/* form button { */ + /* background-color: #0078d7; */ + /* color: white; */ + /* padding: 10px 20px; */ + /* border: none; */ + /* border-radius: 4px; */ + /* cursor: pointer; */ + /* font-size: 16px; */ + /* transition: background-color 0.3s ease; */ +/* } */ + +/* form button:hover { */ + /* background-color: #005bb5; */ +/* } */ + +/* Response Message */ +/* #response-message { */ + /* margin-top: 20px; */ + /* font-size: 16px; */ + /* color: #0078d7; */ +/* } */ + +/* Footer */ +/* footer { */ + /* padding: 10px; */ + /* background: #f1f1f1; */ + /* font-size: 14px; */ +/* } */ diff --git a/clis/teliod/assets/telio.js b/clis/teliod/assets/telio.js new file mode 100644 index 000000000..fa063f2f0 --- /dev/null +++ b/clis/teliod/assets/telio.js @@ -0,0 +1,19 @@ +window.telio = { + validateToken: input => { + const valid = /^[0-9a-f]{64}$/.test(input.value); + if (valid) { + input.setCustomValidity(""); + } else { + input.setCustomValidity("Token must be a valid 64char hex number"); + } + }, + + validateTunnel: input => { + const valid = /^[a-zA-Z][a-zA-Z0-9\-\.:]{0,14}$/.test(input.value); + if (valid) { + input.setCustomValidity(""); + } else { + input.setCustomValidity("Must be a valid linux interface name"); + } + }, +}; diff --git a/clis/teliod/src/cgi/api.rs b/clis/teliod/src/cgi/api.rs index 13529df38..e7dca03c9 100644 --- a/clis/teliod/src/cgi/api.rs +++ b/clis/teliod/src/cgi/api.rs @@ -1,8 +1,10 @@ use std::{ fs, - io::Write, + io::{self}, process::{Command, Stdio}, str, + thread::sleep, + time::Duration, }; use rust_cgi::{ @@ -13,7 +15,7 @@ use rust_cgi::{ use crate::{ command_listener::CommandResponse, config::{TeliodDaemonConfig, TeliodDaemonConfigPartial}, - ClientCmd, DaemonSocket, TeliodError, TIMEOUT_SEC, + ClientCmd, DaemonSocket, TelioStatusReport, TeliodError, TIMEOUT_SEC, }; use super::{ @@ -60,7 +62,7 @@ pub(crate) fn handle_api(request: &CgiRequest) -> Option { } } -fn is_teliod_running() -> bool { +pub(crate) fn is_teliod_running() -> bool { matches!(teliod_blocking_query!(ClientCmd::IsAlive), Ok(Ok(_))) } @@ -75,22 +77,23 @@ fn shutdown_teliod() -> Result<(), TeliodError> { Err(TeliodError::ClientTimeoutError) } -fn start_daemon() -> Response { +pub(crate) 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() + let teliod_log_file = match fs::OpenOptions::new() .create(true) .write(true) + .read(true) .truncate(true) .open(TELIOD_LOG) { Ok(file) => file, - Err(_) => { + Err(err) => { return text_response( StatusCode::INTERNAL_SERVER_ERROR, - "Failed to open teliod log file.", + format!("Failed to open teliod log file {TELIOD_LOG}, err: {err}"), ); } }; @@ -120,9 +123,19 @@ fn start_daemon() -> Response { .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.") + Ok(_process) => { + // Wait for teliod to become available + for _ in 0..10 { + if is_teliod_running() { + return text_response(StatusCode::CREATED, "Application started successfully."); + } + sleep(Duration::from_millis(500)); + } + + text_response( + StatusCode::REQUEST_TIMEOUT, + "Failed to start the application, check logs.", + ) } Err(error) => text_response( StatusCode::INTERNAL_SERVER_ERROR, @@ -131,7 +144,7 @@ fn start_daemon() -> Response { } } -fn stop_daemon() -> Response { +pub(crate) fn stop_daemon() -> Response { match shutdown_teliod() { Ok(_) => text_response(StatusCode::OK, "Application stopped successfully."), Err(error) => text_response( @@ -141,10 +154,13 @@ fn stop_daemon() -> Response { } } -fn update_config(body: &str) -> Response { - let mut config: TeliodDaemonConfig = match fs::read_to_string(TELIOD_CFG) +pub(crate) fn get_config() -> io::Result { + fs::read_to_string(TELIOD_CFG) .and_then(|content| serde_json::from_str(&content).map_err(|e| e.into())) - { +} + +pub(crate) fn update_config(body: &str) -> Response { + let mut config: TeliodDaemonConfig = match get_config() { Ok(config) => config, Err(e) => { eprintln!("Error reading config file: {}", e); @@ -166,7 +182,10 @@ fn update_config(body: &str) -> Response { config.update(updated_config); - match fs::write(TELIOD_CFG, serde_json::to_string_pretty(&config).unwrap()) { + match fs::write( + TELIOD_CFG, + serde_json::to_string_pretty(&config).unwrap_or_default(), + ) { Ok(_) => text_response(StatusCode::OK, "Configuration updated successfully"), Err(_) => text_response( StatusCode::INTERNAL_SERVER_ERROR, @@ -175,11 +194,24 @@ fn update_config(body: &str) -> Response { } } +pub(crate) fn get_status_report() -> Result { + let msg = teliod_blocking_query!(ClientCmd::GetStatus) + .map_err(|_| TeliodError::ClientTimeoutError)??; + + match CommandResponse::deserialize(&msg)? { + CommandResponse::Ok => Err(TeliodError::InvalidResponse("Expected status".to_string())), + CommandResponse::StatusReport(status) => Ok(status), + CommandResponse::Err(err) => Err(TeliodError::InvalidResponse(err)), + } +} + fn get_status() -> Response { if !is_teliod_running() { return text_response(StatusCode::GONE, "Application is not running."); } + // TODO(pna): use get_status_report, add logic to convert TeliodError into Response + match teliod_blocking_query!(ClientCmd::GetStatus) { Ok(Ok(daemon_reply)) => match CommandResponse::deserialize(&daemon_reply) { Ok(CommandResponse::StatusReport(status)) => text_response( @@ -266,7 +298,6 @@ mod tests { "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" @@ -306,7 +337,6 @@ mod tests { "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" @@ -347,7 +377,6 @@ mod tests { "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" diff --git a/clis/teliod/src/cgi/app.rs b/clis/teliod/src/cgi/app.rs new file mode 100644 index 000000000..e0b9bb5f4 --- /dev/null +++ b/clis/teliod/src/cgi/app.rs @@ -0,0 +1,40 @@ +//! App logic for web ui + +use std::collections::HashMap; + +use crate::{config::TeliodDaemonConfig, TelioStatusReport}; + +use super::api::{get_config, get_status_report, is_teliod_running}; + +#[derive(Clone)] +pub struct AppState { + pub running: bool, + pub config: TeliodDaemonConfig, + pub status: Option, + + // TODO: if error occur durring upade_config, this should collect it + // and add info to the generated form + pub errors: HashMap, +} + +impl AppState { + /// Collect app state from current enviroment + pub fn collect() -> Self { + Self { + running: is_teliod_running(), + config: get_local_or_default_config(), + status: get_status_report().ok(), + errors: HashMap::new(), + } + } +} + +pub fn get_local_or_default_config() -> TeliodDaemonConfig { + match get_config() { + Ok(config) => config, + Err(err) => { + eprintln!("Failed to get previous config err: {err}"); + Default::default() + } + } +} diff --git a/clis/teliod/src/cgi/constants.rs b/clis/teliod/src/cgi/constants.rs index 81b08caa0..de089ee14 100644 --- a/clis/teliod/src/cgi/constants.rs +++ b/clis/teliod/src/cgi/constants.rs @@ -3,7 +3,7 @@ use const_format::concatcp; #[cfg(feature = "qnap")] pub const APP_DIR: &str = "/share/CACHEDEV1_DATA/.qpkg/NordSecurityMeshnet"; #[cfg(not(feature = "qnap"))] -pub const APP_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/target/debug"); +pub const APP_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/debug"); pub const TELIOD_BIN: &str = concatcp!(APP_DIR, "/teliod"); pub const MESHNET_LOG: &str = concatcp!(APP_DIR, "/meshnet.log"); diff --git a/clis/teliod/src/cgi/mod.rs b/clis/teliod/src/cgi/mod.rs index bd1ee7e0c..aaf77a0d1 100644 --- a/clis/teliod/src/cgi/mod.rs +++ b/clis/teliod/src/cgi/mod.rs @@ -1,16 +1,22 @@ -use std::{env::var, ops::Deref}; +use std::{env::var, fs, ops::Deref}; -use qnap::QnapUserAuthorization; use rust_cgi::{http::StatusCode, text_response, Request, Response}; -use serde::Deserialize; use crate::TIMEOUT_SEC; +use tracing::{info, Level}; + +#[cfg(feature = "qnap")] +use qnap::QnapUserAuthorization; mod api; +mod app; +mod web; + pub(crate) mod constants; #[cfg(feature = "qnap")] mod qnap; +#[allow(dead_code)] #[derive(Debug, thiserror::Error)] pub enum Error { #[error(transparent)] @@ -54,12 +60,14 @@ impl Deref for CgiRequest { } } +#[allow(dead_code)] #[derive(Debug)] enum TokenCheckStatus { Success, Failed, } +#[allow(dead_code)] #[derive(Debug)] enum AdminGroupStatus { Admin, @@ -76,7 +84,30 @@ pub trait AuthorizationValidator { } pub fn handle_request(request: Request) -> Response { + #[cfg(debug_assertions)] + match fs::File::create("./cgi.log") { + Ok(file) => { + let (non_blocking_writer, _tracing_worker_guard) = tracing_appender::non_blocking(file); + tracing_subscriber::fmt() + .with_max_level(Level::TRACE) + .with_writer(non_blocking_writer) + .with_ansi(false) + .with_line_number(true) + .with_level(true) + .init(); + } + Err(error) => eprintln!("Failed to create debug log file: {error}"), + }; + let request = CgiRequest::new(request); + if let Some(response) = web::handle_web_ui(&request) { + info!( + "Returning response..: {:?}", + std::str::from_utf8(response.body()).ok() + ); + + return response; + } #[cfg(feature = "qnap")] if let Err(error) = authorize::(&request) { @@ -95,6 +126,7 @@ pub fn handle_request(request: Request) -> Response { text_response(StatusCode::BAD_REQUEST, "Invalid request.") } +#[allow(dead_code)] pub fn authorize(request: &Request) -> Result<(), Error> { T::retrieve_token(request).and_then(|sid| { let user_authorization = @@ -111,7 +143,7 @@ pub fn authorize(request: &Request) -> Result<(), Err } #[cfg(debug_assertions)] -fn trace_request(request: &CgiRequest, response: &Response) -> Option { +pub fn trace_request(request: &CgiRequest, response: &Response) -> Option { use std::{env::vars, fmt::Write}; let mut msg = String::new(); let _ = writeln!( diff --git a/clis/teliod/src/cgi/web.rs b/clis/teliod/src/cgi/web.rs new file mode 100644 index 000000000..7010ab1c7 --- /dev/null +++ b/clis/teliod/src/cgi/web.rs @@ -0,0 +1,349 @@ +//! Code for serving static web ui + +use std::{collections::HashMap, fs, str::FromStr}; + +use lazy_static::lazy_static; +use maud::{html, Markup, Render}; +use rust_cgi::{ + http::{header::CONTENT_TYPE, Method}, + Response, +}; +use telio::telio_model::mesh::{Node, NodeState}; +use tracing::{level_filters::LevelFilter, warn, Level}; + +use crate::{ + cgi::constants::TELIOD_CFG, + config::{InterfaceConfig, TeliodDaemonConfigPartial}, +}; + +use super::{ + api::{start_daemon, stop_daemon}, + app::AppState, + CgiRequest, +}; + +macro_rules! asset { + ($path:literal) => { + &include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/", $path))[..] + }; +} + +lazy_static! { + static ref ASSETS: HashMap<&'static str, (&'static str, &'static [u8])> = { + [ + // TODO: It would be nice to turn this into some page() function + // it would make loading expieriance smoother, with no need for + // double load + ("/", ("text/html", asset!("index.html"))), + ("/static/telio.js", ("text/javascript", asset!("telio.js"))), + ( + "/static/spinner.svg", + ("image/svg+xml", asset!("spinner.svg")), + ), + ] + .into_iter() + .collect() + }; +} + +pub fn handle_web_ui(request: &CgiRequest) -> Option { + let render = |markup: Markup| { + let mut resp = Response::new(markup.render().into_string().into_bytes()); + resp.headers_mut() + .insert(CONTENT_TYPE, "text/html".parse().ok()?); + Some(resp) + }; + + match (request.method(), request.route()) { + (&Method::GET, "/pannel") => render(pannel(&AppState::collect())), + (&Method::GET, "/pannel/status") => render(status(&AppState::collect())), + (&Method::POST, "/pannel") => { + let mut app = AppState::collect(); + update_config(&mut app, request); + + // TODO: need quite a bit of cleanup, api should probably be mostly removed + if !app.running { + let res = start_daemon(); + warn!( + "start: {}", + std::str::from_utf8(res.body()).unwrap_or_default() + ) + } else { + stop_daemon(); + }; + + render(pannel(&AppState::collect())) + } + (&Method::GET, mut route) => { + if route.is_empty() { + route = "/"; + } + + let (mime, data) = ASSETS.get(route)?; + let mut resp = Response::new(data.to_vec()); + resp.headers_mut().insert(CONTENT_TYPE, mime.parse().ok()?); + Some(resp) + } + _ => None, + } +} + +fn pannel(app: &AppState) -> Markup { + html! { + (config(app)) + @if app.running { + (status(app)) + } + } +} + +const ACCESS_TOKEN: &str = "access-token"; +const TUNNEL_NAME: &str = "interface-name"; +const LOG_LEVEL: &str = "log-level"; + +/// Try to update persisted config +fn update_config(app: &mut AppState, request: &CgiRequest) { + // TODO: should probably be Result, creating error report for a user in web + let Some(ctype) = request + .headers() + .get("x-cgi-content-type") + .and_then(|v| v.to_str().ok()) + else { + warn!("x-cgi-content-type header not found"); + return; + }; + + if ctype != "application/x-www-form-urlencoded" { + warn!("Expected to recieve form data, got: {}", ctype); + return; + } + + let values: HashMap<_, _> = form_urlencoded::parse(request.body()).collect(); + + let partial = TeliodDaemonConfigPartial { + authentication_token: values.get(ACCESS_TOKEN).map(ToString::to_string), + log_level: values + .get(LOG_LEVEL) + .and_then(|v| LevelFilter::from_str(v).ok()), + interface: values.get(TUNNEL_NAME).map(|name| InterfaceConfig { + name: name.to_string(), + ..Default::default() + }), + ..Default::default() + }; + + warn!("Got new values: {partial:#?}"); + + // Build a new temprorary config + let mut new_config = app.config.clone(); + new_config.update(partial); + + let config = match serde_json::to_string_pretty(&new_config) { + Ok(c) => c, + Err(err) => { + warn!("Failed to serialize config, err: {err}"); + return; + } + }; + + match fs::write(TELIOD_CFG, config) { + Ok(_) => { + app.config = new_config; + } + Err(err) => { + warn!("Failed to perssit a new config into {TELIOD_CFG}, err: {err}"); + } + } +} + +fn config(app: &AppState) -> Markup { + html! { + div class="bg-gray-800 rounded-lg p-4" { + div class="flex items-center justify-between mb-4" { + div class="flex items-center space-x-3" { + div class={ + "w-6 h-6 rounded-full " + (if app.running { "bg-nord-green" } else { "bg-nord-orange" }) + } {} + h2 class="text-lg font-medium" { "Configuration" } + } + button class="bg-nord-blue + text-white + px-4 py-2 + rounded-md + hover:bg-blue-600 + transition" + hx-on:click="htmx.trigger('#config', 'submit')" { + span { + @if app.running { + "Stop" + } @else { + "Start" + } + } + img #config-load + class="htmx-indicator h-6 ml-4" + src="static/spinner.svg" + {} + } + } + + div."space-y-4" { + (config_form(app)) + } + } + } +} + +fn config_form(app: &AppState) -> Markup { + let label_style = "block text-sm font-medium mb-1"; + let input_style = "w-full + bg-gray-700 + border border-gray-600 rounded-md + px-3 py-2 + focus:outline-none focus:ring-2 focus:ring-nord-blue + mb-2"; + let log_options = [ + #[cfg(debug_assertions)] + Level::TRACE.as_str(), + #[cfg(debug_assertions)] + Level::DEBUG.as_str(), + Level::INFO.as_str(), + Level::WARN.as_str(), + Level::ERROR.as_str(), + ]; + + let level = app + .config + .log_level + .into_level() + .unwrap_or(if cfg!(debug_assertions) { + Level::TRACE + } else { + Level::INFO + }); + + html! { + form #config hx-post="pannel" hx-target="#pannel" hx-indicator="#config-load" { + label for=(ACCESS_TOKEN) + class=(label_style) { + "Access Token:" + } + input type="password" + class=(input_style) + id=(ACCESS_TOKEN) + name=(ACCESS_TOKEN) + value=(app.config.authentication_token) + "hx-on:htmx:validation:validate"="telio.validateToken(this)" + {} + + label for=(TUNNEL_NAME) + class=(label_style) { + "Tunnel Name:" + } + input type="text" + class=(input_style) + id=(TUNNEL_NAME) + name=(TUNNEL_NAME) + value=(app.config.interface.name) + "hx-on:htmx:validation:validate"="telio.validateTunnel(this)" + {} + + label for=(LOG_LEVEL) + class=(label_style) { + "Log Level" + } + // Validation not need option enshures correctness + select class=(input_style) + name=(LOG_LEVEL) + id=(LOG_LEVEL) { + @for option in log_options { + option value=(option) selected=(option == level.as_str()) { + (option) + } + } + } + } + } +} + +fn status(app: &AppState) -> Markup { + let Some(status) = &app.status else { + return html!(); + }; + + let my_ip = status + .meshnet_ip + .map(|ip| format!("( {ip} )")) + .unwrap_or_default(); + + let node_name = |node: &Node| match (&node.nickname, &node.hostname) { + (None, None) => "unknown".to_string(), + (None, Some(host)) => host.to_string(), + (Some(nick), None) => nick.to_string(), + (Some(nick), Some(host)) => format!("{nick} ({host})"), + }; + + let show_address = |node: &Node| { + node.ip_addresses + .first() + .map(|ip| ip.to_string()) + .unwrap_or_else(|| "unknown".to_string()) + }; + + let status_color = |node: &Node| match node.state { + NodeState::Disconnected => "text-nord-orange", + NodeState::Connecting => "text-nord-orange", + NodeState::Connected => "text-nord-green", + }; + + let status_text = |node: &Node| match node.state { + NodeState::Disconnected => "Disconnected", + NodeState::Connecting => "Connecting", + NodeState::Connected => "Connected", + }; + + html! { + div id="status" class="bg-gray-800 rounded-lg p-4 mt-6" + hx-get="pannel/status" + hx-trigger="every 2s" + hx-swap="outerHTML" { + div class="flex items-center justify-between mb-4" { + h2 class="text-lg font-medium" { + {"Meshnet Status " (my_ip)} + } + a href="get-teliod-logs" class="text-nord-blue hover:underline" { "Logs >" } + } + div class="space-y-3" { + @for node in &status.external_nodes { + div class="bg-gray-700 rounded-lg p-3 flex items-center justify-between" { + div class="flex items-center space-x-3" { + div class="w-8 h-8 bg-gray-600 rounded-lg flex items-center justify-center" { + (device_icon()) + } + div { + div class="font-medium" { ({node_name(node)}) } + div class="text-sm text-gray-400" { ({show_address(node)}) } + } + } + div class={"text-nord-green " ({status_color(node)})} { + "Status: " ({status_text(node)}) + } + } + } + } + } + } +} + +fn device_icon() -> Markup { + html! { + svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" { + path stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2" + d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" + {} + } + } +} diff --git a/clis/teliod/src/config.rs b/clis/teliod/src/config.rs index e2baba393..a8d1ea369 100644 --- a/clis/teliod/src/config.rs +++ b/clis/teliod/src/config.rs @@ -3,7 +3,7 @@ use std::{num::NonZeroU64, path::PathBuf, str::FromStr}; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use smart_default::SmartDefault; use std::fs; -use tracing::{debug, info, level_filters::LevelFilter, warn}; +use tracing::{debug, info, level_filters::LevelFilter, warn, Level}; use uuid::Uuid; use telio::crypto::SecretKey; @@ -90,7 +90,7 @@ impl DeviceIdentity { } } -#[derive(PartialEq, Eq, Serialize, Deserialize, Debug)] +#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] pub struct TeliodDaemonConfig { #[serde( deserialize_with = "deserialize_log_level", @@ -137,6 +137,35 @@ impl TeliodDaemonConfig { } } +impl Default for TeliodDaemonConfig { + fn default() -> Self { + TeliodDaemonConfig { + log_level: LevelFilter::from_level(if cfg!(debug_assertions) { + Level::TRACE + } else { + Level::INFO + }), + log_file_path: { + #[cfg(feature = "cgi")] + { + crate::cgi::constants::TELIOD_LOG.to_string() + } + #[cfg(not(feature = "cgi"))] + { + "./teliod.log".to_string() + } + }, + interface: InterfaceConfig { + name: "nlx".to_string(), + config_provider: InterfaceConfigurationProvider::Ifconfig, + }, + authentication_token: "".to_string(), + http_certificate_file_path: None, + mqtt: MqttConfig::default(), + } + } +} + fn deserialize_percent<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, @@ -200,24 +229,24 @@ where } } -#[derive(Default, PartialEq, Eq, Deserialize, Serialize, Debug)] +#[derive(Default, PartialEq, Eq, Deserialize, Serialize, Debug, Clone)] pub struct InterfaceConfig { pub name: String, pub config_provider: InterfaceConfigurationProvider, } #[allow(dead_code)] -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Default)] pub struct TeliodDaemonConfigPartial { #[serde(default, deserialize_with = "deserialize_partial_log_level")] - log_level: Option, - log_file_path: Option, - interface: Option, - app_user_uid: Option, + pub log_level: Option, + pub log_file_path: Option, + pub interface: Option, + pub app_user_uid: Option, #[serde(default, deserialize_with = "deserialize_partial_authentication_token")] - authentication_token: Option, - http_certificate_file_path: Option>, - mqtt: Option, + pub authentication_token: Option, + pub http_certificate_file_path: Option>, + pub mqtt: Option, } fn deserialize_partial_log_level<'de, D: Deserializer<'de>>( diff --git a/clis/teliod/src/configure_interface.rs b/clis/teliod/src/configure_interface.rs index c15b974c9..a71865219 100644 --- a/clis/teliod/src/configure_interface.rs +++ b/clis/teliod/src/configure_interface.rs @@ -24,7 +24,7 @@ fn execute(command: &mut Command) -> Result<(), TeliodError> { } } -#[derive(Default, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Default, Debug, Deserialize, Serialize, PartialEq, Eq, Clone)] #[serde(rename_all = "lowercase")] pub enum InterfaceConfigurationProvider { #[default] diff --git a/clis/teliod/src/main.rs b/clis/teliod/src/main.rs index 94054dcf9..e9633bf5e 100644 --- a/clis/teliod/src/main.rs +++ b/clis/teliod/src/main.rs @@ -88,7 +88,7 @@ enum TeliodError { } /// Libtelio and meshnet status report -#[derive(Debug, Serialize, Deserialize, PartialEq, Default)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Default, Clone)] pub struct TelioStatusReport { /// State of telio runner pub telio_is_running: bool, diff --git a/contrib/http_root/cgi-bin/teliod.cgi b/contrib/http_root/cgi-bin/teliod.cgi old mode 100644 new mode 100755 index ab7f09ad4..3858b1f2e --- a/contrib/http_root/cgi-bin/teliod.cgi +++ b/contrib/http_root/cgi-bin/teliod.cgi @@ -10,7 +10,10 @@ fail() { DEV_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" BIN_DIR="$(realpath $DEV_DIR/../../../target/debug/)" -cargo build -F cgi -p teliod >"$DEV_DIR/cargo.log" 2>&1 || fail +export BYPASS_LLT_SECRETS=1 +BUILD_USER=${SUDO_USER:-$(cat $DEV_DIR/builder)} + +sudo --preserve-env=BYPASS_LLT_SECRETS -u $BUILD_USER -- cargo build -F cgi -p teliod >"$DEV_DIR/cargo.log" 2>&1 || fail export PATH=$BIN_DIR:$PATH diff --git a/contrib/http_root/index.html b/contrib/http_root/index.html new file mode 100644 index 000000000..4dc3b9aa0 --- /dev/null +++ b/contrib/http_root/index.html @@ -0,0 +1 @@ +

OK

diff --git a/qnap/shared/teliod.cfg b/qnap/shared/teliod.cfg deleted file mode 100644 index 15c3ffe42..000000000 --- a/qnap/shared/teliod.cfg +++ /dev/null @@ -1,10 +0,0 @@ -{ - "log_level": "debug", - "log_file_path": "/share/CACHEDEV1_DATA/.qpkg/NordSecurityMeshnet/meshnet.log", - "authentication_token": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "app_user_uid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", - "interface": { - "name": "nlx", - "config_provider": "manual" - } -} diff --git a/qnap/shared/web/index.html b/qnap/shared/web/index.html deleted file mode 100644 index 06324b60b..000000000 --- a/qnap/shared/web/index.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - Hello, hello! - - - -
-
-

Welcome to NordVPN on QNAP

-

A simple Rust application.

-
-
-
-

Greet Yourself

-
- - - -
-

-
-
-
-

© 2024 NordSecurity.

-
-
- - - \ No newline at end of file diff --git a/qnap/shared/web/styles.css b/qnap/shared/web/styles.css deleted file mode 100644 index ea4cd3c88..000000000 --- a/qnap/shared/web/styles.css +++ /dev/null @@ -1,99 +0,0 @@ -/* General Styles */ -body { - margin: 0; - padding: 0; - font-family: 'Arial', sans-serif; - background-color: #f9f9f9; - color: #333; - display: flex; - justify-content: center; - align-items: center; - min-height: 100vh; -} - -/* Container */ -.container { - width: 90%; - max-width: 600px; - background: #ffffff; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); - border-radius: 8px; - overflow: hidden; - text-align: center; -} - -/* Header */ -header { - padding: 20px; - background: #0078d7; - color: white; -} - -header h1 { - margin: 0; - font-size: 24px; -} - -header p { - margin: 5px 0 0; - font-size: 16px; -} - -/* Main Section */ -main { - padding: 20px; -} - -main h2 { - font-size: 20px; - margin-bottom: 10px; -} - -form { - display: flex; - flex-direction: column; - align-items: center; -} - -form label { - margin-bottom: 5px; - font-weight: bold; -} - -form input { - padding: 8px; - width: 80%; - margin-bottom: 10px; - border: 1px solid #ccc; - border-radius: 4px; - font-size: 16px; -} - -form button { - background-color: #0078d7; - color: white; - padding: 10px 20px; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 16px; - transition: background-color 0.3s ease; -} - -form button:hover { - background-color: #005bb5; -} - -/* Response Message */ -#response-message { - margin-top: 20px; - font-size: 16px; - color: #0078d7; -} - -/* Footer */ -footer { - padding: 10px; - background: #f1f1f1; - font-size: 14px; -} \ No newline at end of file