Fix VNC connection issue

This commit is contained in:
Pierre HUBERT 2023-12-07 00:23:19 +01:00
parent 7d88cd576c
commit d1a9b6c3bb
7 changed files with 78 additions and 55 deletions

View File

@ -16,6 +16,7 @@ sudo apt install qemu-kvm libvirt-daemon-system
3. Allow the current user to manage VMs: 3. Allow the current user to manage VMs:
``` ```
sudo adduser $USER libvirt sudo adduser $USER libvirt
sudo adduser $USER kvm
``` ```
> Note: You will need to login again for this change to take effect. > Note: You will need to login again for this change to take effect.

View File

@ -6,14 +6,14 @@ use std::time::Duration;
const TOKENS_CLEAN_INTERVAL: Duration = Duration::from_secs(60); const TOKENS_CLEAN_INTERVAL: Duration = Duration::from_secs(60);
const VNC_TOKEN_LEN: usize = 15; const VNC_TOKEN_LEN: usize = 15;
const VNC_TOKEN_LIFETIME: u64 = 120; pub const VNC_TOKEN_LIFETIME: u64 = 30;
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
enum VNCTokenError { enum VNCTokenError {
#[error("Could not consume token, because it does not exist!")] #[error("Could not use token, because it does not exist!")]
ConsumeErrorTokenNotFound, UseErrorTokenNotFound,
#[error("Could not consume token, because it has expired!")] #[error("Could not use token, because it has expired!")]
ConsumeErrorTokenExpired, UseErrorTokenExpired,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -64,24 +64,22 @@ impl Handler<IssueTokenReq> for VNCTokensActor {
#[derive(Message)] #[derive(Message)]
#[rtype(result = "anyhow::Result<XMLUuid>")] #[rtype(result = "anyhow::Result<XMLUuid>")]
pub struct ConsumeTokenReq(String); pub struct UseTokenReq(String);
impl Handler<ConsumeTokenReq> for VNCTokensActor { impl Handler<UseTokenReq> for VNCTokensActor {
type Result = anyhow::Result<XMLUuid>; type Result = anyhow::Result<XMLUuid>;
fn handle(&mut self, msg: ConsumeTokenReq, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: UseTokenReq, _ctx: &mut Self::Context) -> Self::Result {
log::debug!("Attempt to consume a token {:?}", msg.0); log::debug!("Attempt to use a token {:?}", msg.0);
let token_index = self let token = self
.0 .0
.iter() .iter()
.position(|i| i.token == msg.0) .find(|i| i.token == msg.0)
.ok_or(VNCTokenError::ConsumeErrorTokenNotFound)?; .ok_or(VNCTokenError::UseErrorTokenNotFound)?;
let token = self.0.remove(token_index);
if token.is_expired() { if token.is_expired() {
return Err(VNCTokenError::ConsumeErrorTokenExpired.into()); return Err(VNCTokenError::UseErrorTokenExpired.into());
} }
Ok(token.vm) Ok(token.vm)
@ -101,8 +99,8 @@ impl VNCTokensManager {
self.0.send(IssueTokenReq(id)).await? self.0.send(IssueTokenReq(id)).await?
} }
/// Consume a VNC access token /// Use a VNC access token
pub async fn consume_token(&self, token: String) -> anyhow::Result<XMLUuid> { pub async fn use_token(&self, token: String) -> anyhow::Result<XMLUuid> {
self.0.send(ConsumeTokenReq(token)).await? self.0.send(UseTokenReq(token)).await?
} }
} }

View File

@ -1,3 +1,4 @@
use crate::actors::vnc_tokens_actor::VNC_TOKEN_LIFETIME;
use crate::app_config::AppConfig; use crate::app_config::AppConfig;
use crate::constants; use crate::constants;
use crate::constants::{DISK_NAME_MAX_LEN, DISK_NAME_MIN_LEN, DISK_SIZE_MAX, DISK_SIZE_MIN}; use crate::constants::{DISK_NAME_MAX_LEN, DISK_NAME_MIN_LEN, DISK_SIZE_MAX, DISK_SIZE_MIN};
@ -29,6 +30,7 @@ struct LenConstraints {
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
struct ServerConstraints { struct ServerConstraints {
iso_max_size: usize, iso_max_size: usize,
vnc_token_duration: u64,
vm_name_size: LenConstraints, vm_name_size: LenConstraints,
vm_title_size: LenConstraints, vm_title_size: LenConstraints,
memory_size: LenConstraints, memory_size: LenConstraints,
@ -47,6 +49,8 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
constraints: ServerConstraints { constraints: ServerConstraints {
iso_max_size: constants::ISO_MAX_SIZE, iso_max_size: constants::ISO_MAX_SIZE,
vnc_token_duration: VNC_TOKEN_LIFETIME,
vm_name_size: LenConstraints { min: 2, max: 50 }, vm_name_size: LenConstraints { min: 2, max: 50 },
vm_title_size: LenConstraints { min: 0, max: 50 }, vm_title_size: LenConstraints { min: 0, max: 50 },
memory_size: LenConstraints { memory_size: LenConstraints {

View File

@ -269,7 +269,7 @@ pub async fn vnc(
req: HttpRequest, req: HttpRequest,
stream: web::Payload, stream: web::Payload,
) -> HttpResult { ) -> HttpResult {
let domain_id = manager.consume_token(token.0.token).await?; let domain_id = manager.use_token(token.0.token).await?;
let domain = client.get_single_domain(domain_id).await?; let domain = client.get_single_domain(domain_id).await?;
let socket_path = match domain.devices.graphics { let socket_path = match domain.devices.graphics {

View File

@ -10,6 +10,7 @@ export interface ServerConfig {
export interface ServerConstraints { export interface ServerConstraints {
iso_max_size: number; iso_max_size: number;
vnc_token_duration: number;
vm_name_size: LenConstraint; vm_name_size: LenConstraint;
vm_title_size: LenConstraint; vm_title_size: LenConstraint;
memory_size: LenConstraint; memory_size: LenConstraint;

View File

@ -1,10 +1,17 @@
import React from "react"; import React, { useEffect } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { VncScreen } from "react-vnc"; import { VncScreen } from "react-vnc";
import { ServerApi } from "../api/ServerApi";
import { VMApi, VMInfo } from "../api/VMApi"; import { VMApi, VMInfo } from "../api/VMApi";
import { useSnackbar } from "../hooks/providers/SnackbarProvider"; import { useSnackbar } from "../hooks/providers/SnackbarProvider";
import { time } from "../utils/DateUtils";
import { AsyncWidget } from "../widgets/AsyncWidget"; import { AsyncWidget } from "../widgets/AsyncWidget";
interface VNCTokenInfo {
url: string;
expire: number;
}
export function VNCRoute(): React.ReactElement { export function VNCRoute(): React.ReactElement {
const { uuid } = useParams(); const { uuid } = useParams();
@ -27,35 +34,43 @@ export function VNCRoute(): React.ReactElement {
function VNCInner(p: { vm: VMInfo }): React.ReactElement { function VNCInner(p: { vm: VMInfo }): React.ReactElement {
const snackbar = useSnackbar(); const snackbar = useSnackbar();
const counter = React.useRef(false); const [token, setToken] = React.useState<VNCTokenInfo | undefined>();
const [url, setURL] = React.useState<string | undefined>(); const [counter, setCounter] = React.useState(1);
const load = async () => { const connect = async (force: boolean) => {
try { try {
if (force) setCounter(counter + 1);
// Check if getting new time is useless
if ((token?.expire ?? 0) > time()) return;
setToken(undefined);
const u = await VMApi.OneShotVNCURL(p.vm); const u = await VMApi.OneShotVNCURL(p.vm);
console.info(u);
if (counter.current === false) { if (!token)
counter.current = true; setToken({
setURL(u); expire: time() + ServerApi.Config.constraints.vnc_token_duration,
} url: u,
});
} catch (e) { } catch (e) {
console.error(e); console.error(e);
snackbar("Failed to initialize VNC connection!"); snackbar("Failed to initialize VNC connection!");
} }
}; };
const reconnect = () => { const disconnected = () => {
counter.current = false; connect(true);
setURL(undefined);
}; };
useEffect(() => {
connect(false);
});
if (token === undefined)
return <p>Please wait, connecting to the machine...</p>;
return ( return (
<AsyncWidget
loadKey={counter.current}
load={load}
ready={url !== undefined && counter.current}
errMsg="Failed to get a token to initialize VNC connection!"
build={() => (
<div <div
style={{ style={{
display: "flex", display: "flex",
@ -65,14 +80,12 @@ function VNCInner(p: { vm: VMInfo }): React.ReactElement {
}} }}
> >
<VncScreen <VncScreen
url={url!} url={token!.url}
onDisconnect={() => { onDisconnect={() => {
console.info("VNC disconnected " + url); console.info("VNC disconnected " + token?.url);
reconnect(); disconnected();
}} }}
/> />
</div> </div>
)}
/>
); );
} }

View File

@ -0,0 +1,6 @@
/**
* Get current UNIX time, in seconds
*/
export function time(): number {
return Math.floor(new Date().getTime() / 1000);
}