first commit
This commit is contained in:
32
ICONS.md
Normal file
32
ICONS.md
Normal 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
131
INSTALL.md
Normal 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
110
README.md
Normal 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
31
background.js
Normal 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
873
content-script.js
Normal 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
84
create-icons.html
Normal 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
1
crypto-js.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
13
devtools.html
Normal file
13
devtools.html
Normal 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
23
devtools.js
Normal 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
345
inject-script.js
Normal 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
1
json5.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
34
manifest.json
Normal file
34
manifest.json
Normal 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
423
panel.html
Normal 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>
|
||||
344
popup.html
Normal file
344
popup.html
Normal 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
9
popup.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Popup Script - 显示使用指南
|
||||
*/
|
||||
|
||||
// 页面加载完成后的提示
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('✅ XHR 解密工具弹窗已加载');
|
||||
console.log('💡 请按 F12 打开开发者工具查看解密记录');
|
||||
});
|
||||
18272
vue.global.js
Normal file
18272
vue.global.js
Normal file
File diff suppressed because it is too large
Load Diff
12574
vue.runtime.global.js
Normal file
12574
vue.runtime.global.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user