mirror of https://github.com/jetkvm/kvm.git
470 lines
13 KiB
TypeScript
470 lines
13 KiB
TypeScript
import { parse as uuidParse , stringify as uuidStringify } from "uuid";
|
|
|
|
import { hidKeyBufferSize, KeyboardLedState, KeysDownState } from "./stores";
|
|
|
|
export const HID_RPC_MESSAGE_TYPES = {
|
|
Handshake: 0x01,
|
|
KeyboardReport: 0x02,
|
|
PointerReport: 0x03,
|
|
WheelReport: 0x04,
|
|
KeypressReport: 0x05,
|
|
KeypressKeepAliveReport: 0x09,
|
|
MouseReport: 0x06,
|
|
KeyboardMacroReport: 0x07,
|
|
CancelKeyboardMacroReport: 0x08,
|
|
KeyboardLedState: 0x32,
|
|
KeysDownState: 0x33,
|
|
KeyboardMacroState: 0x34,
|
|
CancelKeyboardMacroByTokenReport: 0x35,
|
|
}
|
|
|
|
export type HidRpcMessageType = typeof HID_RPC_MESSAGE_TYPES[keyof typeof HID_RPC_MESSAGE_TYPES];
|
|
|
|
export const HID_RPC_VERSION = 0x01;
|
|
|
|
const withinUint8Range = (value: number) => {
|
|
return value >= 0 && value <= 255;
|
|
};
|
|
|
|
const fromInt32toUint8 = (n: number) => {
|
|
if (n !== n >> 0) {
|
|
throw new Error(`Number ${n} is not within the int32 range`);
|
|
}
|
|
|
|
return new Uint8Array([
|
|
(n >> 24) & 0xFF,
|
|
(n >> 16) & 0xFF,
|
|
(n >> 8) & 0xFF,
|
|
n & 0xFF,
|
|
]);
|
|
};
|
|
|
|
const fromUint16toUint8 = (n: number) => {
|
|
if (n > 65535 || n < 0) {
|
|
throw new Error(`Number ${n} is not within the uint16 range`);
|
|
}
|
|
|
|
return new Uint8Array([
|
|
(n >> 8) & 0xFF,
|
|
n & 0xFF,
|
|
]);
|
|
};
|
|
|
|
const fromUint32toUint8 = (n: number) => {
|
|
if (n > 4294967295 || n < 0) {
|
|
throw new Error(`Number ${n} is not within the uint32 range`);
|
|
}
|
|
|
|
return new Uint8Array([
|
|
(n >> 24) & 0xFF,
|
|
(n >> 16) & 0xFF,
|
|
(n >> 8) & 0xFF,
|
|
n & 0xFF,
|
|
]);
|
|
};
|
|
|
|
const fromInt8ToUint8 = (n: number) => {
|
|
if (n < -128 || n > 127) {
|
|
throw new Error(`Number ${n} is not within the int8 range`);
|
|
}
|
|
|
|
return n & 0xFF;
|
|
};
|
|
|
|
const keyboardLedStateMasks = {
|
|
num_lock: 1 << 0,
|
|
caps_lock: 1 << 1,
|
|
scroll_lock: 1 << 2,
|
|
compose: 1 << 3,
|
|
kana: 1 << 4,
|
|
shift: 1 << 6,
|
|
}
|
|
|
|
export class RpcMessage {
|
|
messageType: HidRpcMessageType;
|
|
|
|
constructor(messageType: HidRpcMessageType) {
|
|
this.messageType = messageType;
|
|
}
|
|
|
|
marshal(): Uint8Array {
|
|
throw new Error("Not implemented");
|
|
}
|
|
|
|
public static unmarshal(_data: Uint8Array): RpcMessage | undefined {
|
|
throw new Error("Not implemented");
|
|
}
|
|
}
|
|
|
|
export class HandshakeMessage extends RpcMessage {
|
|
version: number;
|
|
|
|
constructor(version: number) {
|
|
super(HID_RPC_MESSAGE_TYPES.Handshake);
|
|
this.version = version;
|
|
}
|
|
|
|
marshal(): Uint8Array {
|
|
return new Uint8Array([this.messageType, this.version]);
|
|
}
|
|
|
|
public static unmarshal(data: Uint8Array): HandshakeMessage | undefined {
|
|
if (data.length < 1) {
|
|
throw new Error(`Invalid handshake message length: ${data.length}`);
|
|
}
|
|
|
|
return new HandshakeMessage(data[0]);
|
|
}
|
|
}
|
|
|
|
export class KeypressReportMessage extends RpcMessage {
|
|
private _key = 0;
|
|
private _press = false;
|
|
|
|
get key(): number {
|
|
return this._key;
|
|
}
|
|
|
|
set key(value: number) {
|
|
if (!withinUint8Range(value)) {
|
|
throw new Error(`Key ${value} is not within the uint8 range`);
|
|
}
|
|
|
|
this._key = value;
|
|
}
|
|
|
|
get press(): boolean {
|
|
return this._press;
|
|
}
|
|
|
|
set press(value: boolean) {
|
|
this._press = value;
|
|
}
|
|
|
|
constructor(key: number, press: boolean) {
|
|
super(HID_RPC_MESSAGE_TYPES.KeypressReport);
|
|
this.key = key;
|
|
this.press = press;
|
|
}
|
|
|
|
marshal(): Uint8Array {
|
|
return new Uint8Array([
|
|
this.messageType,
|
|
this.key,
|
|
this.press ? 1 : 0,
|
|
]);
|
|
}
|
|
|
|
public static unmarshal(data: Uint8Array): KeypressReportMessage | undefined {
|
|
if (data.length < 1) {
|
|
throw new Error(`Invalid keypress report message length: ${data.length}`);
|
|
}
|
|
|
|
return new KeypressReportMessage(data[0], data[1] === 1);
|
|
}
|
|
}
|
|
|
|
export class KeyboardReportMessage extends RpcMessage {
|
|
private _keys: number[] = [];
|
|
private _modifier = 0;
|
|
|
|
get keys(): number[] {
|
|
return this._keys;
|
|
}
|
|
|
|
set keys(value: number[]) {
|
|
value.forEach((k) => {
|
|
if (!withinUint8Range(k)) {
|
|
throw new Error(`Key ${k} is not within the uint8 range`);
|
|
}
|
|
});
|
|
|
|
this._keys = value;
|
|
}
|
|
|
|
get modifier(): number {
|
|
return this._modifier;
|
|
}
|
|
|
|
set modifier(value: number) {
|
|
if (!withinUint8Range(value)) {
|
|
throw new Error(`Modifier ${value} is not within the uint8 range`);
|
|
}
|
|
|
|
this._modifier = value;
|
|
}
|
|
|
|
constructor(keys: number[], modifier: number) {
|
|
super(HID_RPC_MESSAGE_TYPES.KeyboardReport);
|
|
this.keys = keys;
|
|
this.modifier = modifier;
|
|
}
|
|
|
|
marshal(): Uint8Array {
|
|
return new Uint8Array([
|
|
this.messageType,
|
|
this.modifier,
|
|
...this.keys,
|
|
]);
|
|
}
|
|
|
|
public static unmarshal(data: Uint8Array): KeyboardReportMessage | undefined {
|
|
if (data.length < 1) {
|
|
throw new Error(`Invalid keyboard report message length: ${data.length}`);
|
|
}
|
|
|
|
return new KeyboardReportMessage(Array.from(data.slice(1)), data[0]);
|
|
}
|
|
}
|
|
|
|
export interface KeyboardMacroStep extends KeysDownState {
|
|
delay: number;
|
|
}
|
|
|
|
export class KeyboardMacroReportMessage extends RpcMessage {
|
|
isPaste: boolean;
|
|
stepCount: number;
|
|
steps: KeyboardMacroStep[];
|
|
|
|
KEYS_LENGTH = hidKeyBufferSize;
|
|
|
|
constructor(isPaste: boolean, stepCount: number, steps: KeyboardMacroStep[]) {
|
|
super(HID_RPC_MESSAGE_TYPES.KeyboardMacroReport);
|
|
this.isPaste = isPaste;
|
|
this.stepCount = stepCount;
|
|
this.steps = steps;
|
|
}
|
|
|
|
marshal(): Uint8Array {
|
|
// validate if length is correct
|
|
if (this.stepCount !== this.steps.length) {
|
|
throw new Error(`Length ${this.stepCount} is not equal to the number of steps ${this.steps.length}`);
|
|
}
|
|
|
|
const data = new Uint8Array(this.stepCount * 9 + 6);
|
|
data.set(new Uint8Array([
|
|
this.messageType,
|
|
this.isPaste ? 1 : 0,
|
|
...fromUint32toUint8(this.stepCount),
|
|
]), 0);
|
|
|
|
let offset = 6;
|
|
|
|
for (let i = 0; i < this.stepCount; i++) {
|
|
const step = this.steps[i];
|
|
if (!withinUint8Range(step.modifier)) {
|
|
throw new Error(`Modifier ${step.modifier} is not within the uint8 range`);
|
|
}
|
|
|
|
// Ensure the keys are within the KEYS_LENGTH range
|
|
const keys = step.keys;
|
|
if (keys.length > this.KEYS_LENGTH) {
|
|
throw new Error(`Keys ${keys} is not within the hidKeyBufferSize range`);
|
|
} else if (keys.length < this.KEYS_LENGTH) {
|
|
keys.push(...Array(this.KEYS_LENGTH - keys.length).fill(0));
|
|
}
|
|
|
|
for (const key of keys) {
|
|
if (!withinUint8Range(key)) {
|
|
throw new Error(`Key ${key} is not within the uint8 range`);
|
|
}
|
|
}
|
|
|
|
const macroBinary = new Uint8Array([
|
|
step.modifier,
|
|
...keys,
|
|
...fromUint16toUint8(step.delay),
|
|
]);
|
|
|
|
data.set(macroBinary, offset);
|
|
offset += 9;
|
|
}
|
|
|
|
return data;
|
|
}
|
|
}
|
|
|
|
export class KeyboardMacroStateMessage extends RpcMessage {
|
|
state: boolean;
|
|
isPaste: boolean;
|
|
|
|
constructor(state: boolean, isPaste: boolean) {
|
|
super(HID_RPC_MESSAGE_TYPES.KeyboardMacroState);
|
|
this.state = state;
|
|
this.isPaste = isPaste;
|
|
}
|
|
|
|
marshal(): Uint8Array {
|
|
return new Uint8Array([
|
|
this.messageType,
|
|
this.state ? 1 : 0,
|
|
this.isPaste ? 1 : 0,
|
|
]);
|
|
}
|
|
|
|
public static unmarshal(data: Uint8Array): KeyboardMacroStateMessage | undefined {
|
|
if (data.length < 2) {
|
|
throw new Error(`Invalid keyboard macro state report message length: ${data.length}`);
|
|
}
|
|
|
|
return new KeyboardMacroStateMessage(data[0] === 1, data[1] === 1);
|
|
}
|
|
}
|
|
|
|
export class KeyboardLedStateMessage extends RpcMessage {
|
|
keyboardLedState: KeyboardLedState;
|
|
|
|
constructor(keyboardLedState: KeyboardLedState) {
|
|
super(HID_RPC_MESSAGE_TYPES.KeyboardLedState);
|
|
this.keyboardLedState = keyboardLedState;
|
|
}
|
|
|
|
public static unmarshal(data: Uint8Array): KeyboardLedStateMessage | undefined {
|
|
if (data.length < 1) {
|
|
throw new Error(`Invalid keyboard led state message length: ${data.length}`);
|
|
}
|
|
|
|
const s = data[0];
|
|
|
|
const state = {
|
|
num_lock: (s & keyboardLedStateMasks.num_lock) !== 0,
|
|
caps_lock: (s & keyboardLedStateMasks.caps_lock) !== 0,
|
|
scroll_lock: (s & keyboardLedStateMasks.scroll_lock) !== 0,
|
|
compose: (s & keyboardLedStateMasks.compose) !== 0,
|
|
kana: (s & keyboardLedStateMasks.kana) !== 0,
|
|
shift: (s & keyboardLedStateMasks.shift) !== 0,
|
|
} as KeyboardLedState;
|
|
|
|
return new KeyboardLedStateMessage(state);
|
|
}
|
|
}
|
|
|
|
export class KeysDownStateMessage extends RpcMessage {
|
|
keysDownState: KeysDownState;
|
|
|
|
constructor(keysDownState: KeysDownState) {
|
|
super(HID_RPC_MESSAGE_TYPES.KeysDownState);
|
|
this.keysDownState = keysDownState;
|
|
}
|
|
|
|
public static unmarshal(data: Uint8Array): KeysDownStateMessage | undefined {
|
|
if (data.length < 1) {
|
|
throw new Error(`Invalid keys down state message length: ${data.length}`);
|
|
}
|
|
|
|
return new KeysDownStateMessage({
|
|
modifier: data[0],
|
|
keys: Array.from(data.slice(1))
|
|
});
|
|
}
|
|
}
|
|
|
|
export class PointerReportMessage extends RpcMessage {
|
|
x: number;
|
|
y: number;
|
|
buttons: number;
|
|
|
|
constructor(x: number, y: number, buttons: number) {
|
|
super(HID_RPC_MESSAGE_TYPES.PointerReport);
|
|
this.x = x;
|
|
this.y = y;
|
|
this.buttons = buttons;
|
|
}
|
|
|
|
marshal(): Uint8Array {
|
|
return new Uint8Array([
|
|
this.messageType,
|
|
...fromInt32toUint8(this.x),
|
|
...fromInt32toUint8(this.y),
|
|
this.buttons,
|
|
]);
|
|
}
|
|
}
|
|
|
|
export class CancelKeyboardMacroReportMessage extends RpcMessage {
|
|
token: string;
|
|
|
|
constructor(token: string) {
|
|
super(HID_RPC_MESSAGE_TYPES.CancelKeyboardMacroReport);
|
|
this.token = (token == null || token === undefined || token === "")
|
|
? "00000000-0000-0000-0000-000000000000"
|
|
: token;
|
|
}
|
|
|
|
marshal(): Uint8Array {
|
|
const tokenBytes = uuidParse(this.token);
|
|
return new Uint8Array([this.messageType, ...tokenBytes]);
|
|
}
|
|
|
|
public static unmarshal(data: Uint8Array): CancelKeyboardMacroReportMessage | undefined {
|
|
if (data.length == 0) {
|
|
return new CancelKeyboardMacroReportMessage("00000000-0000-0000-0000-000000000000");
|
|
}
|
|
|
|
if (data.length != 16) {
|
|
throw new Error(`Invalid cancel message length: ${data.length}`);
|
|
}
|
|
|
|
return new CancelKeyboardMacroReportMessage(uuidStringify(data.slice(0, 16)));
|
|
}
|
|
}
|
|
|
|
export class MouseReportMessage extends RpcMessage {
|
|
dx: number;
|
|
dy: number;
|
|
buttons: number;
|
|
|
|
constructor(dx: number, dy: number, buttons: number) {
|
|
super(HID_RPC_MESSAGE_TYPES.MouseReport);
|
|
this.dx = dx;
|
|
this.dy = dy;
|
|
this.buttons = buttons;
|
|
}
|
|
|
|
marshal(): Uint8Array {
|
|
return new Uint8Array([
|
|
this.messageType,
|
|
fromInt8ToUint8(this.dx),
|
|
fromInt8ToUint8(this.dy),
|
|
this.buttons,
|
|
]);
|
|
}
|
|
}
|
|
|
|
export class KeypressKeepAliveMessage extends RpcMessage {
|
|
constructor() {
|
|
super(HID_RPC_MESSAGE_TYPES.KeypressKeepAliveReport);
|
|
}
|
|
|
|
marshal(): Uint8Array {
|
|
return new Uint8Array([this.messageType]);
|
|
}
|
|
}
|
|
|
|
export const messageRegistry = {
|
|
[HID_RPC_MESSAGE_TYPES.Handshake]: HandshakeMessage,
|
|
[HID_RPC_MESSAGE_TYPES.KeysDownState]: KeysDownStateMessage,
|
|
[HID_RPC_MESSAGE_TYPES.KeyboardLedState]: KeyboardLedStateMessage,
|
|
[HID_RPC_MESSAGE_TYPES.KeyboardReport]: KeyboardReportMessage,
|
|
[HID_RPC_MESSAGE_TYPES.KeypressReport]: KeypressReportMessage,
|
|
[HID_RPC_MESSAGE_TYPES.KeyboardMacroReport]: KeyboardMacroReportMessage,
|
|
[HID_RPC_MESSAGE_TYPES.CancelKeyboardMacroReport]: CancelKeyboardMacroReportMessage,
|
|
[HID_RPC_MESSAGE_TYPES.KeyboardMacroState]: KeyboardMacroStateMessage,
|
|
[HID_RPC_MESSAGE_TYPES.KeypressKeepAliveReport]: KeypressKeepAliveMessage,
|
|
[HID_RPC_MESSAGE_TYPES.CancelKeyboardMacroByTokenReport]: CancelKeyboardMacroReportMessage,
|
|
}
|
|
|
|
export const unmarshalHidRpcMessage = (data: Uint8Array): RpcMessage | undefined => {
|
|
if (data.length < 1) {
|
|
throw new Error(`Invalid HID RPC message length: ${data.length}`);
|
|
}
|
|
|
|
const payload = data.slice(1);
|
|
|
|
const messageType = data[0];
|
|
if (!(messageType in messageRegistry)) {
|
|
throw new Error(`Unknown HID RPC message type: ${messageType}`);
|
|
}
|
|
|
|
return messageRegistry[messageType].unmarshal(payload);
|
|
}; |