Skip to content

Commit b6aaa6f

Browse files
committed
Detect VNC-in-use error state
noVNC provides no direct access the error code returned by socket.on("close") callback. As a workaround the original callback is replaced by a custom proxy. The backend returns the message introduced in #15267 for HTTP requests. For the websocket requests we receive error 1006 (abnormal disconnect). As a workaround an additional dedicated HTTP request is triggered to retrieve the error. Reference-Url: kubevirt/kubevirt#15267 Reference-Url: https://github.com/kubevirt/kubevirt/blob/46aa1b24982cd6fcfd37c950825c8d3acd3dbaf3/pkg/virt-handler/rest/console.go#L163 Reference-Url: https://github.com/novnc/noVNC/blob/d44f7e04fc456844836c7c5ac911d0f4e8dd06e6/core/rfb.js#L662 Reference-Url: https://datatracker.ietf.org/doc/html/rfc6455#section-7.4 Signed-off-by: Radoslaw Szwajkowski <[email protected]>
1 parent 12402ee commit b6aaa6f

File tree

3 files changed

+66
-9
lines changed

3 files changed

+66
-9
lines changed

src/utils/components/Consoles/components/utils/ConsoleConsts.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ export enum ConsoleState {
1616

1717
export const WS = 'ws';
1818
export const WSS = 'wss';
19+
export const HTTP = 'http';
20+
export const HTTPS = 'https';
1921
export const SERIAL_CONSOLE_TYPE = 'Serial console';
2022
export const VNC_CONSOLE_TYPE = 'VNC console';
2123
export const DESKTOP_VIEWER_CONSOLE_TYPE = 'Desktop viewer';

src/utils/components/Consoles/components/vnc-console/VncConsole.tsx

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
import React, { Dispatch, FC, memo, useCallback, useEffect, useRef } from 'react';
33

44
import RFBCreate from '@novnc/novnc/lib/rfb';
5+
import { consoleFetchText } from '@openshift-console/dynamic-plugin-sdk';
56

67
import { INSECURE, SECURE } from '../../utils/constants';
78
import { isConnectionEncrypted } from '../../utils/utils';
8-
import { ConsoleState, VNC_CONSOLE_TYPE, WS, WSS } from '../utils/ConsoleConsts';
9+
import { ConsoleState, HTTP, HTTPS, VNC_CONSOLE_TYPE, WS, WSS } from '../utils/ConsoleConsts';
910
import { ConsoleComponentState } from '../utils/types';
1011

1112
import { isSessionAlreadyInUse } from './utils/util';
@@ -38,6 +39,21 @@ export type VncConsoleProps = {
3839
viewOnly?: boolean;
3940
};
4041

42+
const buildUrl = ({
43+
hostname,
44+
path,
45+
port,
46+
preserveSession,
47+
protocol,
48+
}: {
49+
hostname: string;
50+
path: string;
51+
port: string;
52+
preserveSession: string;
53+
protocol: string;
54+
}): string =>
55+
`${protocol}://${hostname}:${port}${path}?preserveSession=${Boolean(preserveSession).toString()}`;
56+
4157
export const VncConsole: FC<VncConsoleProps> = ({
4258
basePath,
4359
scaleViewport = true,
@@ -76,26 +92,62 @@ export const VncConsole: FC<VncConsoleProps> = ({
7692

7793
const connect = useCallback(
7894
(preserveSession = true) => {
95+
let abnormalDisconnect = false;
7996
setVncState(() => ({ state: connecting }));
8097
const sessionID = ++sessionRef.current;
8198

8299
const isEncrypted = isConnectionEncrypted();
83100
const path = `${basePath}/vnc`;
84101
const port = window.location.port || (isEncrypted ? SECURE : INSECURE);
85-
const url = `${isEncrypted ? WSS : WS}://${
86-
window.location.hostname
87-
}:${port}${path}?preserveSession=${Boolean(preserveSession).toString()}`;
102+
const url = buildUrl({
103+
hostname: window.location.hostname,
104+
path,
105+
port,
106+
preserveSession,
107+
protocol: isEncrypted ? WSS : WS,
108+
});
88109
const rfbInst = new RFBCreate(staticRenderLocationRef.current, url);
89110
rfbInst.addEventListener(
90111
'connect',
91112
() => sessionID === sessionRef.current && setVncState(() => ({ state: connected })),
92113
);
93-
rfbInst.addEventListener('disconnect', (args) => {
114+
rfbInst.addEventListener('disconnect', () => {
94115
// prevent disconnect for old session to interact with current connection attempt
95-
if (sessionID === sessionRef.current) {
96-
postDisconnectCleanup(isSessionAlreadyInUse(args));
116+
if (sessionID === sessionRef.current && !abnormalDisconnect) {
117+
postDisconnectCleanup(false);
118+
}
119+
});
120+
121+
// noVNC's public API (disconnect event) does not expose error codes.
122+
// To detect specific WebSocket close codes we need to add our own callback.
123+
// The original callback will be called inside the wrapper.
124+
// Note that clean-up is not affected (will be performed by rfb as before).
125+
const rfbSocketClose = rfbInst._socketClose.bind(rfbInst);
126+
rfbInst._sock.on('close', (args) => {
127+
// 1006 code === connection was closed abnormally
128+
// triggered by 5xx HTTP server error
129+
abnormalDisconnect = args.code === 1006;
130+
rfbSocketClose(args);
131+
132+
if (!abnormalDisconnect) {
133+
//continue standard disconnect flow
134+
return;
97135
}
136+
// try HTTP request to the same url as websockets
137+
// if it fails with specific error message then VNC is in use
138+
consoleFetchText(
139+
buildUrl({
140+
hostname: window.location.hostname,
141+
path,
142+
port,
143+
preserveSession,
144+
protocol: isEncrypted ? HTTPS : HTTP,
145+
}),
146+
)
147+
.then(() => postDisconnectCleanup(false))
148+
.catch((error) => postDisconnectCleanup(isSessionAlreadyInUse(error)));
98149
});
150+
99151
rfbInst.viewOnly = viewOnly;
100152
rfbInst.scaleViewport = scaleViewport;
101153

src/utils/components/Consoles/components/vnc-console/utils/util.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ export const HORIZONTAL_TAB = 9;
1010
export const LATIN_1_FIRST_CHAR = 0x20;
1111
export const LATIN_1_LAST_CHAR = 0xff;
1212

13-
// TODO implement based on the error returned from the backend
14-
export const isSessionAlreadyInUse = (_arg) => false;
13+
const VNC_IN_USE_ERROR_TEXT = 'Active VNC connection. Request denied.';
14+
15+
export const isSessionAlreadyInUse = (error: Error): boolean => {
16+
return error?.message?.includes?.(VNC_IN_USE_ERROR_TEXT) ?? false;
17+
};
1518

1619
export const isConnectableState = (state: ConsoleState) =>
1720
[

0 commit comments

Comments
 (0)