File size: 15,393 Bytes
ee826ee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
/**
 * stealth-engine.js β€” Motor de Stealth Completo para Zelin
 * ==========================================================
 * Basado en investigaciΓ³n real 2025-2026:
 *
 * HALLAZGO CLAVE: "Blocking or randomizing canvas fingerprinting is easily detected
 * because the fingerprint changes on every page load, which is itself suspicious.
 * Natural fingerprints that match real device configurations are superior."
 * β€” Multilogin Research, 2025
 *
 * HALLAZGO CLAVE 2: "BotBrowser's key advantage: Canvas, WebGL, AudioContext,
 * and Fonts must be CONSISTENT across the session. Inconsistencies instantly
 * expose fake fingerprints."
 * β€” GeeTest Anti-Bot Research, Nov 2025
 *
 * ESTRATEGIA: Fingerprint consistente + realista, NO aleatorio en cada request
 *   β†’ Generar UNA identidad de dispositivo por sesiΓ³n
 *   → Mantenerla IDÉNTICA en todas las pÑginas de esa sesión
 *   β†’ Que corresponda a una configuraciΓ³n de hardware que PODRÍA EXISTIR
 */

import { createHash } from 'crypto';

// ── Pool de configuraciones reales de dispositivos ───────────────────────────
// ExtraΓ­das de datasets de fingerprints reales (BrowserLeaks, CreepJS stats 2025)
// CRÍTICO: cada configuración debe ser INTERNAMENTE CONSISTENTE
// (GPU vendor debe coincidir con canvas output, etc.)

const REAL_DEVICE_CONFIGS = [
  {
    // Windows 11 + Chrome 120 + Intel UHD 770 (configuraciΓ³n mΓ‘s comΓΊn ~18% de usuarios)
    platform       : 'Win32',
    oscpu          : 'Windows NT 10.0',
    vendor         : 'Google Inc.',
    renderer       : 'ANGLE (Intel, Intel(R) UHD Graphics 770 Direct3D11 vs_5_0 ps_5_0, D3D11)',
    vendorUnmasked : 'Intel',
    rendererUnmasked: 'Intel(R) UHD Graphics 770',
    hardwareConcurrency: 8,
    deviceMemory   : 8,
    maxTouchPoints : 0,
    screenRes      : [1920, 1080],
    colorDepth     : 24,
    pixelRatio     : 1,
    timezone       : 'America/Mexico_City',
    language       : 'es-MX',
    fonts          : ['Arial', 'Calibri', 'Cambria', 'Comic Sans MS', 'Consolas', 'Courier New', 'Georgia', 'Segoe UI', 'Tahoma', 'Times New Roman', 'Trebuchet MS', 'Verdana'],
    plugins        : ['Chrome PDF Plugin', 'Chrome PDF Viewer', 'Native Client'],
    chromeVersion  : '120.0.6099.130',
    userAgent      : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
  },
  {
    // Windows 10 + Chrome 119 + NVIDIA RTX 3060 (gamer setup)
    platform       : 'Win32',
    oscpu          : 'Windows NT 10.0',
    vendor         : 'Google Inc.',
    renderer       : 'ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0, D3D11)',
    vendorUnmasked : 'NVIDIA Corporation',
    rendererUnmasked: 'NVIDIA GeForce RTX 3060',
    hardwareConcurrency: 12,
    deviceMemory   : 16,
    maxTouchPoints : 0,
    screenRes      : [2560, 1440],
    colorDepth     : 24,
    pixelRatio     : 1,
    timezone       : 'America/Mexico_City',
    language       : 'es-MX',
    fonts          : ['Arial', 'Calibri', 'Cambria', 'Consolas', 'Courier New', 'Georgia', 'Impact', 'Segoe UI', 'Tahoma', 'Times New Roman', 'Verdana'],
    plugins        : ['Chrome PDF Plugin', 'Chrome PDF Viewer'],
    chromeVersion  : '119.0.6045.200',
    userAgent      : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
  },
  {
    // macOS 14 + Chrome 120 + Apple M2
    platform       : 'MacIntel',
    oscpu          : 'Intel Mac OS X 10_15_7',
    vendor         : 'Google Inc.',
    renderer       : 'ANGLE (Apple, ANGLE Metal Renderer: Apple M2, Unspecified Version)',
    vendorUnmasked : 'Apple',
    rendererUnmasked: 'Apple M2',
    hardwareConcurrency: 8,
    deviceMemory   : 8,
    maxTouchPoints : 0,
    screenRes      : [2560, 1664],
    colorDepth     : 30,
    pixelRatio     : 2,
    timezone       : 'America/Mexico_City',
    language       : 'es-MX',
    fonts          : ['Arial', 'Courier New', 'Geneva', 'Georgia', 'Helvetica', 'Monaco', 'Times New Roman', 'Verdana'],
    plugins        : ['Chrome PDF Plugin', 'Chrome PDF Viewer'],
    chromeVersion  : '120.0.6099.129',
    userAgent      : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
  },
];

// ── Generar identidad de sesiΓ³n (una sola vez, CONSISTENTE toda la sesiΓ³n) ───
let _sessionIdentity = null;

export function generateSessionIdentity(seed = null) {
  if (_sessionIdentity && !seed) return _sessionIdentity;

  // Seleccionar configuraciΓ³n base (misma para toda la sesiΓ³n)
  const configIndex = seed
    ? parseInt(createHash('md5').update(seed).digest('hex').slice(0, 2), 16) % REAL_DEVICE_CONFIGS.length
    : Math.floor(Math.random() * REAL_DEVICE_CONFIGS.length);

  const base = REAL_DEVICE_CONFIGS[configIndex];

  // AΓ±adir variaciΓ³n SUTIL y CONSISTENTE (mismos valores toda la sesiΓ³n)
  const sessionSeed = seed || Date.now().toString();
  const noise = (str, range) => {
    const h = parseInt(createHash('md5').update(str + sessionSeed).digest('hex').slice(0, 4), 16);
    return (h % (range * 2)) - range;
  };

  _sessionIdentity = {
    ...base,
    // PequeΓ±a variaciΓ³n en resoluciΓ³n de pantalla (Β±8px) β€” realista
    screenRes: [
      base.screenRes[0] + noise('sw', 8),
      base.screenRes[1] + noise('sh', 8),
    ],
    // VariaciΓ³n en memoria (+/- una banda) β€” realista
    deviceMemory: base.deviceMemory,
    // Canvas noise value β€” fijo para toda la sesiΓ³n (no cambia entre pΓ‘ginas)
    canvasNoise: noise('canvas', 3) * 0.001,
    audioNoise : noise('audio', 5) * 0.0001,
    sessionId  : sessionSeed,
  };

  return _sessionIdentity;
}

export function resetSessionIdentity() {
  _sessionIdentity = null;
}

// ── Script de inyecciΓ³n completo ─────────────────────────────────────────────
// Se inyecta ANTES de que cualquier script de la pΓ‘gina se ejecute
export function generateStealthScript(identity) {
  const id = identity || generateSessionIdentity();

  return `
(function() {
  'use strict';

  // ── 1. Ocultar webdriver completamente ─────────────────────────────────────
  Object.defineProperty(navigator, 'webdriver', { get: () => undefined, configurable: false });
  delete window.navigator.__proto__.webdriver;

  // ── 2. Chrome runtime y permisos (ausentes en headless real) ──────────────
  if (!window.chrome) {
    window.chrome = {
      app: { isInstalled: false, getDetails: function() {}, getIsInstalled: function() {}, runningState: function() { return 'cannot_run'; } },
      runtime: {
        onMessage: { addListener: function() {}, removeListener: function() {}, hasListeners: function() { return false; } },
        connect: function() { return { onMessage: { addListener: function() {} }, onDisconnect: { addListener: function() {} }, postMessage: function() {} }; },
        sendMessage: function() {},
        id: undefined,
      },
      loadTimes: function() { return { commitLoadTime: ${Date.now() / 1000 - 1.2 - Math.random()}, connectionInfo: 'http/1.1', finishDocumentLoadTime: 0, finishLoadTime: 0, firstPaintAfterLoadTime: 0, firstPaintTime: 0, navigationType: 'Other', npnNegotiatedProtocol: 'unknown', requestTime: ${Date.now() / 1000 - 1.5 - Math.random()}, startLoadTime: ${Date.now() / 1000 - 1.4 - Math.random()}, wasAlternateProtocolAvailable: false, wasFetchedViaSpdy: false, wasNpnNegotiated: false }; },
      csi: function() { return { onloadT: ${Date.now() - 800}, pageT: 1500, startE: ${Date.now() - 1200}, tran: 15 }; },
    };
  }

  // ── 3. Navigator properties β€” CONSISTENTES con el perfil ─────────────────
  const navigatorProps = {
    platform           : '${id.platform}',
    vendor             : '${id.vendor}',
    hardwareConcurrency: ${id.hardwareConcurrency},
    deviceMemory       : ${id.deviceMemory},
    maxTouchPoints     : ${id.maxTouchPoints},
    languages          : ['${id.language}', '${id.language.split('-')[0]}', 'en'],
    language           : '${id.language}',
    plugins            : { length: ${id.plugins.length} },
    mimeTypes          : { length: 4 },
    doNotTrack         : null,
  };
  for (const [key, value] of Object.entries(navigatorProps)) {
    try {
      Object.defineProperty(navigator, key, { get: () => value, configurable: true });
    } catch(e) {}
  }

  // ── 4. Canvas fingerprint β€” ruido CONSISTENTE (mismo en cada carga) ────────
  // CRÍTICO: NO aleatorio en cada llamada, sino fijo para la sesión
  const CANVAS_NOISE = ${id.canvasNoise};
  const _origToDataURL = HTMLCanvasElement.prototype.toDataURL;
  const _origGetImageData = CanvasRenderingContext2D.prototype.getImageData;

  HTMLCanvasElement.prototype.toDataURL = function(type, quality) {
    const result = _origToDataURL.apply(this, arguments);
    // Solo aΓ±adir ruido si parece fingerprinting (canvas pequeΓ±o)
    if (this.width <= 300 && this.height <= 150) {
      try {
        const ctx = this.getContext('2d');
        if (ctx) {
          const id = ctx.getImageData(0, 0, this.width, this.height);
          // Ruido imperceptible pero consistente
          for (let i = 0; i < id.data.length; i += 4) {
            id.data[i]   = Math.max(0, Math.min(255, id.data[i]   + Math.round(CANVAS_NOISE * 255)));
            id.data[i+1] = Math.max(0, Math.min(255, id.data[i+1] + Math.round(CANVAS_NOISE * 127)));
          }
          ctx.putImageData(id, 0, 0);
        }
      } catch(e) {}
    }
    return _origToDataURL.apply(this, arguments);
  };

  // ── 5. WebGL fingerprint β€” valores REALES del perfil ─────────────────────
  const _origGetParam = WebGLRenderingContext.prototype.getParameter;
  WebGLRenderingContext.prototype.getParameter = function(parameter) {
    if (parameter === 0x9245) return '${id.vendorUnmasked}';   // UNMASKED_VENDOR_WEBGL
    if (parameter === 0x9246) return '${id.rendererUnmasked}'; // UNMASKED_RENDERER_WEBGL
    if (parameter === 0x1F00) return '${id.vendor}';           // VENDOR
    if (parameter === 0x1F01) return '${id.renderer}';         // RENDERER
    return _origGetParam.apply(this, arguments);
  };

  // WebGL2 tambiΓ©n
  try {
    const _origGetParam2 = WebGL2RenderingContext.prototype.getParameter;
    WebGL2RenderingContext.prototype.getParameter = function(parameter) {
      if (parameter === 0x9245) return '${id.vendorUnmasked}';
      if (parameter === 0x9246) return '${id.rendererUnmasked}';
      if (parameter === 0x1F00) return '${id.vendor}';
      if (parameter === 0x1F01) return '${id.renderer}';
      return _origGetParam2.apply(this, arguments);
    };
  } catch(e) {}

  // ── 6. Audio fingerprint β€” ruido consistente ──────────────────────────────
  const AUDIO_NOISE = ${id.audioNoise};
  try {
    const _origGetChanData = AudioBuffer.prototype.getChannelData;
    AudioBuffer.prototype.getChannelData = function(channel) {
      const data = _origGetChanData.apply(this, arguments);
      for (let i = 0; i < data.length; i += 100) {
        data[i] += AUDIO_NOISE;
      }
      return data;
    };
  } catch(e) {}

  // ── 7. Timing attacks β€” usar valores realistas ────────────────────────────
  const _origNow = Date.now;
  const timeOffset = ${Math.floor(Math.random() * 200) - 100};
  // Solo aΓ±adir offset mΓ­nimo para no romper lΓ³gica de timers
  // Date.now = function() { return _origNow() + timeOffset; }; // deshabilitado: rompe demasiado

  // ── 8. Permissions API β€” igual que Chrome real ────────────────────────────
  if (navigator.permissions) {
    const _origQuery = navigator.permissions.query.bind(navigator.permissions);
    navigator.permissions.query = async function(params) {
      if (params.name === 'notifications') return { state: Notification.permission, onchange: null };
      return _origQuery(params);
    };
  }

  // ── 9. Screen properties consistentes ────────────────────────────────────
  try {
    Object.defineProperty(screen, 'width',  { get: () => ${id.screenRes[0]}, configurable: true });
    Object.defineProperty(screen, 'height', { get: () => ${id.screenRes[1]}, configurable: true });
    Object.defineProperty(screen, 'availWidth',  { get: () => ${id.screenRes[0]}, configurable: true });
    Object.defineProperty(screen, 'availHeight', { get: () => ${id.screenRes[1] - 40}, configurable: true });
    Object.defineProperty(screen, 'colorDepth',  { get: () => ${id.colorDepth}, configurable: true });
    Object.defineProperty(window, 'devicePixelRatio', { get: () => ${id.pixelRatio}, configurable: true });
  } catch(e) {}

  // ── 10. Eliminar rastros de Playwright/CDP ────────────────────────────────
  try {
    delete window.cdc_adoQpoasnfa76pfcZLmcfl_Array;
    delete window.cdc_adoQpoasnfa76pfcZLmcfl_Promise;
    delete window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol;
    delete window.__playwright;
    delete window.__pw_manual;
    delete window.__PW_inspect;
  } catch(e) {}

  // ── 11. iframe contentWindow stealth ─────────────────────────────────────
  // Anti-bot checks en iframes (como Cloudflare Turnstile)
  const _origContentWindow = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, 'contentWindow');
  if (_origContentWindow) {
    Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', {
      get: function() {
        const cw = _origContentWindow.get.call(this);
        if (!cw) return null;
        try {
          Object.defineProperty(cw.navigator, 'webdriver', { get: () => undefined, configurable: true });
        } catch(e) {}
        return cw;
      },
      configurable: true,
    });
  }

  // ── 12. Ocultar stack traces de automation ────────────────────────────────
  // Algunos sites detectan errores con stack traces de Playwright
  const _origPrepareStackTrace = Error.prepareStackTrace;
  Error.prepareStackTrace = function(err, stack) {
    const filtered = stack.filter(frame => {
      const name = frame.getFileName?.() || '';
      return !name.includes('playwright') && !name.includes('puppeteer') && !name.includes('@playwright');
    });
    return _origPrepareStackTrace ? _origPrepareStackTrace(err, filtered) : filtered.map(f => f.toString()).join('\n');
  };

  window._stealthApplied = true;
  window._sessionId = '${id.sessionId}';
})();
`;
}

export function getIdentityStats(identity) {
  const id = identity || generateSessionIdentity();
  return {
    platform  : id.platform,
    renderer  : id.rendererUnmasked,
    screen    : id.screenRes.join('x'),
    language  : id.language,
    userAgent : id.userAgent.slice(0, 60) + '...',
  };
}