Issue tokens to initialize VNC connections

This commit is contained in:
Pierre HUBERT 2023-10-18 12:27:50 +02:00
parent 3042bbdac6
commit 4f75cd918d
10 changed files with 173 additions and 2 deletions

View File

@ -2523,6 +2523,7 @@ dependencies = [
"lazy_static", "lazy_static",
"light-openid", "light-openid",
"log", "log",
"rand",
"reqwest", "reqwest",
"serde", "serde",
"serde-xml-rs", "serde-xml-rs",

View File

@ -33,3 +33,4 @@ uuid = { version = "1.4.1", features = ["v4", "serde"] }
lazy-regex = "3.0.2" lazy-regex = "3.0.2"
thiserror = "1.0.49" thiserror = "1.0.49"
image = "0.24.7" image = "0.24.7"
rand = "0.8.5"

View File

@ -1 +1,2 @@
pub mod libvirt_actor; pub mod libvirt_actor;
pub mod vnc_tokens_actor;

View File

@ -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<VNCToken>);
impl Actor for VNCTokensActor {
type Context = Context<Self>;
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<String>")]
pub struct IssueTokenReq(DomainXMLUuid);
impl Handler<IssueTokenReq> for VNCTokensActor {
type Result = anyhow::Result<String>;
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<DomainXMLUuid>")]
pub struct ConsumeTokenReq(String);
impl Handler<ConsumeTokenReq> for VNCTokensActor {
type Result = anyhow::Result<DomainXMLUuid>;
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<VNCTokensActor>);
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<String> {
self.0.send(IssueTokenReq(id)).await?
}
/// Consume a VNC access token
pub async fn consume_token(&self, token: String) -> anyhow::Result<DomainXMLUuid> {
self.0.send(ConsumeTokenReq(token)).await?
}
}

View File

@ -1,3 +1,4 @@
use crate::actors::vnc_tokens_actor::VNCTokensManager;
use crate::controllers::{HttpResult, LibVirtReq}; use crate::controllers::{HttpResult, LibVirtReq};
use crate::libvirt_lib_structures::{DomainState, DomainXMLUuid}; use crate::libvirt_lib_structures::{DomainState, DomainXMLUuid};
use crate::libvirt_rest_structures::VMInfo; use crate::libvirt_rest_structures::VMInfo;
@ -208,3 +209,25 @@ pub async fn screenshot(client: LibVirtReq, id: web::Path<SingleVMUUidReq>) -> H
} }
}) })
} }
#[derive(serde::Serialize)]
struct IssueVNCTokenResponse {
token: String,
}
/// Issue a VNC connection token
pub async fn vnc_token(
manager: web::Data<VNCTokensManager>,
id: web::Path<SingleVMUUidReq>,
) -> 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!")
}
})
}

View File

@ -15,6 +15,7 @@ use actix_web::{web, App, HttpServer};
use light_openid::basic_state_manager::BasicStateManager; use light_openid::basic_state_manager::BasicStateManager;
use std::time::Duration; use std::time::Duration;
use virtweb_backend::actors::libvirt_actor::LibVirtActor; use virtweb_backend::actors::libvirt_actor::LibVirtActor;
use virtweb_backend::actors::vnc_tokens_actor::VNCTokensManager;
use virtweb_backend::app_config::AppConfig; use virtweb_backend::app_config::AppConfig;
use virtweb_backend::constants; use virtweb_backend::constants;
use virtweb_backend::constants::{ use virtweb_backend::constants::{
@ -41,6 +42,8 @@ async fn main() -> std::io::Result<()> {
.start(), .start(),
)); ));
let vnc_tokens = Data::new(VNCTokensManager::start());
log::info!("Start to listen on {}", AppConfig::get().listen_address); log::info!("Start to listen on {}", AppConfig::get().listen_address);
let state_manager = Data::new(BasicStateManager::new()); let state_manager = Data::new(BasicStateManager::new());
@ -78,6 +81,7 @@ async fn main() -> std::io::Result<()> {
.wrap(session_mw) .wrap(session_mw)
.wrap(cors) .wrap(cors)
.app_data(state_manager.clone()) .app_data(state_manager.clone())
.app_data(vnc_tokens.clone())
.app_data(Data::new(RemoteIPConfig { .app_data(Data::new(RemoteIPConfig {
proxy: AppConfig::get().proxy_ip.clone(), proxy: AppConfig::get().proxy_ip.clone(),
})) }))
@ -157,6 +161,10 @@ async fn main() -> std::io::Result<()> {
"/api/vm/{uid}/screenshot", "/api/vm/{uid}/screenshot",
web::get().to(vm_controller::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)? .bind(&AppConfig::get().listen_address)?
.run() .run()

View File

@ -1,2 +1,4 @@
pub mod files_utils; pub mod files_utils;
pub mod rand_utils;
pub mod time_utils;
pub mod url_utils; pub mod url_utils;

View File

@ -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
}

View File

@ -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()
}

View File

@ -25,7 +25,7 @@ export function VMScreenshot(p: { vm: VMInfo }): React.ReactElement {
if (int.current === undefined) { if (int.current === undefined) {
refresh(); refresh();
int.current = setInterval(() => refresh(), 5000000); int.current = setInterval(() => refresh(), 5000);
} }
return () => { return () => {