autoreg-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
192
skills/account-validation/references/cdp-browser-automation.md
Normal file
192
skills/account-validation/references/cdp-browser-automation.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# CDP Browser Automation (Windows)
|
||||
|
||||
Reusable Chrome DevTools Protocol pattern for browser automation on Windows.
|
||||
Uses `aiohttp` WebSocket + `subprocess` Chrome launch. Avoids `nodriver`/`playwright` greenlet issues on Python 3.14.
|
||||
|
||||
## CDP Client Class
|
||||
|
||||
```python
|
||||
class CDP:
|
||||
def __init__(self):
|
||||
self.ws = None; self.s = None; self._id = 0
|
||||
|
||||
async def connect_browser(self, ws_url):
|
||||
"""Browser-level connection (for Target.createTarget etc)."""
|
||||
self.s = aiohttp.ClientSession()
|
||||
self.ws = await self.s.ws_connect(ws_url, max_msg_size=0)
|
||||
|
||||
async def connect_page(self, ws_url):
|
||||
"""Page-level connection (for Page.*, Runtime.*)."""
|
||||
self.s = aiohttp.ClientSession()
|
||||
self.ws = await self.s.ws_connect(ws_url, max_msg_size=0)
|
||||
await self.cmd("Page.enable"); await self.cmd("Runtime.enable")
|
||||
|
||||
async def cmd(self, method, params=None):
|
||||
self._id += 1
|
||||
await self.ws.send_json({"id":self._id,"method":method,"params":params or {}})
|
||||
while True:
|
||||
r = await self.ws.receive_json()
|
||||
if r.get("id") == self._id:
|
||||
if "error" in r: return None
|
||||
return r.get("result",{})
|
||||
|
||||
async def js(self, expr, await_promise=False):
|
||||
r = await self.cmd("Runtime.evaluate", {
|
||||
"expression":expr,"returnByValue":True,"awaitPromise":await_promise
|
||||
})
|
||||
if not r or r.get("exceptionDetails"): return None
|
||||
return r.get("result",{}).get("value")
|
||||
```
|
||||
|
||||
## Chrome Launch (Windows)
|
||||
|
||||
```python
|
||||
CHROME = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
|
||||
CDP_PORT = 9229
|
||||
PROFILE = r"C:\path\to\browser_profile"
|
||||
|
||||
def launch_chrome():
|
||||
# Kill existing chrome on port
|
||||
subprocess.run(f'for /f "tokens=5" %a in (\'netstat -ano ^| findstr :{CDP_PORT}\') do taskkill /F /PID %a',
|
||||
shell=True, capture_output=True, timeout=5)
|
||||
subprocess.run('taskkill /F /IM chrome.exe 2>nul', shell=True, capture_output=True, timeout=5)
|
||||
time.sleep(2)
|
||||
|
||||
proc = subprocess.Popen([
|
||||
CHROME,
|
||||
f"--remote-debugging-port={CDP_PORT}",
|
||||
f"--user-data-dir={PROFILE}",
|
||||
"--no-sandbox","--disable-gpu",
|
||||
"--no-first-run","--no-default-browser-check",
|
||||
"--no-restore-last-session", # CRITICAL: prevents extension page restore
|
||||
"--disable-blink-features=AutomationControlled",
|
||||
"--window-size=1280,900",
|
||||
"--new-window","about:blank",
|
||||
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
time.sleep(4)
|
||||
return proc
|
||||
```
|
||||
|
||||
## Connect & Create Tab
|
||||
|
||||
```python
|
||||
# Browser-level
|
||||
async with aiohttp.ClientSession() as s:
|
||||
async with s.get(f"http://127.0.0.1:{CDP_PORT}/json/version") as r:
|
||||
b_ws = (await r.json())["webSocketDebuggerUrl"]
|
||||
|
||||
b_cdp = CDP()
|
||||
await b_cdp.connect_browser(b_ws)
|
||||
tgt = await b_cdp.cmd("Target.createTarget", {"url":"about:blank"})
|
||||
tid = tgt["targetId"]
|
||||
await b_cdp.close()
|
||||
|
||||
# Page-level
|
||||
async with aiohttp.ClientSession() as s:
|
||||
async with s.get(f"http://127.0.0.1:{CDP_PORT}/json") as r:
|
||||
pages = await r.json()
|
||||
p_ws = next(pg["webSocketDebuggerUrl"] for pg in pages if pg.get("id") == tid)
|
||||
|
||||
cdp = CDP()
|
||||
await cdp.connect_page(p_ws)
|
||||
```
|
||||
|
||||
## Common Operations
|
||||
|
||||
```python
|
||||
# Navigate
|
||||
await cdp.cmd("Page.navigate", {"url": url})
|
||||
# Wait for load
|
||||
while await cdp.js("document.readyState") != "complete":
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# Click element by text
|
||||
await cdp.js(f"""
|
||||
(function(){{
|
||||
var els=document.querySelectorAll('button,a,span');
|
||||
for(var i=0;i<els.length;i++){{
|
||||
if(els[i].textContent.includes({json.dumps(text)})){{els[i].click();return true;}}
|
||||
}}
|
||||
return false;
|
||||
}})()
|
||||
""")
|
||||
|
||||
# Fill input — STANDARD (works for vanilla HTML)
|
||||
await cdp.js(f"""
|
||||
(function(){{
|
||||
var el=document.querySelector('input[name="{name}"]');
|
||||
if(!el)return false;
|
||||
el.focus();el.value={json.dumps(value)};
|
||||
el.dispatchEvent(new Event('input',{{bubbles:true}}));
|
||||
el.dispatchEvent(new Event('change',{{bubbles:true}}));
|
||||
return true;
|
||||
}})()
|
||||
""")
|
||||
|
||||
# Fill input — REACT (use for React/Next.js apps)
|
||||
# React hijacks HTMLInputElement.prototype.value. Standard el.value = 'x'
|
||||
# does NOT trigger React state. Buttons stay disabled. Use native setter:
|
||||
await cdp.js(f"""
|
||||
(function(){{
|
||||
var nativeSet = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLInputElement.prototype, 'value'
|
||||
).set;
|
||||
var el = document.querySelector('input[type="email"]');
|
||||
if(!el) return false;
|
||||
nativeSet.call(el, {json.dumps(value)});
|
||||
el.dispatchEvent(new Event('input', {{bubbles: true}}));
|
||||
return true;
|
||||
}})()
|
||||
""")
|
||||
|
||||
# Check all checkboxes
|
||||
await cdp.js("""
|
||||
(function(){var cbs=document.querySelectorAll('input[type="checkbox"]');
|
||||
for(var i=0;i<cbs.length;i++){cbs[i].checked=true;
|
||||
cbs[i].dispatchEvent(new Event('change',{bubbles:true}));}
|
||||
return cbs.length;})()
|
||||
""")
|
||||
|
||||
# Get URL / page text
|
||||
url = await cdp.js("window.location.href")
|
||||
text = await cdp.js("document.body?document.body.innerText.substring(0,3000):''")
|
||||
```
|
||||
|
||||
## Pitfalls
|
||||
|
||||
1. **React inputs — nativeSet REQUIRED**: React apps (TokenHarbor, Mistral, Cursor, ElevenLabs, etc.) hijack `HTMLInputElement.prototype.value`. Standard `el.value = 'x'` fills the field visually but React state doesn't update → buttons stay disabled. Use `Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set.call(el, value)` + `dispatchEvent('input')`. See `references/react-input-fix.md` in `th-autoreg` skill.
|
||||
|
||||
2. **`Page.enable` not found** — connected to browser-level WS instead of page-level. Use `connect_browser()` for browser-level, `connect_page()` for page-level.
|
||||
|
||||
3. **`--no-restore-last-session` is critical** — without it, Chrome restores previous session tabs (often extension pages like `chrome-extension://...`), and the URL argument is ignored.
|
||||
|
||||
4. **Profile reuse** — same profile means same cookies/storage. Use unique profiles for isolation.
|
||||
|
||||
5. **Port conflicts** — kill existing chrome on the same CDP port before launching.
|
||||
|
||||
6. **Python 3.14 + greenlet** — `nodriver` and `patchright` crash with `ModuleNotFoundError: No module named 'greenlet._greenlet'`. Use CDP directly (no greenlet dependency) or Python 3.11.
|
||||
|
||||
7. **CDP WebSocket response ordering** — rapid `Runtime.evaluate` calls can return responses out of order. Add `time.sleep(0.5-1)` between calls, or use sequential `await` with a single `Runtime.evaluate` per turn.
|
||||
|
||||
8. **Tab reuse for multi-account flows** — always create fresh CDP tabs. Reusing logged-in tabs breaks signup flows. Use `PUT /json/new?url=about:blank` for each new registration.
|
||||
|
||||
9. **CRITICAL — Multi-thread cookie sharing**: All CDP tabs in a single Chrome instance share the same cookie jar. When multiple threads each create their own tab, the first thread's login sets cookies that ALL other threads see. Subsequent signups redirect to dashboard (already logged in), the script thinks signup succeeded, and creates API keys on the first account. **Result: 100+ keys on 1 account.** Fix: (a) `Network.clearBrowserCookies` + clear localStorage/IndexedDB before each account, AND (b) run single-threaded. With multiple threads, `clearBrowserCookies` wipes cookies from ALL tabs simultaneously — thread A's login gets wiped by thread B's clear. For parallelism, launch separate Chrome instances with different `--user-data-dir` and `--remote-debugging-port` per thread.
|
||||
|
||||
10. **Python 3.11 + hermes venv PermissionError**: When running `py -3.11` or `python3`, the hermes venv's site-packages can leak into `sys.path`, causing `PermissionError: [Errno 13]` on playwright/camoufox imports. Fix: `PYTHONPATH= py -3.11 script.py` or use `-I` (isolated mode).
|
||||
|
||||
11. **Sync CDP client (simpler than async)**: For single-threaded scripts, use `websocket-client` + `urllib.request` instead of aiohttp. No event loop, no greenlet issues. See `references/github-autoreg-email-domain-blocklist.md` for the full pattern.
|
||||
|
||||
12. **Camoufox proxy format**: `playwright_proxy_config()` from `core/proxy_utils.py` splits proxy URLs into `{server, username, password}`. Embedding credentials in the `server` field causes `NS_ERROR_PROXY_CONNECTION_REFUSED`. Always pass `geoip=True` in Camoufox launch_opts when using a proxy.
|
||||
|
||||
```python
|
||||
def clear_session(cdp):
|
||||
"""Clear all cookies and storage — prevents cross-account key leakage."""
|
||||
cdp["id"] = cdp.get("id", 0) + 1
|
||||
mid = cdp["id"]
|
||||
cdp["ws"].send(json.dumps({"id": mid, "method": "Network.clearBrowserCookies"}))
|
||||
time.sleep(0.3)
|
||||
# Also clear localStorage, sessionStorage, and IndexedDB
|
||||
js(cdp, "try{localStorage.clear();sessionStorage.clear();"
|
||||
"indexedDB.databases().then(dbs=>dbs.forEach(db=>indexedDB.deleteDatabase(db.name)));"
|
||||
"}catch(e){}")
|
||||
```
|
||||
Reference in New Issue
Block a user