first commit
This commit is contained in:
608
modules/ThreeDSecureHandlerModule.js
Normal file
608
modules/ThreeDSecureHandlerModule.js
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user