/** * ============================================================================ * 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; }