#!/usr/bin/env node /** * 用真实 Chrome 的网络栈发一个请求 (TLS/HTTP2/header 顺序全是浏览器的) * * 用途: 验证服务端风控是否在按「客户端网络指纹」判定。 * 从真实验证码域 https://turing.captcha.gtimg.com/ 发起 fetch, * Origin/Referer/Sec-Fetch-* 都由浏览器自己填。 * * 用法: * echo '' | node chrome_fetch.js [originUrl] */ const { spawn } = require('child_process'); const fs = require('fs'); const os = require('os'); const path = require('path'); const CHROME = process.env.REA_BROWSER_EXECUTABLE || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } async function launchChrome() { const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'chrome-fetch-')); const proc = spawn(CHROME, [ '--headless=new', '--remote-debugging-port=0', '--no-first-run', '--no-default-browser-check', '--disable-gpu', '--disable-dev-shm-usage', '--disable-web-security', '--disable-features=IsolateOrigins,site-per-process', `--user-data-dir=${profile}`, '--window-size=1280,800', 'about:blank', ], { stdio: ['ignore', 'pipe', 'pipe'] }); const portFile = path.join(profile, 'DevToolsActivePort'); let port = null; for (let i = 0; i < 120; i++) { if (fs.existsSync(portFile)) { port = parseInt(fs.readFileSync(portFile, 'utf8').split('\n')[0], 10); if (port) break; } await sleep(100); } if (!port) { proc.kill('SIGKILL'); throw new Error('Chrome 未就绪'); } return { proc, port, profile }; } class Cdp { constructor(ws) { this.ws = ws; this.id = 0; this.waits = new Map(); } static async connect(url) { const ws = new WebSocket(url); await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej; }); const c = new Cdp(ws); ws.onmessage = ev => { const m = JSON.parse(ev.data); if (m.id && c.waits.has(m.id)) { const { res, rej } = c.waits.get(m.id); c.waits.delete(m.id); m.error ? rej(new Error(JSON.stringify(m.error))) : res(m.result); } }; return c; } send(method, params = {}, sessionId) { const id = ++this.id; return new Promise((res, rej) => { this.waits.set(id, { res, rej }); this.ws.send(JSON.stringify({ id, method, params, sessionId })); }); } } async function main() { const url = process.argv[2]; const origin = process.argv[3] || 'https://turing.captcha.gtimg.com/'; const body = fs.readFileSync(0, 'utf8').trim(); const { proc, port, profile } = await launchChrome(); try { const ver = await (await fetch(`http://127.0.0.1:${port}/json/version`)).json(); const cdp = await Cdp.connect(ver.webSocketDebuggerUrl); const { targetId } = await cdp.send('Target.createTarget', { url: origin }); const { sessionId } = await cdp.send('Target.attachToTarget', { targetId, flatten: true }); await cdp.send('Page.enable', {}, sessionId); await sleep(2500); // 等页面(真实验证码域)加载完 const loc = await cdp.send('Runtime.evaluate', { expression: 'location.href', returnByValue: true }, sessionId); process.stderr.write('[page] ' + loc.result.value + '\n'); const expr = `(async () => { const r = await fetch(${JSON.stringify(url)}, ${JSON.stringify(body)} ? { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, body: ${JSON.stringify(body)}, credentials: 'include' } : { method: 'GET', credentials: 'include' }); const t = await r.text(); return JSON.stringify({ status: r.status, text: t }); })()`; const out = await cdp.send('Runtime.evaluate', { expression: expr, awaitPromise: true, returnByValue: true }, sessionId); if (out.exceptionDetails) { console.log(JSON.stringify({ _exception: out.exceptionDetails.text, desc: (out.exceptionDetails.exception || {}).description })); } else if (out.result && out.result.value !== undefined) { console.log(out.result.value); } else { console.log(JSON.stringify({ _raw: out })); } cdp.ws.close(); } finally { proc.kill('SIGKILL'); try { fs.rmSync(profile, { recursive: true, force: true }); } catch (_) {} } } main().catch(e => { console.error('ERR', e.message); process.exit(1); });