637 lines
18 KiB
JavaScript
637 lines
18 KiB
JavaScript
/**
|
||
* ============================================================================
|
||
* 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;
|
||
}
|
||
|
||
|
||
// ============================================================================
|
||
// Extension Compatibility Wrapper
|
||
// ============================================================================
|
||
window.addEventListener('message', (event) => {
|
||
if (event.source !== window) return;
|
||
const message = event.data;
|
||
|
||
if (message && message.type === 'INIT_MODULE' && message.moduleName === 'threeDSecure') {
|
||
if (window.__threeDSecureInstance) return;
|
||
try {
|
||
const instance = new ThreeDSecureHandlerModule(message.config);
|
||
instance.init();
|
||
window.__threeDSecureInstance = instance;
|
||
console.log('[Extension] 3D Secure Handler initialized');
|
||
} catch (error) {
|
||
console.error('[Extension] 3D Secure init failed:', error);
|
||
}
|
||
}
|
||
|
||
if (message && message.type === 'DESTROY_MODULE' && message.instanceKey === '__threeDSecureInstance') {
|
||
const instance = window.__threeDSecureInstance;
|
||
if (instance && typeof instance.destroy === 'function') {
|
||
instance.destroy();
|
||
delete window.__threeDSecureInstance;
|
||
}
|
||
}
|
||
});
|