Issue tokens to initialize VNC connections
This commit is contained in:
		
							
								
								
									
										1
									
								
								virtweb_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								virtweb_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -2523,6 +2523,7 @@ dependencies = [
 | 
			
		||||
 "lazy_static",
 | 
			
		||||
 "light-openid",
 | 
			
		||||
 "log",
 | 
			
		||||
 "rand",
 | 
			
		||||
 "reqwest",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde-xml-rs",
 | 
			
		||||
 
 | 
			
		||||
@@ -33,3 +33,4 @@ uuid = { version = "1.4.1", features = ["v4", "serde"] }
 | 
			
		||||
lazy-regex = "3.0.2"
 | 
			
		||||
thiserror = "1.0.49"
 | 
			
		||||
image = "0.24.7"
 | 
			
		||||
rand = "0.8.5"
 | 
			
		||||
@@ -1 +1,2 @@
 | 
			
		||||
pub mod libvirt_actor;
 | 
			
		||||
pub mod vnc_tokens_actor;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										108
									
								
								virtweb_backend/src/actors/vnc_tokens_actor.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								virtweb_backend/src/actors/vnc_tokens_actor.rs
									
									
									
									
									
										Normal 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?
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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<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!")
 | 
			
		||||
        }
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,2 +1,4 @@
 | 
			
		||||
pub mod files_utils;
 | 
			
		||||
pub mod rand_utils;
 | 
			
		||||
pub mod time_utils;
 | 
			
		||||
pub mod url_utils;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								virtweb_backend/src/utils/rand_utils.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								virtweb_backend/src/utils/rand_utils.rs
									
									
									
									
									
										Normal 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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								virtweb_backend/src/utils/time_utils.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								virtweb_backend/src/utils/time_utils.rs
									
									
									
									
									
										Normal 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()
 | 
			
		||||
}
 | 
			
		||||
@@ -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 () => {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user