Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 94 additions & 18 deletions src/server/InputHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,38 @@ export interface InputMessage {
}

export class InputHandler {
// Security Constants (ADDED)
private static readonly MAX_TEXT_LENGTH = 500;
private static readonly MAX_DELTA = 2000;
private static readonly MAX_SCROLL = 500;
private static readonly MAX_ZOOM_INPUT = 1000;
private static readonly MAX_COMBO_KEYS = 5;

constructor() {
mouse.config.mouseSpeed = 1000;
}

// Utility clamp (ADDED)
private clamp(value: number, min: number, max: number): number {
if (!Number.isFinite(value)) return 0;
return Math.max(min, Math.min(max, value));
}

async handleMessage(msg: InputMessage) {
switch (msg.type) {

case 'move':
if (msg.dx !== undefined && msg.dy !== undefined) {

// Clamp dx/dy
const safeDx = this.clamp(msg.dx, -InputHandler.MAX_DELTA, InputHandler.MAX_DELTA);
const safeDy = this.clamp(msg.dy, -InputHandler.MAX_DELTA, InputHandler.MAX_DELTA);

const currentPos = await mouse.getPosition();

await mouse.setPosition(new Point(
currentPos.x + msg.dx,
currentPos.y + msg.dy
currentPos.x + safeDx,
currentPos.y + safeDy
));
}
break;
Expand All @@ -45,21 +64,30 @@ export class InputHandler {
case 'scroll':
const promises: Promise<void>[] = [];

//Clamp scroll values
const safeScrollY = typeof msg.dy === 'number'
? this.clamp(msg.dy, -InputHandler.MAX_SCROLL, InputHandler.MAX_SCROLL)
: 0;

const safeScrollX = typeof msg.dx === 'number'
? this.clamp(msg.dx, -InputHandler.MAX_SCROLL, InputHandler.MAX_SCROLL)
: 0;

// Vertical scroll
if (typeof msg.dy === 'number' && msg.dy !== 0) {
if (msg.dy > 0) {
promises.push(mouse.scrollDown(msg.dy));
if (safeScrollY !== 0) {
if (safeScrollY > 0) {
promises.push(mouse.scrollDown(safeScrollY));
} else {
promises.push(mouse.scrollUp(-msg.dy));
promises.push(mouse.scrollUp(-safeScrollY));
}
}

// Horizontal scroll
if (typeof msg.dx === 'number' && msg.dx !== 0) {
if (msg.dx > 0) {
promises.push(mouse.scrollRight(msg.dx));
if (safeScrollX !== 0) {
if (safeScrollX > 0) {
promises.push(mouse.scrollRight(safeScrollX));
} else {
promises.push(mouse.scrollLeft(-msg.dx));
promises.push(mouse.scrollLeft(-safeScrollX));
}
}

Expand All @@ -70,12 +98,20 @@ export class InputHandler {

case 'zoom':
if (msg.delta !== undefined && msg.delta !== 0) {

// Clamp incoming zoom delta
const safeDelta = this.clamp(
msg.delta,
-InputHandler.MAX_ZOOM_INPUT,
InputHandler.MAX_ZOOM_INPUT
);

const sensitivityFactor = 0.5;
const MAX_ZOOM_STEP = 5;

const scaledDelta =
Math.sign(msg.delta) *
Math.min(Math.abs(msg.delta) * sensitivityFactor, MAX_ZOOM_STEP);
Math.sign(safeDelta) *
Math.min(Math.abs(safeDelta) * sensitivityFactor, MAX_ZOOM_STEP);

const amount = -scaledDelta;

Expand All @@ -91,23 +127,56 @@ export class InputHandler {
case 'key':
if (msg.key) {
console.log(`Processing key: ${msg.key}`);
const nutKey = KEY_MAP[msg.key.toLowerCase()];
const lowerKey = msg.key.toLowerCase();

// Allowlist validation
if (!(lowerKey in KEY_MAP) && lowerKey.length !== 1) {
console.warn(`Blocked unknown key: ${msg.key}`);
return;
}

const nutKey = KEY_MAP[lowerKey];

if (nutKey !== undefined) {
await keyboard.type(nutKey);
} else if (msg.key.length === 1) {
await keyboard.type(msg.key);
} else {
console.log(`Unmapped key: ${msg.key}`);
} else if (lowerKey.length === 1) {
await keyboard.type(lowerKey);
}
}
break;

case 'combo':
if (msg.keys && msg.keys.length > 0) {
const nutKeys: (Key | string)[] = [];

// Limit combo length
if (msg.keys.length > InputHandler.MAX_COMBO_KEYS) {
console.warn('Combo too long — blocked');
return;
}

// Remove duplicate keys
const seen = new Set<string>();
const uniqueKeys: string[] = [];
for (const k of msg.keys) {
const lower = k.toLowerCase();
if (!seen.has(lower)) {
seen.add(lower);
uniqueKeys.push(k); // preserve original for single-char typing
}
}

const nutKeys: (Key | string)[] = [];

for (const k of uniqueKeys) {
const lowerKey = k.toLowerCase();

if (!(lowerKey in KEY_MAP) && lowerKey.length !== 1) {
console.warn(`Blocked unknown key in combo: ${k}`);
continue;
}

const nutKey = KEY_MAP[lowerKey];

if (nutKey !== undefined) {
nutKeys.push(nutKey);
} else if (lowerKey.length === 1) {
Expand Down Expand Up @@ -148,6 +217,13 @@ export class InputHandler {

case 'text':
if (msg.text) {

// Limit text length
if (msg.text.length > InputHandler.MAX_TEXT_LENGTH) {
console.warn('Text input too long — blocked');
return;
}

await keyboard.type(msg.text);
}
break;
Expand Down