SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
7.0 KB · 127 lines typescript
Raw Blame History
1/**2 * Init script injected in every page before any site script runs. Removes the usual headless /3 * automation tells that anti-bot vendors probe (webdriver flag, missing `chrome` object, empty4 * plugin list, SwiftShader WebGL renderer, permission API quirks, notification permission5 * mismatch, `outerWidth === 0`, headless UA hints).6 */7export function stealthScript(opts: { platform: string; languages: string[]; hardwareConcurrency: number; deviceMemory: number; vendor: string; renderer: string; mobile: boolean }): string {8  return `(() => {9  const opts = ${JSON.stringify(opts)};10  const define = (obj, key, value) => { try { Object.defineProperty(obj, key, { get: () => value, configurable: true }); } catch {} };1112  // navigator.webdriver → false (and not enumerable like real Chrome)13  try { Object.defineProperty(Navigator.prototype, 'webdriver', { get: () => false, configurable: true }); } catch {}1415  // window.chrome (present in every Chrome, absent in headless-shell)16  if (!window.chrome) {17    const chrome = {18      app: { isInstalled: false, InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' }, RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' } },19      runtime: { OnInstalledReason: { CHROME_UPDATE: 'chrome_update', INSTALL: 'install', SHARED_MODULE_UPDATE: 'shared_module_update', UPDATE: 'update' }, PlatformArch: { ARM: 'arm', ARM64: 'arm64', MIPS: 'mips', MIPS64: 'mips64', X86_32: 'x86-32', X86_64: 'x86-64' }, PlatformOs: { ANDROID: 'android', CROS: 'cros', LINUX: 'linux', MAC: 'mac', OPENBSD: 'openbsd', WIN: 'win' }, RequestUpdateCheckStatus: { NO_UPDATE: 'no_update', THROTTLED: 'throttled', UPDATE_AVAILABLE: 'update_available' }, connect: function() {}, sendMessage: function() {}, id: undefined },20      csi: function() { return { onloadT: Date.now(), pageT: Math.random() * 1000, startE: Date.now(), tran: 15 }; },21      loadTimes: function() { const t = Date.now() / 1000; return { commitLoadTime: t, connectionInfo: 'h2', finishDocumentLoadTime: t, finishLoadTime: t, firstPaintAfterLoadTime: 0, firstPaintTime: t, navigationType: 'Other', npnNegotiatedProtocol: 'h2', requestTime: t - 0.5, startLoadTime: t - 0.4, wasAlternateProtocolAvailable: false, wasFetchedViaSpdy: true, wasNpnNegotiated: true }; },22    };23    define(window, 'chrome', chrome);24  }2526  // Plugins & mimeTypes (Chrome ships 5 PDF-related plugins)27  try {28    const mk = (name, filename, description) => ({ name, filename, description, length: 1, item: () => null, namedItem: () => null, 0: { type: 'application/pdf', suffixes: 'pdf', description } });29    const list = [mk('PDF Viewer', 'internal-pdf-viewer', 'Portable Document Format'), mk('Chrome PDF Viewer', 'internal-pdf-viewer', 'Portable Document Format'), mk('Chromium PDF Viewer', 'internal-pdf-viewer', 'Portable Document Format'), mk('Microsoft Edge PDF Viewer', 'internal-pdf-viewer', 'Portable Document Format'), mk('WebKit built-in PDF', 'internal-pdf-viewer', 'Portable Document Format')];30    const plugins = Object.create(PluginArray.prototype);31    list.forEach((p, i) => { plugins[i] = p; });32    define(plugins, 'length', opts.mobile ? 0 : list.length);33    plugins.item = (i) => plugins[i] ?? null; plugins.namedItem = (n) => list.find((p) => p.name === n) ?? null; plugins.refresh = () => {};34    if (navigator.plugins.length === 0) define(Navigator.prototype, 'plugins', plugins);35  } catch {}3637  define(Navigator.prototype, 'languages', opts.languages);38  define(Navigator.prototype, 'platform', opts.platform);39  define(Navigator.prototype, 'hardwareConcurrency', opts.hardwareConcurrency);40  define(Navigator.prototype, 'deviceMemory', opts.deviceMemory);41  if (opts.mobile) define(Navigator.prototype, 'maxTouchPoints', 5);4243  // Permissions: headless reports 'denied' for notifications while Notification.permission is 'default'44  try {45    const q = Permissions.prototype.query;46    Permissions.prototype.query = function(p) {47      if (p && p.name === 'notifications') return Promise.resolve({ state: Notification.permission === 'default' ? 'prompt' : Notification.permission, onchange: null });48      return q.call(this, p);49    };50  } catch {}5152  // WebGL vendor/renderer (SwiftShader / "Google Inc. (Google)" is a headless tell)53  try {54    const patch = (proto) => {55      const gp = proto.getParameter;56      proto.getParameter = function(param) {57        if (param === 37445) return opts.vendor;58        if (param === 37446) return opts.renderer;59        return gp.call(this, param);60      };61    };62    if (window.WebGLRenderingContext) patch(WebGLRenderingContext.prototype);63    if (window.WebGL2RenderingContext) patch(WebGL2RenderingContext.prototype);64  } catch {}6566  // outerWidth/outerHeight are 0 in headless67  if (!window.outerWidth) define(window, 'outerWidth', window.innerWidth);68  if (!window.outerHeight) define(window, 'outerHeight', window.innerHeight + 85);6970  // Hide automation-only properties leaking through Function.prototype.toString71  try {72    const nativeToString = Function.prototype.toString;73    const patched = new WeakSet([Permissions.prototype.query]);74    Function.prototype.toString = function() {75      if (patched.has(this)) return 'function query() { [native code] }';76      return nativeToString.call(this);77    };78  } catch {}7980  // Connection / battery plausibility81  try { if (navigator.connection) define(navigator.connection, 'rtt', 50); } catch {}82})();`;83}8485const TIMEZONES: Record<string, string> = {86  CA: "America/Toronto",87  US: "America/New_York",88  GB: "Europe/London",89  FR: "Europe/Paris",90  DE: "Europe/Berlin",91  ES: "Europe/Madrid",92  IT: "Europe/Rome",93  NL: "Europe/Amsterdam",94  BE: "Europe/Brussels",95  CH: "Europe/Zurich",96  SE: "Europe/Stockholm",97  PL: "Europe/Warsaw",98  PT: "Europe/Lisbon",99  IE: "Europe/Dublin",100  AU: "Australia/Sydney",101  NZ: "Pacific/Auckland",102  JP: "Asia/Tokyo",103  KR: "Asia/Seoul",104  IN: "Asia/Kolkata",105  SG: "Asia/Singapore",106  HK: "Asia/Hong_Kong",107  BR: "America/Sao_Paulo",108  MX: "America/Mexico_City",109  AR: "America/Argentina/Buenos_Aires",110  ZA: "Africa/Johannesburg",111  AE: "Asia/Dubai",112  TR: "Europe/Istanbul",113  RU: "Europe/Moscow",114  IL: "Asia/Jerusalem",115};116117export function timezoneFor(country: string | null | undefined): string {118  return (country && TIMEZONES[country.toUpperCase()]) || "America/New_York";119}120121export function webglFor(platform: string, tls: string): { vendor: string; renderer: string } {122  if (tls === "safari" || /mac/i.test(platform)) return { vendor: "Google Inc. (Apple)", renderer: "ANGLE (Apple, ANGLE Metal Renderer: Apple M2, Unspecified Version)" };123  if (/linux/i.test(platform) && !/arm/i.test(platform)) return { vendor: "Google Inc. (Intel)", renderer: "ANGLE (Intel, Mesa Intel(R) UHD Graphics 630 (CFL GT2), OpenGL 4.6)" };124  if (/arm/i.test(platform)) return { vendor: "Google Inc. (Qualcomm)", renderer: "ANGLE (Qualcomm, Adreno (TM) 750, OpenGL ES 3.2 V@0744.0)" };125  return { vendor: "Google Inc. (NVIDIA)", renderer: "ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 (0x00002504) Direct3D11 vs_5_0 ps_5_0, D3D11)" };126}127