first commit

This commit is contained in:
dela
2026-01-10 11:36:27 +08:00
commit 9eba656dbd
7 changed files with 5480 additions and 0 deletions

View File

@@ -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;
}