Files
gpt-plus-gpt/harvest_fp_puppeteer.js
2026-03-15 20:48:19 +08:00

443 lines
16 KiB
JavaScript

#!/usr/bin/env node
/**
* 指纹采集器 - puppeteer-with-fingerprints 版
*
* 使用 BAS FingerprintSwitcher 在 C++ 层修改浏览器指纹
* 每次启动获取一个真实设备指纹 + 独立代理 IP
*
* 用法:
* node harvest_fp_puppeteer.js # 采集 5 个 KR 指纹
* node harvest_fp_puppeteer.js --count 10 --workers 5 # 10 个, 5 并行
* node harvest_fp_puppeteer.js --region US --socks5 # US 地区, SOCKS5
* node harvest_fp_puppeteer.js --test # 测试指纹 (bot.sannysoft.com)
* node harvest_fp_puppeteer.js --list # 列出已有指纹
*/
const { plugin } = require('puppeteer-with-fingerprints');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
// ── 配置 ──
const PROXY_HOST = 'us.nexip.cc';
const PROXY_PORT = 3010;
const PROXY_BASE_USER = '8uk59M1TIb';
const PROXY_PASSWORD = 'RabyyxxkRxXZ';
const REGION_CITIES = {
KR: 'Seoul', JP: 'Tokyo', US: 'LosAngeles', GB: 'London',
SG: 'Singapore', HK: 'HongKong', TW: 'Taipei', DE: 'Berlin',
};
const REGION_TZ = {
KR: 'Asia/Seoul', JP: 'Asia/Tokyo', US: 'America/New_York',
GB: 'Europe/London', SG: 'Asia/Singapore', HK: 'Asia/Hong_Kong',
TW: 'Asia/Taipei', DE: 'Europe/Berlin',
};
const FP_DIR = path.join(__dirname, 'fingerprints');
// ── 代理 URL 生成 ──
function buildProxyUrl(region, sid, useSocks5 = false) {
return 'http://192.168.1.2:10810';
}
// ── JS 采集脚本 (浏览器内执行) ──
const COLLECT_JS = `(() => {
const scr = screen;
return {
cookie_support: navigator.cookieEnabled,
do_not_track: navigator.doNotTrack === '1',
language: navigator.language || 'en-US',
platform: navigator.platform,
screen_size: scr.width + 'w_' + scr.height + 'h_' + scr.colorDepth + 'd_' + (window.devicePixelRatio||1) + 'r',
timezone_offset: String(-new Date().getTimezoneOffset() / 60),
timezone_name: Intl.DateTimeFormat().resolvedOptions().timeZone,
touch_support: 'ontouchstart' in window || navigator.maxTouchPoints > 0,
user_agent: navigator.userAgent,
cpu_cores: navigator.hardwareConcurrency,
memory_gb: navigator.deviceMemory || 0,
plugins: Array.from(navigator.plugins||[]).map(p => p.name).join(', '),
webgl_vendor: (() => {
try {
const c = document.createElement('canvas');
const gl = c.getContext('webgl') || c.getContext('experimental-webgl');
const ext = gl.getExtension('WEBGL_debug_renderer_info');
return ext ? gl.getParameter(ext.UNMASKED_VENDOR_WEBGL) : '';
} catch(e) { return ''; }
})(),
webgl_renderer: (() => {
try {
const c = document.createElement('canvas');
const gl = c.getContext('webgl') || c.getContext('experimental-webgl');
const ext = gl.getExtension('WEBGL_debug_renderer_info');
return ext ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) : '';
} catch(e) { return ''; }
})(),
};
})()`;
const CANVAS_JS = `(() => {
const c = document.createElement('canvas');
c.height = 60; c.width = 400;
const ctx = c.getContext('2d');
ctx.textBaseline = 'alphabetic';
ctx.fillStyle = '#f60'; ctx.fillRect(125, 1, 62, 20);
ctx.fillStyle = '#069'; ctx.font = '11pt Arial';
ctx.fillText('Cwm fjordbank glyphs vext quiz, \\u{1F603}', 2, 15);
ctx.fillStyle = 'rgba(102, 204, 0, 0.7)'; ctx.font = '18pt Arial';
ctx.fillText('Cwm fjordbank glyphs vext quiz, \\u{1F603}', 4, 45);
return c.toDataURL();
})()`;
const FONTS_JS = `(() => {
const baseFonts = ['monospace', 'sans-serif', 'serif'];
const testFonts = [
'Andale Mono','Arial','Arial Black','Arial Hebrew','Arial Narrow',
'Arial Rounded MT Bold','Arial Unicode MS','Bitstream Vera Sans Mono',
'Book Antiqua','Bookman Old Style','Calibri','Cambria','Cambria Math',
'Century','Century Gothic','Century Schoolbook','Comic Sans MS',
'Consolas','Courier','Courier New','Geneva','Georgia','Helvetica',
'Helvetica Neue','Impact','Lucida Bright','Lucida Calligraphy',
'Lucida Console','Lucida Fax','Lucida Grande','Lucida Handwriting',
'Lucida Sans','Lucida Sans Typewriter','Lucida Sans Unicode',
'Microsoft Sans Serif','Monaco','Monotype Corsiva','MS Gothic',
'MS PGothic','MS Reference Sans Serif','MS Sans Serif','MS Serif',
'MYRIAD PRO','Palatino','Palatino Linotype','Segoe Print',
'Segoe Script','Segoe UI','Segoe UI Light','Segoe UI Semibold',
'Segoe UI Symbol','Tahoma','Times','Times New Roman','Trebuchet MS',
'Verdana','Wingdings','Wingdings 2','Wingdings 3'
];
const span = document.createElement('span');
span.style.fontSize = '72px';
span.style.visibility = 'hidden';
span.style.position = 'absolute';
span.textContent = 'mmmmmmmmmmlli';
document.body.appendChild(span);
const baseWidths = {};
for (const base of baseFonts) {
span.style.fontFamily = base;
baseWidths[base] = span.offsetWidth;
}
let bits = '';
for (const font of testFonts) {
let detected = false;
for (const base of baseFonts) {
span.style.fontFamily = "'" + font + "', " + base;
if (span.offsetWidth !== baseWidths[base]) { detected = true; break; }
}
bits += detected ? '1' : '0';
}
document.body.removeChild(span);
return bits;
})()`;
// ── Stripe fingerprint ID 计算 ──
function computeStripeId(fp) {
const vals = [
String(fp.cookie_support || true).toLowerCase(),
String(fp.do_not_track || false).toLowerCase(),
fp.language || 'en-US',
fp.platform || 'Win32',
fp.plugins || '',
fp.screen_size || '1920w_1080h_32d_1r',
fp.timezone_offset || '0',
String(fp.touch_support || false).toLowerCase(),
'sessionStorage-enabled, localStorage-enabled',
fp.fonts_bits || '',
'', // WebGL
fp.user_agent || '',
'', // Flash
'false',
fp.canvas_hash || '',
];
return crypto.createHash('md5').update(vals.join(' ')).digest('hex');
}
// ── 采集一个指纹 ──
async function harvestOne(region, index, useSocks5 = false, testMode = false, harvestUrl = null) {
const sid = crypto.randomBytes(4).toString('hex');
const proxyUrl = buildProxyUrl(region, sid, useSocks5);
console.log(`\n[${index}] 代理 sid=${sid}, ${useSocks5 ? 'socks5' : 'http'}`);
try {
// 获取真实设备指纹 (从 FingerprintSwitcher 服务)
console.log(` [指纹] 正在获取真实设备指纹...`);
const fingerprint = await plugin.fetch({
tags: ['Microsoft Windows', 'Chrome'],
});
console.log(` [指纹] 已获取`);
// 应用指纹 + 代理
plugin.useFingerprint(fingerprint);
plugin.useProxy(proxyUrl, {
changeTimezone: true,
changeGeolocation: true,
});
// 启动浏览器
const browser = await plugin.launch({
headless: false,
args: ['--disable-blink-features=AutomationControlled'],
});
const page = await browser.newPage();
page.setDefaultTimeout(45000);
if (testMode) {
// 测试模式: 访问指纹检测网站
console.log(` [测试] 访问 bot.sannysoft.com ...`);
await page.goto('https://bot.sannysoft.com/', { waitUntil: 'networkidle2', timeout: 60000 });
await new Promise(r => setTimeout(r, 5000));
const screenshotPath = path.join(__dirname, `fp_test_${index}.png`);
await page.screenshot({ path: screenshotPath, fullPage: true });
console.log(` [截图] ${screenshotPath}`);
// 再测试 browserscan
const page2 = await browser.newPage();
console.log(` [测试] 访问 browserscan.net ...`);
await page2.goto('https://www.browserscan.net/', { waitUntil: 'networkidle2', timeout: 60000 });
await new Promise(r => setTimeout(r, 8000));
const screenshotPath2 = path.join(__dirname, `fp_test_browserscan_${index}.png`);
await page2.screenshot({ path: screenshotPath2, fullPage: true });
console.log(` [截图] ${screenshotPath2}`);
await browser.close();
return { test: true, screenshots: [screenshotPath, screenshotPath2] };
}
// 正常模式: 访问指定 URL 或 chatgpt.com 采集指纹
const targetUrl = harvestUrl || 'https://chatgpt.com/';
console.log(` [采集] 访问 ${targetUrl.substring(0, 80)}...`);
await page.goto(targetUrl, { waitUntil: 'networkidle2', timeout: 60000 });
// 等待页面完全加载 (SPA + Stripe 指纹脚本)
await new Promise(r => setTimeout(r, 5000));
let info = {};
let canvasDataUrl = '';
let fontsBits = '';
try {
info = await page.evaluate(COLLECT_JS);
console.log(` [采集] 基础信息: OK (UA=${(info.user_agent||'').substring(0,40)}...)`);
} catch (e) {
console.log(` [采集] 基础信息失败: ${e.message}`);
// 回退: 直接从 navigator 取关键值
info = await page.evaluate(() => ({
user_agent: navigator.userAgent,
platform: navigator.platform,
language: navigator.language,
screen_size: screen.width + 'w_' + screen.height + 'h_' + screen.colorDepth + 'd_' + (window.devicePixelRatio||1) + 'r',
timezone_offset: String(-new Date().getTimezoneOffset() / 60),
timezone_name: Intl.DateTimeFormat().resolvedOptions().timeZone,
cookie_support: navigator.cookieEnabled,
do_not_track: navigator.doNotTrack === '1',
touch_support: navigator.maxTouchPoints > 0,
cpu_cores: navigator.hardwareConcurrency,
memory_gb: navigator.deviceMemory || 0,
plugins: '',
webgl_vendor: '',
webgl_renderer: '',
}));
}
try {
canvasDataUrl = await page.evaluate(CANVAS_JS);
if (typeof canvasDataUrl !== 'string') canvasDataUrl = '';
console.log(` [采集] Canvas: OK`);
} catch (e) {
console.log(` [采集] Canvas 失败: ${e.message}`);
}
try {
const rawFonts = await page.evaluate(FONTS_JS);
fontsBits = typeof rawFonts === 'string' ? rawFonts : '';
console.log(` [采集] Fonts: OK (${fontsBits.length}位)`);
} catch (e) {
console.log(` [采集] Fonts 失败: ${e.message}`);
}
// 采集 chatgpt.com 的 cookies (oai-did, cf_clearance, csrf-token 等)
const cookies = await page.cookies();
const oaiDid = cookies.find(c => c.name === 'oai-did')?.value || '';
const cfClearance = cookies.find(c => c.name === 'cf_clearance')?.value || '';
const csrfCookie = cookies.find(c => c.name === '__Host-next-auth.csrf-token')?.value || '';
console.log(` [采集] Cookies: ${cookies.length} 个, oai-did=${oaiDid ? '有' : '无'}, cf=${cfClearance ? '有' : '无'}`);
// 获取真实出口 IP
let realIp = 'unknown';
try {
await page.goto('https://api.ipify.org?format=json', { waitUntil: 'domcontentloaded', timeout: 15000 });
const ipText = await page.evaluate(() => document.body.innerText);
realIp = JSON.parse(ipText).ip || 'unknown';
} catch (e) {
// ignore
}
await browser.close();
const canvasStr = typeof canvasDataUrl === 'string' ? canvasDataUrl : JSON.stringify(canvasDataUrl);
const canvasHash = crypto.createHash('md5').update(canvasStr).digest('hex');
const fontsStr = typeof fontsBits === 'string' ? fontsBits : String(fontsBits || '');
const fp = {
...info,
fonts_bits: fontsStr,
canvas_hash: canvasHash,
// chatgpt.com 采集的 cookies
oai_did: oaiDid,
cf_clearance: cfClearance,
csrf_token: csrfCookie,
cookies: cookies.map(c => ({ name: c.name, value: c.value, domain: c.domain })),
region,
proxy_ip: realIp,
proxy_sid: sid,
harvested_at: new Date().toISOString(),
source: 'puppeteer-with-fingerprints',
};
fp.stripe_fingerprint_id = computeStripeId(fp);
console.log(` IP=${realIp}, UA=${(info.user_agent || '').substring(0, 50)}...`);
console.log(` Canvas=${canvasHash}, Fonts=${fontsStr.substring(0, 30)}...(${fontsStr.length}位)`);
console.log(` WebGL=${info.webgl_renderer?.substring(0, 50) || '?'}`);
console.log(` OAI-DID=${oaiDid || '无'}, CF=${cfClearance ? '有' : '无'}`);
console.log(` Cookies: ${cookies.length}`);
console.log(` Stripe ID=${fp.stripe_fingerprint_id}`);
return fp;
} catch (e) {
console.log(` [!] 失败: ${e.message}`);
return null;
}
}
// ── 并行批量采集 ──
async function harvestBatch(count, region, useSocks5, workers, testMode, harvestUrl) {
if (!fs.existsSync(FP_DIR)) fs.mkdirSync(FP_DIR, { recursive: true });
console.log('='.repeat(60));
console.log(` 指纹批量挖取: ${count} 个, 地区=${region}`);
console.log(` 并行数=${workers}, 代理协议=${useSocks5 ? 'socks5' : 'http'}`);
console.log(` 引擎: puppeteer-with-fingerprints (FingerprintSwitcher)`);
console.log(` 保存目录: ${FP_DIR}`);
console.log('='.repeat(60));
const results = [];
let failures = 0;
// 分批并行 (每批 workers 个)
for (let batch = 0; batch < count; batch += workers) {
const batchSize = Math.min(workers, count - batch);
const promises = [];
for (let i = 0; i < batchSize; i++) {
const idx = batch + i + 1;
promises.push(harvestOne(region, idx, useSocks5, testMode, harvestUrl));
}
const batchResults = await Promise.allSettled(promises);
for (let i = 0; i < batchResults.length; i++) {
const idx = batch + i + 1;
const result = batchResults[i];
if (result.status === 'fulfilled' && result.value && !result.value.test) {
const fp = result.value;
const fname = `${region}_${fp.proxy_sid}_${Date.now()}_${idx}.json`;
const fpath = path.join(FP_DIR, fname);
fs.writeFileSync(fpath, JSON.stringify(fp, null, 2), 'utf-8');
console.log(` -> [${idx}] 已保存: ${fname}`);
results.push(fp);
} else if (result.status === 'rejected') {
console.log(` [!] [${idx}] 异常: ${result.reason?.message || result.reason}`);
failures++;
} else if (!result.value) {
failures++;
}
}
}
// 更新 fingerprint.json
if (results.length > 0) {
const latest = results[results.length - 1];
fs.writeFileSync(
path.join(__dirname, 'fingerprint.json'),
JSON.stringify(latest, null, 2), 'utf-8'
);
console.log(`\nfingerprint.json 已更新 (Stripe ID=${latest.stripe_fingerprint_id})`);
}
console.log(`\n${'='.repeat(60)}`);
console.log(` 完成: ${results.length} 成功, ${failures} 失败`);
console.log('='.repeat(60));
}
// ── 列出已有指纹 ──
function listFingerprints() {
if (!fs.existsSync(FP_DIR)) {
console.log('无指纹目录');
return;
}
const files = fs.readdirSync(FP_DIR).filter(f => f.endsWith('.json')).sort();
if (!files.length) {
console.log('无已保存的指纹');
return;
}
console.log('='.repeat(70));
console.log(` 已保存 ${files.length} 个指纹:`);
console.log('='.repeat(70));
for (const fname of files) {
try {
const fp = JSON.parse(fs.readFileSync(path.join(FP_DIR, fname), 'utf-8'));
const src = fp.source === 'puppeteer-with-fingerprints' ? '[PWF]' : '[PW]';
console.log(` ${src} ${fname}`);
console.log(` IP=${fp.proxy_ip || '?'}, Screen=${fp.screen_size || '?'}`);
console.log(` Stripe ID=${fp.stripe_fingerprint_id || '?'}`);
console.log(` UA=${(fp.user_agent || '?').substring(0, 60)}...`);
if (fp.webgl_renderer) {
console.log(` WebGL=${fp.webgl_renderer.substring(0, 50)}`);
}
} catch (e) {
console.log(` ${fname} (读取失败)`);
}
}
console.log('='.repeat(70));
}
// ── CLI ──
async function main() {
const args = process.argv.slice(2);
const getArg = (name, def) => {
const i = args.indexOf(name);
return i >= 0 && i + 1 < args.length ? args[i + 1] : def;
};
const hasFlag = (name) => args.includes(name);
if (hasFlag('--list')) {
listFingerprints();
return;
}
const count = parseInt(getArg('--count', '5'));
const region = (getArg('--region', 'KR')).toUpperCase();
const workers = parseInt(getArg('--workers', '3'));
const useSocks5 = hasFlag('--socks5');
const testMode = hasFlag('--test');
const harvestUrl = getArg('--url', null);
// 注: puppeteer-with-fingerprints 需要先下载浏览器引擎 (首次运行自动下载)
plugin.setServiceKey(''); // 免费版
await harvestBatch(
testMode ? Math.min(count, 2) : count,
region, useSocks5, workers, testMode, harvestUrl
);
}
main().catch(console.error);