Files
2026-07-13 12:20:32 +08:00

246 lines
8.1 KiB
JavaScript

#!/usr/bin/env node
//
// CDP input dispatcher — sends trusted mouse/keyboard events via Chrome DevTools Protocol.
// These produce isTrusted: true events that pass anti-bot checks (LinkedIn, Cloudflare, etc.)
//
// Usage:
// node cdp-input.js click <x> <y> [target_url]
// node cdp-input.js type <text> [target_url]
// node cdp-input.js key <key> [target_url] (e.g. "Enter", "Tab", "Escape")
//
// Connects to http://127.0.0.1:9222, attaches to a page target, dispatches native input.
const WebSocket = require("ws");
const http = require("http");
const action = process.argv[2];
if (!action || !["click", "type", "key"].includes(action)) {
process.stderr.write("Usage: node cdp-input.js <click|type|key> <args...> [target_url]\n");
process.exit(1);
}
function httpGet(url) {
return new Promise((resolve, reject) => {
http.get(url, (res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => resolve(data));
res.on("error", reject);
}).on("error", reject);
});
}
let msgId = 1;
function cdpSend(ws, method, params, sessionId) {
return new Promise((resolve, reject) => {
const id = msgId++;
const msg = { id, method, params: params || {} };
if (sessionId) msg.sessionId = sessionId;
const timeout = setTimeout(() => {
ws.off("message", handler);
reject(new Error(`CDP timeout: ${method}`));
}, 15000);
function handler(raw) {
let parsed;
try { parsed = JSON.parse(raw.toString()); } catch { return; }
if (parsed.id !== id) return;
ws.off("message", handler);
clearTimeout(timeout);
if (parsed.error) {
reject(new Error(`CDP error: ${parsed.error.message}`));
} else {
resolve(parsed.result);
}
}
ws.on("message", handler);
ws.send(JSON.stringify(msg));
});
}
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
// Randomized human-like delay between keystrokes
function keystrokeDelay() {
return 30 + Math.floor(Math.random() * 60);
}
async function main() {
const versionRaw = await httpGet("http://127.0.0.1:9222/json/version");
const version = JSON.parse(versionRaw);
const browserWsUrl = version.webSocketDebuggerUrl;
if (!browserWsUrl) throw new Error("No webSocketDebuggerUrl — is Chromium running?");
const ws = await new Promise((resolve, reject) => {
const socket = new WebSocket(browserWsUrl);
const t = setTimeout(() => reject(new Error("WS connect timeout")), 5000);
socket.on("open", () => { clearTimeout(t); resolve(socket); });
socket.on("error", (err) => { clearTimeout(t); reject(err); });
});
try {
// Find target page
const { targetInfos } = await cdpSend(ws, "Target.getTargets");
const pages = targetInfos.filter(
(t) => t.type === "page" && !t.url.startsWith("chrome://") && !t.url.startsWith("devtools://")
);
if (pages.length === 0) throw new Error("No browser pages open.");
// Determine target URL filter (always last arg if it doesn't look like a coordinate/text)
let targetUrl = "";
if (action === "click") {
targetUrl = process.argv[5] || "";
} else if (action === "type") {
targetUrl = process.argv[4] || "";
} else if (action === "key") {
targetUrl = process.argv[4] || "";
}
let target = pages[pages.length - 1];
if (targetUrl) {
const match = pages.find(
(p) => p.url.includes(targetUrl) || p.title.toLowerCase().includes(targetUrl.toLowerCase())
);
if (!match) {
const openTabs = pages.map((p) => `- ${p.title} ${p.url}`).join("\n");
throw new Error(`No browser tab matching "${targetUrl}". Open tabs:\n${openTabs}`);
}
if (match) target = match;
}
const { sessionId } = await cdpSend(ws, "Target.attachToTarget", {
targetId: target.targetId,
flatten: true,
});
if (action === "click") {
const x = parseFloat(process.argv[3]);
const y = parseFloat(process.argv[4]);
if (isNaN(x) || isNaN(y)) throw new Error("click requires <x> <y> coordinates");
// Move mouse to position first (triggers hover states)
await cdpSend(ws, "Input.dispatchMouseEvent", {
type: "mouseMoved", x, y
}, sessionId);
await sleep(50);
// Press
await cdpSend(ws, "Input.dispatchMouseEvent", {
type: "mousePressed", x, y, button: "left", clickCount: 1
}, sessionId);
await sleep(30 + Math.floor(Math.random() * 40));
// Release
await cdpSend(ws, "Input.dispatchMouseEvent", {
type: "mouseReleased", x, y, button: "left", clickCount: 1
}, sessionId);
process.stdout.write(`clicked at (${x}, ${y})`);
} else if (action === "type") {
const text = process.argv[3];
if (!text) throw new Error("type requires <text> argument");
// Type each character individually with realistic delays
for (let i = 0; i < text.length; i++) {
const char = text[i];
// dispatchKeyEvent with char sends the character as a trusted keyboard input
await cdpSend(ws, "Input.dispatchKeyEvent", {
type: "keyDown",
text: char,
key: char,
code: charToCode(char),
unmodifiedText: char,
}, sessionId);
await cdpSend(ws, "Input.dispatchKeyEvent", {
type: "keyUp",
key: char,
code: charToCode(char),
}, sessionId);
await sleep(keystrokeDelay());
}
process.stdout.write(`typed ${text.length} chars`);
} else if (action === "key") {
const key = process.argv[3];
if (!key) throw new Error("key requires <key> argument (e.g. Enter, Tab, Escape)");
const keyDef = specialKeys[key] || { key, code: `Key${key.toUpperCase()}` };
await cdpSend(ws, "Input.dispatchKeyEvent", {
type: "keyDown",
key: keyDef.key,
code: keyDef.code,
windowsVirtualKeyCode: keyDef.keyCode || 0,
nativeVirtualKeyCode: keyDef.keyCode || 0,
}, sessionId);
await sleep(50);
await cdpSend(ws, "Input.dispatchKeyEvent", {
type: "keyUp",
key: keyDef.key,
code: keyDef.code,
windowsVirtualKeyCode: keyDef.keyCode || 0,
nativeVirtualKeyCode: keyDef.keyCode || 0,
}, sessionId);
process.stdout.write(`pressed ${key}`);
}
} finally {
ws.close();
}
}
// Maps common characters to their Key code values
function charToCode(char) {
if (char >= 'a' && char <= 'z') return `Key${char.toUpperCase()}`;
if (char >= 'A' && char <= 'Z') return `Key${char}`;
if (char >= '0' && char <= '9') return `Digit${char}`;
if (char === ' ') return 'Space';
if (char === '.') return 'Period';
if (char === ',') return 'Comma';
if (char === '/') return 'Slash';
if (char === '@') return 'Digit2';
if (char === '-') return 'Minus';
if (char === '=') return 'Equal';
if (char === '[') return 'BracketLeft';
if (char === ']') return 'BracketRight';
if (char === ';') return 'Semicolon';
if (char === "'") return 'Quote';
if (char === '\\') return 'Backslash';
if (char === '`') return 'Backquote';
return '';
}
const specialKeys = {
Enter: { key: "Enter", code: "Enter", keyCode: 13 },
Tab: { key: "Tab", code: "Tab", keyCode: 9 },
Escape: { key: "Escape", code: "Escape", keyCode: 27 },
Backspace: { key: "Backspace", code: "Backspace", keyCode: 8 },
Delete: { key: "Delete", code: "Delete", keyCode: 46 },
ArrowUp: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
ArrowDown: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
ArrowLeft: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
ArrowRight:{ key: "ArrowRight",code: "ArrowRight",keyCode: 39 },
Home: { key: "Home", code: "Home", keyCode: 36 },
End: { key: "End", code: "End", keyCode: 35 },
PageUp: { key: "PageUp", code: "PageUp", keyCode: 33 },
PageDown: { key: "PageDown", code: "PageDown", keyCode: 34 },
Space: { key: " ", code: "Space", keyCode: 32 },
};
main().catch((err) => {
process.stderr.write(`${err.message}\n`);
process.exit(1);
});