/** * browser-stealth.ts — Clone Architect Phase 4 * Lazy-load playwright-extra + stealth plugin pour contourner Cloudflare/DataDome * Fallback gracieux vers playwright standard si non disponible */ import type { Browser, BrowserContext, Page } from 'playwright'; import { chromium as stdChromium } from 'playwright'; // Lazy init : ne charge stealth que si --stealth est demandé let stealthChromium: any = null; let stealthLoaded = false; async function loadStealth(): Promise { if (stealthLoaded) return stealthChromium; stealthLoaded = true; try { // @ts-ignore const { chromium: chromiumExtra } = await import('playwright-extra'); // @ts-ignore const stealthMod = await import('puppeteer-extra-plugin-stealth'); const StealthPlugin = stealthMod.default || stealthMod; const stealth = StealthPlugin(); // Retirer le plugin iframe.contentWindow qui cause des issues en playwright try { stealth.enabledEvasions.delete('iframe.contentWindow'); } catch {} chromiumExtra.use(stealth); stealthChromium = chromiumExtra; return chromiumExtra; } catch (err) { console.warn(` ⚠️ Stealth mode unavailable: ${(err as Error).message}`); console.warn(' Falling back to standard playwright'); return null; } } export interface StealthOptions { stealth?: boolean; viewport?: { width: number; height: number }; mobile?: boolean; } export async function launchBrowser(options: StealthOptions = {}): Promise { const launchArgs = [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-blink-features=AutomationControlled', '--disable-infobars', '--disable-dev-shm-usage', `--window-size=${options.viewport?.width || 1440},${options.viewport?.height || 900}`, ]; if (options.stealth) { const stealthBrowser = await loadStealth(); if (stealthBrowser) { return stealthBrowser.launch({ headless: true, args: launchArgs }); } } return stdChromium.launch({ headless: true, args: launchArgs }); } export async function createStealthContext( browser: Browser, options: StealthOptions = {}, ): Promise { const vp = options.viewport || { width: 1440, height: 900 }; const mobile = options.mobile; const context = await browser.newContext({ viewport: vp, userAgent: mobile ? 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', deviceScaleFactor: mobile ? 3 : 2, locale: 'fr-FR', timezoneId: 'Europe/Paris', extraHTTPHeaders: { 'Accept-Language': 'fr-FR,fr;q=0.9,en;q=0.8', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', 'sec-ch-ua': '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"', 'sec-ch-ua-mobile': mobile ? '?1' : '?0', 'sec-ch-ua-platform': mobile ? '"Android"' : '"Windows"', 'sec-fetch-dest': 'document', 'sec-fetch-mode': 'navigate', 'sec-fetch-site': 'none', 'sec-fetch-user': '?1', 'upgrade-insecure-requests': '1', }, }); // Anti-detection script (applied even without stealth plugin) await context.addInitScript(() => { // navigator.webdriver Object.defineProperty(navigator, 'webdriver', { get: () => false, configurable: true }); // navigator.plugins (make it look realistic) Object.defineProperty(navigator, 'plugins', { get: () => [ { name: 'PDF Viewer', description: 'Portable Document Format', filename: 'internal-pdf-viewer' }, { name: 'Chrome PDF Viewer', description: '', filename: 'internal-pdf-viewer' }, { name: 'Chromium PDF Viewer', description: '', filename: 'internal-pdf-viewer' }, ], configurable: true, }); // navigator.languages Object.defineProperty(navigator, 'languages', { get: () => ['fr-FR', 'fr', 'en-US', 'en'], configurable: true, }); // window.chrome (window as any).chrome = { runtime: {}, loadTimes: () => ({}), csi: () => ({}), app: {}, }; // Permissions API const originalQuery = window.navigator.permissions?.query; if (originalQuery) { window.navigator.permissions.query = (parameters: any): Promise => parameters.name === 'notifications' ? Promise.resolve({ state: Notification.permission } as any) : originalQuery(parameters); } // Hide CDP detection try { const nav = navigator as any; delete nav.__proto__.webdriver; } catch {} // WebGL vendor/renderer spoofing (very common bot detection) try { const getParameter = WebGLRenderingContext.prototype.getParameter; WebGLRenderingContext.prototype.getParameter = function(param: number) { if (param === 37445) return 'Intel Inc.'; // UNMASKED_VENDOR_WEBGL if (param === 37446) return 'Intel Iris OpenGL Engine'; // UNMASKED_RENDERER_WEBGL return getParameter.call(this, param); }; } catch {} }); return context; } // ─── Cloudflare detection + wait ────────────────────────────────────────────── export async function waitForCloudflare(page: Page, maxWait = 15000): Promise { const start = Date.now(); while (Date.now() - start < maxWait) { const isChallenge = await page.evaluate(() => { const title = document.title.toLowerCase(); const body = document.body?.innerText?.toLowerCase() || ''; return ( title.includes('just a moment') || title.includes('attendez') || body.includes('checking your browser') || body.includes('vérification') || !!document.querySelector('#cf-challenge-stage') || !!document.querySelector('[data-cf-challenge]') || !!document.querySelector('#turnstile-wrapper') ); }).catch(() => false); if (!isChallenge) return true; await page.waitForTimeout(500); } return false; } export async function detectBotProtection(page: Page): Promise { return page.evaluate(() => { const html = document.documentElement.outerHTML.toLowerCase(); const title = document.title.toLowerCase(); if (title.includes('just a moment') || document.querySelector('#cf-challenge-stage')) return 'cloudflare'; if (html.includes('datadome')) return 'datadome'; if (html.includes('perimeterx')) return 'perimeterx'; if (html.includes('imperva') || html.includes('incapsula')) return 'imperva'; if (document.querySelector('iframe[src*="recaptcha"]')) return 'recaptcha'; if (document.querySelector('.h-captcha, iframe[src*="hcaptcha"]')) return 'hcaptcha'; return null; }).catch(() => null); }