443 lines
16 KiB
JavaScript
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);
|