diff --git a/Cargo.toml b/Cargo.toml index 71b8536..f09f50f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,8 @@ base64 = "0.22.1" sha2 = "0.10.8" rand = "0.8.5" fernet = "0.2.2" +sysinfo = "0.26.7" +reqwest = { version = "0.11", features = ["blocking", "json"] } minijinja = { version = "2.3.1", features = ["loader"] } url = "2.5.2" regex = "1.10.6" diff --git a/src/lib.rs b/src/lib.rs index 37b5836..a3d95c8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,7 @@ mod routes; /// Module to store all the helper functions. mod squire; mod templates; +mod system_info; /// Contains entrypoint and initializer settings to trigger the asynchronous `HTTPServer` /// @@ -65,7 +66,11 @@ pub async fn start() -> io::Result<()> { .wrap(middleware::Logger::default()) // Adds a default logger middleware to the application .service(routes::basics::health) // Registers a service for handling requests .service(routes::basics::root) + .service(routes::basics::health) // Registers a service for handling requests .service(routes::auth::login) + .service(routes::monitor::monitor) + .service(routes::auth::logout) + .service(routes::auth::error) }; let server = HttpServer::new(application) .workers(config.workers) diff --git a/src/routes/mod.rs b/src/routes/mod.rs index fedc6f0..fe0840d 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,4 +1,6 @@ -/// Module for `/` and `/health` entrypoints. +/// Module for `/` and `/health` entrypoint. pub mod basics; -/// Module for `/login`, `/logout` and `/error` entrypoints. +/// Module for `/login`, `/logout` and `/error` entrypoint. pub mod auth; +/// Module for `/monitor` entrypoint. +pub mod monitor; diff --git a/src/routes/monitor.rs b/src/routes/monitor.rs new file mode 100644 index 0000000..eb14331 --- /dev/null +++ b/src/routes/monitor.rs @@ -0,0 +1,59 @@ +use crate::{constant, routes, squire, system_info}; +use actix_web::cookie::{Cookie, SameSite}; +use actix_web::http::StatusCode; +use actix_web::{web, HttpRequest, HttpResponse}; +use fernet::Fernet; +use std::sync::Arc; + +/// Handles the monitor endpoint and rendering the appropriate HTML page. +/// +/// # Arguments +/// +/// * `request` - A reference to the Actix web `HttpRequest` object. +/// * `fernet` - Fernet object to encrypt the auth payload that will be set as `session_token` cookie. +/// * `session` - Session struct that holds the `session_mapping` to handle sessions. +/// * `metadata` - Struct containing metadata of the application. +/// * `config` - Configuration data for the application. +/// * `template` - Configuration container for the loaded templates. +/// +/// # Returns +/// +/// Returns an `HTTPResponse` with the cookie for `session_token` reset if available. +#[get("/monitor")] +pub async fn monitor(request: HttpRequest, + fernet: web::Data>, + session: web::Data>, + metadata: web::Data>, + config: web::Data>, + template: web::Data>>) -> HttpResponse { + let monitor_template = template.get_template("monitor").unwrap(); + let mut response = HttpResponse::build(StatusCode::OK); + response.content_type("text/html; charset=utf-8"); + + let auth_response = squire::authenticator::verify_token(&request, &config, &fernet, &session); + if !auth_response.ok { + return routes::auth::failed_auth(auth_response); + } + log::debug!("Session Validation Response: {}", auth_response.detail); + + let (sys_info_basic_struct, sys_info_mem_storage_struct) = system_info::sys_info::get_sys_info(); + let sys_info_network_struct = system_info::network::get_network_info().await; + + let sys_info_basic = serde_json::to_value(&sys_info_basic_struct).unwrap(); + let sys_info_mem_storage = serde_json::to_value(&sys_info_mem_storage_struct).unwrap(); + let sys_info_network = serde_json::to_value(&sys_info_network_struct).unwrap(); + + let rendered = monitor_template.render(minijinja::context!( + version => metadata.pkg_version, + logout => "/logout", + sys_info_basic => sys_info_basic, + sys_info_mem_storage => sys_info_mem_storage, + sys_info_network => sys_info_network, + )).unwrap(); + + let mut cookie = Cookie::new("session_token", ""); + cookie.set_same_site(SameSite::Strict); + cookie.make_removal(); + response.cookie(cookie); + response.body(rendered) +} diff --git a/src/system_info/mod.rs b/src/system_info/mod.rs new file mode 100644 index 0000000..703f015 --- /dev/null +++ b/src/system_info/mod.rs @@ -0,0 +1,2 @@ +pub mod sys_info; +pub mod network; diff --git a/src/system_info/network.rs b/src/system_info/network.rs new file mode 100644 index 0000000..38a2b4c --- /dev/null +++ b/src/system_info/network.rs @@ -0,0 +1,74 @@ +use regex::Regex; +use reqwest; +use serde::{Deserialize, Serialize}; +use std::net::{SocketAddr, UdpSocket}; + +// Define the IP regex pattern +fn ip_regex() -> Regex { + Regex::new( + r"^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$" + ).unwrap() +} + +// Synchronous function to retrieve the public IP address +async fn public_ip_address() -> Option { + let ip_regex = ip_regex(); + + // Mapping URLs to their expected response types + let mapping: Vec<(&str, bool)> = vec![ + ("https://checkip.amazonaws.com/", true), // expects text + ("https://api.ipify.org/", true), // expects text + ("https://ipinfo.io/ip/", true), // expects text + ("https://v4.ident.me/", true), // expects text + ("https://httpbin.org/ip", false), // expects JSON + ("https://myip.dnsomatic.com/", true), // expects text + ]; + + for (url, expects_text) in mapping { + match reqwest::get(url).await { + Ok(response) => { + let extracted_ip = if expects_text { + response.text().await.unwrap_or_default().trim().to_string() + } else { + response.json::().await.ok() + .and_then(|json| json["origin"].as_str().map(str::to_string)) + .unwrap_or_default() + }; + + if ip_regex.is_match(&extracted_ip) { + return Some(extracted_ip); + } + } + Err(e) => { + eprintln!("Failed to fetch from {}: {}", url, e); + continue; // Move on to the next URL + } + } + } + + None +} + +// Function to retrieve the private IP address +fn private_ip_address() -> Option { + let socket = UdpSocket::bind("0.0.0.0:0").ok()?; + socket.connect("8.8.8.8:80").ok()?; + let local_addr: SocketAddr = socket.local_addr().ok()?; + Some(local_addr.ip().to_string()) +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct SystemInfoNetwork { + private_ip_address: String, + public_ip_address: String, +} + + +pub async fn get_network_info() -> SystemInfoNetwork { + let private_ip = private_ip_address().unwrap_or_default(); + let public_ip = public_ip_address().await.unwrap_or_default(); + SystemInfoNetwork { + private_ip_address: private_ip, + public_ip_address: public_ip, + } +} diff --git a/src/system_info/sys_info.rs b/src/system_info/sys_info.rs new file mode 100644 index 0000000..fbc6f83 --- /dev/null +++ b/src/system_info/sys_info.rs @@ -0,0 +1,89 @@ +use chrono::Utc; +use serde::{Deserialize, Serialize}; +use sysinfo::{DiskExt, System, SystemExt}; + +// Helper function to format the duration +fn convert_seconds(seconds: i64) -> String { + let days = seconds / 86_400; // 86,400 seconds in a day + let hours = (seconds % 86_400) / 3_600; // 3,600 seconds in an hour + let minutes = (seconds % 3_600) / 60; // 60 seconds in a minute + let remaining_seconds = seconds % 60; + + let mut result = Vec::new(); + + if days > 0 { + result.push(format!("{} day{}", days, if days > 1 { "s" } else { "" })); + } + if hours > 0 { + result.push(format!("{} hour{}", hours, if hours > 1 { "s" } else { "" })); + } + if minutes > 0 && result.len() < 2 { + result.push(format!("{} minute{}", minutes, if minutes > 1 { "s" } else { "" })); + } + if remaining_seconds > 0 && result.len() < 2 { + result.push(format!("{} second{}", remaining_seconds, if remaining_seconds > 1 { "s" } else { "" })); + } + result.join(" and ") +} + +// Helper function to convert size +fn size_converter(byte_size: u64) -> String { + let size_name = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + let mut index = 0; + let mut size = byte_size as f64; + + while size >= 1024.0 && index < size_name.len() - 1 { + size /= 1024.0; + index += 1; + } + + format!("{:.2} {}", size, size_name[index]) +} + +// Struct to hold system information in a JSON-serializable format +#[derive(Serialize, Deserialize, Debug)] +pub struct SystemInfoBasic { + node: String, + system: String, + architecture: String, + cpu_cores: String, + uptime: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct SystemInfoMemStorage { + memory: String, + swap: String, + storage: String, +} + + +// Function to collect system information and return as a JSON-serializable struct +pub fn get_sys_info() -> (SystemInfoBasic, SystemInfoMemStorage) { + let mut sys = System::new_all(); + sys.refresh_all(); + + // Uptime + let boot_time = sys.boot_time(); + let uptime_duration = Utc::now().timestamp() - boot_time as i64; + let uptime = convert_seconds(uptime_duration); + + let total_memory = size_converter(sys.total_memory()); // in bytes + let total_swap = size_converter(sys.total_swap()); // in bytes + let total_storage = size_converter(sys.disks().iter().map(|disk| disk.total_space()).sum::()); + + // Basic and Memory/Storage Info + let basic = SystemInfoBasic { + node: sys.host_name().unwrap_or_else(|| "Unknown".to_string()), + system: sys.name().unwrap_or_else(|| "Unknown".to_string()), + architecture: std::env::consts::ARCH.to_string(), + uptime, + cpu_cores: sys.cpus().len().to_string(), + }; + let mem_storage = SystemInfoMemStorage { + memory: total_memory, + swap: total_swap, + storage: total_storage, + }; + (basic, mem_storage) +} diff --git a/src/templates/error.rs b/src/templates/error.rs new file mode 100644 index 0000000..4ad1590 --- /dev/null +++ b/src/templates/error.rs @@ -0,0 +1,96 @@ +/// Get the HTML content to render the session unauthorized page. +/// +/// # See Also +/// +/// - This page is served as a response for all the entry points, +/// when the user tries to access a page without valid authentication. +/// +/// # Returns +/// +/// A `String` version of the HTML, CSS and JS content. +pub fn get_content() -> String { + r###" + + + + RuStream - Self-hosted Streaming Engine - v{{ version }} + + + + + + + + + + + +

{{ title }}

+

{{ description }}

+

+ Image +

+ +
+ +

Click HERE to reach out.

+ +{% if block_navigation %} + + +{% endif %} + +"###.to_string() +} diff --git a/src/templates/mod.rs b/src/templates/mod.rs index a6465f7..9b8dcf1 100644 --- a/src/templates/mod.rs +++ b/src/templates/mod.rs @@ -8,8 +8,10 @@ mod logout; mod session; /// Monitor page template that is served as HTML response for the monitor endpoint. mod monitor; -/// Monitor page template that is served as HTML response when the user is unauthorized. +/// Unauthorized template that is served as HTML response when the user is unauthorized. mod unauthorized; +/// Error page template that is served as HTML response for any error message to be conveyed. +mod error; /// Error page template that is served as HTML response for any error message to be conveyed. @@ -24,6 +26,7 @@ pub fn environment() -> Arc> { env.add_template_owned("index", index::get_content()).unwrap(); env.add_template_owned("monitor", monitor::get_content()).unwrap(); env.add_template_owned("logout", logout::get_content()).unwrap(); + env.add_template_owned("error", error::get_content()).unwrap(); env.add_template_owned("session", session::get_content()).unwrap(); env.add_template_owned("unauthorized", unauthorized::get_content()).unwrap(); Arc::new(env) diff --git a/src/templates/monitor.rs b/src/templates/monitor.rs index 7c1fa6f..d3fa5db 100644 --- a/src/templates/monitor.rs +++ b/src/templates/monitor.rs @@ -211,24 +211,24 @@ pub fn get_content() -> String {
System Information (Basic)
- {% for key, value in sys_info_basic.items() %} - {{ key|capwords }}: {{ value }}
+ {% for key, value in sys_info_basic|items() %} + {{ key }}: {{ value }}
{% endfor %}

System Information (Memory & Storage)
- {% for key, value in sys_info_mem_storage.items() %} - {{ key|capwords }}: {{ value }}
+ {% for key, value in sys_info_mem_storage|items() %} + {{ key }}: {{ value }}
{% endfor %}

System Information (Network)
- {% for key, value in sys_info_network.items() %} - {{ key|capwords }}: {{ value }}
+ {% for key, value in sys_info_network|items() %} + {{ key }}: {{ value }}
{% endfor %}
{% if sys_info_disks %} @@ -237,7 +237,7 @@ pub fn get_content() -> String { System Information (Disks) {% for disk_info in sys_info_disks %}
- {% for key, value in disk_info.items() %} + {% for key, value in disk_info|items() %} {{ key }}: {{ value }}
{% endfor %} {% endfor %} @@ -272,7 +272,7 @@ pub fn get_content() -> String {

Memory: 0%

- {% if 'swap' in sys_info_mem_storage.keys() %} + {% if 'swap' in sys_info_mem_storage %}

Swap Usage

@@ -298,7 +298,7 @@ pub fn get_content() -> String {
- {% if 'swap' in sys_info_mem_storage.keys() %} + {% if 'swap' in sys_info_mem_storage %}

Swap Usage