diff --git a/virtweb_backend/Cargo.lock b/virtweb_backend/Cargo.lock index 831d44d..3d6c94a 100644 --- a/virtweb_backend/Cargo.lock +++ b/virtweb_backend/Cargo.lock @@ -2523,6 +2523,7 @@ dependencies = [ "lazy_static", "light-openid", "log", + "rand", "reqwest", "serde", "serde-xml-rs", diff --git a/virtweb_backend/Cargo.toml b/virtweb_backend/Cargo.toml index 78731bc..92bd1b4 100644 --- a/virtweb_backend/Cargo.toml +++ b/virtweb_backend/Cargo.toml @@ -32,4 +32,5 @@ sysinfo = { version = "0.29.10", features = ["serde"] } uuid = { version = "1.4.1", features = ["v4", "serde"] } lazy-regex = "3.0.2" thiserror = "1.0.49" -image = "0.24.7" \ No newline at end of file +image = "0.24.7" +rand = "0.8.5" \ No newline at end of file diff --git a/virtweb_backend/src/actors/mod.rs b/virtweb_backend/src/actors/mod.rs index 5e9ba0b..488569f 100644 --- a/virtweb_backend/src/actors/mod.rs +++ b/virtweb_backend/src/actors/mod.rs @@ -1 +1,2 @@ pub mod libvirt_actor; +pub mod vnc_tokens_actor; diff --git a/virtweb_backend/src/actors/vnc_tokens_actor.rs b/virtweb_backend/src/actors/vnc_tokens_actor.rs new file mode 100644 index 0000000..3f8c664 --- /dev/null +++ b/virtweb_backend/src/actors/vnc_tokens_actor.rs @@ -0,0 +1,108 @@ +use crate::libvirt_lib_structures::DomainXMLUuid; +use crate::utils::rand_utils::rand_str; +use crate::utils::time_utils::time; +use actix::{Actor, Addr, AsyncContext, Context, Handler, Message}; +use std::time::Duration; + +const TOKENS_CLEAN_INTERVAL: Duration = Duration::from_secs(60); +const VNC_TOKEN_LEN: usize = 15; +const VNC_TOKEN_LIFETIME: u64 = 120; + +#[derive(thiserror::Error, Debug)] +enum VNCTokenError { + #[error("Could not consume token, because it does not exist!")] + ConsumeErrorTokenNotFound, + #[error("Could not consume token, because it has expired!")] + ConsumeErrorTokenExpired, +} + +#[derive(Debug, Clone)] +struct VNCToken { + token: String, + vm: DomainXMLUuid, + expire: u64, +} + +impl VNCToken { + fn is_expired(&self) -> bool { + self.expire < time() + } +} + +struct VNCTokensActor(Vec); + +impl Actor for VNCTokensActor { + type Context = Context; + + fn started(&mut self, ctx: &mut Self::Context) { + ctx.run_interval(TOKENS_CLEAN_INTERVAL, |act, _ctx| { + // Regularly remove outdated entries + log::debug!("Remove outdated VNC token entries"); + act.0.retain(|e| !e.is_expired()) + }); + } +} + +#[derive(Message)] +#[rtype(result = "anyhow::Result")] +pub struct IssueTokenReq(DomainXMLUuid); + +impl Handler for VNCTokensActor { + type Result = anyhow::Result; + + fn handle(&mut self, msg: IssueTokenReq, _ctx: &mut Self::Context) -> Self::Result { + log::debug!("Issue a new VNC token for domain {:?}", msg.0); + let token = VNCToken { + token: rand_str(VNC_TOKEN_LEN), + vm: msg.0, + expire: time() + VNC_TOKEN_LIFETIME, + }; + self.0.push(token.clone()); + Ok(token.token) + } +} + +#[derive(Message)] +#[rtype(result = "anyhow::Result")] +pub struct ConsumeTokenReq(String); + +impl Handler for VNCTokensActor { + type Result = anyhow::Result; + + fn handle(&mut self, msg: ConsumeTokenReq, _ctx: &mut Self::Context) -> Self::Result { + log::debug!("Attempt to consume a token {:?}", msg.0); + + let token_index = self + .0 + .iter() + .position(|i| i.token == msg.0) + .ok_or(VNCTokenError::ConsumeErrorTokenNotFound)?; + + let token = self.0.remove(token_index); + + if token.is_expired() { + return Err(VNCTokenError::ConsumeErrorTokenExpired.into()); + } + + Ok(token.vm) + } +} + +#[derive(Clone)] +pub struct VNCTokensManager(Addr); + +impl VNCTokensManager { + pub fn start() -> Self { + Self(VNCTokensActor(vec![]).start()) + } + + /// Issue a new VNC access token for a domain + pub async fn issue_token(&self, id: DomainXMLUuid) -> anyhow::Result { + self.0.send(IssueTokenReq(id)).await? + } + + /// Consume a VNC access token + pub async fn consume_token(&self, token: String) -> anyhow::Result { + self.0.send(ConsumeTokenReq(token)).await? + } +} diff --git a/virtweb_backend/src/controllers/vm_controller.rs b/virtweb_backend/src/controllers/vm_controller.rs index 3e9cc9b..cf91025 100644 --- a/virtweb_backend/src/controllers/vm_controller.rs +++ b/virtweb_backend/src/controllers/vm_controller.rs @@ -1,3 +1,4 @@ +use crate::actors::vnc_tokens_actor::VNCTokensManager; use crate::controllers::{HttpResult, LibVirtReq}; use crate::libvirt_lib_structures::{DomainState, DomainXMLUuid}; use crate::libvirt_rest_structures::VMInfo; @@ -208,3 +209,25 @@ pub async fn screenshot(client: LibVirtReq, id: web::Path) -> H } }) } + +#[derive(serde::Serialize)] +struct IssueVNCTokenResponse { + token: String, +} + +/// Issue a VNC connection token +pub async fn vnc_token( + manager: web::Data, + id: web::Path, +) -> HttpResult { + Ok(match manager.issue_token(id.uid).await { + Ok(token) => HttpResponse::Ok().json(IssueVNCTokenResponse { token }), + Err(e) => { + log::error!( + "Failed to issue a token for a domain domain {:?} ! {e}", + id.uid + ); + HttpResponse::InternalServerError().json("Failed to issue a token for the domain!") + } + }) +} diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index 4f740ca..2deaadf 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -15,6 +15,7 @@ use actix_web::{web, App, HttpServer}; use light_openid::basic_state_manager::BasicStateManager; use std::time::Duration; use virtweb_backend::actors::libvirt_actor::LibVirtActor; +use virtweb_backend::actors::vnc_tokens_actor::VNCTokensManager; use virtweb_backend::app_config::AppConfig; use virtweb_backend::constants; use virtweb_backend::constants::{ @@ -41,6 +42,8 @@ async fn main() -> std::io::Result<()> { .start(), )); + let vnc_tokens = Data::new(VNCTokensManager::start()); + log::info!("Start to listen on {}", AppConfig::get().listen_address); let state_manager = Data::new(BasicStateManager::new()); @@ -78,6 +81,7 @@ async fn main() -> std::io::Result<()> { .wrap(session_mw) .wrap(cors) .app_data(state_manager.clone()) + .app_data(vnc_tokens.clone()) .app_data(Data::new(RemoteIPConfig { proxy: AppConfig::get().proxy_ip.clone(), })) @@ -157,6 +161,10 @@ async fn main() -> std::io::Result<()> { "/api/vm/{uid}/screenshot", web::get().to(vm_controller::screenshot), ) + .route( + "/api/vm/{uid}/vnc_token", + web::get().to(vm_controller::vnc_token), + ) }) .bind(&AppConfig::get().listen_address)? .run() diff --git a/virtweb_backend/src/utils/mod.rs b/virtweb_backend/src/utils/mod.rs index bd18501..01babdb 100644 --- a/virtweb_backend/src/utils/mod.rs +++ b/virtweb_backend/src/utils/mod.rs @@ -1,2 +1,4 @@ pub mod files_utils; +pub mod rand_utils; +pub mod time_utils; pub mod url_utils; diff --git a/virtweb_backend/src/utils/rand_utils.rs b/virtweb_backend/src/utils/rand_utils.rs new file mode 100644 index 0000000..2735a1f --- /dev/null +++ b/virtweb_backend/src/utils/rand_utils.rs @@ -0,0 +1,12 @@ +use rand::distributions::Alphanumeric; +use rand::Rng; + +/// Generate a random string +pub fn rand_str(len: usize) -> String { + let s: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(len) + .map(char::from) + .collect(); + s +} diff --git a/virtweb_backend/src/utils/time_utils.rs b/virtweb_backend/src/utils/time_utils.rs new file mode 100644 index 0000000..5ff9a92 --- /dev/null +++ b/virtweb_backend/src/utils/time_utils.rs @@ -0,0 +1,15 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +/// Get the current time since epoch +/// +/// ``` +/// use virtweb_backend::utils::time_utils::time; +/// +/// let time = time(); +/// ``` +pub fn time() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() +} diff --git a/virtweb_frontend/src/widgets/vms/VMScreenshot.tsx b/virtweb_frontend/src/widgets/vms/VMScreenshot.tsx index a21bc37..0173d87 100644 --- a/virtweb_frontend/src/widgets/vms/VMScreenshot.tsx +++ b/virtweb_frontend/src/widgets/vms/VMScreenshot.tsx @@ -25,7 +25,7 @@ export function VMScreenshot(p: { vm: VMInfo }): React.ReactElement { if (int.current === undefined) { refresh(); - int.current = setInterval(() => refresh(), 5000000); + int.current = setInterval(() => refresh(), 5000); } return () => {