mirror of https://github.com/jetkvm/kvm.git
fix: hidRPC handshake packet should be only sent once (#969)
This commit is contained in:
parent
05446df047
commit
2175e5f6b6
|
|
@ -37,6 +37,7 @@
|
||||||
"react-xtermjs": "^1.0.10",
|
"react-xtermjs": "^1.0.10",
|
||||||
"recharts": "^3.3.0",
|
"recharts": "^3.3.0",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"tslog": "^4.10.2",
|
||||||
"usehooks-ts": "^3.1.1",
|
"usehooks-ts": "^3.1.1",
|
||||||
"validator": "^13.15.20",
|
"validator": "^13.15.20",
|
||||||
"zustand": "^4.5.2"
|
"zustand": "^4.5.2"
|
||||||
|
|
@ -7330,6 +7331,18 @@
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
|
"node_modules/tslog": {
|
||||||
|
"version": "4.10.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslog/-/tslog-4.10.2.tgz",
|
||||||
|
"integrity": "sha512-XuELoRpMR+sq8fuWwX7P0bcj+PRNiicOKDEb3fGNURhxWVyykCi9BNq7c4uVz7h7P0sj8qgBsr5SWS6yBClq3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fullstack-build/tslog?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@
|
||||||
"react-xtermjs": "^1.0.10",
|
"react-xtermjs": "^1.0.10",
|
||||||
"recharts": "^3.3.0",
|
"recharts": "^3.3.0",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"tslog": "^4.10.2",
|
||||||
"usehooks-ts": "^3.1.1",
|
"usehooks-ts": "^3.1.1",
|
||||||
"validator": "^13.15.20",
|
"validator": "^13.15.20",
|
||||||
"zustand": "^4.5.2"
|
"zustand": "^4.5.2"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useCallback, useEffect, useMemo } from "react";
|
import { useCallback, useEffect, useMemo } from "react";
|
||||||
|
import { Logger } from "tslog";
|
||||||
|
|
||||||
import { useRTCStore } from "@hooks/stores";
|
import { useRTCStore } from "@hooks/stores";
|
||||||
|
|
||||||
|
|
@ -25,6 +26,128 @@ interface sendMessageParams {
|
||||||
requireOrdered?: boolean;
|
requireOrdered?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HANDSHAKE_TIMEOUT = 30 * 1000; // 30 seconds
|
||||||
|
const HANDSHAKE_MAX_ATTEMPTS = 10;
|
||||||
|
const logger = new Logger({ name: "hidrpc" });
|
||||||
|
|
||||||
|
export function doRpcHidHandshake(rpcHidChannel: RTCDataChannel, setRpcHidProtocolVersion: (version: number | null) => void) {
|
||||||
|
let attempts = 0;
|
||||||
|
let lastConnectedTime: Date | undefined;
|
||||||
|
let lastSendTime: Date | undefined;
|
||||||
|
let handshakeCompleted = false;
|
||||||
|
let handshakeInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
const shouldGiveUp = () => {
|
||||||
|
if (attempts > HANDSHAKE_MAX_ATTEMPTS) {
|
||||||
|
logger.error(`Failed to send handshake message after ${HANDSHAKE_MAX_ATTEMPTS} attempts`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeSinceConnected = lastConnectedTime ? Date.now() - lastConnectedTime.getTime() : 0;
|
||||||
|
if (timeSinceConnected > HANDSHAKE_TIMEOUT) {
|
||||||
|
logger.error(`Handshake timed out after ${timeSinceConnected}ms`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendHandshake = (initial: boolean) => {
|
||||||
|
if (handshakeCompleted) return;
|
||||||
|
|
||||||
|
attempts++;
|
||||||
|
lastSendTime = new Date();
|
||||||
|
|
||||||
|
if (!initial && shouldGiveUp()) {
|
||||||
|
if (handshakeInterval) {
|
||||||
|
clearInterval(handshakeInterval);
|
||||||
|
handshakeInterval = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data: Uint8Array | undefined;
|
||||||
|
try {
|
||||||
|
const message = new HandshakeMessage(HID_RPC_VERSION);
|
||||||
|
data = message.marshal();
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("Failed to marshal message", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!data) return;
|
||||||
|
rpcHidChannel.send(data as unknown as ArrayBuffer);
|
||||||
|
|
||||||
|
if (initial) {
|
||||||
|
handshakeInterval = setInterval(() => {
|
||||||
|
sendHandshake(false);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMessage = (ev: MessageEvent) => {
|
||||||
|
const message = unmarshalHidRpcMessage(new Uint8Array(ev.data));
|
||||||
|
if (!message || !(message instanceof HandshakeMessage)) return;
|
||||||
|
|
||||||
|
if (!message.version) {
|
||||||
|
logger.error("Received handshake message without version", message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.version > HID_RPC_VERSION) {
|
||||||
|
// we assume that the UI is always using the latest version of the HID RPC protocol
|
||||||
|
// so we can't support this
|
||||||
|
// TODO: use capabilities to determine rather than version number
|
||||||
|
logger.error("Server is using a newer version than the client", message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRpcHidProtocolVersion(message.version);
|
||||||
|
|
||||||
|
const timeUsed = lastSendTime ? Date.now() - lastSendTime.getTime() : 0;
|
||||||
|
logger.info(`Handshake completed in ${timeUsed}ms after ${attempts} attempts (Version: ${message.version} / ${HID_RPC_VERSION})`);
|
||||||
|
|
||||||
|
// clean up
|
||||||
|
rpcHidChannel.removeEventListener("message", onMessage);
|
||||||
|
resetHandshake({ completed: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetHandshake = ({ lastConnectedTime: newLastConnectedTime, completed }: { lastConnectedTime?: Date | undefined, completed?: boolean }) => {
|
||||||
|
if (newLastConnectedTime) lastConnectedTime = newLastConnectedTime;
|
||||||
|
lastSendTime = undefined;
|
||||||
|
attempts = 0;
|
||||||
|
if (completed !== undefined) handshakeCompleted = completed;
|
||||||
|
if (handshakeInterval) {
|
||||||
|
clearInterval(handshakeInterval);
|
||||||
|
handshakeInterval = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onConnected = () => {
|
||||||
|
resetHandshake({ lastConnectedTime: new Date() });
|
||||||
|
logger.info("Channel connected");
|
||||||
|
|
||||||
|
sendHandshake(true);
|
||||||
|
rpcHidChannel.addEventListener("message", onMessage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
resetHandshake({ lastConnectedTime: undefined, completed: false });
|
||||||
|
|
||||||
|
logger.info("Channel closed");
|
||||||
|
setRpcHidProtocolVersion(null);
|
||||||
|
|
||||||
|
rpcHidChannel.removeEventListener("message", onMessage);
|
||||||
|
};
|
||||||
|
|
||||||
|
rpcHidChannel.addEventListener("open", onConnected);
|
||||||
|
rpcHidChannel.addEventListener("close", onClose);
|
||||||
|
|
||||||
|
// handle case where channel is already open when the hook is mounted
|
||||||
|
if (rpcHidChannel.readyState === "open") {
|
||||||
|
onConnected();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
|
export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
|
||||||
const {
|
const {
|
||||||
rpcHidChannel,
|
rpcHidChannel,
|
||||||
|
|
@ -78,7 +201,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
|
||||||
try {
|
try {
|
||||||
data = message.marshal();
|
data = message.marshal();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to marshal HID RPC message", e);
|
logger.error("Failed to marshal message", e);
|
||||||
}
|
}
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
|
|
@ -151,99 +274,46 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
|
||||||
sendMessage(KEEPALIVE_MESSAGE);
|
sendMessage(KEEPALIVE_MESSAGE);
|
||||||
}, [sendMessage]);
|
}, [sendMessage]);
|
||||||
|
|
||||||
const sendHandshake = useCallback(() => {
|
|
||||||
if (hidRpcDisabled) return;
|
|
||||||
if (rpcHidProtocolVersion) return;
|
|
||||||
if (!rpcHidChannel) return;
|
|
||||||
|
|
||||||
sendMessage(new HandshakeMessage(HID_RPC_VERSION), { ignoreHandshakeState: true });
|
|
||||||
}, [rpcHidChannel, rpcHidProtocolVersion, sendMessage, hidRpcDisabled]);
|
|
||||||
|
|
||||||
const handleHandshake = useCallback(
|
|
||||||
(message: HandshakeMessage) => {
|
|
||||||
if (hidRpcDisabled) return;
|
|
||||||
|
|
||||||
if (!message.version) {
|
|
||||||
console.error("Received handshake message without version", message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.version > HID_RPC_VERSION) {
|
|
||||||
// we assume that the UI is always using the latest version of the HID RPC protocol
|
|
||||||
// so we can't support this
|
|
||||||
// TODO: use capabilities to determine rather than version number
|
|
||||||
console.error("Server is using a newer HID RPC version than the client", message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setRpcHidProtocolVersion(message.version);
|
|
||||||
},
|
|
||||||
[setRpcHidProtocolVersion, hidRpcDisabled],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!rpcHidChannel) return;
|
if (!rpcHidChannel) return;
|
||||||
if (hidRpcDisabled) return;
|
if (hidRpcDisabled) return;
|
||||||
|
|
||||||
// send handshake message
|
|
||||||
sendHandshake();
|
|
||||||
|
|
||||||
const messageHandler = (e: MessageEvent) => {
|
const messageHandler = (e: MessageEvent) => {
|
||||||
if (typeof e.data === "string") {
|
if (typeof e.data === "string") {
|
||||||
console.warn("Received string data in HID RPC message handler", e.data);
|
logger.warn("Received string data in message handler", e.data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = unmarshalHidRpcMessage(new Uint8Array(e.data));
|
const message = unmarshalHidRpcMessage(new Uint8Array(e.data));
|
||||||
if (!message) {
|
if (!message) {
|
||||||
console.warn("Received invalid HID RPC message", e.data);
|
logger.warn("Received invalid message", e.data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug("Received HID RPC message", message);
|
if (message instanceof HandshakeMessage) return; // handshake message is handled by the doRpcHidHandshake function
|
||||||
switch (message.constructor) {
|
|
||||||
case HandshakeMessage:
|
// to remove it from the production build, we need to use the /* @__PURE__ */ comment here
|
||||||
handleHandshake(message as HandshakeMessage);
|
// setting `esbuild.pure` doesn't work
|
||||||
break;
|
/* @__PURE__ */ logger.debug("Received message", message);
|
||||||
default:
|
|
||||||
// not all events are handled here, the rest are handled by the onHidRpcMessage callback
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
onHidRpcMessage?.(message);
|
onHidRpcMessage?.(message);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openHandler = () => {
|
|
||||||
console.info("HID RPC channel opened");
|
|
||||||
sendHandshake();
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeHandler = () => {
|
|
||||||
console.info("HID RPC channel closed");
|
|
||||||
setRpcHidProtocolVersion(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const errorHandler = (e: Event) => {
|
const errorHandler = (e: Event) => {
|
||||||
console.error(`Error on rpcHidChannel '${rpcHidChannel.label}': ${e}`)
|
logger.error(`Error on channel '${rpcHidChannel.label}'`, e);
|
||||||
};
|
};
|
||||||
|
|
||||||
rpcHidChannel.addEventListener("message", messageHandler);
|
rpcHidChannel.addEventListener("message", messageHandler);
|
||||||
rpcHidChannel.addEventListener("close", closeHandler);
|
|
||||||
rpcHidChannel.addEventListener("error", errorHandler);
|
rpcHidChannel.addEventListener("error", errorHandler);
|
||||||
rpcHidChannel.addEventListener("open", openHandler);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
rpcHidChannel.removeEventListener("message", messageHandler);
|
rpcHidChannel.removeEventListener("message", messageHandler);
|
||||||
rpcHidChannel.removeEventListener("close", closeHandler);
|
|
||||||
rpcHidChannel.removeEventListener("error", errorHandler);
|
rpcHidChannel.removeEventListener("error", errorHandler);
|
||||||
rpcHidChannel.removeEventListener("open", openHandler);
|
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
rpcHidChannel,
|
rpcHidChannel,
|
||||||
onHidRpcMessage,
|
onHidRpcMessage,
|
||||||
setRpcHidProtocolVersion,
|
setRpcHidProtocolVersion,
|
||||||
sendHandshake,
|
|
||||||
handleHandshake,
|
|
||||||
hidRpcDisabled,
|
hidRpcDisabled,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ import {
|
||||||
} from "@components/VideoOverlay";
|
} from "@components/VideoOverlay";
|
||||||
import { FeatureFlagProvider } from "@providers/FeatureFlagProvider";
|
import { FeatureFlagProvider } from "@providers/FeatureFlagProvider";
|
||||||
import { m } from "@localizations/messages.js";
|
import { m } from "@localizations/messages.js";
|
||||||
|
import { doRpcHidHandshake } from "@hooks/useHidRpc";
|
||||||
|
|
||||||
export type AuthMode = "password" | "noPassword" | null;
|
export type AuthMode = "password" | "noPassword" | null;
|
||||||
|
|
||||||
|
|
@ -127,6 +128,7 @@ export default function KvmIdRoute() {
|
||||||
setRpcHidChannel,
|
setRpcHidChannel,
|
||||||
setRpcHidUnreliableNonOrderedChannel,
|
setRpcHidUnreliableNonOrderedChannel,
|
||||||
setRpcHidUnreliableChannel,
|
setRpcHidUnreliableChannel,
|
||||||
|
setRpcHidProtocolVersion,
|
||||||
} = useRTCStore();
|
} = useRTCStore();
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
@ -498,6 +500,7 @@ export default function KvmIdRoute() {
|
||||||
rpcHidChannel.onopen = () => {
|
rpcHidChannel.onopen = () => {
|
||||||
setRpcHidChannel(rpcHidChannel);
|
setRpcHidChannel(rpcHidChannel);
|
||||||
};
|
};
|
||||||
|
doRpcHidHandshake(rpcHidChannel, setRpcHidProtocolVersion);
|
||||||
|
|
||||||
const rpcHidUnreliableChannel = pc.createDataChannel("hidrpc-unreliable-ordered", {
|
const rpcHidUnreliableChannel = pc.createDataChannel("hidrpc-unreliable-ordered", {
|
||||||
ordered: true,
|
ordered: true,
|
||||||
|
|
@ -534,6 +537,7 @@ export default function KvmIdRoute() {
|
||||||
setRpcHidChannel,
|
setRpcHidChannel,
|
||||||
setRpcHidUnreliableNonOrderedChannel,
|
setRpcHidUnreliableNonOrderedChannel,
|
||||||
setRpcHidUnreliableChannel,
|
setRpcHidUnreliableChannel,
|
||||||
|
setRpcHidProtocolVersion,
|
||||||
setTransceiver,
|
setTransceiver,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ export default defineConfig(({ mode, command }) => {
|
||||||
return {
|
return {
|
||||||
plugins,
|
plugins,
|
||||||
esbuild: {
|
esbuild: {
|
||||||
pure: ["console.debug"],
|
pure: command === "build" ? ["console.debug"]: [],
|
||||||
},
|
},
|
||||||
assetsInclude: ["**/*.woff2"],
|
assetsInclude: ["**/*.woff2"],
|
||||||
build: {
|
build: {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue