267 lines
11 KiB
JavaScript
267 lines
11 KiB
JavaScript
/**
|
|
* Browser fingerprint randomizer for Playwright.
|
|
*
|
|
* generateFingerprint() -> profile object
|
|
* buildInitScript(fp) -> JS string yang di-inject ke setiap halaman
|
|
* via context.addInitScript() biar override jalan
|
|
* sebelum kode situs sempat baca navigator/screen/canvas.
|
|
*
|
|
* Catatan: ini ngacak permukaan-permukaan yang dibaca skrip anti-bot umum
|
|
* (FingerprintJS, Akamai BMP, dsb). Bukan stealth penuh — kalau target
|
|
* pasang deteksi level network (TLS JA3, IP, ASN), butuh proxy + tools lain.
|
|
*/
|
|
|
|
import crypto from 'crypto';
|
|
|
|
// ---- Pool data ----------------------------------------------------------
|
|
|
|
const CHROME_VERSIONS = [
|
|
'122.0.0.0', '123.0.0.0', '124.0.0.0', '125.0.0.0', '126.0.0.0',
|
|
'127.0.0.0', '128.0.0.0', '129.0.0.0', '130.0.0.0', '131.0.0.0',
|
|
];
|
|
|
|
// Windows 10 dan 11 dua-duanya report "10.0" di UA string
|
|
const WINDOWS_NT_VERSION = '10.0';
|
|
|
|
const VIEWPORTS = [
|
|
{ width: 1920, height: 1080 },
|
|
{ width: 1536, height: 864 },
|
|
{ width: 1440, height: 900 },
|
|
{ width: 1366, height: 768 },
|
|
{ width: 1600, height: 900 },
|
|
{ width: 1680, height: 1050 },
|
|
{ width: 1280, height: 720 },
|
|
{ width: 1280, height: 800 },
|
|
];
|
|
|
|
const LOCALES = ['en-US', 'en-GB', 'en-AU', 'en-CA', 'en-SG'];
|
|
|
|
const TIMEZONES = [
|
|
'Asia/Singapore', 'Asia/Bangkok',
|
|
'Asia/Kuala_Lumpur', 'Asia/Manila', 'Asia/Ho_Chi_Minh',
|
|
];
|
|
|
|
const HARDWARE_CONCURRENCY = [4, 6, 8, 12, 16];
|
|
const DEVICE_MEMORY = [4, 8, 16];
|
|
const COLOR_DEPTHS = [24, 30];
|
|
|
|
// Pasangan vendor + renderer realistis (gak campur — kalau vendor NVIDIA,
|
|
// renderer-nya juga GPU NVIDIA biar konsisten saat skrip cocokkan keduanya)
|
|
const WEBGL_RENDERERS = [
|
|
{ vendor: 'Google Inc. (NVIDIA)', renderer: 'ANGLE (NVIDIA, NVIDIA GeForce GTX 1660 Direct3D11 vs_5_0 ps_5_0, D3D11)' },
|
|
{ vendor: 'Google Inc. (NVIDIA)', renderer: 'ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0, D3D11)' },
|
|
{ vendor: 'Google Inc. (NVIDIA)', renderer: 'ANGLE (NVIDIA, NVIDIA GeForce RTX 4060 Ti Direct3D11 vs_5_0 ps_5_0, D3D11)' },
|
|
{ vendor: 'Google Inc. (NVIDIA)', renderer: 'ANGLE (NVIDIA, NVIDIA GeForce GTX 1050 Ti Direct3D11 vs_5_0 ps_5_0, D3D11)' },
|
|
{ vendor: 'Google Inc. (Intel)', renderer: 'ANGLE (Intel, Intel(R) UHD Graphics 630 Direct3D11 vs_5_0 ps_5_0, D3D11)' },
|
|
{ vendor: 'Google Inc. (Intel)', renderer: 'ANGLE (Intel, Intel(R) Iris(R) Xe Graphics Direct3D11 vs_5_0 ps_5_0, D3D11)' },
|
|
{ vendor: 'Google Inc. (Intel)', renderer: 'ANGLE (Intel, Intel(R) HD Graphics 620 Direct3D11 vs_5_0 ps_5_0, D3D11)' },
|
|
{ vendor: 'Google Inc. (AMD)', renderer: 'ANGLE (AMD, AMD Radeon RX 6600 XT Direct3D11 vs_5_0 ps_5_0, D3D11)' },
|
|
{ vendor: 'Google Inc. (AMD)', renderer: 'ANGLE (AMD, AMD Radeon Graphics Direct3D11 vs_5_0 ps_5_0, D3D11)' },
|
|
];
|
|
|
|
// ---- Helpers ------------------------------------------------------------
|
|
|
|
function pick(arr) {
|
|
return arr[Math.floor(Math.random() * arr.length)];
|
|
}
|
|
|
|
function randInt(min, max) {
|
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
}
|
|
|
|
// ---- Fingerprint generator ---------------------------------------------
|
|
|
|
export function generateFingerprint() {
|
|
const chromeVersion = pick(CHROME_VERSIONS);
|
|
const chromeMajor = chromeVersion.split('.')[0];
|
|
const viewport = pick(VIEWPORTS);
|
|
const locale = pick(LOCALES);
|
|
const timezone = pick(TIMEZONES);
|
|
const cores = pick(HARDWARE_CONCURRENCY);
|
|
const memory = pick(DEVICE_MEMORY);
|
|
const webgl = pick(WEBGL_RENDERERS);
|
|
const colorDepth = pick(COLOR_DEPTHS);
|
|
const deviceScaleFactor = pick([1, 1, 1, 1.25, 1.5]);
|
|
|
|
const userAgent =
|
|
`Mozilla/5.0 (Windows NT ${WINDOWS_NT_VERSION}; Win64; x64) ` +
|
|
`AppleWebKit/537.36 (KHTML, like Gecko) ` +
|
|
`Chrome/${chromeVersion} Safari/537.36`;
|
|
|
|
// Screen biasanya >= viewport. Kasih taskbar offset acak.
|
|
const screenWidth = viewport.width;
|
|
const screenHeight = viewport.height + randInt(40, 120);
|
|
const availHeight = screenHeight - randInt(30, 60);
|
|
|
|
// Seed unik buat noise canvas / audio
|
|
const seed = crypto.randomBytes(16).toString('hex');
|
|
|
|
return {
|
|
userAgent,
|
|
chromeMajor,
|
|
chromeVersion,
|
|
viewport,
|
|
deviceScaleFactor,
|
|
screen: {
|
|
width: screenWidth,
|
|
height: screenHeight,
|
|
availWidth: screenWidth,
|
|
availHeight,
|
|
colorDepth,
|
|
},
|
|
locale,
|
|
timezone,
|
|
platform: 'Win32',
|
|
hardwareConcurrency: cores,
|
|
deviceMemory: memory,
|
|
webgl,
|
|
seed,
|
|
pluginsCount: randInt(2, 5),
|
|
};
|
|
}
|
|
|
|
// HTTP headers tambahan untuk konsisten dengan UA yang diset.
|
|
// Sec-CH-UA dkk biasanya dikirim Chrome berdasar binary version aslinya;
|
|
// kita override eksplisit biar match sama UA yang kita rotasikan.
|
|
export function buildExtraHeaders(fp) {
|
|
const major = fp.chromeMajor;
|
|
const baseLang = fp.locale.split('-')[0];
|
|
return {
|
|
'Sec-CH-UA': `"Chromium";v="${major}", "Not_A Brand";v="24", "Google Chrome";v="${major}"`,
|
|
'Sec-CH-UA-Mobile': '?0',
|
|
'Sec-CH-UA-Platform': '"Windows"',
|
|
'Accept-Language': `${fp.locale},${baseLang};q=0.9`,
|
|
};
|
|
}
|
|
|
|
// Init script di-inject sebelum semua script halaman jalan.
|
|
// Pakai Object.defineProperty(..., { configurable: true }) supaya pemeriksa
|
|
// fingerprint yang nge-cek descriptor gak langsung curiga.
|
|
export function buildInitScript(fp) {
|
|
const safe = (v) => JSON.stringify(v);
|
|
return `
|
|
(() => {
|
|
try {
|
|
const seed = ${safe(fp.seed)};
|
|
|
|
// ---- navigator overrides ----
|
|
Object.defineProperty(navigator, 'webdriver', { get: () => undefined, configurable: true });
|
|
Object.defineProperty(navigator, 'platform', { get: () => ${safe(fp.platform)}, configurable: true });
|
|
Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => ${fp.hardwareConcurrency}, configurable: true });
|
|
Object.defineProperty(navigator, 'deviceMemory', { get: () => ${fp.deviceMemory}, configurable: true });
|
|
Object.defineProperty(navigator, 'languages', { get: () => [${safe(fp.locale)}, ${safe(fp.locale.split('-')[0])}], configurable: true });
|
|
|
|
// Plugins palsu, panjangnya berbeda-beda
|
|
const fakePlugins = Array.from({ length: ${fp.pluginsCount} }, (_, i) => ({
|
|
name: 'Plugin ' + i,
|
|
filename: 'plugin' + i + '.dll',
|
|
description: '',
|
|
length: 1,
|
|
}));
|
|
Object.defineProperty(navigator, 'plugins', { get: () => fakePlugins, configurable: true });
|
|
|
|
// ---- screen overrides ----
|
|
Object.defineProperty(screen, 'width', { get: () => ${fp.screen.width}, configurable: true });
|
|
Object.defineProperty(screen, 'height', { get: () => ${fp.screen.height}, configurable: true });
|
|
Object.defineProperty(screen, 'availWidth', { get: () => ${fp.screen.availWidth}, configurable: true });
|
|
Object.defineProperty(screen, 'availHeight', { get: () => ${fp.screen.availHeight}, configurable: true });
|
|
Object.defineProperty(screen, 'colorDepth', { get: () => ${fp.screen.colorDepth}, configurable: true });
|
|
Object.defineProperty(screen, 'pixelDepth', { get: () => ${fp.screen.colorDepth}, configurable: true });
|
|
|
|
// ---- WebGL vendor/renderer ----
|
|
const spoofWebGL = (proto) => {
|
|
if (!proto) return;
|
|
const orig = proto.getParameter;
|
|
proto.getParameter = function (parameter) {
|
|
// UNMASKED_VENDOR_WEBGL
|
|
if (parameter === 37445) return ${safe(fp.webgl.vendor)};
|
|
// UNMASKED_RENDERER_WEBGL
|
|
if (parameter === 37446) return ${safe(fp.webgl.renderer)};
|
|
return orig.apply(this, arguments);
|
|
};
|
|
};
|
|
if (typeof WebGLRenderingContext !== 'undefined') spoofWebGL(WebGLRenderingContext.prototype);
|
|
if (typeof WebGL2RenderingContext !== 'undefined') spoofWebGL(WebGL2RenderingContext.prototype);
|
|
|
|
// ---- Canvas noise ----
|
|
// Geser 1 pixel dengan alpha sangat rendah, posisi & warna ditentukan seed.
|
|
// Nilai akhir toDataURL/getImageData jadi unik per fingerprint.
|
|
const sx = parseInt(seed.substring(0, 4), 16);
|
|
const sy = parseInt(seed.substring(4, 8), 16);
|
|
const sr = parseInt(seed.substring(8, 10), 16);
|
|
const sg = parseInt(seed.substring(10, 12), 16);
|
|
const sb = parseInt(seed.substring(12, 14), 16);
|
|
|
|
const _toDataURL = HTMLCanvasElement.prototype.toDataURL;
|
|
HTMLCanvasElement.prototype.toDataURL = function () {
|
|
try {
|
|
if (this.width > 16 && this.height > 16) {
|
|
const ctx = this.getContext('2d');
|
|
if (ctx) {
|
|
ctx.save();
|
|
ctx.fillStyle = 'rgba(' + sr + ',' + sg + ',' + sb + ',0.005)';
|
|
ctx.fillRect(sx % this.width, sy % this.height, 1, 1);
|
|
ctx.restore();
|
|
}
|
|
}
|
|
} catch (e) {}
|
|
return _toDataURL.apply(this, arguments);
|
|
};
|
|
|
|
const _getImageData = CanvasRenderingContext2D.prototype.getImageData;
|
|
CanvasRenderingContext2D.prototype.getImageData = function () {
|
|
const data = _getImageData.apply(this, arguments);
|
|
try {
|
|
// Geser sedikit di byte tertentu — tetap deterministik per seed.
|
|
const offset = (sx + sy) % Math.max(1, data.data.length - 1);
|
|
data.data[offset] = (data.data[offset] + (sr % 3)) & 0xff;
|
|
} catch (e) {}
|
|
return data;
|
|
};
|
|
|
|
// ---- AudioContext noise ----
|
|
if (typeof AnalyserNode !== 'undefined') {
|
|
const _getFloat = AnalyserNode.prototype.getFloatFrequencyData;
|
|
AnalyserNode.prototype.getFloatFrequencyData = function (array) {
|
|
_getFloat.apply(this, arguments);
|
|
const drift = ((sr / 256) - 0.5) * 0.0001;
|
|
for (let i = 0; i < array.length; i++) array[i] += drift;
|
|
};
|
|
}
|
|
|
|
// ---- Permissions API tweak (notifications gak boleh "denied" + webdriver true) ----
|
|
if (navigator.permissions && navigator.permissions.query) {
|
|
const origQuery = navigator.permissions.query.bind(navigator.permissions);
|
|
navigator.permissions.query = (params) => {
|
|
if (params && params.name === 'notifications') {
|
|
return Promise.resolve({ state: Notification.permission, onchange: null });
|
|
}
|
|
return origQuery(params);
|
|
};
|
|
}
|
|
} catch (e) {
|
|
// Jangan biarkan error fingerprint mematikan halaman — log aja.
|
|
console.error('[fp-init] error:', e && e.message);
|
|
}
|
|
})();
|
|
`;
|
|
}
|
|
|
|
// CLI test: `node fingerprint.js` — print 3 contoh fingerprint
|
|
if (import.meta.url === `file:///${process.argv[1].replace(/\\/g, '/')}` ||
|
|
(process.argv[1] && process.argv[1].endsWith('fingerprint.js'))) {
|
|
for (let i = 0; i < 3; i++) {
|
|
const fp = generateFingerprint();
|
|
console.log(`\n--- Sample ${i + 1} ---`);
|
|
console.log('UA :', fp.userAgent);
|
|
console.log('Viewport :', `${fp.viewport.width}x${fp.viewport.height} (DPR ${fp.deviceScaleFactor})`);
|
|
console.log('Screen :', `${fp.screen.width}x${fp.screen.height} @ ${fp.screen.colorDepth}bpp`);
|
|
console.log('Locale / TZ :', fp.locale, '/', fp.timezone);
|
|
console.log('CPU / Mem :', fp.hardwareConcurrency, 'cores /', fp.deviceMemory, 'GB');
|
|
console.log('GPU vendor :', fp.webgl.vendor);
|
|
console.log('GPU renderer :', fp.webgl.renderer);
|
|
console.log('Plugins count :', fp.pluginsCount);
|
|
console.log('Seed :', fp.seed.substring(0, 16) + '…');
|
|
}
|
|
}
|