diff --git a/.gitignore b/.gitignore
index ae7889a7d..25463705b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,3 +48,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 4e76ac58d..1f9bdcb5f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2459,6 +2459,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.90",
+]
+
[[package]]
name = "md5"
version = "0.7.0"
@@ -5121,8 +5143,11 @@ dependencies = [
"clap 3.2.25",
"const_format",
"dirs",
+ "form_urlencoded",
"futures",
"interprocess 2.2.1",
+ "lazy_static",
+ "maud",
"nix 0.28.0",
"rand",
"regex",
diff --git a/Justfile b/Justfile
index a9bcf69fb..e5f81be50 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 b2769215c..5ad6e78fc 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, Write},
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::{
@@ -57,7 +59,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(_)))
}
@@ -72,7 +74,7 @@ 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.");
}
@@ -84,10 +86,10 @@ fn start_daemon() -> Response {
.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}"),
);
}
};
@@ -118,6 +120,16 @@ fn start_daemon() -> Response {
.spawn()
{
Ok(process) => {
+ // Wait for teliod to become available
+
+ // Wait at max for 5 seconds for teliod to become responsive
+ for _ in 0..10 {
+ if is_teliod_running() {
+ break;
+ }
+ sleep(Duration::from_millis(500));
+ }
+
let _ = teliod_log_file.write_all(format!("Process ID: {}\n", process.id()).as_bytes());
text_response(StatusCode::CREATED, "Application started successfully.")
}
@@ -128,7 +140,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(
@@ -138,10 +150,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);
@@ -172,11 +187,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(
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 afa7f2f6e..e32e869d8 100644
--- a/clis/teliod/src/cgi/mod.rs
+++ b/clis/teliod/src/cgi/mod.rs
@@ -1,8 +1,12 @@
-use std::{env::var, ops::Deref};
+use std::{env::var, fs, ops::Deref};
use rust_cgi::{http::StatusCode, text_response, Request, Response};
+use tracing::{info, Level};
mod api;
+mod app;
+mod web;
+
pub(crate) mod constants;
pub struct CgiRequest {
@@ -31,8 +35,28 @@ impl Deref for CgiRequest {
}
pub fn handle_request(request: Request) -> Response {
+ // TODO: enanle only for debug
+ let (non_blocking_writer, _tracing_worker_guard) =
+ tracing_appender::non_blocking(fs::File::create("./cgi.log").unwrap());
+ tracing_subscriber::fmt()
+ .with_max_level(Level::TRACE)
+ .with_writer(non_blocking_writer)
+ .with_ansi(false)
+ .with_line_number(true)
+ .with_level(true)
+ .init();
+
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;
+ }
+
if let Some(response) = api::handle_api(&request) {
#[cfg(debug_assertions)]
let response = trace_request(&request, &response).unwrap_or(text_response(
@@ -46,7 +70,7 @@ pub fn handle_request(request: Request) -> Response {
}
#[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..8fc186d9d
--- /dev/null
+++ b/clis/teliod/src/cgi/web.rs
@@ -0,0 +1,348 @@
+//! 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::{info, 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())
+ } 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 {
+ info!("render pannel");
+
+ 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_else(|| String::new());
+
+ let node_name = |node: &Node| match (&node.nickname, &node.hostname) {
+ (None, None) => "unknown".to_string(),
+ (None, Some(host)) => format!("{host}"),
+ (Some(nick), None) => format!("{nick}"),
+ (Some(nick), Some(host)) => format!("{nick} ({host})"),
+ };
+
+ let show_address = |node: &Node| {
+ node.ip_addresses
+ .first()
+ .map(|ip| ip.to_string())
+ .unwrap_or_else(|| format!("unknown"))
+ };
+
+ 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