"use strict"; const { connect } = require('puppeteer-real-browser'); const { PuppeteerScreenRecorder } = require('puppeteer-screen-recorder'); const fs = require('fs'); const path = require('path'); const FAKE_PAGE = ` Turnstile
`; function ensureDir(dir) { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); } class TurnstileSolver { /** * @param {object} opts * @param {number} opts.timeout - ms tunggu token (default 60000) * @param {boolean} opts.record - aktifkan screen recording (default false) * @param {string} opts.recordDir - folder simpan recording (default './recordings') * @param {object} opts.proxy - { host, port, username, password } * @param {number} opts.width - viewport width (default 1280) * @param {number} opts.height - viewport height (default 720) */ constructor(opts = {}) { this.timeout = opts.timeout ?? 25000; this.record = opts.record ?? false; this.recordDir = opts.recordDir ?? path.join(process.cwd(), 'recordings'); this.proxy = opts.proxy ?? null; this.width = opts.width ?? 1280; this.height = opts.height ?? 720; this.browser = null; this.isReady = false; } async initialize() { if (this.isReady) return; const { browser } = await connect({ headless: false, turnstile: true, connectOption: { defaultViewport: null, // pakai ukuran window asli, lebih natural timeout: 120000, protocolTimeout: 300000, args: [ `--window-size=${this.width},${this.height}`, '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-blink-features=AutomationControlled', '--disable-web-security', '--disable-features=IsolateOrigins,site-per-process', '--ignore-certificate-errors', '--lang=en-US,en;q=0.9', // Paksa software rendering agar GPU tidak terdeteksi kosong '--use-gl=swiftshader', '--use-angle=swiftshader', // Profil user agent Windows agar terlihat seperti desktop biasa '--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', ], }, disableXvfb: true, }); this.browser = browser; this.isReady = true; console.log('[Solver] Browser siap.'); } async cleanup() { if (this.browser) { try { await this.browser.close(); } catch {} this.browser = null; this.isReady = false; } } async _startRecording(page, label) { if (!this.record) return null; ensureDir(this.recordDir); const safe = label.replace(/[^a-z0-9_-]/gi, '_'); const output = path.join(this.recordDir, `${safe}_${Date.now()}.mp4`); const recorder = new PuppeteerScreenRecorder(page, { followNewTab: false, fps: 25, videoFrame: { width: this.width, height: this.height }, videoCrf: 18, videoCodec: 'libx264', videoPreset: 'ultrafast', aspectRatio: '16:9', }); await recorder.start(output); return { recorder, output }; } async _stopRecording(rec) { if (!rec) return; try { await rec.recorder.stop(); } catch (err) { console.warn('[Turnstile] Failed to stop recording:', err.message); } } async _newPage() { const page = await this.browser.newPage(); await page.setDefaultTimeout(25000); await page.setDefaultNavigationTimeout(25000); if (this.proxy?.username && this.proxy?.password) { await page.authenticate({ username: this.proxy.username, password: this.proxy.password, }); } return page; } async solveWithSitekey(url, siteKey) { if (!this.isReady) await this.initialize(); const t0 = Date.now(); const page = await this._newPage(); const rec = await this._startRecording(page, `sitekey_${siteKey}`); try { const fakeHtml = FAKE_PAGE.replace(//g, siteKey); const baseUrl = url.endsWith('/') ? url : url + '/'; // Tangkap token via exposed function — dipanggil dari callback turnstile let capturedToken = null; await page.exposeFunction('__onTurnstileToken', (t) => { capturedToken = t; }); // Fake HTML pakai callback yang memanggil exposed function const injectedHtml = fakeHtml.replace( 'callback: function (token) {', 'callback: function (token) { window.__onTurnstileToken(token);' ); await page.setRequestInterception(true); page.on('request', async (req) => { if ([url, baseUrl].includes(req.url()) && req.resourceType() === 'document') { await req.respond({ status: 200, contentType: 'text/html', body: injectedHtml }); } else { await req.continue().catch(() => {}); } }); await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 25000 }); console.log('[Solver] Halaman fake dimuat, mulai polling token...'); // Poll: cek capturedToken atau window.turnstile.getResponse() const deadline = Date.now() + this.timeout; let token = null; let pollCount = 0; while (Date.now() < deadline) { if (capturedToken) { token = capturedToken; break; } token = await page.evaluate(() => { try { return window.turnstile?.getResponse() || null; } catch { return null; } }); if (token && token.length > 10) break; // Cek juga DOM fallback token = await page.evaluate(() => document.querySelector('[name="cf-response"]')?.value ?? null ); if (token && token.length > 10) break; token = null; pollCount++; // Log status setiap 5 detik if (pollCount % 10 === 0) { const hasTurnstile = await page.evaluate(() => typeof window.turnstile !== 'undefined').catch(() => false); console.log(`[Solver] Poll ke-${pollCount}, window.turnstile ada: ${hasTurnstile}`); } await new Promise(r => setTimeout(r, 500)); } // Screenshot debug jika token kosong if (!token) { try { await page.screenshot({ path: '/tmp/debug_turnstile.png', fullPage: true }); console.log('[Solver] Screenshot debug disimpan ke /tmp/debug_turnstile.png'); } catch {} } await this._stopRecording(rec); await page.close(); if (!token || token.length < 10) throw new Error('Token invalid or empty'); return { success: true, creator: 'XAi Community', // Jangan hapus! | Don't remove! token, time: +((Date.now() - t0) / 1000).toFixed(3), }; } catch (err) { await this._stopRecording(rec); try { await page.close(); } catch {} return { success: false, error: err.message, time: +((Date.now() - t0) / 1000).toFixed(3), }; } } async solveFromPage(url) { if (!this.isReady) await this.initialize(); const t0 = Date.now(); const page = await this._newPage(); const rec = await this._startRecording(page, 'page_solve'); try { await page.evaluateOnNewDocument(() => { async function waitForToken() { let token = null; while (!token) { try { token = window.turnstile?.getResponse(); } catch {} await new Promise((r) => setTimeout(r, 500)); } const c = document.createElement('input'); c.type = 'hidden'; c.name = 'cf-response'; c.value = token; document.body.appendChild(c); } waitForToken(); }); await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 25000 }); await page.waitForSelector('[name="cf-response"]', { timeout: this.timeout }); const token = await page.evaluate(() => document.querySelector('[name="cf-response"]')?.value ?? null ); await this._stopRecording(rec); await page.close(); if (!token || token.length < 10) throw new Error('Token invalid or empty'); return { success: true, creator: 'XAi Community', // Jangan hapus! | Don't remove! token, time: +((Date.now() - t0) / 1000).toFixed(3), }; } catch (err) { await this._stopRecording(rec); try { await page.close(); } catch {} return { success: false, error: err.message, time: +((Date.now() - t0) / 1000).toFixed(3), }; } } async solve(url, siteKey = null) { return siteKey ? this.solveWithSitekey(url, siteKey) : this.solveFromPage(url); } } module.exports = TurnstileSolver;