use crate::app_config::AppConfig; use crate::utils::time; use lazy_regex::regex; use std::fmt::Display; use std::str::FromStr; use thiserror::Error; use uuid::{Error, Uuid}; #[derive(Error, Debug)] pub enum VirtWebClientError { #[error("Invalid status code from VirtWeb: {0}")] InvalidStatusCode(u16), } #[derive(Eq, PartialEq, Debug, Copy, Clone, serde::Serialize, serde::Deserialize)] pub struct VMUuid(Uuid); impl VMUuid { pub fn route_info(&self) -> String { format!("/api/vm/{}", self.0) } pub fn route_state(&self) -> String { format!("/api/vm/{}/state", self.0) } pub fn route_start(&self) -> String { format!("/api/vm/{}/start", self.0) } pub fn route_shutdown(&self) -> String { format!("/api/vm/{}/shutdown", self.0) } pub fn route_kill(&self) -> String { format!("/api/vm/{}/kill", self.0) } pub fn route_reset(&self) -> String { format!("/api/vm/{}/reset", self.0) } pub fn route_suspend(&self) -> String { format!("/api/vm/{}/suspend", self.0) } pub fn route_resume(&self) -> String { format!("/api/vm/{}/resume", self.0) } pub fn route_screenshot(&self) -> String { format!("/api/vm/{}/screenshot", self.0) } } impl FromStr for VMUuid { type Err = Error; fn from_str(s: &str) -> Result { Ok(VMUuid(Uuid::from_str(s)?)) } } #[derive(serde::Serialize, Debug)] pub struct TokenClaims { pub sub: String, pub iat: u64, pub exp: u64, pub verb: String, pub path: String, pub nonce: String, } #[derive(serde::Deserialize, Debug)] pub struct VMInfo { pub uuid: VMUuid, pub name: String, pub description: Option, pub architecture: String, pub memory: usize, pub number_vcpu: usize, } #[derive(serde::Deserialize, serde::Serialize, Debug)] pub struct VMState { pub state: String, } #[derive(serde::Deserialize, serde::Serialize, Debug)] pub struct LoadAverage { one: f64, five: f64, fifteen: f64, } #[derive(serde::Deserialize, serde::Serialize, Debug)] pub struct SystemSystemInfo { physical_core_count: usize, uptime: usize, used_memory: usize, available_memory: usize, free_memory: usize, load_average: LoadAverage, } #[derive(serde::Deserialize, serde::Serialize, Debug)] pub struct SystemInfo { system: SystemSystemInfo, } #[derive(serde::Deserialize, Debug)] pub struct TokenRight { verb: String, path: String, } pub type TokenRights = Vec; #[derive(serde::Deserialize)] pub struct TokenInfo { rights: TokenRights, } impl TokenInfo { /// Check whether a route is allowed or not pub fn is_route_allowed(&self, verb: &str, route: &str) -> bool { let search_route_split = route.split('/').collect::>(); for r in &self.rights { if r.verb != verb { continue; } let curr_route_split = r.path.split('/').collect::>(); if search_route_split.len() != curr_route_split.len() { continue; } if curr_route_split .iter() .zip(search_route_split.iter()) .all(|(curr, search)| curr == &"*" || curr == search) { return true; } } false } /// List the virtual machines with access pub fn list_vm(&self) -> Vec { self.rights .iter() .filter(|r| r.verb == "GET") .filter(|r| regex!("^/api/vm/[^/]+$").is_match(&r.path)) .map(|r| VMUuid::from_str(r.path.rsplit_once('/').unwrap().1).unwrap()) .collect::>() } /// Check if system info can be retrived pub fn can_retrieve_system_info(&self) -> bool { self.is_route_allowed("GET", "/api/server/info") } } /// Perform a request on the API async fn request(uri: D) -> anyhow::Result { let url = format!("{}{}", AppConfig::get().virtweb_base_url, uri); log::debug!("Will query {uri}..."); let jwt = TokenClaims { sub: AppConfig::get().virtweb_token_id.to_string(), iat: time() - 60 * 2, exp: time() + 60 * 3, verb: "GET".to_string(), path: uri.to_string(), nonce: Uuid::new_v4().to_string(), }; let jwt = AppConfig::get().token_private_key().sign_jwt(&jwt)?; let res = reqwest::Client::new() .get(url) .header("x-token-id", &AppConfig::get().virtweb_token_id) .header("x-token-content", jwt) .send() .await?; if !res.status().is_success() { return Err(VirtWebClientError::InvalidStatusCode(res.status().as_u16()).into()); } Ok(res) } /// Perform a request on the API async fn json_request(uri: D) -> anyhow::Result { Ok(request(uri).await?.json().await?) } /// Get current token information pub async fn get_token_info() -> anyhow::Result { let res: TokenInfo = json_request(format!("/api/token/{}", AppConfig::get().virtweb_token_id)).await?; Ok(res) } /// Get a vm information pub async fn vm_info(id: VMUuid) -> anyhow::Result { json_request(id.route_info()).await } /// Get a vm information pub async fn vm_state(id: VMUuid) -> anyhow::Result { json_request(id.route_state()).await } /// Start a vm pub async fn vm_start(id: VMUuid) -> anyhow::Result<()> { request(id.route_start()).await?; Ok(()) } /// Shutdown a vm pub async fn vm_shutdown(id: VMUuid) -> anyhow::Result<()> { request(id.route_shutdown()).await?; Ok(()) } /// Kill a vm pub async fn vm_kill(id: VMUuid) -> anyhow::Result<()> { request(id.route_kill()).await?; Ok(()) } /// Reset a vm pub async fn vm_reset(id: VMUuid) -> anyhow::Result<()> { request(id.route_reset()).await?; Ok(()) } /// Suspend a vm pub async fn vm_suspend(id: VMUuid) -> anyhow::Result<()> { request(id.route_suspend()).await?; Ok(()) } /// Resume a vm pub async fn vm_resume(id: VMUuid) -> anyhow::Result<()> { request(id.route_resume()).await?; Ok(()) } /// Grab a screenshot of the VM pub async fn vm_screenshot(id: VMUuid) -> anyhow::Result> { Ok(request(id.route_screenshot()) .await? .bytes() .await? .to_vec()) } /// Get current server information pub async fn get_server_info() -> anyhow::Result { json_request("/api/server/info").await }