first commit

This commit is contained in:
2026-01-29 12:03:28 +08:00
commit 3ecfab1212
18 changed files with 34678 additions and 0 deletions

32
ICONS.md Normal file
View File

@@ -0,0 +1,32 @@
# 图标文件说明
插件需要以下图标文件:
- `icon16.png` (16x16 像素)
- `icon48.png` (48x48 像素)
- `icon128.png` (128x128 像素)
## 快速生成图标
你可以使用以下方法创建图标:
### 方法1: 在线生成
访问 https://www.favicon-generator.org/ 或类似网站生成图标
### 方法2: 使用图片
找一个锁图标或解密相关的图标,使用图片编辑工具调整大小
### 方法3: 使用临时占位图
暂时可以使用任何 16x16、48x48、128x128 的 PNG 图片作为占位
### 方法4: 创建简单 SVG 然后转换
创建一个简单的 SVG 图标,然后转换为 PNG
## 推荐图标设计
- 使用锁和解锁的图标
- 使用密钥或加密相关的符号
- 颜色:蓝色或紫色主题
插件可以暂时在没有图标的情况下工作,浏览器会使用默认图标。

131
INSTALL.md Normal file
View File

@@ -0,0 +1,131 @@
# 安装指南
## 安装步骤
### 1. 准备图标文件(可选)
插件需要图标文件才能正常显示。如果你暂时没有图标,可以:
- 跳过这一步,浏览器会使用默认图标
- 或者创建三个 PNG 文件:
- `icon16.png` (16x16)
- `icon48.png` (48x48)
- `icon128.png` (128x128)
### 2. 在 Chrome/Edge/Brave 中安装
1. 打开浏览器
2. 访问扩展管理页面:
- Chrome: 在地址栏输入 `chrome://extensions/`
- Edge: 在地址栏输入 `edge://extensions/`
- Brave: 在地址栏输入 `brave://extensions/`
3. 开启右上角的"开发者模式"开关
4. 点击"加载已解压的扩展程序"按钮
5. 选择 `browser-extension` 文件夹(包含 manifest.json 的文件夹)
6. 插件安装完成!
### 3. 使用插件
1. 访问目标网站(你的项目)
2. 打开开发者工具F12在控制台可以看到
```
✅ XHR 解密插件已启动
```
3. 触发一些请求后,控制台会输出解密信息:
```
🔓 XHR 解密 [POST] /api/example
🔑 密钥: 1234567890123456
📤 请求: { ... }
📥 响应: { ... }
```
4. 点击浏览器工具栏中的插件图标,打开弹窗查看所有解密记录
## 注意事项
### 重要:解密功能需要从页面获取解密函数
由于项目代码没有暴露解密工具,插件需要从页面环境获取解密函数。有以下几种方案:
#### 方案1从页面模块系统获取推荐
插件会尝试注入脚本到页面,访问页面的模块系统来获取 `Decrypt` 函数。
如果这种方式不起作用,需要:
#### 方案2修改插件添加 CryptoJS 支持
1. 下载 CryptoJS 库https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js
2. 将 CryptoJS 库放入 `browser-extension` 文件夹
3. 修改 `manifest.json`,添加:
```json
"web_accessible_resources": [
{
"resources": ["crypto-js.min.js"],
"matches": ["<all_urls>"]
}
]
```
4. 修改 `content-script.js`,在开头加载 CryptoJS
```javascript
// 从本地加载 CryptoJS
const script = document.createElement('script');
script.src = chrome.runtime.getURL('crypto-js.min.js');
script.onload = function() {
// CryptoJS 加载完成
};
document.head.appendChild(script);
```
#### 方案3临时修改项目代码不推荐
如果需要最简单的方式,可以临时修改项目代码暴露解密工具:
在 `src/utils/request.ts` 的末尾添加:
```typescript
if (typeof window !== 'undefined') {
(window as any).__DECRYPT_TOOLS__ = {
Decrypt,
Encrypt,
getMap: () => map,
getKey: (url: string, uuid: string) => {
return map?.[url + '/' + uuid]?.timestampKey;
}
};
}
```
## 故障排查
### 插件没有反应
1. 检查控制台是否有错误信息
2. 确认页面已完全加载
3. 检查是否有 XHR 请求发生
### 解密失败
1. 检查是否能从页面获取解密函数
2. 查看控制台的错误信息
3. 尝试使用方案2或方案3
### 插件图标不显示
1. 确认图标文件存在
2. 检查 manifest.json 中的图标路径
3. 重新加载插件
## 卸载
1. 访问扩展管理页面
2. 找到"XHR 请求解密工具"
3. 点击"移除"按钮

110
README.md Normal file
View File

@@ -0,0 +1,110 @@
# XHR 请求解密浏览器插件
这是一个用于解密项目中加密的 XHR 请求/响应的浏览器插件。
## 功能特性
- ✅ 自动拦截所有 XHR 请求
- ✅ 自动解密加密的请求数据和响应数据
- ✅ 在控制台输出解密后的数据
- ✅ 在插件弹窗中查看所有解密记录
- ✅ 支持导出解密记录为 JSON
- ✅ 支持复制单个请求/响应数据
## 安装方法
### Chrome / Edge / Brave
1. 打开浏览器,访问扩展管理页面:
- Chrome: `chrome://extensions/`
- Edge: `edge://extensions/`
- Brave: `brave://extensions/`
2. 开启"开发者模式"(右上角开关)
3. 点击"加载已解压的扩展程序"
4. 选择 `browser-extension` 文件夹
5. 插件安装完成!
## 使用方法
1. 安装插件后,访问目标网站
2. 插件会自动拦截并解密所有 XHR 请求
3. 在浏览器控制台可以看到解密后的数据输出:
```
🔓 XHR 解密 [POST] /api/example
🔑 密钥: 1234567890123456
📤 请求: { ... }
📥 响应: { ... }
```
4. 点击插件图标,打开弹窗查看所有解密记录
5. 在弹窗中可以:
- 查看所有请求的详细信息
- 复制单个请求/响应数据
- 清空所有记录
- 导出为 JSON 文件
## 工作原理
1. **拦截 XHR 请求**:通过重写 `XMLHttpRequest` 的原型方法来拦截所有请求
2. **获取密钥**
- 优先从页面的 `map` 对象获取(如果项目暴露了工具)
- 或者从请求头计算:`(timestamp + TraceId).slice(0, 16)`
3. **解密数据**
- 请求数据格式:`{ data: "base64String" }`
- 响应数据格式:`"encryptedString"`
- 使用 AES-ECB 模式Pkcs7 填充
4. **存储记录**:解密后的数据存储在插件的 storage 中
## 注意事项
1. **依赖页面环境**:插件会尝试从页面获取解密函数,确保页面已加载完成
2. **跳过特定接口**:某些接口不需要解密(如 `v1/picture/upload`),会自动跳过
3. **性能影响**:只保留最近 100 条记录在内存中,避免影响性能
4. **安全提醒**:此插件仅用于开发和调试,请勿在生产环境中暴露敏感信息
## 文件结构
```
browser-extension/
├── manifest.json # 插件配置
├── content-script.js # 内容脚本(拦截 XHR
├── popup.html # 弹窗界面
├── popup.js # 弹窗逻辑
├── background.js # 后台服务
├── icon16.png # 图标 16x16
├── icon48.png # 图标 48x48
├── icon128.png # 图标 128x128
└── README.md # 说明文档
```
## 开发建议
如果需要从页面获取解密函数,可以考虑:
1. **修改项目代码**(不推荐):在 `request.ts` 中暴露工具到 `window`
2. **注入脚本**:插件可以注入脚本到页面,尝试访问模块系统
3. **独立实现**:在插件中直接实现 AES 解密(需要引入 CryptoJS
## 更新日志
### v1.0.0
- 初始版本
- 支持拦截和解密 XHR 请求/响应
- 提供弹窗界面查看记录
- 支持导出功能

31
background.js Normal file
View File

@@ -0,0 +1,31 @@
/**
* Background Service Worker
*/
// 监听来自 content script 的消息
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'DECRYPTED_REQUEST') {
// 保存解密记录到 storage
chrome.storage.local.get(['decryptedRequests'], (result) => {
const requests = result.decryptedRequests || [];
requests.push(message.data);
// 只保留最近1000条
if (requests.length > 1000) {
requests.shift();
}
chrome.storage.local.set({ decryptedRequests: requests });
});
}
return true;
});
// 监听标签页更新
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete') {
// 页面加载完成,可以注入脚本
console.log('页面加载完成:', tab.url);
}
});

873
content-script.js Normal file
View File

@@ -0,0 +1,873 @@
/**
* Content Script - 使用 DevTools Network API 获取 XHR 请求
*
* 注意:这个脚本只在 DevTools 面板中使用
* 实际的请求捕获通过 chrome.devtools.network.onRequestFinished 实现
*/
// 这个文件现在已经不需要了,因为请求捕获在 DevTools 面板中完成
// 但保留文件以避免错误
// 将完整的拦截脚本注入到页面上下文
const script = document.createElement('script');
script.textContent = `
(function() {
'use strict';
console.log('🔧 [页面上下文] 拦截脚本已执行');
console.log('🔧 [页面上下文] XMLHttpRequest 类型:', typeof XMLHttpRequest);
console.log('🔧 [页面上下文] fetch 类型:', typeof fetch);
// 在页面上下文中存储解密记录
if (!window.__XHR_DECRYPT_DATA__) {
window.__XHR_DECRYPT_DATA__ = {
requests: [],
addRequest: function(record) {
this.requests.push(record);
if (this.requests.length > 100) {
this.requests.shift();
}
// 通知 content script
window.dispatchEvent(new CustomEvent('__XHR_DECRYPTED__', { detail: record }));
},
getRequests: function() {
return this.requests;
},
clear: function() {
this.requests = [];
}
};
}
const decryptedData = window.__XHR_DECRYPT_DATA__;
const requestHeadersMap = new Map();
const skipDecryptPaths = ['v1/picture/upload'];
// ========== 密钥获取策略管理器 ==========
const KeyGetStrategies = {
strategies: [],
// 注册密钥获取策略
register: function(strategy) {
if (strategy && typeof strategy.getName === 'function' && typeof strategy.getKey === 'function') {
this.strategies.push({
name: strategy.getName(),
priority: strategy.priority || 100,
getKey: strategy.getKey,
canHandle: strategy.canHandle || (() => true)
});
// 按优先级排序(数字越小优先级越高)
this.strategies.sort((a, b) => a.priority - b.priority);
}
},
// 获取密钥(按优先级尝试所有策略)
getKey: function(url, uuid, requestHeaders) {
for (const strategy of this.strategies) {
try {
if (strategy.canHandle(url, uuid, requestHeaders)) {
const key = strategy.getKey(url, uuid, requestHeaders);
if (key) {
console.log('🔑 [密钥策略] 使用策略:', strategy.name, '获取密钥成功');
return { key: key, strategy: strategy.name };
}
}
} catch (error) {
console.warn('🔑 [密钥策略] 策略', strategy.name, '执行失败:', error);
continue;
}
}
console.warn('🔑 [密钥策略] 所有策略都无法获取密钥');
return null;
},
// 初始化默认策略
init: function() {
// 策略1: 从页面工具获取密钥(优先级最高)
this.register({
name: '页面工具获取',
priority: 10,
canHandle: function(url, uuid, requestHeaders) {
return window.__DECRYPT_TOOLS__ && window.__DECRYPT_TOOLS__.getKey;
},
getKey: function(url, uuid, requestHeaders) {
const tools = window.__DECRYPT_TOOLS__;
if (tools && tools.getKey) {
return tools.getKey(url, uuid);
}
return null;
}
});
// 策略2: 从请求头计算密钥timestamp + TraceId
this.register({
name: '请求头计算',
priority: 20,
canHandle: function(url, uuid, requestHeaders) {
const timestamp = requestHeaders?.time || requestHeaders?.Time || '';
const traceId = requestHeaders?.TraceId || requestHeaders?.['trace-id'] || '';
return !!(timestamp && traceId);
},
getKey: function(url, uuid, requestHeaders) {
const timestamp = requestHeaders?.time || requestHeaders?.Time || '';
const traceId = requestHeaders?.TraceId || requestHeaders?.['trace-id'] || '';
if (timestamp && traceId) {
return String(timestamp + traceId).slice(0, 16);
}
return null;
}
});
// 可以在这里添加更多密钥获取策略
// 例如:从 localStorage 获取、从 URL 参数获取、从 Cookie 获取等
}
};
// ========== 解密策略管理器 ==========
const DecryptStrategies = {
strategies: [],
// 注册解密策略
register: function(strategy) {
if (strategy && typeof strategy.getName === 'function' && typeof strategy.decrypt === 'function') {
this.strategies.push({
name: strategy.getName(),
priority: strategy.priority || 100,
decrypt: strategy.decrypt,
canHandle: strategy.canHandle || (() => true)
});
// 按优先级排序
this.strategies.sort((a, b) => a.priority - b.priority);
}
},
// 解密数据(按优先级尝试所有策略)
decrypt: function(encryptedData, key, isRequest = false) {
for (const strategy of this.strategies) {
try {
if (strategy.canHandle(encryptedData, key, isRequest)) {
const decrypted = strategy.decrypt(encryptedData, key, isRequest);
if (decrypted !== null && decrypted !== undefined && decrypted !== encryptedData) {
console.log('🔓 [解密策略] 使用策略:', strategy.name, '解密成功');
return { data: decrypted, strategy: strategy.name };
}
}
} catch (error) {
console.warn('🔓 [解密策略] 策略', strategy.name, '执行失败:', error);
continue;
}
}
console.warn('🔓 [解密策略] 所有策略都无法解密数据');
return { data: encryptedData, strategy: null };
},
// 初始化默认策略
init: function() {
// 策略1: 使用页面工具解密(优先级最高)
this.register({
name: '页面工具解密',
priority: 10,
canHandle: function(encryptedData, key, isRequest) {
return window.__DECRYPT_TOOLS__ && window.__DECRYPT_TOOLS__.Decrypt;
},
decrypt: function(encryptedData, key, isRequest) {
const tools = window.__DECRYPT_TOOLS__;
if (!tools || !tools.Decrypt) {
return null;
}
try {
if (isRequest) {
// 请求数据格式:{ data: "base64String" }
if (encryptedData && encryptedData.data && typeof encryptedData.data === 'string') {
const decrypted = tools.Decrypt(encryptedData.data, key);
if (decrypted) {
return JSON.parse(decrypted);
}
}
} else {
// 响应数据格式:字符串或对象
if (typeof encryptedData === 'string') {
const decrypted = tools.Decrypt(encryptedData, key);
if (decrypted) {
try {
return JSON.parse(decrypted);
} catch (e) {
if (window.JSON5) {
return window.JSON5.parse(decrypted);
}
return decrypted;
}
}
}
}
} catch (error) {
console.error('解密失败:', error);
}
return null;
}
});
// 策略2: 使用 CryptoJS 直接解密(如果可用)
this.register({
name: 'CryptoJS 解密',
priority: 20,
canHandle: function(encryptedData, key, isRequest) {
return typeof CryptoJS !== 'undefined' && key;
},
decrypt: function(encryptedData, key, isRequest) {
try {
if (typeof CryptoJS === 'undefined') {
return null;
}
let dataToDecrypt = null;
if (isRequest) {
if (encryptedData && encryptedData.data && typeof encryptedData.data === 'string') {
dataToDecrypt = encryptedData.data;
} else {
return null;
}
} else {
if (typeof encryptedData === 'string') {
dataToDecrypt = encryptedData;
} else {
return null;
}
}
if (!dataToDecrypt) {
return null;
}
// 清理数据
const cleanData = dataToDecrypt.replace(/\s/g, '').replace(/"/g, '');
const cleanKey = key.replace(/\s/g, '').replace(/"/g, '');
// AES 解密
const decrypt = CryptoJS.AES.decrypt(
cleanData,
CryptoJS.enc.Utf8.parse(cleanKey),
{
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}
);
const decrypted = decrypt.toString(CryptoJS.enc.Utf8);
if (!decrypted) {
return null;
}
// 尝试解析 JSON
try {
return JSON.parse(decrypted);
} catch (e) {
if (window.JSON5) {
return window.JSON5.parse(decrypted);
}
return decrypted;
}
} catch (error) {
return null;
}
}
});
// 可以在这里添加更多解密策略
// 例如:其他加密算法、不同的填充方式等
}
};
// 初始化策略管理器
KeyGetStrategies.init();
DecryptStrategies.init();
// ========== 便捷函数 ==========
// 获取密钥
function getTimestampKey(url, uuid, requestHeaders) {
const result = KeyGetStrategies.getKey(url, uuid, requestHeaders);
return result ? result.key : null;
}
// 解密请求数据
function decryptRequestData(encryptedData, timestampKey) {
if (!timestampKey) {
return encryptedData;
}
const result = DecryptStrategies.decrypt(encryptedData, timestampKey, true);
return result.data;
}
// 解密响应数据
function decryptResponseData(encryptedData, timestampKey) {
if (!timestampKey) {
return encryptedData;
}
const result = DecryptStrategies.decrypt(encryptedData, timestampKey, false);
return result.data;
}
// 暴露策略管理器,允许外部注册新策略
window.__XHR_DECRYPT_STRATEGIES__ = {
keyGetStrategies: KeyGetStrategies,
decryptStrategies: DecryptStrategies
};
// ========== 拦截 XMLHttpRequest ==========
// 必须在页面脚本运行前拦截,否则会被覆盖
(function() {
if (typeof XMLHttpRequest === 'undefined') {
console.warn('⚠️ [页面上下文] XMLHttpRequest 未定义');
return;
}
// 保存原始函数(必须在任何其他脚本运行前保存)
const originalXHR = window.XMLHttpRequest;
const originalOpen = XMLHttpRequest.prototype.open;
const originalSend = XMLHttpRequest.prototype.send;
const originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
// 检查是否已经被拦截过
const openStr = originalOpen.toString();
if (openStr.includes('_method') || openStr.includes('__XHR_DECRYPT')) {
console.log('⚠️ [页面上下文] XHR 可能已被其他脚本拦截');
}
XMLHttpRequest.prototype.open = function(method, url, ...args) {
this._method = method;
this._url = url;
this._requestHeaders = {};
requestHeadersMap.set(this, {});
console.log('🔍 [页面上下文] 拦截 XHR open:', method, url);
// 确保返回正确的结果
const result = originalOpen.apply(this, [method, url, ...args]);
return result;
};
XMLHttpRequest.prototype.setRequestHeader = function(name, value) {
if (!this._requestHeaders) {
this._requestHeaders = {};
}
this._requestHeaders[name] = value;
const headers = requestHeadersMap.get(this) || {};
headers[name] = value;
requestHeadersMap.set(this, headers);
return originalSetRequestHeader.apply(this, [name, value]);
};
XMLHttpRequest.prototype.send = function(body) {
const url = this._url;
const method = this._method;
const requestId = Date.now() + Math.random();
const shouldSkip = skipDecryptPaths.some(path => url.includes(path));
// 监听响应
this.addEventListener('loadend', function() {
if (shouldSkip) return;
try {
const headers = requestHeadersMap.get(this) || {};
const uuid = this.getResponseHeader('uuid');
if (!uuid) {
return;
}
// 获取密钥(包含策略信息)
const keyResult = KeyGetStrategies.getKey(url, uuid, headers);
if (!keyResult || !keyResult.key) {
console.warn('无法获取密钥', { url, uuid, headers });
return;
}
const timestampKey = keyResult.key;
const keyStrategy = keyResult.strategy;
// 解密请求数据(包含策略信息)
let requestBody = null;
let requestDecryptStrategy = null;
if (body) {
try {
const parsed = typeof body === 'string' ? JSON.parse(body) : body;
const decryptResult = DecryptStrategies.decrypt(parsed, timestampKey, true);
requestBody = decryptResult.data;
requestDecryptStrategy = decryptResult.strategy;
} catch (e) {
requestBody = body;
}
}
// 解密响应数据(包含策略信息)
let responseData = null;
try {
if (this.responseText) {
responseData = JSON.parse(this.responseText);
}
} catch (e) {
responseData = this.responseText || this.response;
}
const decryptResult = DecryptStrategies.decrypt(responseData, timestampKey, false);
const decryptedResponse = decryptResult.data;
const responseDecryptStrategy = decryptResult.strategy;
// 记录解密结果
const decryptedRecord = {
id: requestId,
url: url,
method: method,
uuid: uuid,
timestampKey: timestampKey,
keyStrategy: keyStrategy,
request: requestBody,
requestDecryptStrategy: requestDecryptStrategy,
response: decryptedResponse,
responseDecryptStrategy: responseDecryptStrategy,
timestamp: new Date().toISOString()
};
// 存储到页面上下文
decryptedData.addRequest(decryptedRecord);
// 输出到控制台
console.group('🔓 XHR 解密 [' + method + '] ' + url);
console.log('🔑 密钥:', timestampKey, '(策略: ' + keyStrategy + ')');
if (requestDecryptStrategy) {
console.log('📤 请求:', requestBody, '(解密策略: ' + requestDecryptStrategy + ')');
} else {
console.log('📤 请求:', requestBody);
}
if (responseDecryptStrategy) {
console.log('📥 响应:', decryptedResponse, '(解密策略: ' + responseDecryptStrategy + ')');
} else {
console.log('📥 响应:', decryptedResponse);
}
console.log('📍 UUID:', uuid);
console.groupEnd();
} catch (error) {
console.error('❌ 解密过程出错:', error);
}
});
return originalSend.apply(this, [body]);
};
console.log('✅ [页面上下文] XHR 拦截已设置');
console.log('✅ [页面上下文] originalOpen 类型:', typeof originalOpen);
console.log('✅ [页面上下文] XMLHttpRequest.prototype.open:', typeof XMLHttpRequest.prototype.open);
// 验证拦截是否真的生效
setTimeout(function() {
const testXhr = new XMLHttpRequest();
const testOpenStr = testXhr.open.toString();
if (testOpenStr.includes('_method')) {
console.log('✅ [页面上下文] XHR 拦截验证成功');
} else {
console.error('❌ [页面上下文] XHR 拦截验证失败,可能被覆盖');
console.error(' 当前 open 函数:', testOpenStr.substring(0, 100));
}
}, 500);
})();
// ========== 拦截 Fetch ==========
// 必须在页面脚本运行前拦截
(function() {
if (typeof fetch === 'undefined' || !window.fetch) {
console.warn('⚠️ [页面上下文] fetch 未定义');
return;
}
// 保存原始函数
const originalFetch = window.fetch;
// 检查是否已经被拦截过
const fetchStr = originalFetch.toString();
if (fetchStr.includes('clonedResponse') || fetchStr.includes('__XHR_DECRYPT')) {
console.log('⚠️ [页面上下文] Fetch 可能已被其他脚本拦截');
}
window.fetch = function(...args) {
const [url, options = {}] = args;
const method = options.method || 'GET';
const requestId = Date.now() + Math.random();
const shouldSkip = skipDecryptPaths.some(path => url.includes(path));
const originalBody = options.body;
console.log('🔍 [页面上下文] 拦截 Fetch:', method, url);
return originalFetch.apply(this, args).then(async (response) => {
if (shouldSkip) return response;
try {
const clonedResponse = response.clone();
const uuid = clonedResponse.headers.get('uuid');
if (!uuid) {
return response;
}
const headers = options.headers || {};
const timestamp = headers.time || headers.Time || '';
const traceId = headers.TraceId || headers['trace-id'] || '';
// 获取密钥(包含策略信息)
const keyResult = KeyGetStrategies.getKey(url, uuid, { time: timestamp, TraceId: traceId });
if (!keyResult || !keyResult.key) {
console.warn('无法获取密钥 (Fetch)', { url, uuid });
return response;
}
const timestampKey = keyResult.key;
const keyStrategy = keyResult.strategy;
// 解密请求数据(包含策略信息)
let requestBody = null;
let requestDecryptStrategy = null;
if (originalBody) {
try {
if (typeof originalBody === 'string') {
const parsed = JSON.parse(originalBody);
const decryptResult = DecryptStrategies.decrypt(parsed, timestampKey, true);
requestBody = decryptResult.data;
requestDecryptStrategy = decryptResult.strategy;
} else {
requestBody = originalBody;
}
} catch (e) {
requestBody = originalBody;
}
}
// 解密响应数据(包含策略信息)
let responseData = null;
try {
const text = await clonedResponse.text();
if (text) {
try {
responseData = JSON.parse(text);
} catch (e) {
responseData = text;
}
}
} catch (e) {
console.error('读取响应数据失败:', e);
}
const decryptResult = DecryptStrategies.decrypt(responseData, timestampKey, false);
const decryptedResponse = decryptResult.data;
const responseDecryptStrategy = decryptResult.strategy;
// 记录解密结果
const decryptedRecord = {
id: requestId,
url: url,
method: method,
uuid: uuid,
timestampKey: timestampKey,
keyStrategy: keyStrategy,
request: requestBody,
requestDecryptStrategy: requestDecryptStrategy,
response: decryptedResponse,
responseDecryptStrategy: responseDecryptStrategy,
timestamp: new Date().toISOString()
};
// 存储到页面上下文
decryptedData.addRequest(decryptedRecord);
// 输出到控制台
console.group('🔓 Fetch 解密 [' + method + '] ' + url);
console.log('🔑 密钥:', timestampKey, '(策略: ' + keyStrategy + ')');
if (requestDecryptStrategy) {
console.log('📤 请求:', requestBody, '(解密策略: ' + requestDecryptStrategy + ')');
} else {
console.log('📤 请求:', requestBody);
}
if (responseDecryptStrategy) {
console.log('📥 响应:', decryptedResponse, '(解密策略: ' + responseDecryptStrategy + ')');
} else {
console.log('📥 响应:', decryptedResponse);
}
console.log('📍 UUID:', uuid);
console.groupEnd();
} catch (error) {
console.error('❌ Fetch 解密过程出错:', error);
}
return response;
}).catch((error) => {
console.error('❌ Fetch 请求失败:', error);
return Promise.reject(error);
});
};
console.log('✅ [页面上下文] Fetch 拦截已设置');
console.log('✅ [页面上下文] originalFetch 类型:', typeof originalFetch);
console.log('✅ [页面上下文] window.fetch:', typeof window.fetch);
// 验证拦截是否真的生效
setTimeout(function() {
const fetchStr = window.fetch.toString();
if (fetchStr.includes('clonedResponse') || fetchStr.includes('requestId')) {
console.log('✅ [页面上下文] Fetch 拦截验证成功');
} else {
console.error('❌ [页面上下文] Fetch 拦截验证失败,可能被覆盖');
console.error(' 当前 fetch 函数:', fetchStr.substring(0, 100));
}
}, 500);
})();
// 暴露 API 供 DevTools 面板使用
window.__XHR_DECRYPT_EXTENSION__ = {
getDecryptedRequests: function() {
return decryptedData.getRequests();
},
clearDecryptedRequests: function() {
decryptedData.clear();
console.log('✅ 已清空解密记录');
}
};
console.log('✅ [页面上下文] 拦截脚本初始化完成');
console.log('✅ [页面上下文] 已拦截 XHR:', typeof XMLHttpRequest !== 'undefined');
console.log('✅ [页面上下文] 已拦截 Fetch:', typeof fetch !== 'undefined');
// 测试一下拦截是否真的工作
setTimeout(function() {
console.log('🧪 [页面上下文] 测试:检查拦截状态');
console.log('🧪 [页面上下文] XMLHttpRequest.prototype.open:', typeof XMLHttpRequest.prototype.open);
console.log('🧪 [页面上下文] window.fetch:', typeof window.fetch);
// 尝试创建一个测试请求(仅用于验证)
try {
const testXhr = new XMLHttpRequest();
console.log('🧪 [页面上下文] 测试:可以创建 XMLHttpRequest 实例');
} catch (e) {
console.error('🧪 [页面上下文] 测试:无法创建 XMLHttpRequest:', e);
}
}, 1000);
})();
`;
// 注入到页面上下文
// 必须在页面脚本运行之前注入,否则无法拦截
function injectToPage() {
try {
// 方式1尝试注入到 head
if (document.head) {
document.head.appendChild(script.cloneNode(true));
console.log('✅ 拦截脚本已注入到 head');
return;
}
// 方式2注入到 documentElement
if (document.documentElement) {
document.documentElement.appendChild(script.cloneNode(true));
console.log('✅ 拦截脚本已注入到 documentElement');
return;
}
// 方式3如果都不行等待 DOM 加载
console.warn('⚠️ DOM 未准备好,等待加载...');
const checkInterval = setInterval(function () {
if (document.head || document.documentElement) {
clearInterval(checkInterval);
injectToPage();
}
}, 50);
// 超时保护
setTimeout(function () {
clearInterval(checkInterval);
console.error('❌ 注入超时DOM 可能无法加载');
}, 5000);
} catch (e) {
console.error('❌ 注入脚本失败:', e);
}
}
// 立即尝试注入(不等待)
try {
// 如果 DOM 已准备好,立即注入
if (document.head || document.documentElement) {
injectToPage();
} else {
// 如果还没准备好,使用多种方式等待
console.log('⏳ DOM 未准备好,准备注入...');
// 方式1等待 DOMContentLoaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () {
console.log('📄 DOMContentLoaded 触发,开始注入');
injectToPage();
}, { once: true });
}
// 方式2也尝试立即注入某些浏览器允许
setTimeout(function () {
if (document.head || document.documentElement) {
console.log('⏱️ 延迟注入尝试');
injectToPage();
}
}, 0);
// 方式3监听 readystatechange
document.addEventListener('readystatechange', function () {
if (document.readyState !== 'loading') {
console.log('📄 readystatechange 触发readyState:', document.readyState);
injectToPage();
}
}, { once: true });
}
} catch (e) {
console.error('❌ 注入过程出错:', e);
}
// 在 Content Script 中监听来自页面上下文的事件
window.addEventListener('__XHR_DECRYPTED__', function (event) {
if (event.detail) {
decryptedRequests.push(event.detail);
if (decryptedRequests.length > 100) {
decryptedRequests.shift();
}
// 发送到 background
if (typeof chrome !== 'undefined' && chrome.runtime) {
chrome.runtime.sendMessage({
type: 'DECRYPTED_REQUEST',
data: event.detail
});
}
}
});
// 监听来自 popup 的消息
if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.onMessage) {
chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
if (message.action === 'getDecryptedRequests') {
// 直接返回 content script 中存储的数据
// content script 会从页面事件中接收更新
sendResponse({ data: decryptedRequests });
return true;
} else if (message.action === 'clearDecryptedRequests') {
decryptedRequests = [];
// 清除页面上下文的记录
const clearScript = document.createElement('script');
clearScript.textContent = `
(function() {
if (window.__XHR_DECRYPT_EXTENSION__) {
window.__XHR_DECRYPT_EXTENSION__.clearDecryptedRequests();
}
})();
`;
document.documentElement.appendChild(clearScript);
clearScript.remove();
sendResponse({ success: true });
}
});
}
// 暴露 API 供 popup 使用Content Script 上下文)
window.__XHR_DECRYPT_EXTENSION__ = {
getDecryptedRequests: function () {
// 返回 content script 中存储的数据
return decryptedRequests;
},
clearDecryptedRequests: function () {
decryptedRequests = [];
// 清除页面上下文的记录
const clearScript = document.createElement('script');
clearScript.textContent = `
(function() {
if (window.__XHR_DECRYPT_EXTENSION__) {
window.__XHR_DECRYPT_EXTENSION__.clearDecryptedRequests();
}
})();
`;
document.documentElement.appendChild(clearScript);
clearScript.remove();
}
};
console.log('✅ Content Script 已启动');
console.log('📊 Content Script 当前记录数:', decryptedRequests.length);
console.log('📊 Content Script document.readyState:', document.readyState);
console.log('📊 Content Script 准备注入拦截脚本...');
console.log('📊 Content Script window.location:', window.location.href);
// 立即检查是否已经注入成功(某些情况下可能已经注入)
try {
const testScript = document.createElement('script');
testScript.textContent = `
(function() {
if (window.__XHR_DECRYPT_EXTENSION__) {
console.log('✅ [页面上下文] 拦截对象已存在');
} else {
console.warn('⚠️ [页面上下文] 拦截对象不存在');
}
if (window.__XHR_DECRYPT_DATA__) {
console.log('✅ [页面上下文] 数据对象已存在');
} else {
console.warn('⚠️ [页面上下文] 数据对象不存在');
}
})();
`;
if (document.head || document.documentElement) {
(document.head || document.documentElement).appendChild(testScript);
testScript.remove();
}
} catch (e) {
console.error('❌ 检查注入状态失败:', e);
}
// 延迟检查注入是否成功
setTimeout(function () {
// 通过 DevTools API 检查
if (typeof chrome !== 'undefined' && chrome.devtools && chrome.devtools.inspectedWindow) {
chrome.devtools.inspectedWindow.eval('typeof window.__XHR_DECRYPT_EXTENSION__', function (result, isException) {
if (isException) {
console.warn('⚠️ 无法访问页面上下文,可能注入失败。错误:', result);
} else {
if (result === 'object') {
console.log('✅ 拦截脚本注入成功,页面上下文对象存在');
} else {
console.warn('⚠️ 拦截脚本可能未注入成功,页面上下文对象类型:', result);
}
}
});
} else {
// 如果没有 DevTools使用其他方式检查
const checkScript = document.createElement('script');
checkScript.textContent = `
(function() {
console.log('🔍 [页面上下文] 检查拦截状态...');
console.log('🔍 [页面上下文] __XHR_DECRYPT_EXTENSION__:', typeof window.__XHR_DECRYPT_EXTENSION__);
console.log('🔍 [页面上下文] __XHR_DECRYPT_DATA__:', typeof window.__XHR_DECRYPT_DATA__);
console.log('🔍 [页面上下文] XMLHttpRequest:', typeof XMLHttpRequest);
console.log('🔍 [页面上下文] fetch:', typeof fetch);
// 检查拦截是否真的工作
if (XMLHttpRequest.prototype.open.toString().includes('_method')) {
console.log('✅ [页面上下文] XHR 拦截已生效');
} else {
console.warn('⚠️ [页面上下文] XHR 拦截可能未生效');
}
if (window.fetch.toString().includes('clonedResponse') || window.fetch.toString().includes('requestId')) {
console.log('✅ [页面上下文] Fetch 拦截已生效');
} else {
console.warn('⚠️ [页面上下文] Fetch 拦截可能未生效');
}
})();
`;
if (document.head || document.documentElement) {
(document.head || document.documentElement).appendChild(checkScript);
checkScript.remove();
}
}
}, 3000);
}) ();

84
create-icons.html Normal file
View File

@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>生成插件图标</title>
</head>
<body>
<h1>生成插件图标</h1>
<p>点击下面的按钮生成图标,然后右键保存图片</p>
<canvas id="canvas16" width="16" height="16"></canvas>
<button onclick="downloadIcon(16)">下载 icon16.png</button>
<br><br>
<canvas id="canvas48" width="48" height="48"></canvas>
<button onclick="downloadIcon(48)">下载 icon48.png</button>
<br><br>
<canvas id="canvas128" width="128" height="128"></canvas>
<button onclick="downloadIcon(128)">下载 icon128.png</button>
<script>
function createIcon(size) {
const canvas = document.getElementById(`canvas${size}`);
const ctx = canvas.getContext('2d');
// 背景
const gradient = ctx.createLinearGradient(0, 0, size, size);
gradient.addColorStop(0, '#667eea');
gradient.addColorStop(1, '#764ba2');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, size, size);
// 绘制锁图标
ctx.fillStyle = 'white';
ctx.strokeStyle = 'white';
ctx.lineWidth = size / 8;
// 锁身
const lockWidth = size * 0.4;
const lockHeight = size * 0.5;
const lockX = (size - lockWidth) / 2;
const lockY = size * 0.3;
// 锁体
ctx.fillRect(lockX, lockY, lockWidth, lockHeight);
// 锁孔
ctx.fillStyle = '#667eea';
const holeSize = size * 0.15;
ctx.beginPath();
ctx.arc(lockX + lockWidth / 2, lockY + lockHeight * 0.6, holeSize / 2, 0, Math.PI * 2);
ctx.fill();
// 锁扣
ctx.strokeStyle = 'white';
ctx.lineWidth = size / 12;
ctx.beginPath();
ctx.arc(lockX + lockWidth / 2, lockY, lockWidth * 0.3, Math.PI, 0, false);
ctx.stroke();
}
function downloadIcon(size) {
const canvas = document.getElementById(`canvas${size}`);
canvas.toBlob(function (blob) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `icon${size}.png`;
a.click();
URL.revokeObjectURL(url);
});
}
// 创建图标
createIcon(16);
createIcon(48);
createIcon(128);
</script>
</body>
</html>

1
crypto-js.min.js vendored Normal file

File diff suppressed because one or more lines are too long

13
devtools.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<!-- DevTools Page - 用于创建 DevTools 面板 -->
<script src="devtools.js"></script>
</body>
</html>

23
devtools.js Normal file
View File

@@ -0,0 +1,23 @@
/**
* DevTools Panel - 创建开发者工具面板
*/
// 创建 DevTools 面板
chrome.devtools.panels.create(
"🔓 XHR 解密",
"icon16.png", // 图标(可选,如果不想显示图标可以设为 null
"panel.html",
function (panel) {
console.log('✅ XHR 解密面板已创建');
// 面板创建完成后的回调
panel.onShown.addListener(function (window) {
console.log('面板已显示');
});
panel.onHidden.addListener(function () {
console.log('面板已隐藏');
});
}
);

345
inject-script.js Normal file
View File

@@ -0,0 +1,345 @@
/**
* 独立的注入脚本 - 在页面上下文中最先执行
* 这个文件会被注入到页面上下文中,确保在任何页面脚本运行前拦截
*/
(function () {
'use strict';
// 立即拦截,不等待任何其他代码
console.log('🚀 [页面上下文] 拦截脚本开始执行 - 时间:', new Date().toISOString());
// 在页面上下文中存储解密记录
if (!window.__XHR_DECRYPT_DATA__) {
window.__XHR_DECRYPT_DATA__ = {
requests: [],
addRequest: function (record) {
this.requests.push(record);
if (this.requests.length > 100) {
this.requests.shift();
}
// 通知 content script
window.dispatchEvent(new CustomEvent('__XHR_DECRYPTED__', { detail: record }));
},
getRequests: function () {
return this.requests;
},
clear: function () {
this.requests = [];
}
};
}
const decryptedData = window.__XHR_DECRYPT_DATA__;
const requestHeadersMap = new Map();
const skipDecryptPaths = ['v1/picture/upload'];
// 获取解密工具
function getDecryptTools() {
if (window.__DECRYPT_TOOLS__) {
return window.__DECRYPT_TOOLS__;
}
return null;
}
// 计算密钥
function calculateKey(timestamp, traceId) {
if (!timestamp || !traceId) return null;
return String(timestamp + traceId).slice(0, 16);
}
// 获取解密函数
function getDecryptFunction() {
const tools = getDecryptTools();
if (tools && tools.Decrypt) {
return tools.Decrypt;
}
return null;
}
// 解密请求数据
function decryptRequestData(encryptedData, timestampKey) {
try {
const Decrypt = getDecryptFunction();
if (!Decrypt) {
console.warn('[页面上下文] 无法获取解密函数');
return encryptedData;
}
if (encryptedData && encryptedData.data && typeof encryptedData.data === 'string') {
const decrypted = Decrypt(encryptedData.data, timestampKey);
return JSON.parse(decrypted);
}
return encryptedData;
} catch (error) {
console.error('[页面上下文] 解密请求数据失败:', error);
return encryptedData;
}
}
// 解密响应数据
function decryptResponseData(encryptedData, timestampKey) {
try {
const Decrypt = getDecryptFunction();
if (!Decrypt) {
console.warn('[页面上下文] 无法获取解密函数');
return encryptedData;
}
if (typeof encryptedData === 'string') {
const decrypted = Decrypt(encryptedData, timestampKey);
try {
return JSON.parse(decrypted);
} catch (e) {
if (window.JSON5) {
return window.JSON5.parse(decrypted);
}
return decrypted;
}
}
return encryptedData;
} catch (error) {
console.error('[页面上下文] 解密响应数据失败:', error);
return encryptedData;
}
}
// 获取密钥
function getTimestampKey(url, uuid, requestHeaders) {
const tools = getDecryptTools();
if (tools && tools.getKey) {
const key = tools.getKey(url, uuid);
if (key) return key;
}
const timestamp = requestHeaders?.time || requestHeaders?.Time || '';
const traceId = requestHeaders?.TraceId || requestHeaders?.['trace-id'] || '';
if (timestamp && traceId) {
return calculateKey(timestamp, traceId);
}
return null;
}
// ========== 拦截 Fetch (优先,因为 umi-request 使用 fetch) ==========
if (typeof fetch !== 'undefined' && window.fetch) {
const originalFetch = window.fetch;
window.fetch = function (...args) {
const [url, options = {}] = args;
const method = options.method || 'GET';
const requestId = Date.now() + Math.random();
const shouldSkip = skipDecryptPaths.some(path => url.includes(path));
const originalBody = options.body;
console.log('🔍 [页面上下文] 拦截 Fetch:', method, url);
return originalFetch.apply(this, args).then(async (response) => {
if (shouldSkip) return response;
try {
const clonedResponse = response.clone();
const uuid = clonedResponse.headers.get('uuid');
if (!uuid) {
return response;
}
const headers = options.headers || {};
const timestamp = headers.time || headers.Time || '';
const traceId = headers.TraceId || headers['trace-id'] || '';
const timestampKey = getTimestampKey(url, uuid, { time: timestamp, TraceId: traceId });
if (!timestampKey) {
console.warn('[页面上下文] 无法获取密钥 (Fetch)', { url, uuid });
return response;
}
// 解密请求数据
let requestBody = null;
if (originalBody) {
try {
if (typeof originalBody === 'string') {
const parsed = JSON.parse(originalBody);
requestBody = decryptRequestData(parsed, timestampKey);
} else {
requestBody = originalBody;
}
} catch (e) {
requestBody = originalBody;
}
}
// 解密响应数据
let responseData = null;
try {
const text = await clonedResponse.text();
if (text) {
try {
responseData = JSON.parse(text);
} catch (e) {
responseData = text;
}
}
} catch (e) {
console.error('[页面上下文] 读取响应数据失败:', e);
}
const decryptedResponse = decryptResponseData(responseData, timestampKey);
// 记录解密结果
const decryptedRecord = {
id: requestId,
url: url,
method: method,
uuid: uuid,
timestampKey: timestampKey,
request: requestBody,
response: decryptedResponse,
timestamp: new Date().toISOString()
};
// 存储到页面上下文
decryptedData.addRequest(decryptedRecord);
// 输出到控制台
console.group('🔓 Fetch 解密 [' + method + '] ' + url);
console.log('🔑 密钥:', timestampKey);
console.log('📤 请求:', requestBody);
console.log('📥 响应:', decryptedResponse);
console.log('📍 UUID:', uuid);
console.groupEnd();
} catch (error) {
console.error('[页面上下文] Fetch 解密过程出错:', error);
}
return response;
}).catch((error) => {
console.error('[页面上下文] Fetch 请求失败:', error);
return Promise.reject(error);
});
};
console.log('✅ [页面上下文] Fetch 拦截已设置');
}
// ========== 拦截 XMLHttpRequest ==========
if (typeof XMLHttpRequest !== 'undefined') {
const originalOpen = XMLHttpRequest.prototype.open;
const originalSend = XMLHttpRequest.prototype.send;
const originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
XMLHttpRequest.prototype.open = function (method, url, ...args) {
this._method = method;
this._url = url;
this._requestHeaders = {};
requestHeadersMap.set(this, {});
console.log('🔍 [页面上下文] 拦截 XHR open:', method, url);
return originalOpen.apply(this, [method, url, ...args]);
};
XMLHttpRequest.prototype.setRequestHeader = function (name, value) {
if (!this._requestHeaders) {
this._requestHeaders = {};
}
this._requestHeaders[name] = value;
const headers = requestHeadersMap.get(this) || {};
headers[name] = value;
requestHeadersMap.set(this, headers);
return originalSetRequestHeader.apply(this, [name, value]);
};
XMLHttpRequest.prototype.send = function (body) {
const url = this._url;
const method = this._method;
const requestId = Date.now() + Math.random();
const shouldSkip = skipDecryptPaths.some(path => url.includes(path));
// 监听响应
this.addEventListener('loadend', function () {
if (shouldSkip) return;
try {
const headers = requestHeadersMap.get(this) || {};
const uuid = this.getResponseHeader('uuid');
if (!uuid) {
return;
}
const timestampKey = getTimestampKey(url, uuid, headers);
if (!timestampKey) {
console.warn('[页面上下文] 无法获取密钥', { url, uuid, headers });
return;
}
// 解密请求数据
let requestBody = null;
if (body) {
try {
const parsed = typeof body === 'string' ? JSON.parse(body) : body;
requestBody = decryptRequestData(parsed, timestampKey);
} catch (e) {
requestBody = body;
}
}
// 解密响应数据
let responseData = null;
try {
if (this.responseText) {
responseData = JSON.parse(this.responseText);
}
} catch (e) {
responseData = this.responseText || this.response;
}
const decryptedResponse = decryptResponseData(responseData, timestampKey);
// 记录解密结果
const decryptedRecord = {
id: requestId,
url: url,
method: method,
uuid: uuid,
timestampKey: timestampKey,
request: requestBody,
response: decryptedResponse,
timestamp: new Date().toISOString()
};
// 存储到页面上下文
decryptedData.addRequest(decryptedRecord);
// 输出到控制台
console.group('🔓 XHR 解密 [' + method + '] ' + url);
console.log('🔑 密钥:', timestampKey);
console.log('📤 请求:', requestBody);
console.log('📥 响应:', decryptedResponse);
console.log('📍 UUID:', uuid);
console.groupEnd();
} catch (error) {
console.error('[页面上下文] 解密过程出错:', error);
}
});
return originalSend.apply(this, [body]);
};
console.log('✅ [页面上下文] XHR 拦截已设置');
}
// 暴露 API 供 DevTools 面板使用
window.__XHR_DECRYPT_EXTENSION__ = {
getDecryptedRequests: function () {
return decryptedData.getRequests();
},
clearDecryptedRequests: function () {
decryptedData.clear();
console.log('✅ [页面上下文] 已清空解密记录');
}
};
console.log('✅ [页面上下文] 拦截脚本初始化完成');
console.log('✅ [页面上下文] 已拦截 Fetch:', typeof window.fetch !== 'undefined');
console.log('✅ [页面上下文] 已拦截 XHR:', typeof XMLHttpRequest !== 'undefined');
})();

1
json5.min.js vendored Normal file

File diff suppressed because one or more lines are too long

34
manifest.json Normal file
View File

@@ -0,0 +1,34 @@
{
"manifest_version": 3,
"name": "XHR 请求解密工具",
"version": "1.0.0",
"description": "自动解密项目的加密请求和响应数据",
"permissions": [
"storage",
"tabs",
"scripting"
],
"host_permissions": [
"<all_urls>"
],
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": [
"content-script.js"
],
"run_at": "document_start",
"all_frames": true
}
],
"devtools_page": "devtools.html",
"action": {
"default_popup": "popup.html",
"default_title": "XHR 解密工具"
},
"background": {
"service_worker": "background.js"
}
}

423
panel.html Normal file
View File

@@ -0,0 +1,423 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>XHR 解密工具</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
font-size: 13px;
background: #1a202c;
color: #e2e8f0;
margin: 0;
padding: 0;
overflow: auto;
min-height: 100vh;
}
body.dark-theme {
background: #1a202c;
color: #e2e8f0;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 12px 16px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
.header h1 {
font-size: 14px;
font-weight: 600;
}
.controls {
padding: 8px 12px;
background: #2d3748;
border-bottom: 1px solid #4a5568;
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
body.light-theme .controls {
background: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
}
.btn {
padding: 6px 12px;
border: 1px solid #4a5568;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
background: #2d3748;
color: #e2e8f0;
transition: all 0.2s;
height: 28px;
}
body.light-theme .btn {
border: 1px solid #d9d9d9;
background: white;
color: #333;
}
.btn:hover {
background: #4a5568;
border-color: #4ec9b0;
}
body.light-theme .btn:hover {
background: #f0f0f0;
border-color: #40a9ff;
}
.btn-primary {
background: #1890ff;
color: white;
border-color: #1890ff;
}
.btn-primary:hover {
background: #40a9ff;
border-color: #40a9ff;
}
.btn-danger {
background: #ff4d4f;
color: white;
border-color: #ff4d4f;
}
.btn-danger:hover {
background: #ff7875;
border-color: #ff7875;
}
.stats {
padding: 6px 12px;
background: #4a5568;
border-left: 3px solid #4ec9b0;
font-size: 12px;
color: #e2e8f0;
flex-shrink: 0;
}
body.light-theme .stats {
background: #fff3cd;
border-left: 3px solid #ffc107;
color: #856404;
}
.requests-list {
flex: 1;
overflow-y: auto;
padding: 12px;
background: #1a202c;
}
body.light-theme .requests-list {
background: #ffffff;
}
.request-item {
background: #2d3748;
border: 1px solid #4a5568;
border-radius: 6px;
padding: 12px;
margin-bottom: 12px;
transition: all 0.2s;
}
body.light-theme .request-item {
background: white;
border: 1px solid #e0e0e0;
}
.request-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
border-color: #4ec9b0;
}
body.light-theme .request-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-color: #40a9ff;
}
.request-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.method-badge {
display: inline-block;
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
font-weight: 600;
margin-right: 8px;
}
.method-GET {
background: #52c41a;
color: white;
}
.method-POST {
background: #1890ff;
color: white;
}
.method-PUT {
background: #faad14;
color: white;
}
.method-DELETE {
background: #ff4d4f;
color: white;
}
.url {
flex: 1;
font-weight: 500;
color: #e2e8f0;
word-break: break-all;
font-size: 13px;
font-family: 'Courier New', monospace;
}
body.light-theme .url {
color: #333;
}
.timestamp {
font-size: 11px;
color: #a0aec0;
margin-left: 8px;
}
body.light-theme .timestamp {
color: #999;
}
.data-section {
margin-top: 12px;
}
.data-label {
font-size: 12px;
font-weight: 600;
color: #a0aec0;
margin-bottom: 6px;
display: flex;
align-items: center;
gap: 8px;
}
body.light-theme .data-label {
color: #666;
}
.data-content {
background: #1a202c;
border: 1px solid #4a5568;
border-radius: 4px;
padding: 8px;
font-family: 'Courier New', monospace;
font-size: 11px;
max-height: 300px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
line-height: 1.5;
color: #e2e8f0;
}
body.light-theme .data-content {
background: #f8f9fa;
border: 1px solid #e9ecef;
color: #333;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #a0aec0;
}
body.light-theme .empty-state {
color: #999;
}
.empty-state svg {
width: 64px;
height: 64px;
margin-bottom: 16px;
opacity: 0.5;
}
.copy-btn {
padding: 2px 8px;
font-size: 10px;
background: #f0f0f0;
border: 1px solid #d9d9d9;
border-radius: 3px;
cursor: pointer;
height: 20px;
line-height: 16px;
}
.copy-btn:hover {
background: #e0e0e0;
}
.key-info {
font-size: 10px;
color: #999;
margin-top: 4px;
padding: 4px 8px;
background: #f5f5f5;
border-radius: 3px;
}
.filter-bar {
padding: 8px 12px;
background: #2d3748;
border-bottom: 1px solid #4a5568;
display: flex;
gap: 8px;
align-items: center;
}
body.light-theme .filter-bar {
background: #fafafa;
border-bottom: 1px solid #e0e0e0;
}
.filter-input {
flex: 1;
padding: 4px 8px;
border: 1px solid #4a5568;
border-radius: 4px;
font-size: 12px;
height: 28px;
background: #1a202c;
color: #e2e8f0;
}
body.light-theme .filter-input {
border: 1px solid #d9d9d9;
background: white;
color: #333;
}
.filter-input:focus {
outline: none;
border-color: #4ec9b0;
}
body.light-theme .filter-input:focus {
border-color: #40a9ff;
}
body.light-theme .filter-input::placeholder {
color: #999;
}
.filter-input::placeholder {
color: #a0aec0;
}
.theme-toggle-btn {
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
width: 32px;
height: 32px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
transition: all 0.2s;
padding: 0;
}
.theme-toggle-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.1);
}
/* 亮色主题 */
body.light-theme {
background: #f5f5f5;
}
body.light-theme .header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
body.light-theme .requests-list {
background: #ffffff;
}
body.light-theme .request-item {
background: #ffffff;
border: 1px solid #e0e0e0;
color: #333;
}
body.light-theme .request-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-color: #40a9ff;
}
body.light-theme .url {
color: #333;
}
body.light-theme .data-content {
background: #f8f9fa;
color: #333;
border: 1px solid #e9ecef;
}
body.light-theme .section {
color: #333;
}
body.light-theme pre {
background: #f8f9fa !important;
color: #333 !important;
border: 1px solid #e9ecef !important;
}
</style>
</head>
<body>
<div id="app"></div>
<script src="crypto-js.min.js"></script>
<script src="json5.min.js"></script>
<script src="panel.js"></script>
</body>
</html>

1378
panel.js Normal file

File diff suppressed because it is too large Load Diff

344
popup.html Normal file
View File

@@ -0,0 +1,344 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>XHR 解密工具</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 500px;
max-height: 600px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
font-size: 14px;
background: #f5f5f5;
margin: 0;
padding: 0;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 16px;
text-align: center;
}
.header h1 {
font-size: 18px;
font-weight: 600;
}
.controls {
padding: 12px;
background: white;
border-bottom: 1px solid #e0e0e0;
display: flex;
gap: 8px;
align-items: center;
}
.btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.btn-primary {
background: #1890ff;
color: white;
}
.btn-primary:hover {
background: #40a9ff;
}
.btn-danger {
background: #ff4d4f;
color: white;
}
.btn-danger:hover {
background: #ff7875;
}
.btn-default {
background: #f0f0f0;
color: #333;
}
.btn-default:hover {
background: #d9d9d9;
}
.stats {
padding: 8px 12px;
background: #fff3cd;
border-left: 3px solid #ffc107;
font-size: 12px;
color: #856404;
}
.requests-list {
padding: 12px;
max-height: 600px;
overflow-y: auto;
}
.request-item {
background: white;
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 12px;
margin-bottom: 12px;
transition: all 0.2s;
}
.request-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.request-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.method-badge {
display: inline-block;
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
font-weight: 600;
margin-right: 8px;
}
.method-GET {
background: #52c41a;
color: white;
}
.method-POST {
background: #1890ff;
color: white;
}
.method-PUT {
background: #faad14;
color: white;
}
.method-DELETE {
background: #ff4d4f;
color: white;
}
.url {
flex: 1;
font-weight: 500;
color: #333;
word-break: break-all;
font-size: 13px;
}
.timestamp {
font-size: 11px;
color: #999;
}
.data-section {
margin-top: 8px;
}
.data-label {
font-size: 12px;
font-weight: 600;
color: #666;
margin-bottom: 4px;
}
.data-content {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 4px;
padding: 8px;
font-family: 'Courier New', monospace;
font-size: 11px;
max-height: 200px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #999;
}
.empty-state svg {
width: 64px;
height: 64px;
margin-bottom: 16px;
opacity: 0.5;
}
.copy-btn {
margin-left: 8px;
padding: 2px 6px;
font-size: 10px;
background: #f0f0f0;
border: 1px solid #d9d9d9;
border-radius: 3px;
cursor: pointer;
}
.copy-btn:hover {
background: #e0e0e0;
}
.key-info {
font-size: 10px;
color: #999;
margin-top: 4px;
}
.guide-container {
padding: 30px 25px;
text-align: center;
background: white;
min-height: 450px;
}
.guide-icon {
font-size: 64px;
margin-bottom: 20px;
animation: bounce 2s infinite;
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-10px);
}
60% {
transform: translateY(-5px);
}
}
.guide-title {
font-size: 20px;
color: #333;
margin-bottom: 30px;
font-weight: 600;
}
.guide-content {
text-align: left;
max-width: 500px;
margin: 0 auto 30px;
}
.guide-step {
display: flex;
align-items: flex-start;
margin-bottom: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #667eea;
}
.step-number {
width: 32px;
height: 32px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 16px;
flex-shrink: 0;
margin-right: 15px;
}
.step-text {
flex: 1;
font-size: 14px;
color: #555;
line-height: 1.6;
}
.step-text strong {
color: #667eea;
font-weight: 600;
}
.guide-tip {
padding: 15px 20px;
background: #fff3cd;
border-left: 4px solid #ffc107;
border-radius: 6px;
font-size: 13px;
color: #856404;
text-align: left;
max-width: 500px;
margin: 0 auto;
}
.guide-tip strong {
color: #856404;
}
</style>
</head>
<body>
<div class="header">
<h1>🔓 XHR 请求解密工具</h1>
</div>
<div class="guide-container">
<div class="guide-icon">🔧</div>
<h2 class="guide-title">如何使用</h2>
<div class="guide-content">
<div class="guide-step">
<div class="step-number">1</div>
<div class="step-text">
<strong>按 F12 键</strong> 或右键选择"检查"打开开发者工具
</div>
</div>
<div class="guide-step">
<div class="step-number">2</div>
<div class="step-text">
在开发者工具中,找到并点击 <strong>"XHR 解密工具"</strong> 标签页
</div>
</div>
<div class="guide-step">
<div class="step-number">3</div>
<div class="step-text">
在标签页中查看所有解密后的请求和响应数据
</div>
</div>
</div>
<div class="guide-tip">
💡 <strong>提示:</strong> 插件会自动拦截并解密所有 XHR 请求,你可以在 DevTools 面板中查看详细信息
</div>
</div>
<script src="popup.js"></script>
</body>
</html>

9
popup.js Normal file
View File

@@ -0,0 +1,9 @@
/**
* Popup Script - 显示使用指南
*/
// 页面加载完成后的提示
document.addEventListener('DOMContentLoaded', () => {
console.log('✅ XHR 解密工具弹窗已加载');
console.log('💡 请按 F12 打开开发者工具查看解密记录');
});

18272
vue.global.js Normal file

File diff suppressed because it is too large Load Diff

12574
vue.runtime.global.js Normal file

File diff suppressed because it is too large Load Diff