Skip to content

Commit

Permalink
Implement monitor page with basic info
Browse files Browse the repository at this point in the history
Include error and logout endpoints
  • Loading branch information
dormant-user committed Sep 21, 2024
1 parent 8511ef1 commit 230cc46
Show file tree
Hide file tree
Showing 10 changed files with 344 additions and 12 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`
///
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 4 additions & 2 deletions src/routes/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
59 changes: 59 additions & 0 deletions src/routes/monitor.rs
Original file line number Diff line number Diff line change
@@ -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<Arc<Fernet>>,
session: web::Data<Arc<constant::Session>>,
metadata: web::Data<Arc<constant::MetaData>>,
config: web::Data<Arc<squire::settings::Config>>,
template: web::Data<Arc<minijinja::Environment<'static>>>) -> 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)
}
2 changes: 2 additions & 0 deletions src/system_info/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod sys_info;
pub mod network;
74 changes: 74 additions & 0 deletions src/system_info/network.rs
Original file line number Diff line number Diff line change
@@ -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<String> {
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::<serde_json::Value>().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<String> {
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,
}
}
89 changes: 89 additions & 0 deletions src/system_info/sys_info.rs
Original file line number Diff line number Diff line change
@@ -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::<u64>());

// 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)
}
96 changes: 96 additions & 0 deletions src/templates/error.rs
Original file line number Diff line number Diff line change
@@ -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###"<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>RuStream - Self-hosted Streaming Engine - v{{ version }}</title>
<meta property="og:type" content="MediaStreaming">
<meta name="keywords" content="Python, streaming, fastapi, JavaScript, HTML, CSS">
<meta name="author" content="Vignesh Rao">
<meta content="width=device-width, initial-scale=1" name="viewport">
<!-- Favicon.ico and Apple Touch Icon -->
<link rel="icon" href="https://thevickypedia.github.io/open-source/images/logo/actix.ico">
<link rel="apple-touch-icon" href="https://thevickypedia.github.io/open-source/images/logo/actix.png">
<style>
/* Google fonts with a backup alternative */
@import url('https://fonts.googleapis.com/css2?family=Ubuntu:wght@400;500;700&display=swap');
* {
font-family: 'Ubuntu', 'PT Serif', sans-serif;
}
img {
display: block;
margin-left: auto;
margin-right: auto;
}
:is(h1, h2, h3, h4, h5, h6)
{
text-align: center;
color: #F0F0F0;
}
button {
width: 100px;
margin: 0 auto;
display: block;
}
body {
background-color: #151515;
}
</style>
<noscript>
<style>
body {
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
<div style="position: fixed; text-align:center; height: 100%; width: 100%; background-color: #151515;">
<h2 style="margin-top:5%">This page requires JavaScript
to be enabled.
<br><br>
Please refer <a href="https://www.enable-javascript.com/">enable-javascript</a> for how to.
</h2>
<form>
<button type="submit" onClick="<meta httpEquiv='refresh' content='0'>">RETRY</button>
</form>
</div>
</noscript>
</head>
<body>
<h2 style="margin-top:5%">{{ title }}</h2>
<h3>{{ description }}</h3>
<p>
<img src="https://thevickypedia.github.io/open-source/images/gif/lockscape.gif"
onerror="this.src='https://vigneshrao.com/open-source/images/gif/lockscape.gif'"
width="200" height="170" alt="Image" class="center">
</p>
<button style="text-align:center" onClick="window.location.href = '{{ button_link }}';">{{ button_text }}</button>
<br>
<button style="text-align:center" onClick="alert('{{ help }}');">HELP
</button>
<h4>Click <a href="https://vigneshrao.com/contact">HERE</a> to reach out.</h4>
</body>
{% if block_navigation %}
<!-- control the behavior of the browser's navigation without triggering a full page reload -->
<script>
document.addEventListener('DOMContentLoaded', function() {
history.pushState(null, document.title, location.href);
window.addEventListener('popstate', function (event) {
history.pushState(null, document.title, location.href);
});
});
</script>
{% endif %}
</html>
"###.to_string()
}
5 changes: 4 additions & 1 deletion src/templates/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -24,6 +26,7 @@ pub fn environment() -> Arc<minijinja::Environment<'static>> {
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)
Expand Down
Loading

0 comments on commit 230cc46

Please sign in to comment.