Files
Dotbypasser/extension/content/modules/FetchInterceptorModule.js
2026-01-10 16:53:02 +08:00

720 lines
22 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* ============================================================================
* 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;
}
// ============================================================================
// Extension Compatibility Wrapper
// ============================================================================
window.addEventListener('message', (event) => {
if (event.source !== window) return;
const message = event.data;
if (message && message.type === 'INIT_MODULE' && message.moduleName === 'fetchSpy') {
if (window.__fetchSpyInstance) return;
try {
const instance = new FetchInterceptorModule(message.config);
instance.init();
window.__fetchSpyInstance = instance;
console.log('[Extension] Fetch Spy initialized');
} catch (error) {
console.error('[Extension] Fetch Spy init failed:', error);
}
}
if (message && message.type === 'DESTROY_MODULE' && message.instanceKey === '__fetchSpyInstance') {
const instance = window.__fetchSpyInstance;
if (instance && typeof instance.destroy === 'function') {
instance.destroy();
delete window.__fetchSpyInstance;
}
}
});