feat(humanize): implement CDP Isolated Worlds and trusted keyboard events (fixes #110)

This commit is contained in:
lilos
2026-04-06 01:43:46 +02:00
parent eb4efef329
commit ccda93669e
8 changed files with 3471 additions and 89 deletions
+97 -20
View File
@@ -1,9 +1,14 @@
"""cloakbrowser-human — Human-like keyboard input."""
"""cloakbrowser-human — Human-like keyboard input.
Stealth-aware: when a CDP session is provided, shift symbols are typed
via CDP Input.dispatchKeyEvent (isTrusted=true, no evaluate stack trace).
Falls back to page.evaluate when no CDP session is available.
"""
from __future__ import annotations
import random
from typing import Any, Protocol
from typing import Any, Optional, Protocol
from .config import HumanConfig, rand, rand_range, sleep_ms
@@ -28,6 +33,25 @@ NEARBY_KEYS = {
'6': '57ty', '7': '68yu', '8': '79ui', '9': '80io', '0': '9p',
}
# CDP key code for each shift symbol's physical key.
_SHIFT_SYMBOL_CODES: dict[str, str] = {
'!': 'Digit1', '@': 'Digit2', '#': 'Digit3', '$': 'Digit4',
'%': 'Digit5', '^': 'Digit6', '&': 'Digit7', '*': 'Digit8',
'(': 'Digit9', ')': 'Digit0', '_': 'Minus', '+': 'Equal',
'{': 'BracketLeft', '}': 'BracketRight', '|': 'Backslash',
':': 'Semicolon', '"': 'Quote', '<': 'Comma', '>': 'Period',
'?': 'Slash', '~': 'Backquote',
}
# Windows virtual key codes for Input.dispatchKeyEvent.
_SHIFT_SYMBOL_KEYCODES: dict[str, int] = {
'!': 49, '@': 50, '#': 51, '$': 52, '%': 53,
'^': 54, '&': 55, '*': 56, '(': 57, ')': 48,
'_': 189, '+': 187, '{': 219, '}': 221, '|': 220,
':': 186, '"': 222, '<': 188, '>': 190, '?': 191,
'~': 192,
}
def _get_nearby_key(ch: str) -> str:
"""Return a random adjacent key for the given character."""
@@ -39,7 +63,17 @@ def _get_nearby_key(ch: str) -> str:
return ch
def human_type(page: Any, raw: RawKeyboard, text: str, cfg: HumanConfig) -> None:
def human_type(
page: Any, raw: RawKeyboard, text: str, cfg: HumanConfig,
cdp_session: Any = None,
) -> None:
"""Type text with human-like per-character timing.
Args:
cdp_session: If provided, shift symbols use CDP Input.dispatchKeyEvent
producing isTrusted=true events with no evaluate stack trace.
If None, falls back to page.evaluate (detectable).
"""
for i, ch in enumerate(text):
# Non-ASCII characters (Cyrillic, CJK, emoji) — use insertText
if not ch.isascii():
@@ -62,7 +96,7 @@ def human_type(page: Any, raw: RawKeyboard, text: str, cfg: HumanConfig) -> None
if ch.isupper() and ch.isalpha():
_type_shifted_char(page, raw, ch, cfg)
elif ch in SHIFT_SYMBOLS:
_type_shift_symbol(page, raw, ch, cfg)
_type_shift_symbol(page, raw, ch, cfg, cdp_session)
else:
_type_normal_char(raw, ch, cfg)
@@ -86,22 +120,65 @@ def _type_shifted_char(page: Any, raw: RawKeyboard, ch: str, cfg: HumanConfig) -
raw.up("Shift")
def _type_shift_symbol(page: Any, raw: RawKeyboard, ch: str, cfg: HumanConfig) -> None:
raw.down("Shift")
sleep_ms(rand_range(cfg.shift_down_delay))
raw.insert_text(ch)
page.evaluate(
"""(key) => {
const el = document.activeElement;
if (el) {
el.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));
el.dispatchEvent(new KeyboardEvent('keyup', { key, bubbles: true }));
}
}""",
ch,
)
sleep_ms(rand_range(cfg.shift_up_delay))
raw.up("Shift")
def _type_shift_symbol(
page: Any, raw: RawKeyboard, ch: str, cfg: HumanConfig,
cdp_session: Any = None,
) -> None:
"""Type a shift symbol character.
Stealth path (cdp_session provided):
Uses CDP Input.dispatchKeyEvent → isTrusted=true, clean stack.
Fallback path (no cdp_session):
Uses raw.insertText + page.evaluate to dispatch synthetic KeyboardEvent.
Detectable via isTrusted=false and evaluate stack frame.
"""
if cdp_session is not None:
# --- Stealth path: CDP Input.dispatchKeyEvent ---
code = _SHIFT_SYMBOL_CODES.get(ch, '')
key_code = _SHIFT_SYMBOL_KEYCODES.get(ch, 0)
raw.down("Shift")
sleep_ms(rand_range(cfg.shift_down_delay))
cdp_session.send("Input.dispatchKeyEvent", {
"type": "keyDown",
"modifiers": 8, # Shift modifier flag
"key": ch,
"code": code,
"windowsVirtualKeyCode": key_code,
"text": ch,
"unmodifiedText": ch,
})
sleep_ms(rand_range(cfg.key_hold))
cdp_session.send("Input.dispatchKeyEvent", {
"type": "keyUp",
"modifiers": 8,
"key": ch,
"code": code,
"windowsVirtualKeyCode": key_code,
})
sleep_ms(rand_range(cfg.shift_up_delay))
raw.up("Shift")
else:
# --- Fallback path: page.evaluate (detectable) ---
raw.down("Shift")
sleep_ms(rand_range(cfg.shift_down_delay))
raw.insert_text(ch)
page.evaluate(
"""(key) => {
const el = document.activeElement;
if (el) {
el.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));
el.dispatchEvent(new KeyboardEvent('keyup', { key, bubbles: true }));
}
}""",
ch,
)
sleep_ms(rand_range(cfg.shift_up_delay))
raw.up("Shift")
def _inter_char_delay(cfg: HumanConfig) -> None: