VirtWebRemote/remote_backend/src/virtweb_client.rs
2024-05-04 09:11:30 +02:00

267 lines
6.5 KiB
Rust

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<Self, Self::Err> {
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<String>,
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<TokenRight>;
#[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::<Vec<_>>();
for r in &self.rights {
if r.verb != verb {
continue;
}
let curr_route_split = r.path.split('/').collect::<Vec<_>>();
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<VMUuid> {
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::<Vec<_>>()
}
/// 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<D: Display>(uri: D) -> anyhow::Result<reqwest::Response> {
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<D: Display, E: serde::de::DeserializeOwned>(uri: D) -> anyhow::Result<E> {
Ok(request(uri).await?.json().await?)
}
/// Get current token information
pub async fn get_token_info() -> anyhow::Result<TokenInfo> {
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<VMInfo> {
json_request(id.route_info()).await
}
/// Get a vm information
pub async fn vm_state(id: VMUuid) -> anyhow::Result<VMState> {
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<Vec<u8>> {
Ok(request(id.route_screenshot())
.await?
.bytes()
.await?
.to_vec())
}
/// Get current server information
pub async fn get_server_info() -> anyhow::Result<SystemInfo> {
json_request("/api/server/info").await
}