/** * tdc_browser.js —— 在「真实 Chrome」中执行服务端下发的 tdc.js, 产出 collect / eks。 * * 为什么需要它 * ------------ * `tdc_worker.js` 用 Node VM 伪造 DOM/BOM 环境, tdc.js 采集到的环境指纹不真实, * 服务端在答题正确后进入风控评估会返回 errorCode=12 (环境异常/自动化)。 * 本脚本用 CDP 驱动本机真实 Chrome: 在验证码真实来源域下加载 tdc.js, 用 * Input.dispatchMouseEvent 派发可信鼠标事件, 再读回 TDC.getData()/getInfo()。 * * 零依赖: 只用 Node >= 22 内置的全局 WebSocket 与 child_process。 * * 用法: * node tdc_browser.js * 选项: { aid, referer, userAgent, chromePath, headless, origin, timeoutMs } * 输出: {"collect":..,"tlg":..,"eks":..,"source":"browser"} 或 {"error":..} */ const { spawn } = require('child_process'); const fs = require('fs'); const os = require('os'); const path = require('path'); const DEFAULT_CHROME = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } async function waitForFile(file, timeoutMs) { const t0 = Date.now(); while (Date.now() - t0 < timeoutMs) { if (fs.existsSync(file)) { const txt = fs.readFileSync(file, 'utf8').trim().split('\n'); if (txt[0]) return txt[0].trim(); } await sleep(120); } throw new Error(`等待 DevToolsActivePort 超时: ${file}`); } class Cdp { constructor(ws) { this.ws = ws; this.id = 0; this.pending = new Map(); ws.addEventListener('message', ev => { let msg; try { msg = JSON.parse(ev.data); } catch { return; } if (msg.id && this.pending.has(msg.id)) { const { resolve, reject } = this.pending.get(msg.id); this.pending.delete(msg.id); if (msg.error) reject(new Error(JSON.stringify(msg.error))); else resolve(msg.result); } }); } send(method, params = {}, sessionId) { const id = ++this.id; const payload = { id, method, params }; if (sessionId) payload.sessionId = sessionId; return new Promise((resolve, reject) => { this.pending.set(id, { resolve, reject }); this.ws.send(JSON.stringify(payload)); setTimeout(() => { if (this.pending.has(id)) { this.pending.delete(id); reject(new Error(`CDP 超时: ${method}`)); } }, 45000); }); } } function connect(url) { return new Promise((resolve, reject) => { const ws = new WebSocket(url); ws.addEventListener('open', () => resolve(ws)); ws.addEventListener('error', e => reject(new Error('WebSocket 连接失败: ' + (e.message || e.type)))); }); } async function httpJson(url) { const res = await fetch(url); return res.json(); } /** * 生成鼠标轨迹 (相对位移), 与 tdc_worker.js 保持同样的形态: * 起点在 iframe 底部中间, 依次移动到 3 个目标点, 每段用贝塞尔插值 + 抖动 + 停留。 */ function buildTrajectory(points, opts) { const scale = opts.displayScale != null ? opts.displayScale : (320.0 / 672.0); const targets = points.map(p => ([ Math.round((opts.offsetX || 0) + p[0] * scale), Math.round((opts.offsetY || 0) + p[1] * scale), ])); const frameW = opts.frameWidth || 360; const frameH = opts.frameHeight || 360; let cur = [Math.round(frameW * 0.42 + (Math.random() - 0.5) * 60), Math.round(frameH * 0.89 + (Math.random() - 0.5) * 20)]; const events = [cur.slice()]; for (const t of targets) { const steps = 18 + Math.floor(Math.random() * 12); const dx = t[0] - cur[0], dy = t[1] - cur[1]; const dist = Math.hypot(dx, dy) || 1; const cx = (Math.random() - 0.5) * Math.min(50, dist * 0.4); const cp1 = [cur[0] + dx * 0.3 - dy / dist * cx, cur[1] + dy * 0.3 + dx / dist * cx]; const cp2 = [cur[0] + dx * 0.7 + dy / dist * cx * 0.5, cur[1] + dy * 0.7 - dx / dist * cx * 0.5]; for (let i = 1; i <= steps; i++) { const u = i / steps, e = u * u * (3 - 2 * u), v = 1 - e; const x = Math.round(v ** 3 * cur[0] + 3 * v ** 2 * e * cp1[0] + 3 * v * e ** 2 * cp2[0] + e ** 3 * t[0]); const y = Math.round(v ** 3 * cur[1] + 3 * v ** 2 * e * cp1[1] + 3 * v * e ** 2 * cp2[1] + e ** 3 * t[1]); cur = [x, y]; events.push(cur.slice()); } for (let i = 0; i < 3 + Math.floor(Math.random() * 3); i++) { events.push([cur[0] + Math.round((Math.random() - 0.5) * 2), cur[1] + Math.round((Math.random() - 0.5) * 2)]); } } return { events, targets }; } async function generate(rawCode, points, opts) { const opt = Object.assign({ aid: '2039519451', referer: 'https://1ms.run/', userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36', chromePath: process.env.TDC_CHROME || DEFAULT_CHROME, headless: true, origin: 'https://turing.captcha.gtimg.com/', frameWidth: 360, frameHeight: 360, timeoutMs: 45000, }, opts || {}); if (!fs.existsSync(opt.chromePath)) throw new Error(`找不到 Chrome: ${opt.chromePath}`); const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'tdc-chrome-')); const args = [ '--remote-debugging-port=0', `--user-data-dir=${profile}`, '--no-first-run', '--no-default-browser-check', '--disable-background-networking', '--disable-blink-features=AutomationControlled', '--disable-features=Translate,OptimizationHints', `--user-agent=${opt.userAgent}`, '--window-size=420,760', 'about:blank', ]; if (opt.headless) args.unshift('--headless=new'); const chrome = spawn(opt.chromePath, args, { stdio: ['ignore', 'ignore', 'pipe'] }); let stderr = ''; chrome.stderr.on('data', d => { stderr += d.toString(); }); let ws = null; try { const port = await waitForFile(path.join(profile, 'DevToolsActivePort'), opt.timeoutMs); const version = await httpJson(`http://127.0.0.1:${port}/json/version`); ws = await connect(version.webSocketDebuggerUrl); const cdp = new Cdp(ws); const { targetId } = await cdp.send('Target.createTarget', { url: 'about:blank' }); const list = await httpJson(`http://127.0.0.1:${port}/json/list`); const page = list.find(t => t.id === targetId); if (!page) throw new Error('未找到新建的页面 target'); const pageWs = await connect(page.webSocketDebuggerUrl); const pcdp = new Cdp(pageWs); await pcdp.send('Page.enable'); await pcdp.send('Runtime.enable'); await pcdp.send('Page.navigate', { url: opt.origin }); await sleep(1200); // 注入并执行真实 tdc.js const inject = await pcdp.send('Runtime.evaluate', { expression: `(function(){ ${rawCode}\n; return typeof TDC !== 'undefined' && !!TDC.getData; })()`, awaitPromise: false, returnByValue: true, }); if (!inject.result || inject.result.value !== true) { throw new Error(`tdc.js 执行后未暴露 TDC.getData (result=${JSON.stringify(inject.result)})`); } // 用 CDP Input 派发可信鼠标事件 const { events } = buildTrajectory(points, opt); for (const [x, y] of events) { await pcdp.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x, y, button: 'none', buttons: 0, clickCount: 0, }); } // 三次真实点击 for (const t of points.map(p => [ Math.round((opt.offsetX || 0) + p[0] * (opt.displayScale != null ? opt.displayScale : 320 / 672)), Math.round((opt.offsetY || 0) + p[1] * (opt.displayScale != null ? opt.displayScale : 320 / 672)), ])) { await pcdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: t[0], y: t[1], button: 'left', buttons: 1, clickCount: 1 }); await pcdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: t[0], y: t[1], button: 'left', buttons: 0, clickCount: 1 }); await sleep(120); } const out = await pcdp.send('Runtime.evaluate', { expression: `JSON.stringify({collect: TDC.getData(), eks: (TDC.getInfo()||{}).info || ''})`, returnByValue: true, }); const payload = JSON.parse(out.result.value); pageWs.close(); if (!payload.collect) throw new Error('TDC.getData() 返回空'); return { collect: payload.collect, tlg: payload.collect.length, eks: payload.eks, source: 'browser' }; } finally { try { if (ws) ws.close(); } catch { /* ignore */ } try { chrome.kill('SIGKILL'); } catch { /* ignore */ } await sleep(150); try { fs.rmSync(profile, { recursive: true, force: true }); } catch { /* ignore */ } } } if (require.main === module) { const tdcPath = process.argv[2]; const pointsArg = process.argv[3]; const optionsArg = process.argv[4]; const points = pointsArg ? JSON.parse(pointsArg) : [[162, 249], [104, 37], [561, 156]]; const options = optionsArg ? JSON.parse(optionsArg) : {}; const run = code => generate(code, points, options).then( r => { console.log(JSON.stringify(r)); }, e => { console.log(JSON.stringify({ error: String((e && e.stack) || e) })); process.exitCode = 1; } ); if (tdcPath === '-') { let input = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', c => { input += c; }); process.stdin.on('end', () => run(input)); } else { run(fs.readFileSync(tdcPath, 'utf8')); } } module.exports = { generate };