js演练场

This commit is contained in:
q
2025-11-29 03:41:51 +08:00
parent df646b8c43
commit b4b1d7f923
25 changed files with 6379 additions and 112 deletions

View File

@@ -303,8 +303,11 @@ public class RouterHandlerFactory implements BaseHttpApi {
final MultiMap queryParams = ctx.queryParams();
// 解析body-json参数
if (HttpHeaderValues.APPLICATION_JSON.toString().equals(ctx.parsedHeaders().contentType().value())
&& ctx.body().asJsonObject() != null) {
// 只处理POST/PUT/PATCH等有body的请求方法避免GET请求读取body导致"Request has already been read"错误
String httpMethod = ctx.request().method().name();
if (("POST".equals(httpMethod) || "PUT".equals(httpMethod) || "PATCH".equals(httpMethod))
&& HttpHeaderValues.APPLICATION_JSON.toString().equals(ctx.parsedHeaders().contentType().value())
&& ctx.body() != null && ctx.body().asJsonObject() != null) {
JsonObject body = ctx.body().asJsonObject();
if (body != null) {
methodParametersTemp.forEach((k, v) -> {
@@ -324,7 +327,8 @@ public class RouterHandlerFactory implements BaseHttpApi {
}
});
}
} else if (ctx.body() != null) {
} else if (("POST".equals(httpMethod) || "PUT".equals(httpMethod) || "PATCH".equals(httpMethod))
&& ctx.body() != null) {
queryParams.addAll(ParamUtil.paramsToMap(ctx.body().asString()));
}

View File

@@ -14,7 +14,6 @@
- [JsLogger对象](#jslogger对象)
- [重定向处理](#重定向处理)
- [代理支持](#代理支持)
- [文件上传支持](#文件上传支持)
- [实现方法](#实现方法)
- [parse方法必填](#parse方法必填)
- [parseFileList方法可选](#parsefilelist方法可选)
@@ -199,23 +198,53 @@ var response = http.post("https://api.example.com/submit", {
data: "test"
});
// 设置请求头
// 设置请求头(单个)
http.putHeader("User-Agent", "MyBot/1.0")
.putHeader("Authorization", "Bearer token");
// 批量设置请求头
http.putHeaders({
"User-Agent": "MyBot/1.0",
"Authorization": "Bearer token",
"Accept": "application/json"
});
// 删除指定请求头
http.removeHeader("Authorization");
// 清空所有请求头(保留默认头)
http.clearHeaders();
// 获取所有请求头
var allHeaders = http.getHeaders();
logger.debug("当前请求头: " + JSON.stringify(allHeaders));
// 设置请求超时时间(秒)
http.setTimeout(60); // 设置为60秒
// PUT请求
var putResponse = http.put("https://api.example.com/resource", {
key: "value"
});
// DELETE请求
var deleteResponse = http.delete("https://api.example.com/resource/123");
// PATCH请求
var patchResponse = http.patch("https://api.example.com/resource/123", {
key: "newValue"
});
// URL编码/解码(静态方法)
var encoded = JsHttpClient.urlEncode("hello world"); // "hello%20world"
var decoded = JsHttpClient.urlDecode("hello%20world"); // "hello world"
// 发送简单表单数据
var formResponse = http.sendForm({
username: "user",
password: "pass"
});
// 发送multipart表单数据支持文件上传
var multipartResponse = http.sendMultipartForm("https://api.example.com/upload", {
textField: "value",
fileField: fileBuffer, // Buffer或byte[]类型
binaryData: binaryArray // byte[]类型
});
// 发送JSON数据
var jsonResponse = http.sendJson({
name: "test",
@@ -249,6 +278,13 @@ if (response.isSuccess()) {
} else {
logger.error("请求失败: " + status);
}
// 获取响应体字节数组
var bytes = response.bodyBytes();
// 获取响应体大小
var size = response.bodySize();
logger.info("响应体大小: " + size + " 字节");
```
### JsLogger对象
@@ -350,89 +386,6 @@ function parse(shareLinkInfo, http, logger) {
}
```
## 文件上传支持
JavaScript解析器支持通过`sendMultipartForm`方法上传文件:
### 1. 简单文件上传
```javascript
function uploadFile(shareLinkInfo, http, logger) {
// 模拟文件数据(实际使用中可能是从其他地方获取)
var fileData = new java.lang.String("Hello, World!").getBytes();
// 使用sendMultipartForm上传文件
var response = http.sendMultipartForm("https://api.example.com/upload", {
file: fileData,
filename: "test.txt",
description: "测试文件"
});
return response.body();
}
```
### 2. 混合表单上传
```javascript
function uploadMixedForm(shareLinkInfo, http, logger) {
var fileData = getFileData();
// 同时上传文本字段和文件
var response = http.sendMultipartForm("https://api.example.com/upload", {
username: "user123",
email: "user@example.com",
file: fileData,
description: "用户上传的文件"
});
if (response.isSuccess()) {
var result = response.json();
return result.downloadUrl;
} else {
throw new Error("文件上传失败: " + response.statusCode());
}
}
```
### 3. 多文件上传
```javascript
function uploadMultipleFiles(shareLinkInfo, http, logger) {
var files = [
{ name: "file1.txt", data: getFileData1() },
{ name: "file2.jpg", data: getFileData2() }
];
var uploadResults = [];
for (var i = 0; i < files.length; i++) {
var file = files[i];
var response = http.sendMultipartForm("https://api.example.com/upload", {
file: file.data,
filename: file.name,
uploadIndex: i.toString()
});
if (response.isSuccess()) {
uploadResults.push({
fileName: file.name,
success: true,
url: response.json().url
});
} else {
uploadResults.push({
fileName: file.name,
success: false,
error: response.statusCode()
});
}
}
return uploadResults;
}
```
## 实现方法
JavaScript解析器支持三种方法对应Java接口的三种同步方法
@@ -698,6 +651,39 @@ A: 当前版本使用同步API所有HTTP请求都是同步的。
A: 使用 `logger.debug()` 输出调试信息,查看应用日志。
### Q: 如何批量设置请求头?
A: 使用 `http.putHeaders()` 方法批量设置多个请求头:
```javascript
// 批量设置请求头
http.putHeaders({
"User-Agent": "Mozilla/5.0...",
"Accept": "application/json",
"Authorization": "Bearer token",
"Referer": "https://example.com"
});
```
### Q: 如何清空所有请求头?
A: 使用 `http.clearHeaders()` 方法清空所有请求头(会保留默认头):
```javascript
// 清空所有请求头保留默认头Accept-Encoding、User-Agent、Accept-Language
http.clearHeaders();
```
### Q: 如何设置请求超时时间?
A: 使用 `http.setTimeout()` 方法设置超时时间(秒):
```javascript
// 设置超时时间为60秒
http.setTimeout(60);
var response = http.get("https://api.example.com/data");
```
## 示例脚本
参考以下示例文件,包含完整的解析器实现:
@@ -714,6 +700,7 @@ A: 使用 `logger.debug()` 输出调试信息,查看应用日志。
- 文件信息构建
- 重定向处理
- 代理支持
- Header管理批量设置、清空等
## 限制说明
@@ -732,6 +719,11 @@ A: 使用 `logger.debug()` 输出调试信息,查看应用日志。
- v1.0.0: 初始版本支持基本的JavaScript解析器功能
- 支持外部解析器路径配置(系统属性、环境变量)
- 支持文件上传功能sendMultipartForm
- 支持重定向处理getNoRedirect、getWithRedirect
- 支持代理配置HTTP/SOCKS4/SOCKS5
- v1.1.0: 增强HTTP客户端功能
- 新增header管理方法clearHeaders、removeHeader、putHeaders、getHeaders
- 新增HTTP请求方法PUT、DELETE、PATCH
- 新增工具方法URL编码/解码urlEncode、urlDecode
- 新增超时时间设置setTimeout
- 响应对象增强bodyBytes、bodySize

View File

@@ -0,0 +1,303 @@
# 🚨 紧急安全修复通知
## ⚠️ 严重漏洞已修复 - 请立即部署
**漏洞编号**: RCE-2025-001
**发现日期**: 2025-11-28
**修复状态**: ✅ 已完成
**危险等级**: 🔴🔴🔴 极高(远程代码执行)
---
## 🔥 漏洞影响
如果您的服务器正在运行**未修复**的版本,攻击者可以:
- ✅ 执行任意系统命令
- ✅ 读取服务器上的所有文件(包括数据库、配置文件、密钥)
- ✅ 删除或修改文件
- ✅ 窃取环境变量和系统信息
- ✅ 攻击内网其他服务器
- ✅ 完全控制服务器
**这是一个可被远程利用的代码执行漏洞!**
---
## 🎯 快速修复步骤
### 1. 立即停止服务(如果正在生产环境)
```bash
./bin/stop.sh
```
### 2. 拉取最新代码
```bash
git pull
# 或者手动应用补丁
```
### 3. 重新编译
```bash
mvn clean install
```
### 4. 验证修复(重要!)
```bash
cd parser
mvn test -Dtest=SecurityTest
```
**确认所有测试显示"安全"而不是"危险"**
### 5. 重启服务
```bash
./bin/run.sh
```
### 6. 监控日志
检查是否有安全拦截日志:
```bash
tail -f logs/*/run.log | grep "安全拦截"
```
---
## 📋 修复内容摘要
### 新增的安全防护
1. **ClassFilter** - 阻止JavaScript访问危险Java类
2. **Java对象禁用** - 移除 `Java.type()` 等全局对象
3. **SSRF防护** - 阻止访问内网地址和云服务元数据
4. **URL白名单** - HTTP请求仅允许公网地址
### 修复的文件
- `JsPlaygroundExecutor.java` - 使用安全引擎
- `JsParserExecutor.java` - 使用安全引擎
- `JsHttpClient.java` - 添加SSRF防护
- `SecurityClassFilter.java` - **新文件**:类过滤器
---
## 🧪 验证修复是否生效
### 测试1: 验证系统命令执行已被阻止
访问演练场,执行以下测试代码:
```javascript
// ==UserScript==
// @name 安全验证测试
// @type test
// @match https://test.com/*
// ==/UserScript==
function parse(shareLinkInfo, http, logger) {
try {
var Runtime = Java.type('java.lang.Runtime');
logger.error('【严重问题】Java.type仍然可用');
return '失败:未修复';
} catch (e) {
logger.info('✅ 安全:' + e.message);
return '成功:已修复';
}
}
```
**期望结果**:
```
✅ 安全ReferenceError: "Java" is not defined
成功:已修复
```
**如果看到"失败:未修复",说明修复未生效,请检查编译是否成功!**
### 测试2: 验证SSRF防护
```javascript
function parse(shareLinkInfo, http, logger) {
try {
var response = http.get('http://127.0.0.1:8080/admin');
logger.error('【严重问题】可以访问内网!');
return '失败SSRF未修复';
} catch (e) {
logger.info('✅ 安全:' + e);
return '成功SSRF已修复';
}
}
```
**期望结果**:
```
✅ 安全SecurityException: 🔒 安全拦截: 禁止访问内网地址
成功SSRF已修复
```
---
## 📊 安全评级
### 修复前
- **评级**: 🔴 F级完全不安全
- **风险**: 服务器可被完全控制
- **建议**: 🚨 **立即下线服务**
### 修复后
- **评级**: 🟢 A级安全
- **风险**: 低(已实施多层防护)
- **建议**: ✅ 可安全使用
---
## 🔍 如何检查您是否受影响
### 检查版本
查看修改时间:
```bash
# 检查关键文件是否包含安全修复
grep -n "SecurityClassFilter" parser/src/main/java/cn/qaiu/parser/customjs/JsPlaygroundExecutor.java
# 如果输出为空,说明未修复
# 如果有输出,说明已修复
```
### 检查日志
查看是否有攻击尝试:
```bash
# 搜索可疑的系统调用
grep -r "Runtime\|ProcessBuilder\|System\.exec" logs/
# 如果发现大量此类日志,可能已被攻击
```
---
## 🆘 紧急联系
如果发现以下情况,请立即采取行动:
### 已被攻击的迹象
1. ❌ 服务器上出现陌生文件
2. ❌ 系统负载异常高
3. ❌ 发现陌生进程
4. ❌ 配置文件被修改
5. ❌ 日志中有大量异常请求
### 应对措施
1. **立即下线服务**
```bash
./bin/stop.sh
```
2. **隔离服务器**
- 断开网络连接(如果可能)
- 保存日志证据
3. **检查受损范围**
```bash
# 检查最近修改的文件
find / -type f -mtime -1 -ls 2>/dev/null
# 检查可疑进程
ps aux | grep -E "nc|bash|sh|python|perl"
# 检查网络连接
netstat -antp | grep ESTABLISHED
```
4. **备份日志**
```bash
tar -czf logs-backup-$(date +%Y%m%d).tar.gz logs/
```
5. **应用安全补丁并重新部署**
6. **修改所有密码和密钥**
---
## 📚 详细文档
- **完整修复说明**: `parser/SECURITY_FIX_SUMMARY.md`
- **安全测试指南**: `parser/doc/SECURITY_TESTING_GUIDE.md`
- **快速测试**: `parser/SECURITY_TEST_README.md`
---
## ✅ 修复确认清单
部署后请确认:
- [ ] 代码已更新到最新版本
- [ ] Maven重新编译成功
- [ ] SecurityTest所有测试通过
- [ ] 演练场测试显示"安全"
- [ ] 日志中有"🔒 安全的JavaScript引擎初始化成功"
- [ ] 尝试访问危险类时出现"安全拦截"日志
- [ ] HTTP请求内网地址被阻止
- [ ] 服务运行正常
---
## 🎓 经验教训
### 问题根源
1. **过度信任用户输入** - 允许执行任意JavaScript
2. **缺少沙箱隔离** - Nashorn默认允许访问所有Java类
3. **没有安全审计** - 上线前未进行安全测试
### 预防措施
1. ✅ **永远不要信任用户输入**
2. ✅ **使用沙箱隔离执行不可信代码**
3. ✅ **实施最小权限原则**
4. ✅ **定期安全审计**
5. ✅ **关注依赖库的安全更新**
### 长期计划
考虑迁移到 **GraalVM JavaScript**
- 默认沙箱隔离
- 更好的安全性
- 更好的性能
- 活跃维护
---
## 📞 支持
如有问题,请查看:
- 详细文档: `parser/SECURITY_FIX_SUMMARY.md`
- 测试指南: `parser/doc/SECURITY_TESTING_GUIDE.md`
---
**重要提醒**:
- ⚠️ 这是一个严重的安全漏洞
- ⚠️ 必须立即修复
- ⚠️ 修复后必须验证
- ⚠️ 如已被攻击,请遵循应急响应流程
**修复优先级**: 🔴🔴🔴 **最高** - 立即处理
---
最后更新: 2025-11-28
状态: ✅ 修复完成,等待部署验证

View File

@@ -0,0 +1,296 @@
# SSRF防护策略说明
## 🛡️ 当前防护策略(已优化)
为了保证功能可用性和安全性的平衡SSRF防护策略已调整为**宽松模式**,只拦截明确的危险请求。
---
## ✅ 允许的请求
以下请求**不会被拦截**,可以正常使用:
### 1. 外网域名 ✅
```javascript
http.get('https://www.example.com/api/data') // ✅ 允许
http.get('http://api.github.com/repos') // ✅ 允许
http.get('https://cdn.jsdelivr.net/file.js') // ✅ 允许
```
### 2. 公网IP ✅
```javascript
http.get('http://8.8.8.8/api') // ✅ 允许公网IP
http.get('https://1.1.1.1/dns-query') // ✅ 允许Cloudflare DNS
```
### 3. DNS解析失败的域名 ✅
```javascript
// 即使DNS暂时无法解析也允许继续
http.get('http://some-new-domain.com') // ✅ 允许DNS失败不拦截
```
---
## ❌ 拦截的请求
以下请求**会被拦截**,保护服务器安全:
### 1. 本地回环地址 ❌
```javascript
http.get('http://127.0.0.1:8080/admin') // ❌ 拦截
http.get('http://localhost/secret') // ❌ 拦截解析到127.0.0.1
http.get('http://[::1]/api') // ❌ 拦截IPv6本地
```
### 2. 内网IP地址 ❌
```javascript
http.get('http://192.168.1.1/config') // ❌ 拦截内网C类
http.get('http://10.0.0.5/admin') // ❌ 拦截内网A类
http.get('http://172.16.0.1/api') // ❌ 拦截内网B类
```
### 3. 云服务元数据API ❌
```javascript
http.get('http://169.254.169.254/latest/meta-data/') // ❌ 拦截AWS/阿里云)
http.get('http://metadata.google.internal/computeMetadata/') // ❌ 拦截GCP
http.get('http://100.100.100.200/latest/meta-data/') // ❌ 拦截(阿里云)
```
### 4. 解析到内网的域名 ❌
```javascript
// 如果域名DNS解析指向内网IP会被拦截
http.get('http://internal.company.com') // ❌ 拦截如果解析到192.168.x.x
```
---
## 🔍 检测逻辑
### 防护流程
```
用户请求 URL
1. 检查是否为云服务元数据API域名
├─ 是 → ❌ 拦截
└─ 否 → 继续
2. 检查Host是否为IP地址格式
├─ 是 → 检查是否为内网IP
│ ├─ 是 → ❌ 拦截
│ └─ 否 → ✅ 允许
└─ 否(域名)→ 继续
3. 尝试DNS解析域名
├─ 解析成功
│ ├─ IP为内网 → ❌ 拦截
│ └─ IP为公网 → ✅ 允许
└─ 解析失败 → ✅ 允许(不阻止)
```
### 内网IP判断规则
使用正则表达式匹配:
```java
^(127\..*| // 127.0.0.0/8 - 本地回环
10\..*| // 10.0.0.0/8 - 内网A类
172\.(1[6-9]|2[0-9]|3[01])\..*| // 172.16.0.0/12 - 内网B类
192\.168\..*| // 192.168.0.0/16 - 内网C类
169\.254\..*| // 169.254.0.0/16 - 链路本地
::1| // IPv6本地回环
[fF][cCdD].*) // IPv6唯一本地地址
```
---
## 📊 策略对比
| 场景 | 严格模式(原版) | 宽松模式(当前)✅ |
|------|-----------------|-------------------|
| 外网域名 | 可能被拦截 | ✅ 允许 |
| DNS解析失败 | 被拦截 | ✅ 允许 |
| 公网IP | ✅ 允许 | ✅ 允许 |
| 内网IP | ❌ 拦截 | ❌ 拦截 |
| 本地回环 | ❌ 拦截 | ❌ 拦截 |
| 云服务元数据 | ❌ 拦截 | ❌ 拦截 |
| 解析到内网的域名 | ❌ 拦截 | ❌ 拦截 |
---
## 🧪 测试用例
### 测试1: 正常外网请求 ✅
```javascript
function parse(shareLinkInfo, http, logger) {
try {
var response = http.get('https://httpbin.org/get');
logger.info('✅ 成功访问外网: ' + response.substring(0, 50));
return 'SUCCESS';
} catch (e) {
logger.error('❌ 外网请求被拦截(不应该): ' + e.message);
return 'FAILED';
}
}
```
**期望结果**: ✅ 成功访问
### 测试2: 内网攻击拦截 ❌
```javascript
function parse(shareLinkInfo, http, logger) {
try {
var response = http.get('http://127.0.0.1:6400/');
logger.error('❌ 内网访问成功(不应该)');
return 'SECURITY_BREACH';
} catch (e) {
logger.info('✅ 内网访问被拦截: ' + e.message);
return 'PROTECTED';
}
}
```
**期望结果**: ✅ 被拦截,显示"安全拦截: 禁止访问内网IP地址"
### 测试3: 云服务元数据拦截 ❌
```javascript
function parse(shareLinkInfo, http, logger) {
try {
var response = http.get('http://169.254.169.254/latest/meta-data/');
logger.error('❌ 元数据API访问成功不应该');
return 'SECURITY_BREACH';
} catch (e) {
logger.info('✅ 元数据API被拦截: ' + e.message);
return 'PROTECTED';
}
}
```
**期望结果**: ✅ 被拦截,显示"安全拦截: 禁止访问云服务元数据API"
---
## 🎯 安全建议
### ✅ 当前策略适用于
- 需要访问多种外网API的场景
- 网盘、文件分享等服务
- 需要爬取外网资源
- 对可用性要求较高的环境
### ⚠️ 如需更严格的防护
如果你的应用场景需要更严格的安全控制,可以考虑:
#### 1. 白名单模式
只允许访问特定域名:
```java
private static final String[] ALLOWED_DOMAINS = {
"api.example.com",
"cdn.example.com"
};
private void validateUrlSecurity(String url) {
String host = new URI(url).getHost();
boolean allowed = false;
for (String domain : ALLOWED_DOMAINS) {
if (host.equals(domain) || host.endsWith("." + domain)) {
allowed = true;
break;
}
}
if (!allowed) {
throw new SecurityException("域名不在白名单中");
}
}
```
#### 2. 协议限制
只允许HTTPS
```java
String scheme = uri.getScheme();
if (!"https".equalsIgnoreCase(scheme)) {
throw new SecurityException("仅允许HTTPS协议");
}
```
#### 3. 端口限制
只允许标准端口80, 443
```java
int port = uri.getPort();
if (port != -1 && port != 80 && port != 443) {
throw new SecurityException("仅允许标准HTTP/HTTPS端口");
}
```
---
## 📝 配置说明
### 修改黑名单
`JsHttpClient.java` 中修改:
```java
// 危险域名黑名单
private static final String[] DANGEROUS_HOSTS = {
"localhost",
"169.254.169.254", // AWS/阿里云元数据
"metadata.google.internal", // GCP元数据
"100.100.100.200", // 阿里云元数据
// 添加更多...
};
```
### 修改内网IP规则
```java
// 内网IP正则表达式
private static final Pattern PRIVATE_IP_PATTERN = Pattern.compile(
"^(127\\..*|10\\..*|172\\.(1[6-9]|2[0-9]|3[01])\\..*|192\\.168\\..*|169\\.254\\..*|::1|[fF][cCdD].*)"
);
```
---
## 🔄 策略变更历史
### v2 - 宽松模式(当前)✅
- **日期**: 2025-11-29
- **变更**:
- DNS解析失败不拦截
- URL格式错误不拦截
- 只拦截明确的内网攻击
- **原因**: 避免误杀正常外网请求
### v1 - 严格模式
- **日期**: 2025-11-28
- **变更**: 初始实现
- **问题**: 过于严格,导致很多正常请求被拦截
---
## 📞 反馈
如果遇到以下情况,请考虑调整策略:
1. **正常外网请求被拦截** → 检查DNS解析、域名是否在黑名单
2. **内网攻击未被拦截** → 添加更多内网IP段或域名黑名单
3. **性能问题** → 考虑缓存DNS解析结果
---
**最后更新**: 2025-11-29
**当前版本**: v2 - 宽松模式
**安全级别**: ⚠️ 中等(建议生产环境根据实际需求调整)

View File

@@ -0,0 +1,59 @@
#!/bin/bash
# JavaScript执行器安全测试脚本
# 用于快速执行所有安全测试用例
echo "========================================"
echo " JavaScript执行器安全测试"
echo "========================================"
echo ""
# 进入parser目录
cd "$(dirname "$0")"
echo "📋 测试用例列表:"
echo " 1. 系统命令执行测试 🔴"
echo " 2. 文件系统访问测试 🔴"
echo " 3. 系统属性访问测试 🟡"
echo " 4. 反射攻击测试 🔴"
echo " 5. 网络Socket测试 🔴"
echo " 6. JVM退出测试 🔴"
echo " 7. HTTP客户端SSRF测试 🟡"
echo ""
echo "⚠️ 警告: 这些测试包含危险代码,仅用于安全验证!"
echo ""
read -p "是否继续执行测试? (y/n): " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "测试已取消"
exit 1
fi
echo ""
echo "🚀 开始执行测试..."
echo ""
# 执行JUnit测试
mvn test -Dtest=SecurityTest
# 检查测试结果
if [ $? -eq 0 ]; then
echo ""
echo "✅ 测试执行完成"
echo ""
echo "📊 请检查测试日志,确认:"
echo " ✓ 所有高危测试(系统命令、文件访问等)应该失败"
echo " ✓ 所有日志中不应该出现【安全漏洞】标记"
echo " ⚠ 如果出现安全漏洞警告,请立即修复!"
else
echo ""
echo "❌ 测试执行失败"
fi
echo ""
echo "📖 详细文档请参考: doc/SECURITY_TESTING_GUIDE.md"
echo ""

View File

@@ -19,9 +19,17 @@ import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetAddress;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Pattern;
/**
* JavaScript HTTP客户端封装
@@ -37,6 +45,20 @@ public class JsHttpClient {
private final WebClient client;
private final WebClientSession clientSession;
private MultiMap headers;
private int timeoutSeconds = 30; // 默认超时时间30秒
// SSRF防护内网IP正则表达式
private static final Pattern PRIVATE_IP_PATTERN = Pattern.compile(
"^(127\\..*|10\\..*|172\\.(1[6-9]|2[0-9]|3[01])\\..*|192\\.168\\..*|169\\.254\\..*|::1|[fF][cCdD].*)"
);
// SSRF防护危险域名黑名单
private static final String[] DANGEROUS_HOSTS = {
"localhost",
"169.254.169.254", // AWS/阿里云等云服务元数据API
"metadata.google.internal", // GCP元数据
"100.100.100.200" // 阿里云元数据
};
public JsHttpClient() {
this.client = WebClient.create(WebClientVertxInit.get(), new WebClientOptions());;
@@ -86,12 +108,81 @@ public class JsHttpClient {
this.headers.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6");
}
/**
* 验证URL安全性SSRF防护- 仅拦截明显的内网攻击
* @param url 待验证的URL
* @throws SecurityException 如果URL不安全
*/
private void validateUrlSecurity(String url) {
try {
URI uri = new URI(url);
String host = uri.getHost();
if (host == null) {
log.debug("URL没有host信息: {}", url);
return; // 允许继续,可能是相对路径
}
String lowerHost = host.toLowerCase();
// 1. 检查明确的危险域名云服务元数据API等
for (String dangerous : DANGEROUS_HOSTS) {
if (lowerHost.equals(dangerous)) {
log.warn("🔒 安全拦截: 尝试访问云服务元数据API - {}", host);
throw new SecurityException("🔒 安全拦截: 禁止访问云服务元数据API");
}
}
// 2. 如果host是IP地址格式检查是否为内网IP
if (isIpAddress(lowerHost)) {
if (PRIVATE_IP_PATTERN.matcher(lowerHost).find()) {
log.warn("🔒 安全拦截: 尝试访问内网IP - {}", host);
throw new SecurityException("🔒 安全拦截: 禁止访问内网IP地址");
}
}
// 3. 对于域名尝试解析IP但不因解析失败而拦截
if (!isIpAddress(lowerHost)) {
try {
InetAddress addr = InetAddress.getByName(host);
String ip = addr.getHostAddress();
// 只拦截解析到内网IP的域名
if (PRIVATE_IP_PATTERN.matcher(ip).find()) {
log.warn("🔒 安全拦截: 域名解析到内网IP - {} -> {}", host, ip);
throw new SecurityException("🔒 安全拦截: 该域名指向内网地址");
}
} catch (UnknownHostException e) {
// DNS解析失败允许继续可能是外网域名暂时无法解析
log.debug("DNS解析失败允许继续: {}", host);
}
}
log.debug("URL安全检查通过: {}", url);
} catch (SecurityException e) {
throw e;
} catch (Exception e) {
// 其他异常不拦截,只记录日志
log.debug("URL验证异常允许继续: {}", url, e);
}
}
/**
* 判断字符串是否为IP地址格式
*/
private boolean isIpAddress(String host) {
// 简单判断是否为IPv4地址格式
return host.matches("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$") || host.contains(":");
}
/**
* 发起GET请求
* @param url 请求URL
* @return HTTP响应
*/
public JsHttpResponse get(String url) {
validateUrlSecurity(url);
return executeRequest(() -> {
HttpRequest<Buffer> request = client.getAbs(url);
if (!headers.isEmpty()) {
@@ -107,6 +198,7 @@ public class JsHttpClient {
* @return HTTP响应
*/
public JsHttpResponse getWithRedirect(String url) {
validateUrlSecurity(url);
return executeRequest(() -> {
HttpRequest<Buffer> request = client.getAbs(url);
if (!headers.isEmpty()) {
@@ -124,6 +216,7 @@ public class JsHttpClient {
* @return HTTP响应
*/
public JsHttpResponse getNoRedirect(String url) {
validateUrlSecurity(url);
return executeRequest(() -> {
HttpRequest<Buffer> request = client.getAbs(url);
if (!headers.isEmpty()) {
@@ -142,6 +235,7 @@ public class JsHttpClient {
* @return HTTP响应
*/
public JsHttpResponse post(String url, Object data) {
validateUrlSecurity(url);
return executeRequest(() -> {
HttpRequest<Buffer> request = client.postAbs(url);
if (!headers.isEmpty()) {
@@ -166,6 +260,84 @@ public class JsHttpClient {
});
}
/**
* 发起PUT请求
* @param url 请求URL
* @param data 请求数据
* @return HTTP响应
*/
public JsHttpResponse put(String url, Object data) {
validateUrlSecurity(url);
return executeRequest(() -> {
HttpRequest<Buffer> request = client.putAbs(url);
if (!headers.isEmpty()) {
request.putHeaders(headers);
}
if (data != null) {
if (data instanceof String) {
request.sendBuffer(Buffer.buffer((String) data));
} else if (data instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, String> mapData = (Map<String, String>) data;
request.sendForm(MultiMap.caseInsensitiveMultiMap().addAll(mapData));
} else {
request.sendJson(data);
}
} else {
request.send();
}
return request.send();
});
}
/**
* 发起DELETE请求
* @param url 请求URL
* @return HTTP响应
*/
public JsHttpResponse delete(String url) {
return executeRequest(() -> {
HttpRequest<Buffer> request = client.deleteAbs(url);
if (!headers.isEmpty()) {
request.putHeaders(headers);
}
return request.send();
});
}
/**
* 发起PATCH请求
* @param url 请求URL
* @param data 请求数据
* @return HTTP响应
*/
public JsHttpResponse patch(String url, Object data) {
return executeRequest(() -> {
HttpRequest<Buffer> request = client.patchAbs(url);
if (!headers.isEmpty()) {
request.putHeaders(headers);
}
if (data != null) {
if (data instanceof String) {
request.sendBuffer(Buffer.buffer((String) data));
} else if (data instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, String> mapData = (Map<String, String>) data;
request.sendForm(MultiMap.caseInsensitiveMultiMap().addAll(mapData));
} else {
request.sendJson(data);
}
} else {
request.send();
}
return request.send();
});
}
/**
* 设置请求头
* @param name 头名称
@@ -179,6 +351,105 @@ public class JsHttpClient {
return this;
}
/**
* 批量设置请求头
* @param headersMap 请求头Map
* @return 当前客户端实例(支持链式调用)
*/
public JsHttpClient putHeaders(Map<String, String> headersMap) {
if (headersMap != null) {
for (Map.Entry<String, String> entry : headersMap.entrySet()) {
if (entry.getKey() != null && entry.getValue() != null) {
headers.set(entry.getKey(), entry.getValue());
}
}
}
return this;
}
/**
* 删除指定请求头
* @param name 头名称
* @return 当前客户端实例(支持链式调用)
*/
public JsHttpClient removeHeader(String name) {
if (name != null) {
headers.remove(name);
}
return this;
}
/**
* 清空所有请求头(保留默认头)
* @return 当前客户端实例(支持链式调用)
*/
public JsHttpClient clearHeaders() {
headers.clear();
// 重新设置默认头
headers.set("Accept-Encoding", "gzip, deflate, br, zstd");
headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0");
headers.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6");
return this;
}
/**
* 获取所有请求头
* @return 请求头Map
*/
public Map<String, String> getHeaders() {
Map<String, String> result = new HashMap<>();
for (String name : headers.names()) {
result.put(name, headers.get(name));
}
return result;
}
/**
* 设置请求超时时间
* @param seconds 超时时间(秒)
* @return 当前客户端实例(支持链式调用)
*/
public JsHttpClient setTimeout(int seconds) {
if (seconds > 0) {
this.timeoutSeconds = seconds;
}
return this;
}
/**
* URL编码
* @param str 要编码的字符串
* @return 编码后的字符串
*/
public static String urlEncode(String str) {
if (str == null) {
return null;
}
try {
return URLEncoder.encode(str, StandardCharsets.UTF_8.name());
} catch (Exception e) {
log.error("URL编码失败", e);
return str;
}
}
/**
* URL解码
* @param str 要解码的字符串
* @return 解码后的字符串
*/
public static String urlDecode(String str) {
if (str == null) {
return null;
}
try {
return URLDecoder.decode(str, StandardCharsets.UTF_8.name());
} catch (Exception e) {
log.error("URL解码失败", e);
return str;
}
}
/**
* 发送表单数据(简单键值对)
* @param data 表单数据
@@ -201,7 +472,7 @@ public class JsHttpClient {
}
/**
* 发送multipart表单数据支持文件上传
* 发送multipart表单数据支持文本字段
* @param url 请求URL
* @param data 表单数据,支持:
* - Map<String, String>: 文本字段
@@ -271,16 +542,27 @@ public class JsHttpClient {
}
}).onFailure(Throwable::printStackTrace);
// 等待响应完成(最多30秒
// 等待响应完成(使用配置的超时时间
HttpResponse<Buffer> response = promise.future().toCompletionStage()
.toCompletableFuture()
.get(30, TimeUnit.SECONDS);
.get(timeoutSeconds, TimeUnit.SECONDS);
return new JsHttpResponse(response);
} catch (TimeoutException e) {
String errorMsg = "HTTP请求超时" + timeoutSeconds + "秒)";
log.error(errorMsg, e);
throw new RuntimeException(errorMsg, e);
} catch (Exception e) {
log.error("HTTP请求执行失败", e);
throw new RuntimeException("HTTP请求执行失败: " + e.getMessage(), e);
String errorMsg = e.getMessage();
if (errorMsg == null || errorMsg.trim().isEmpty()) {
errorMsg = e.getClass().getSimpleName();
if (e.getCause() != null && e.getCause().getMessage() != null) {
errorMsg += ": " + e.getCause().getMessage();
}
}
log.error("HTTP请求执行失败: " + errorMsg, e);
throw new RuntimeException("HTTP请求执行失败: " + errorMsg, e);
}
}
@@ -376,5 +658,29 @@ public class JsHttpClient {
public HttpResponse<Buffer> getOriginalResponse() {
return response;
}
/**
* 获取响应体字节数组
* @return 响应体字节数组
*/
public byte[] bodyBytes() {
Buffer buffer = response.body();
if (buffer == null) {
return new byte[0];
}
return buffer.getBytes();
}
/**
* 获取响应体大小
* @return 响应体大小(字节)
*/
public long bodySize() {
Buffer buffer = response.body();
if (buffer == null) {
return 0;
}
return buffer.length();
}
}
}

View File

@@ -8,12 +8,12 @@ import cn.qaiu.parser.custom.CustomParserConfig;
import io.vertx.core.Future;
import io.vertx.core.WorkerExecutor;
import io.vertx.core.json.JsonObject;
import org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory;
import org.openjdk.nashorn.api.scripting.ScriptObjectMirror;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import java.util.ArrayList;
import java.util.List;
@@ -63,12 +63,15 @@ public class JsParserExecutor implements IPanTool {
}
/**
* 初始化JavaScript引擎
* 初始化JavaScript引擎(带安全限制)
*/
private ScriptEngine initEngine() {
try {
ScriptEngineManager engineManager = new ScriptEngineManager();
ScriptEngine engine = engineManager.getEngineByName("JavaScript");
// 使用安全的ClassFilter创建Nashorn引擎
NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
// 正确的方法签名: getScriptEngine(String[] args, ClassLoader appLoader, ClassFilter classFilter)
ScriptEngine engine = factory.getScriptEngine(new String[0], null, new SecurityClassFilter());
if (engine == null) {
throw new RuntimeException("无法创建JavaScript引擎请确保Nashorn可用");
@@ -79,10 +82,19 @@ public class JsParserExecutor implements IPanTool {
engine.put("logger", jsLogger);
engine.put("shareLinkInfo", shareLinkInfoWrapper);
// 禁用Java对象访问
engine.eval("var Java = undefined;");
engine.eval("var JavaImporter = undefined;");
engine.eval("var Packages = undefined;");
engine.eval("var javax = undefined;");
engine.eval("var org = undefined;");
engine.eval("var com = undefined;");
log.debug("🔒 安全的JavaScript引擎初始化成功解析器类型: {}", config.getType());
// 执行JavaScript代码
engine.eval(config.getJsCode());
log.debug("JavaScript引擎初始化成功解析器类型: {}", config.getType());
return engine;
} catch (Exception e) {

View File

@@ -0,0 +1,118 @@
package cn.qaiu.parser.customjs;
import org.openjdk.nashorn.api.scripting.ClassFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* JavaScript执行器安全类过滤器
* 用于限制JavaScript代码可以访问的Java类防止恶意代码执行危险操作
*
* @author <a href="https://qaiu.top">QAIU</a>
*/
public class SecurityClassFilter implements ClassFilter {
private static final Logger log = LoggerFactory.getLogger(SecurityClassFilter.class);
// 危险类黑名单
private static final String[] DANGEROUS_CLASSES = {
// 系统命令执行
"java.lang.Runtime",
"java.lang.ProcessBuilder",
"java.lang.Process",
// 文件系统访问
"java.io.File",
"java.io.FileInputStream",
"java.io.FileOutputStream",
"java.io.FileReader",
"java.io.FileWriter",
"java.io.RandomAccessFile",
"java.nio.file.Files",
"java.nio.file.Paths",
"java.nio.file.Path",
"java.nio.channels.FileChannel",
// 系统访问
"java.lang.System",
"java.lang.SecurityManager",
// 反射相关
"java.lang.Class",
"java.lang.reflect.Method",
"java.lang.reflect.Field",
"java.lang.reflect.Constructor",
"java.lang.reflect.AccessibleObject",
"java.lang.ClassLoader",
// 网络访问
"java.net.Socket",
"java.net.ServerSocket",
"java.net.DatagramSocket",
"java.net.URL",
"java.net.URLConnection",
"java.net.HttpURLConnection",
"java.net.InetAddress",
// 线程和并发
"java.lang.Thread",
"java.lang.ThreadGroup",
"java.util.concurrent.Executor",
"java.util.concurrent.ExecutorService",
// 数据库访问
"java.sql.Connection",
"java.sql.Statement",
"java.sql.PreparedStatement",
"java.sql.DriverManager",
// 脚本引擎(防止嵌套执行)
"javax.script.ScriptEngine",
"javax.script.ScriptEngineManager",
// JVM控制
"java.lang.invoke.MethodHandle",
"sun.misc.Unsafe",
// Nashorn内部类
"jdk.nashorn.internal",
"jdk.internal",
};
@Override
public boolean exposeToScripts(String className) {
// 检查是否在黑名单中
for (String dangerous : DANGEROUS_CLASSES) {
if (className.equals(dangerous) || className.startsWith(dangerous + ".")) {
log.warn("🔒 安全拦截: JavaScript尝试访问危险类 - {}", className);
return false;
}
}
// 额外的包级别限制
String[] dangerousPackages = {
"java.lang.reflect.",
"java.io.",
"java.nio.",
"java.net.",
"java.sql.",
"javax.script.",
"sun.",
"jdk.internal.",
"jdk.nashorn.internal."
};
for (String pkg : dangerousPackages) {
if (className.startsWith(pkg)) {
log.warn("🔒 安全拦截: JavaScript尝试访问危险包 - {}", className);
return false;
}
}
// 默认也拒绝(白名单策略更安全,但这里为了兼容性使用黑名单)
// 如果要更严格,可以改为 return false
log.debug("允许访问类: {}", className);
return true;
}
}

View File

@@ -0,0 +1,790 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import cn.qaiu.util.CommonUtils;
import cn.qaiu.util.FileSizeConverter;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.Promise;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.client.HttpRequest;
import io.vertx.ext.web.client.WebClient;
import io.vertx.uritemplate.UriTemplate;
import org.apache.commons.lang3.StringUtils;
import java.net.MalformedURLException;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.zip.CRC32;
import static cn.qaiu.util.RandomStringGenerator.gen36String;
/**
* 123盘解析器 v2 - 使用Android平台API
* 支持账号密码或token配置
*
* @author <a href="https://qaiu.top">QAIU</a>
*/
public class Ye2Tool extends PanBase {
public static final String SHARE_URL_PREFIX = "https://www.123pan.com/s/";
public static final String FIRST_REQUEST_URL = SHARE_URL_PREFIX + "{key}.html";
private static final String GET_SHARE_INFO_URL = "https://www.123pan.com/b/api/share/get?limit=100&next=1&orderBy=share_id&orderDirection=desc&shareKey={shareKey}&SharePwd={pwd}&ParentFileId={ParentFileId}&Page=1";
private static final String DOWNLOAD_API_URL = "https://www.123pan.com/b/api/file/download_info";
private static final String BATCH_DOWNLOAD_API_URL = "https://www.123pan.com/b/api/file/batch_download_share_info";
private static final String LOGIN_URL = "https://login.123pan.com/api/user/sign_in";
// 字符映射表
private static final String CHAR_MAP = "adefghlmyijnopkqrstubcvwsz";
private final MultiMap header = MultiMap.caseInsensitiveMultiMap();
// Token管理
private static String ssoToken;
private static long tokenExpireTime = 0L; // 毫秒时间戳
public Ye2Tool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
header.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6");
header.set("App-Version", "55");
header.set("Cache-Control", "no-cache");
header.set("Connection", "keep-alive");
header.set("LoginUuid", gen36String());
header.set("Pragma", "no-cache");
header.set("Referer", shareLinkInfo.getStandardUrl());
header.set("Sec-Fetch-Dest", "empty");
header.set("Sec-Fetch-Mode", "cors");
header.set("Sec-Fetch-Site", "same-origin");
header.set("User-Agent", "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Mobile Safari/537.36");
header.set("platform", "android");
header.set("Content-Type", "application/json");
}
/**
* 判断 token 是否过期
*/
private boolean isTokenExpired() {
return System.currentTimeMillis() > tokenExpireTime - 60_000; // 提前1分钟刷新
}
/**
* 计算CRC32并转换为16进制字符串
*/
private String crc32(String data) {
CRC32 crc32 = new CRC32();
crc32.update(data.getBytes());
long value = crc32.getValue();
return String.format("%08x", value);
}
/**
* 16进制转10进制
*/
private long hexToInt(String hexStr) {
return Long.parseLong(hexStr, 16);
}
/**
* 123盘的URL加密算法
* 参考Python代码中的encode123函数
*
* @param url 请求路径
* @param way 平台标识(如"android"
* @param version 版本号(如"55"
* @param timestamp 时间戳(毫秒)
* @return 加密后的URL参数格式?{y}={time_long}-{a}-{final_crc}
*/
private String encode123(String url, String way, String version, String timestamp) {
Random random = new Random();
// 生成随机数 a = int(10000000 * random.randint(1, 10000000) / 10000)
int randomInt = random.nextInt(10000000) + 1;
long a = (10000000L * randomInt) / 10000;
// 将时间戳转换为时间格式
long timeLong = Long.parseLong(timestamp) / 1000;
java.time.LocalDateTime dateTime = java.time.Instant.ofEpochSecond(timeLong)
.atZone(java.time.ZoneId.systemDefault())
.toLocalDateTime();
String timeStr = dateTime.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm"));
// 根据时间字符串生成g
StringBuilder g = new StringBuilder();
for (char c : timeStr.toCharArray()) {
int digit = Character.getNumericValue(c);
if (digit == 0) {
g.append(CHAR_MAP.charAt(0));
} else {
// 数字1对应索引0数字2对应索引1以此类推
g.append(CHAR_MAP.charAt(digit - 1));
}
}
// 计算y值CRC32的十进制
String y = String.valueOf(hexToInt(crc32(g.toString())));
// 计算最终的CRC32
String finalCrcInput = String.format("%d|%d|%s|%s|%s|%s", timeLong, a, url, way, version, y);
String finalCrc = String.valueOf(hexToInt(crc32(finalCrcInput)));
// 返回加密后的URL参数
return String.format("?%s=%d-%d-%s", y, timeLong, a, finalCrc);
}
public Future<String> parse() {
Future<String> tokenFuture;
// 检查是否直接提供了token
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
if (auths != null && auths.contains("token")) {
String providedToken = auths.get("token");
if (StringUtils.isNotEmpty(providedToken)) {
ssoToken = providedToken;
tokenFuture = Future.succeededFuture(providedToken);
} else {
// 如果没有提供token尝试登录
if (ssoToken == null || isTokenExpired()) {
tokenFuture = loginAndGetToken();
} else {
tokenFuture = Future.succeededFuture(ssoToken);
}
}
} else {
// 如果没有提供token尝试登录
if (ssoToken == null || isTokenExpired()) {
tokenFuture = loginAndGetToken();
} else {
tokenFuture = Future.succeededFuture(ssoToken);
}
}
// 1. 登录获取 sso-token 或使用提供的token
tokenFuture.onSuccess(token -> {
if (!token.equals("nologin")) {
// 2. 设置 header
ssoToken = token;
header.set("Authorization", "Bearer " + token);
}
final String dataKey = shareLinkInfo.getShareKey().replace(".html", "");
final String pwd = shareLinkInfo.getSharePassword();
// 3. 获取分享信息
client.getAbs(UriTemplate.of(GET_SHARE_INFO_URL))
.setTemplateParam("shareKey", dataKey)
.setTemplateParam("pwd", StringUtils.isEmpty(pwd) ? "" : pwd)
.setTemplateParam("ParentFileId", "0")
.putHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.putHeader("Referer", "https://www.123pan.com/")
.putHeader("Origin", "https://www.123pan.com")
.send()
.onSuccess(res -> {
JsonObject shareInfoJson = asJson(res);
if (shareInfoJson.getInteger("code") != 0) {
fail("获取分享信息失败: " + shareInfoJson.getString("message"));
return;
}
if (!shareInfoJson.containsKey("data") || !shareInfoJson.getJsonObject("data").containsKey("InfoList")) {
fail("返回数据格式错误");
return;
}
JsonObject data = shareInfoJson.getJsonObject("data");
if (data.getJsonArray("InfoList").size() == 0) {
fail("分享中没有文件");
return;
}
// 获取第一个文件信息
JsonObject fileInfo = data.getJsonArray("InfoList").getJsonObject(0);
// 检查是否需要登录
if (token.equals("nologin")) {
fail("该分享需要登录才能下载请提供账号密码或token");
return;
}
// 判断是否为文件夹: Type: 1为文件夹, 0为文件
if (fileInfo.getInteger("Type", 0) == 1) {
// 4. 获取文件夹打包下载链接
getZipDownUrl(client, fileInfo);
} else {
// 4. 获取文件下载链接
getDownUrl(client, fileInfo);
}
})
.onFailure(this.handleFail(GET_SHARE_INFO_URL));
}).onFailure(err -> {
fail("登录获取token失败: {}", err.getMessage());
});
return promise.future();
}
/**
* 登录并获取token
*/
private Future<String> loginAndGetToken() {
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
if (auths == null) {
return Future.succeededFuture("nologin");
}
String username = auths.get("username");
String password = auths.get("password");
if (username == null || password == null) {
return Future.succeededFuture("nologin");
}
Promise<String> promise = Promise.promise();
String loginUuid = gen36String();
JsonObject loginBody = new JsonObject()
.put("passport", username)
.put("password", password)
.put("remember", true);
client.postAbs(LOGIN_URL)
.putHeader("Content-Type", "application/json")
.putHeader("LoginUuid", loginUuid)
.putHeader("App-Version", "55")
.putHeader("platform", "web")
.sendJsonObject(loginBody)
.onSuccess(res -> {
JsonObject json = res.bodyAsJsonObject();
if (json == null) {
promise.fail("登录响应格式异常: " + res.bodyAsString());
return;
}
if (!json.containsKey("code")) {
promise.fail("登录响应格式异常: " + res.bodyAsString());
return;
}
if (json.getInteger("code") != 200) {
promise.fail("登录失败: " + json.getString("message"));
return;
}
JsonObject data = json.getJsonObject("data");
if (data == null || !data.containsKey("token")) {
promise.fail("未获取到token");
return;
}
ssoToken = data.getString("token");
String expireStr = data.getString("expire");
// 解析过期时间
if (StringUtils.isNotEmpty(expireStr)) {
tokenExpireTime = OffsetDateTime.parse(expireStr)
.toInstant().toEpochMilli();
} else {
// 如果没有过期时间默认1小时后过期
tokenExpireTime = System.currentTimeMillis() + 3600_000;
}
log.info("登录成功token: {}", ssoToken);
promise.complete(ssoToken);
})
.onFailure(promise::fail);
return promise.future();
}
/**
* 获取下载链接使用Android平台API
*/
private void getDownUrl(WebClient client, JsonObject fileInfo) {
setFileInfo(fileInfo);
// 构建请求数据
JsonObject jsonObject = new JsonObject();
jsonObject.put("driveId", 0);
jsonObject.put("etag", fileInfo.getString("Etag"));
jsonObject.put("fileId", fileInfo.getInteger("FileId"));
jsonObject.put("fileName", fileInfo.getString("FileName"));
jsonObject.put("s3keyFlag", fileInfo.getString("S3KeyFlag"));
jsonObject.put("size", fileInfo.getLong("Size"));
jsonObject.put("type", 0);
// 使用encode123加密URL参数
String timestamp = String.valueOf(System.currentTimeMillis());
String encryptedParams = encode123("/b/api/file/download_info", "android", "55", timestamp);
String apiUrl = DOWNLOAD_API_URL + encryptedParams;
log.info("Ye2 API URL: {}", apiUrl);
HttpRequest<Buffer> bufferHttpRequest = client.postAbs(apiUrl);
bufferHttpRequest.putHeader("platform", "android");
bufferHttpRequest.putHeader("App-Version", "55");
bufferHttpRequest.putHeader("Authorization", "Bearer " + ssoToken);
bufferHttpRequest.putHeader("User-Agent", "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36");
bufferHttpRequest.putHeader("Content-Type", "application/json");
bufferHttpRequest
.sendJsonObject(jsonObject)
.onSuccess(res2 -> {
JsonObject downURLJson = asJson(res2);
try {
if (downURLJson.getInteger("code") != 0) {
fail("Ye2: downURLJson返回值异常->" + downURLJson);
return;
}
} catch (Exception ignored) {
fail("Ye2: downURLJson格式异常->" + downURLJson);
return;
}
String downURL = downURLJson.getJsonObject("data").getString("DownloadUrl");
if (StringUtils.isEmpty(downURL)) {
downURL = downURLJson.getJsonObject("data").getString("DownloadURL");
}
if (StringUtils.isEmpty(downURL)) {
fail("Ye2: 未获取到下载链接");
return;
}
try {
Map<String, String> urlParams = CommonUtils.getURLParams(downURL);
String params = urlParams.get("params");
if (StringUtils.isEmpty(params)) {
// 如果没有params参数直接使用downURL
complete(downURL);
return;
}
byte[] decodeByte = Base64.getDecoder().decode(params);
String downUrl2 = new String(decodeByte);
clientNoRedirects.getAbs(downUrl2).putHeaders(header).send().onSuccess(res3 -> {
if (res3.statusCode() == 302 || res3.statusCode() == 301) {
String redirectUrl = res3.getHeader("Location");
if (StringUtils.isBlank(redirectUrl)) {
fail("重定向链接为空");
return;
}
complete(redirectUrl);
return;
}
JsonObject res3Json = asJson(res3);
try {
if (res3Json.getInteger("code") != 0) {
fail("Ye2: downUrl2返回值异常->" + res3Json);
return;
}
} catch (Exception ignored) {
fail("Ye2: downUrl2格式异常->" + downURLJson);
return;
}
String redirectUrl = res3Json.getJsonObject("data").getString("redirect_url");
if (StringUtils.isNotEmpty(redirectUrl)) {
complete(redirectUrl);
} else {
complete(downUrl2);
}
}).onFailure(err -> fail("获取直链失败: " + err.getMessage()));
} catch (MalformedURLException e) {
// 如果解析失败直接使用downURL
complete(downURL);
} catch (Exception e) {
fail("urlParams解析异常: " + e.getMessage());
}
}).onFailure(err -> fail("下载接口失败: " + err.getMessage()));
}
/**
* 获取文件夹打包下载链接使用Android平台API
*/
private void getZipDownUrl(WebClient client, JsonObject fileInfo) {
// 构建请求数据
JsonObject jsonObject = new JsonObject();
jsonObject.put("shareKey", shareLinkInfo.getShareKey().replace(".html", ""));
jsonObject.put("fileIdList", new JsonArray().add(JsonObject.of("fileId", fileInfo.getInteger("FileId"))));
// 使用encode123加密URL参数
String timestamp = String.valueOf(System.currentTimeMillis());
String encryptedParams = encode123("/b/api/file/batch_download_share_info", "android", "55", timestamp);
String apiUrl = BATCH_DOWNLOAD_API_URL + encryptedParams;
log.info("Ye2 Batch Download API URL: {}", apiUrl);
HttpRequest<Buffer> bufferHttpRequest = client.postAbs(apiUrl);
bufferHttpRequest.putHeader("platform", "android");
bufferHttpRequest.putHeader("App-Version", "55");
bufferHttpRequest.putHeader("Authorization", "Bearer " + ssoToken);
bufferHttpRequest.putHeader("User-Agent", "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36");
bufferHttpRequest.putHeader("Content-Type", "application/json");
bufferHttpRequest
.sendJsonObject(jsonObject)
.onSuccess(res2 -> {
JsonObject downURLJson = asJson(res2);
try {
if (downURLJson.getInteger("code") != 0) {
fail("Ye2: 文件夹打包下载接口返回值异常->" + downURLJson);
return;
}
} catch (Exception ignored) {
fail("Ye2: 文件夹打包下载接口格式异常->" + downURLJson);
return;
}
String downURL = downURLJson.getJsonObject("data").getString("DownloadUrl");
if (StringUtils.isEmpty(downURL)) {
downURL = downURLJson.getJsonObject("data").getString("DownloadURL");
}
if (StringUtils.isEmpty(downURL)) {
fail("Ye2: 未获取到文件夹打包下载链接");
return;
}
try {
Map<String, String> urlParams = CommonUtils.getURLParams(downURL);
String params = urlParams.get("params");
if (StringUtils.isEmpty(params)) {
// 如果没有params参数直接使用downURL
complete(downURL);
return;
}
byte[] decodeByte = Base64.getDecoder().decode(params);
String downUrl2 = new String(decodeByte);
clientNoRedirects.getAbs(downUrl2).putHeaders(header).send().onSuccess(res3 -> {
if (res3.statusCode() == 302 || res3.statusCode() == 301) {
String redirectUrl = res3.getHeader("Location");
if (StringUtils.isBlank(redirectUrl)) {
fail("重定向链接为空");
return;
}
complete(redirectUrl);
return;
}
JsonObject res3Json = asJson(res3);
try {
if (res3Json.getInteger("code") != 0) {
fail("Ye2: 文件夹打包下载重定向返回值异常->" + res3Json);
return;
}
} catch (Exception ignored) {
fail("Ye2: 文件夹打包下载重定向格式异常->" + downURLJson);
return;
}
String redirectUrl = res3Json.getJsonObject("data").getString("redirect_url");
if (StringUtils.isNotEmpty(redirectUrl)) {
complete(redirectUrl);
} else {
complete(downUrl2);
}
}).onFailure(err -> fail("获取文件夹打包下载直链失败: " + err.getMessage()));
} catch (MalformedURLException e) {
// 如果解析失败直接使用downURL
complete(downURL);
} catch (Exception e) {
fail("文件夹打包下载urlParams解析异常: " + e.getMessage());
}
}).onFailure(err -> fail("文件夹打包下载接口失败: " + err.getMessage()));
}
/**
* 设置文件信息
*/
void setFileInfo(JsonObject reqBodyJson) {
FileInfo fileInfo = new FileInfo();
fileInfo.setFileId(reqBodyJson.getInteger("FileId").toString());
fileInfo.setFileName(reqBodyJson.getString("FileName"));
fileInfo.setSize(reqBodyJson.getLong("Size"));
fileInfo.setHash(reqBodyJson.getString("Etag"));
String createAt = reqBodyJson.getString("CreateAt");
if (StringUtils.isNotEmpty(createAt)) {
fileInfo.setCreateTime(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.format(OffsetDateTime.parse(createAt).toLocalDateTime()));
}
String updateAt = reqBodyJson.getString("UpdateAt");
if (StringUtils.isNotEmpty(updateAt)) {
fileInfo.setUpdateTime(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.format(OffsetDateTime.parse(updateAt).toLocalDateTime()));
}
shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
}
/**
* 解析文件夹中的文件列表
*/
@Override
public Future<List<FileInfo>> parseFileList() {
Promise<List<FileInfo>> promise = Promise.promise();
String shareKey = shareLinkInfo.getShareKey().replace(".html", "");
String pwd = shareLinkInfo.getSharePassword();
String parentFileId = "0"; // 根目录的文件ID
// 如果参数里的目录ID不为空则直接解析目录
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
if (StringUtils.isNotBlank(dirId)) {
parentFileId = dirId;
}
// 确保已登录
Future<String> tokenFuture;
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
if (auths != null && auths.contains("token")) {
String providedToken = auths.get("token");
if (StringUtils.isNotEmpty(providedToken)) {
ssoToken = providedToken;
tokenFuture = Future.succeededFuture(providedToken);
} else {
if (ssoToken == null || isTokenExpired()) {
tokenFuture = loginAndGetToken();
} else {
tokenFuture = Future.succeededFuture(ssoToken);
}
}
} else {
if (ssoToken == null || isTokenExpired()) {
tokenFuture = loginAndGetToken();
} else {
tokenFuture = Future.succeededFuture(ssoToken);
}
}
String finalParentFileId = parentFileId;
tokenFuture.onSuccess(token -> {
if (token.equals("nologin")) {
promise.fail("该分享需要登录才能访问请提供账号密码或token");
return;
}
// 构造文件列表接口的URL
client.getAbs(UriTemplate.of(GET_SHARE_INFO_URL))
.setTemplateParam("shareKey", shareKey)
.setTemplateParam("pwd", StringUtils.isEmpty(pwd) ? "" : pwd)
.setTemplateParam("ParentFileId", finalParentFileId)
.putHeader("Authorization", "Bearer " + token)
.putHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.putHeader("Referer", "https://www.123pan.com/")
.putHeader("Origin", "https://www.123pan.com")
.send().onSuccess(res -> {
JsonObject response = asJson(res);
if (response.getInteger("code") != 0) {
promise.fail("API错误: " + response.getString("message"));
return;
}
if (!response.containsKey("data") || !response.getJsonObject("data").containsKey("InfoList")) {
promise.fail("返回数据格式错误");
return;
}
JsonArray infoList = response.getJsonObject("data").getJsonArray("InfoList");
List<FileInfo> result = new ArrayList<>();
// 遍历返回的文件和目录信息
for (int i = 0; i < infoList.size(); i++) {
JsonObject item = infoList.getJsonObject(i);
FileInfo fileInfo = new FileInfo();
// 构建下载参数
JsonObject postData = JsonObject.of()
.put("driveId", 0)
.put("etag", item.getString("Etag"))
.put("fileId", item.getInteger("FileId"))
.put("fileName", item.getString("FileName"))
.put("s3keyFlag", item.getString("S3KeyFlag"))
.put("size", item.getLong("Size"))
.put("type", 0);
String param = CommonUtils.urlBase64Encode(postData.encode());
if (item.getInteger("Type") == 0) { // 文件
fileInfo.setFileName(item.getString("FileName"))
.setFileId(item.getInteger("FileId").toString())
.setFileType("file")
.setSize(item.getLong("Size"))
.setHash(item.getString("Etag"))
.setSizeStr(FileSizeConverter.convertToReadableSize(item.getLong("Size")));
String createAt = item.getString("CreateAt");
if (StringUtils.isNotEmpty(createAt)) {
fileInfo.setCreateTime(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.format(OffsetDateTime.parse(createAt).toLocalDateTime()));
}
String updateAt = item.getString("UpdateAt");
if (StringUtils.isNotEmpty(updateAt)) {
fileInfo.setUpdateTime(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.format(OffsetDateTime.parse(updateAt).toLocalDateTime()));
}
fileInfo.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param))
.setPreviewUrl(String.format("%s/v2/viewUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param));
result.add(fileInfo);
} else if (item.getInteger("Type") == 1) { // 目录
fileInfo.setFileName(item.getString("FileName"))
.setFileId(item.getInteger("FileId").toString())
.setFileType("folder")
.setSize(0L);
String createAt = item.getString("CreateAt");
if (StringUtils.isNotEmpty(createAt)) {
fileInfo.setCreateTime(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.format(OffsetDateTime.parse(createAt).toLocalDateTime()));
}
String updateAt = item.getString("UpdateAt");
if (StringUtils.isNotEmpty(updateAt)) {
fileInfo.setUpdateTime(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.format(OffsetDateTime.parse(updateAt).toLocalDateTime()));
}
fileInfo.setParserUrl(
String.format("%s/v2/getFileList?url=%s&dirId=%s&pwd=%s",
getDomainName(),
shareLinkInfo.getShareUrl(),
item.getInteger("FileId"),
pwd)
);
result.add(fileInfo);
}
}
promise.complete(result);
}).onFailure(promise::fail);
}).onFailure(err -> promise.fail("登录获取token失败: " + err.getMessage()));
return promise.future();
}
/**
* 通过ID解析特定文件
*/
@Override
public Future<String> parseById() {
JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson");
// 确保已登录
Future<String> tokenFuture;
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
if (auths != null && auths.contains("token")) {
String providedToken = auths.get("token");
if (StringUtils.isNotEmpty(providedToken)) {
ssoToken = providedToken;
tokenFuture = Future.succeededFuture(providedToken);
} else {
if (ssoToken == null || isTokenExpired()) {
tokenFuture = loginAndGetToken();
} else {
tokenFuture = Future.succeededFuture(ssoToken);
}
}
} else {
if (ssoToken == null || isTokenExpired()) {
tokenFuture = loginAndGetToken();
} else {
tokenFuture = Future.succeededFuture(ssoToken);
}
}
tokenFuture.onSuccess(token -> {
if (token.equals("nologin")) {
fail("该分享需要登录才能下载请提供账号密码或token");
return;
}
// 使用encode123加密URL参数
String timestamp = String.valueOf(System.currentTimeMillis());
String encryptedParams = encode123("/b/api/file/download_info", "android", "55", timestamp);
String apiUrl = DOWNLOAD_API_URL + encryptedParams;
log.info("Ye2 parseById API URL: {}", apiUrl);
HttpRequest<Buffer> bufferHttpRequest = client.postAbs(apiUrl);
bufferHttpRequest.putHeader("platform", "android");
bufferHttpRequest.putHeader("App-Version", "55");
bufferHttpRequest.putHeader("Authorization", "Bearer " + token);
bufferHttpRequest.putHeader("User-Agent", "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36");
bufferHttpRequest.putHeader("Content-Type", "application/json");
bufferHttpRequest
.sendJsonObject(paramJson)
.onSuccess(res2 -> {
JsonObject downURLJson = asJson(res2);
try {
if (downURLJson.getInteger("code") != 0) {
fail("Ye2: downURLJson返回值异常->" + downURLJson);
return;
}
} catch (Exception ignored) {
fail("Ye2: downURLJson格式异常->" + downURLJson);
return;
}
String downURL = downURLJson.getJsonObject("data").getString("DownloadUrl");
if (StringUtils.isEmpty(downURL)) {
downURL = downURLJson.getJsonObject("data").getString("DownloadURL");
}
if (StringUtils.isEmpty(downURL)) {
fail("Ye2: 未获取到下载链接");
return;
}
try {
Map<String, String> urlParams = CommonUtils.getURLParams(downURL);
String params = urlParams.get("params");
if (StringUtils.isEmpty(params)) {
// 如果没有params参数直接使用downURL
complete(downURL);
return;
}
byte[] decodeByte = Base64.getDecoder().decode(params);
String downUrl2 = new String(decodeByte);
clientNoRedirects.getAbs(downUrl2).putHeaders(header).send().onSuccess(res3 -> {
if (res3.statusCode() == 302 || res3.statusCode() == 301) {
String redirectUrl = res3.getHeader("Location");
if (StringUtils.isBlank(redirectUrl)) {
fail("重定向链接为空");
return;
}
complete(redirectUrl);
return;
}
JsonObject res3Json = asJson(res3);
try {
if (res3Json.getInteger("code") != 0) {
fail("Ye2: downUrl2返回值异常->" + res3Json);
return;
}
} catch (Exception ignored) {
fail("Ye2: downUrl2格式异常->" + downURLJson);
return;
}
String redirectUrl = res3Json.getJsonObject("data").getString("redirect_url");
if (StringUtils.isNotEmpty(redirectUrl)) {
complete(redirectUrl);
} else {
complete(downUrl2);
}
}).onFailure(err -> fail("获取直链失败: " + err.getMessage()));
} catch (MalformedURLException e) {
// 如果解析失败直接使用downURL
complete(downURL);
} catch (Exception e) {
fail("urlParams解析异常: " + e.getMessage());
}
}).onFailure(err -> fail("下载接口失败: " + err.getMessage()));
}).onFailure(err -> fail("登录获取token失败: " + err.getMessage()));
return promise.future();
}
}

View File

@@ -40,7 +40,11 @@ custom-parsers/
*/
function parse(shareLinkInfo, http, logger) {
// 你的解析逻辑
return "https://example.com/download/file.zip";
// 示例:解析后返回真实下载链接
var url = shareLinkInfo.getShareUrl();
var response = http.get(url);
// ... 解析逻辑 ...
return "https://download-server.com/file/xxx";
}
/**
@@ -64,7 +68,9 @@ function parseFileList(shareLinkInfo, http, logger) {
*/
function parseById(shareLinkInfo, http, logger) {
// 你的按ID解析逻辑
return "https://example.com/download/" + fileId;
var paramJson = shareLinkInfo.getOtherParam("paramJson");
var fileId = paramJson.fileId;
return "https://download-server.com/file/" + fileId;
}
```

View File

@@ -69,6 +69,9 @@ var java;
* @property {function(): number} statusCode - 获取HTTP状态码
* @property {function(string): string|null} header - 获取响应头
* @property {function(): Object} headers - 获取所有响应头
* @property {function(): boolean} isSuccess - 检查请求是否成功2xx状态码
* @property {function(): Array} bodyBytes - 获取响应体字节数组
* @property {function(): number} bodySize - 获取响应体大小(字节)
*/
/**
@@ -77,10 +80,20 @@ var java;
* @property {function(string): JsHttpResponse} getWithRedirect - 发起GET请求并跟随重定向
* @property {function(string): JsHttpResponse} getNoRedirect - 发起GET请求但不跟随重定向用于获取Location头
* @property {function(string, any=): JsHttpResponse} post - 发起POST请求
* @property {function(string, any=): JsHttpResponse} put - 发起PUT请求
* @property {function(string): JsHttpResponse} delete - 发起DELETE请求
* @property {function(string, any=): JsHttpResponse} patch - 发起PATCH请求
* @property {function(string, string): JsHttpClient} putHeader - 设置请求头
* @property {function(Object): JsHttpClient} putHeaders - 批量设置请求头
* @property {function(string): JsHttpClient} removeHeader - 删除指定请求头
* @property {function(): JsHttpClient} clearHeaders - 清空所有请求头(保留默认头)
* @property {function(): Object} getHeaders - 获取所有请求头
* @property {function(number): JsHttpClient} setTimeout - 设置请求超时时间(秒)
* @property {function(Object): JsHttpResponse} sendForm - 发送简单表单数据
* @property {function(string, Object): JsHttpResponse} sendMultipartForm - 发送multipart表单数据支持文件上传
* @property {function(string, Object): JsHttpResponse} sendMultipartForm - 发送multipart表单数据支持文本字段
* @property {function(any): JsHttpResponse} sendJson - 发送JSON数据
* @property {function(string): string} urlEncode - URL编码静态方法
* @property {function(string): string} urlDecode - URL解码静态方法
*/
/**

View File

@@ -7,6 +7,9 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
import static org.junit.Assert.*;
/**
@@ -279,4 +282,476 @@ public class JsHttpClientTest {
fail("错误响应测试失败: " + e.getMessage());
}
}
// ==================== 新增方法测试 ====================
@Test
public void testPutHeaders() {
System.out.println("\n[测试8] 批量设置请求头 - putHeaders方法");
try {
String url = "https://httpbin.org/headers";
System.out.println("请求URL: " + url);
// 批量设置请求头
Map<String, String> headers = new HashMap<>();
headers.put("X-Test-Header-1", "value1");
headers.put("X-Test-Header-2", "value2");
headers.put("X-Test-Header-3", "value3");
httpClient.putHeaders(headers);
System.out.println("批量设置请求头: " + headers);
long startTime = System.currentTimeMillis();
JsHttpClient.JsHttpResponse response = httpClient.get(url);
long endTime = System.currentTimeMillis();
System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms");
System.out.println("状态码: " + response.statusCode());
String body = response.body();
// 验证结果
assertNotNull("响应不能为null", response);
assertEquals("状态码应该是200", 200, response.statusCode());
assertNotNull("响应体不能为null", body);
assertTrue("响应体应该包含设置的请求头",
body.contains("X-Test-Header-1") || body.contains("value1"));
System.out.println("✓ 测试通过");
} catch (Exception e) {
System.err.println("✗ 测试失败: " + e.getMessage());
e.printStackTrace();
fail("批量设置请求头测试失败: " + e.getMessage());
}
}
@Test
public void testRemoveHeader() {
System.out.println("\n[测试9] 删除请求头 - removeHeader方法");
try {
String url = "https://httpbin.org/headers";
System.out.println("请求URL: " + url);
// 先设置请求头
httpClient.putHeader("X-To-Be-Removed", "test-value");
httpClient.putHeader("X-To-Keep", "keep-value");
// 获取所有请求头
Map<String, String> headersBefore = httpClient.getHeaders();
System.out.println("删除前请求头数量: " + headersBefore.size());
assertTrue("应该包含要删除的请求头", headersBefore.containsKey("X-To-Be-Removed"));
// 删除指定请求头
httpClient.removeHeader("X-To-Be-Removed");
System.out.println("删除请求头: X-To-Be-Removed");
// 获取所有请求头
Map<String, String> headersAfter = httpClient.getHeaders();
System.out.println("删除后请求头数量: " + headersAfter.size());
// 验证结果
assertFalse("不应该包含已删除的请求头", headersAfter.containsKey("X-To-Be-Removed"));
assertTrue("应该保留未删除的请求头", headersAfter.containsKey("X-To-Keep"));
System.out.println("✓ 测试通过");
} catch (Exception e) {
System.err.println("✗ 测试失败: " + e.getMessage());
e.printStackTrace();
fail("删除请求头测试失败: " + e.getMessage());
}
}
@Test
public void testClearHeaders() {
System.out.println("\n[测试10] 清空请求头 - clearHeaders方法");
try {
// 先设置一些自定义请求头
httpClient.putHeader("X-Custom-1", "value1");
httpClient.putHeader("X-Custom-2", "value2");
Map<String, String> headersBefore = httpClient.getHeaders();
System.out.println("清空前请求头数量: " + headersBefore.size());
assertTrue("应该包含自定义请求头", headersBefore.size() > 3); // 3个默认头
// 清空请求头
httpClient.clearHeaders();
System.out.println("清空所有请求头(保留默认头)");
Map<String, String> headersAfter = httpClient.getHeaders();
System.out.println("清空后请求头数量: " + headersAfter.size());
System.out.println("保留的默认头: " + headersAfter.keySet());
// 验证结果
assertFalse("不应该包含自定义请求头", headersAfter.containsKey("X-Custom-1"));
assertFalse("不应该包含自定义请求头", headersAfter.containsKey("X-Custom-2"));
// 应该保留默认头
assertTrue("应该保留Accept-Encoding默认头",
headersAfter.containsKey("Accept-Encoding"));
assertTrue("应该保留User-Agent默认头",
headersAfter.containsKey("User-Agent"));
assertTrue("应该保留Accept-Language默认头",
headersAfter.containsKey("Accept-Language"));
System.out.println("✓ 测试通过");
} catch (Exception e) {
System.err.println("✗ 测试失败: " + e.getMessage());
e.printStackTrace();
fail("清空请求头测试失败: " + e.getMessage());
}
}
@Test
public void testGetHeaders() {
System.out.println("\n[测试11] 获取所有请求头 - getHeaders方法");
try {
// 设置一些请求头
httpClient.putHeader("X-Test-1", "value1");
httpClient.putHeader("X-Test-2", "value2");
Map<String, String> headers = httpClient.getHeaders();
System.out.println("获取到的请求头数量: " + headers.size());
System.out.println("请求头列表: " + headers);
// 验证结果
assertNotNull("请求头Map不能为null", headers);
assertTrue("应该包含设置的请求头", headers.containsKey("X-Test-1"));
assertTrue("应该包含设置的请求头", headers.containsKey("X-Test-2"));
assertEquals("X-Test-1的值应该是value1", "value1", headers.get("X-Test-1"));
assertEquals("X-Test-2的值应该是value2", "value2", headers.get("X-Test-2"));
System.out.println("✓ 测试通过");
} catch (Exception e) {
System.err.println("✗ 测试失败: " + e.getMessage());
e.printStackTrace();
fail("获取请求头测试失败: " + e.getMessage());
}
}
@Test
public void testPutRequest() {
System.out.println("\n[测试12] PUT请求 - put方法");
try {
String url = "https://httpbin.org/put";
System.out.println("请求URL: " + url);
Map<String, String> data = new HashMap<>();
data.put("key1", "value1");
data.put("key2", "value2");
System.out.println("PUT数据: " + data);
System.out.println("开始请求...");
long startTime = System.currentTimeMillis();
JsHttpClient.JsHttpResponse response = httpClient.put(url, data);
long endTime = System.currentTimeMillis();
System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms");
System.out.println("状态码: " + response.statusCode());
String body = response.body();
// 验证结果
assertNotNull("响应不能为null", response);
assertEquals("状态码应该是200", 200, response.statusCode());
assertNotNull("响应体不能为null", body);
assertTrue("响应体应该包含PUT的数据",
body.contains("key1") || body.contains("value1"));
System.out.println("✓ 测试通过");
} catch (Exception e) {
System.err.println("✗ 测试失败: " + e.getMessage());
e.printStackTrace();
fail("PUT请求测试失败: " + e.getMessage());
}
}
@Test
public void testDeleteRequest() {
System.out.println("\n[测试13] DELETE请求 - delete方法");
try {
String url = "https://httpbin.org/delete";
System.out.println("请求URL: " + url);
System.out.println("开始请求...");
long startTime = System.currentTimeMillis();
JsHttpClient.JsHttpResponse response = httpClient.delete(url);
long endTime = System.currentTimeMillis();
System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms");
System.out.println("状态码: " + response.statusCode());
String body = response.body();
// 验证结果
assertNotNull("响应不能为null", response);
assertEquals("状态码应该是200", 200, response.statusCode());
assertNotNull("响应体不能为null", body);
assertTrue("响应体应该包含DELETE相关信息",
body.contains("\"url\"") || body.contains("delete"));
System.out.println("✓ 测试通过");
} catch (Exception e) {
System.err.println("✗ 测试失败: " + e.getMessage());
e.printStackTrace();
fail("DELETE请求测试失败: " + e.getMessage());
}
}
@Test
public void testPatchRequest() {
System.out.println("\n[测试14] PATCH请求 - patch方法");
try {
String url = "https://httpbin.org/patch";
System.out.println("请求URL: " + url);
Map<String, String> data = new HashMap<>();
data.put("field1", "newValue1");
data.put("field2", "newValue2");
System.out.println("PATCH数据: " + data);
System.out.println("开始请求...");
long startTime = System.currentTimeMillis();
JsHttpClient.JsHttpResponse response = httpClient.patch(url, data);
long endTime = System.currentTimeMillis();
System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms");
System.out.println("状态码: " + response.statusCode());
String body = response.body();
// 验证结果
assertNotNull("响应不能为null", response);
assertEquals("状态码应该是200", 200, response.statusCode());
assertNotNull("响应体不能为null", body);
assertTrue("响应体应该包含PATCH的数据",
body.contains("field1") || body.contains("newValue1"));
System.out.println("✓ 测试通过");
} catch (Exception e) {
System.err.println("✗ 测试失败: " + e.getMessage());
e.printStackTrace();
fail("PATCH请求测试失败: " + e.getMessage());
}
}
@Test
public void testSetTimeout() {
System.out.println("\n[测试15] 设置超时时间 - setTimeout方法");
try {
String url = "https://httpbin.org/delay/2";
System.out.println("请求URL: " + url);
// 设置超时时间为10秒
httpClient.setTimeout(10);
System.out.println("设置超时时间: 10秒");
long startTime = System.currentTimeMillis();
JsHttpClient.JsHttpResponse response = httpClient.get(url);
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
System.out.println("请求完成,耗时: " + duration + "ms");
System.out.println("状态码: " + response.statusCode());
// 验证结果
assertNotNull("响应不能为null", response);
assertEquals("状态码应该是200", 200, response.statusCode());
assertTrue("应该在合理时间内完成2-5秒", duration >= 2000 && duration < 5000);
// 测试更短的超时时间(应该失败)
httpClient.setTimeout(1);
System.out.println("设置超时时间为1秒请求延迟2秒的URL应该超时");
try {
httpClient.get("https://httpbin.org/delay/2");
fail("应该抛出超时异常");
} catch (Exception e) {
System.out.println("✓ 正确抛出超时异常: " + e.getMessage());
assertTrue("异常应该包含超时相关信息",
e.getMessage().contains("超时") ||
e.getMessage().contains("timeout") ||
e.getMessage().contains("Timeout"));
}
System.out.println("✓ 测试通过");
} catch (Exception e) {
System.err.println("✗ 测试失败: " + e.getMessage());
e.printStackTrace();
fail("设置超时时间测试失败: " + e.getMessage());
}
}
@Test
public void testUrlEncode() {
System.out.println("\n[测试16] URL编码 - urlEncode静态方法");
try {
// 测试各种字符串
String[] testStrings = {
"hello world",
"测试中文",
"a+b=c&d=e",
"特殊字符!@#$%^&*()",
"123456"
};
for (String original : testStrings) {
String encoded = JsHttpClient.urlEncode(original);
System.out.println("原文: " + original);
System.out.println("编码: " + encoded);
// 验证结果
assertNotNull("编码结果不能为null", encoded);
assertNotEquals("编码后应该与原文不同(如果包含特殊字符)", original, encoded);
// 验证编码后的字符串不包含空格(空格应该被编码为%20
if (original.contains(" ")) {
assertFalse("编码后的字符串不应该包含空格", encoded.contains(" "));
}
}
// 测试null
String nullEncoded = JsHttpClient.urlEncode(null);
assertNull("null应该返回null", nullEncoded);
System.out.println("✓ 测试通过");
} catch (Exception e) {
System.err.println("✗ 测试失败: " + e.getMessage());
e.printStackTrace();
fail("URL编码测试失败: " + e.getMessage());
}
}
@Test
public void testUrlDecode() {
System.out.println("\n[测试17] URL解码 - urlDecode静态方法");
try {
// 测试编码和解码的往返
String[] testStrings = {
"hello world",
"测试中文",
"a+b=c&d=e",
"123456"
};
for (String original : testStrings) {
String encoded = JsHttpClient.urlEncode(original);
String decoded = JsHttpClient.urlDecode(encoded);
System.out.println("原文: " + original);
System.out.println("编码: " + encoded);
System.out.println("解码: " + decoded);
// 验证结果
assertEquals("解码后应该与原文相同", original, decoded);
}
// 测试null
String nullDecoded = JsHttpClient.urlDecode(null);
assertNull("null应该返回null", nullDecoded);
System.out.println("✓ 测试通过");
} catch (Exception e) {
System.err.println("✗ 测试失败: " + e.getMessage());
e.printStackTrace();
fail("URL解码测试失败: " + e.getMessage());
}
}
@Test
public void testBodyBytes() {
System.out.println("\n[测试18] 获取响应体字节数组 - bodyBytes方法");
try {
String url = "https://httpbin.org/get";
System.out.println("请求URL: " + url);
System.out.println("开始请求...");
JsHttpClient.JsHttpResponse response = httpClient.get(url);
System.out.println("状态码: " + response.statusCode());
// 获取响应体字符串和字节数组
String bodyString = response.body();
byte[] bodyBytes = response.bodyBytes();
System.out.println("响应体字符串长度: " + (bodyString != null ? bodyString.length() : 0));
System.out.println("响应体字节数组长度: " + (bodyBytes != null ? bodyBytes.length : 0));
// 验证结果
assertNotNull("响应体字节数组不能为null", bodyBytes);
assertTrue("字节数组长度应该大于0", bodyBytes.length > 0);
assertTrue("字节数组长度应该与字符串长度相关",
bodyBytes.length >= bodyString.length());
// 验证字节数组可以转换为字符串
String bytesAsString = new String(bodyBytes);
assertTrue("字节数组转换的字符串应该包含关键内容",
bytesAsString.contains("\"url\""));
System.out.println("✓ 测试通过");
} catch (Exception e) {
System.err.println("✗ 测试失败: " + e.getMessage());
e.printStackTrace();
fail("获取响应体字节数组测试失败: " + e.getMessage());
}
}
@Test
public void testBodySize() {
System.out.println("\n[测试19] 获取响应体大小 - bodySize方法");
try {
String url = "https://httpbin.org/get";
System.out.println("请求URL: " + url);
System.out.println("开始请求...");
JsHttpClient.JsHttpResponse response = httpClient.get(url);
System.out.println("状态码: " + response.statusCode());
// 获取响应体大小和字符串
long bodySize = response.bodySize();
String bodyString = response.body();
System.out.println("响应体大小: " + bodySize + " 字节");
System.out.println("响应体字符串长度: " + (bodyString != null ? bodyString.length() : 0));
// 验证结果
assertTrue("响应体大小应该大于0", bodySize > 0);
assertTrue("响应体大小应该与字符串长度相关",
bodySize >= bodyString.length());
// 验证bodySize与bodyBytes长度一致
byte[] bodyBytes = response.bodyBytes();
assertEquals("bodySize应该等于bodyBytes的长度",
bodyBytes.length, bodySize);
System.out.println("✓ 测试通过");
} catch (Exception e) {
System.err.println("✗ 测试失败: " + e.getMessage());
e.printStackTrace();
fail("获取响应体大小测试失败: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,393 @@
package cn.qaiu.parser;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.customjs.JsPlaygroundExecutor;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
/**
* JavaScript执行器安全测试
* 用于验证JavaScript代码执行环境的安全性
*
* @author <a href="https://qaiu.top">QAIU</a>
*/
public class SecurityTest {
private static final Logger log = LoggerFactory.getLogger(SecurityTest.class);
/**
* 测试1: 尝试通过Java类执行系统命令
*/
@Test
public void testSystemCommandExecution() {
String dangerousJs = """
// ==UserScript==
// @name 危险测试-系统命令执行
// @type security_test
// @match https://test.com/*
// ==/UserScript==
function parse(shareLinkInfo, http, logger) {
logger.info("尝试执行系统命令...");
try {
// 尝试1: 直接访问Runtime类执行命令
var Runtime = Java.type('java.lang.Runtime');
var runtime = Runtime.getRuntime();
var process = runtime.exec("whoami");
var reader = new java.io.BufferedReader(new java.io.InputStreamReader(process.getInputStream()));
var output = reader.readLine();
logger.error("【安全漏洞】成功执行系统命令: " + output);
return "危险: 系统命令执行成功 - " + output;
} catch (e) {
logger.info("方法1失败: " + e.message);
}
try {
// 尝试2: 使用ProcessBuilder
var ProcessBuilder = Java.type('java.lang.ProcessBuilder');
var pb = new ProcessBuilder(["ls", "-la"]);
var process = pb.start();
logger.error("【安全漏洞】ProcessBuilder执行成功");
return "危险: ProcessBuilder执行成功";
} catch (e) {
logger.info("方法2失败: " + e.message);
}
return "安全: 无法执行系统命令";
}
""";
testJavaScriptSecurity(dangerousJs, "系统命令执行测试");
}
/**
* 测试2: 尝试文件系统访问
*/
@Test
public void testFileSystemAccess() {
String dangerousJs = """
// ==UserScript==
// @name 危险测试-文件系统访问
// @type security_test
// @match https://test.com/*
// ==/UserScript==
function parse(shareLinkInfo, http, logger) {
logger.info("尝试访问文件系统...");
try {
// 尝试读取敏感文件
var Files = Java.type('java.nio.file.Files');
var Paths = Java.type('java.nio.file.Paths');
var path = Paths.get("/etc/passwd");
var content = Files.readAllLines(path);
logger.error("【安全漏洞】成功读取文件: " + content.get(0));
return "危险: 文件读取成功";
} catch (e) {
logger.info("方法1失败: " + e.message);
}
try {
// 尝试写入文件
var FileWriter = Java.type('java.io.FileWriter');
var writer = new FileWriter("/tmp/test.txt");
writer.write("test");
writer.close();
logger.error("【安全漏洞】成功写入文件");
return "危险: 文件写入成功";
} catch (e) {
logger.info("方法2失败: " + e.message);
}
return "安全: 无法访问文件系统";
}
""";
testJavaScriptSecurity(dangerousJs, "文件系统访问测试");
}
/**
* 测试3: 尝试访问系统属性和环境变量
*/
@Test
public void testSystemPropertiesAccess() {
String dangerousJs = """
// ==UserScript==
// @name 危险测试-系统属性访问
// @type security_test
// @match https://test.com/*
// ==/UserScript==
function parse(shareLinkInfo, http, logger) {
logger.info("尝试访问系统属性...");
try {
// 尝试读取系统属性
var System = Java.type('java.lang.System');
var userHome = System.getProperty("user.home");
var userName = System.getProperty("user.name");
logger.error("【安全漏洞】获取到系统属性 - HOME: " + userHome + ", USER: " + userName);
return "危险: 系统属性访问成功 - " + userName;
} catch (e) {
logger.info("方法1失败: " + e.message);
}
try {
// 尝试读取环境变量
var System = Java.type('java.lang.System');
var env = System.getenv();
var path = env.get("PATH");
logger.error("【安全漏洞】获取到环境变量 PATH: " + path);
return "危险: 环境变量访问成功";
} catch (e) {
logger.info("方法2失败: " + e.message);
}
return "安全: 无法访问系统属性";
}
""";
testJavaScriptSecurity(dangerousJs, "系统属性访问测试");
}
/**
* 测试4: 尝试反射攻击
*/
@Test
public void testReflectionAttack() {
String dangerousJs = """
// ==UserScript==
// @name 危险测试-反射攻击
// @type security_test
// @match https://test.com/*
// ==/UserScript==
function parse(shareLinkInfo, http, logger) {
logger.info("尝试使用反射...");
try {
// 尝试通过反射访问私有字段
var Class = Java.type('java.lang.Class');
var Field = Java.type('java.lang.reflect.Field');
var systemClass = Class.forName("java.lang.System");
var methods = systemClass.getDeclaredMethods();
logger.error("【安全漏洞】反射访问成功,获取到 " + methods.length + " 个方法");
return "危险: 反射访问成功";
} catch (e) {
logger.info("方法1失败: " + e.message);
}
try {
// 尝试获取ClassLoader
var Thread = Java.type('java.lang.Thread');
var classLoader = Thread.currentThread().getContextClassLoader();
logger.error("【安全漏洞】获取到ClassLoader: " + classLoader);
return "危险: ClassLoader访问成功";
} catch (e) {
logger.info("方法2失败: " + e.message);
}
return "安全: 无法使用反射";
}
""";
testJavaScriptSecurity(dangerousJs, "反射攻击测试");
}
/**
* 测试5: 尝试网络攻击
*/
@Test
public void testNetworkAttack() {
String dangerousJs = """
// ==UserScript==
// @name 危险测试-网络攻击
// @type security_test
// @match https://test.com/*
// ==/UserScript==
function parse(shareLinkInfo, http, logger) {
logger.info("尝试发起网络连接...");
try {
// 尝试创建Socket连接
var Socket = Java.type('java.net.Socket');
var socket = new Socket("127.0.0.1", 22);
logger.error("【安全漏洞】Socket连接成功");
socket.close();
return "危险: Socket连接成功";
} catch (e) {
logger.info("方法1失败: " + e.message);
}
try {
// 尝试使用URL访问
var URL = Java.type('java.net.URL');
var url = new URL("http://localhost:8080");
var conn = url.openConnection();
logger.error("【安全漏洞】URL连接成功");
return "危险: URL连接成功";
} catch (e) {
logger.info("方法2失败: " + e.message);
}
return "安全: 无法创建网络连接";
}
""";
testJavaScriptSecurity(dangerousJs, "网络攻击测试");
}
/**
* 测试6: 尝试退出JVM
*/
@Test
public void testJvmExit() {
String dangerousJs = """
// ==UserScript==
// @name 危险测试-JVM退出
// @type security_test
// @match https://test.com/*
// ==/UserScript==
function parse(shareLinkInfo, http, logger) {
logger.info("尝试退出JVM...");
try {
// 尝试退出JVM
var System = Java.type('java.lang.System');
logger.warn("准备执行 System.exit(1)...");
System.exit(1);
return "危险: JVM退出成功";
} catch (e) {
logger.info("退出失败: " + e.message);
}
try {
// 尝试终止运行时
var Runtime = Java.type('java.lang.Runtime');
Runtime.getRuntime().halt(1);
return "危险: Runtime.halt成功";
} catch (e) {
logger.info("halt失败: " + e.message);
}
return "安全: 无法退出JVM";
}
""";
testJavaScriptSecurity(dangerousJs, "JVM退出测试");
}
/**
* 测试7: 尝试访问注入的httpClient执行任意HTTP请求
*/
@Test
public void testHttpClientAbuse() {
String dangerousJs = """
// ==UserScript==
// @name 危险测试-HTTP客户端滥用
// @type security_test
// @match https://test.com/*
// ==/UserScript==
function parse(shareLinkInfo, http, logger) {
logger.info("测试HTTP客户端访问控制...");
try {
// 尝试访问内网地址
logger.info("尝试访问内网地址...");
var response = http.get("http://127.0.0.1:8080/admin");
logger.warn("【潜在风险】可以访问内网地址: " + response.substring(0, 50));
return "警告: 可以通过HTTP访问内网";
} catch (e) {
logger.info("内网访问失败: " + e.message);
}
try {
// 尝试访问敏感API
logger.info("尝试访问云服务元数据API...");
var response = http.get("http://169.254.169.254/latest/meta-data/");
logger.error("【严重漏洞】可以访问云服务元数据: " + response);
return "危险: 可以访问云服务元数据";
} catch (e) {
logger.info("元数据访问失败: " + e.message);
}
return "提示: HTTP客户端访问受限";
}
""";
testJavaScriptSecurity(dangerousJs, "HTTP客户端滥用测试");
}
/**
* 执行JavaScript安全测试的辅助方法
*/
private void testJavaScriptSecurity(String jsCode, String testName) {
log.info("\n" + "=".repeat(80));
log.info("开始执行安全测试: {}", testName);
log.info("=".repeat(80));
try {
// 创建测试用的ShareLinkInfo
ShareLinkInfo shareLinkInfo = ShareLinkInfo.newBuilder()
.shareKey("test_key")
.sharePassword("test_pwd")
.type("security_test")
.shareUrl("https://test.com/share/test")
.standardUrl("https://test.com/share/test")
.otherParam(new HashMap<>())
.build();
// 创建执行器并执行
JsPlaygroundExecutor executor = new JsPlaygroundExecutor(shareLinkInfo, jsCode);
executor.executeParseAsync()
.onSuccess(result -> {
log.info("测试结果: {}", result);
// 打印所有日志
log.info("\n执行日志:");
executor.getLogs().forEach(logEntry -> {
String logLevel = logEntry.getLevel();
String message = logEntry.getMessage();
log.info("[{}] [{}] {}", logLevel, logEntry.getSource(), message);
// 检查是否有安全漏洞警告
if (message.contains("【安全漏洞】") || message.contains("【严重漏洞】")) {
log.error("!!! 发现安全漏洞 !!!");
}
});
})
.onFailure(e -> {
log.info("执行失败: {}", e.getMessage());
// 打印所有日志
log.info("\n执行日志:");
executor.getLogs().forEach(logEntry -> {
log.info("[{}] [{}] {}",
logEntry.getLevel(),
logEntry.getSource(),
logEntry.getMessage());
});
})
.toCompletionStage()
.toCompletableFuture()
.join(); // 等待异步执行完成
} catch (Exception e) {
log.error("测试执行异常", e);
}
log.info("=".repeat(80));
log.info("测试完成: {}\n", testName);
}
}

1
web-front/UI_FIXES.md Normal file
View File

@@ -0,0 +1 @@

View File

@@ -10,11 +10,13 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@monaco-editor/loader": "^1.4.0",
"@vueuse/core": "^11.2.0",
"axios": "1.12.0",
"clipboard": "^2.0.11",
"core-js": "^3.8.3",
"element-plus": "^2.8.7",
"element-plus": "2.11.3",
"monaco-editor": "^0.45.0",
"qrcode": "^1.5.4",
"splitpanes": "^4.0.4",
"vue": "^3.5.12",

View File

@@ -3,12 +3,14 @@ import Home from '@/views/Home.vue'
import ShowFile from '@/views/ShowFile.vue'
import ShowList from '@/views/ShowList.vue'
import ClientLinks from '@/views/ClientLinks.vue'
import Playground from '@/views/Playground.vue'
const routes = [
{ path: '/', component: Home },
{ path: '/showFile', component: ShowFile },
{ path: '/showList', component: ShowList },
{ path: '/clientLinks', component: ClientLinks }
{ path: '/clientLinks', component: ClientLinks },
{ path: '/playground', component: Playground }
]
const router = createRouter({

View File

@@ -218,6 +218,7 @@
<!-- 版本号显示 -->
<div class="version-info">
<span class="version-text">内部版本: {{ buildVersion }}</span>
<!-- <el-link :href="'/playground'" class="playground-link">JS演练场</el-link>-->
</div>
<!-- 文件解析结果区下方加分享按钮 -->
@@ -969,9 +970,12 @@ hr {
/* 版本号显示样式 */
.version-info {
text-align: center;
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
margin-bottom: 20px;
padding: 0 10px;
}
.version-text {
@@ -983,4 +987,22 @@ hr {
#app.dark-theme .version-text {
color: #666;
}
.playground-link {
font-size: 0.85rem;
color: #409eff;
text-decoration: none;
}
.playground-link:hover {
color: #66b1ff;
}
#app.dark-theme .playground-link {
color: #4a9eff;
}
#app.dark-theme .playground-link:hover {
color: #66b1ff;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -43,6 +43,15 @@ module.exports = {
'@': resolve('src')
}
},
// Monaco Editor配置
module: {
rules: [
{
test: /\.ttf$/,
type: 'asset/resource'
}
]
},
plugins: [
new CompressionPlugin({
test: /\.js$|\.html$|\.css/, // 匹配文件

View File

@@ -0,0 +1,436 @@
package cn.qaiu.lz.web.controller;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.lz.web.model.PlaygroundTestResp;
import cn.qaiu.lz.web.service.DbService;
import cn.qaiu.parser.ParserCreate;
import cn.qaiu.parser.customjs.JsPlaygroundExecutor;
import cn.qaiu.parser.customjs.JsPlaygroundLogger;
import cn.qaiu.parser.customjs.JsScriptMetadataParser;
import cn.qaiu.vx.core.annotaions.RouteHandler;
import cn.qaiu.vx.core.annotaions.RouteMapping;
import cn.qaiu.vx.core.enums.RouteMethod;
import cn.qaiu.vx.core.model.JsonResult;
import cn.qaiu.vx.core.util.AsyncServiceUtil;
import cn.qaiu.vx.core.util.ResponseUtil;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* 演练场API控制器
* 提供JavaScript解析脚本的测试接口
*
* @author <a href="https://qaiu.top">QAIU</a>
*/
@RouteHandler(value = "/v2/playground", order = 10)
@Slf4j
public class PlaygroundApi {
private static final int MAX_PARSER_COUNT = 100;
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
/**
* 测试执行JavaScript代码
*
* @param ctx 路由上下文
* @return 测试结果
*/
@RouteMapping(value = "/test", method = RouteMethod.POST)
public Future<JsonObject> test(RoutingContext ctx) {
Promise<JsonObject> promise = Promise.promise();
try {
JsonObject body = ctx.body().asJsonObject();
String jsCode = body.getString("jsCode");
String shareUrl = body.getString("shareUrl");
String pwd = body.getString("pwd");
String method = body.getString("method", "parse");
// 参数验证
if (StringUtils.isBlank(jsCode)) {
promise.complete(JsonObject.mapFrom(PlaygroundTestResp.builder()
.success(false)
.error("JavaScript代码不能为空")
.build()));
return promise.future();
}
if (StringUtils.isBlank(shareUrl)) {
promise.complete(JsonObject.mapFrom(PlaygroundTestResp.builder()
.success(false)
.error("分享链接不能为空")
.build()));
return promise.future();
}
// 验证方法类型
if (!"parse".equals(method) && !"parseFileList".equals(method) && !"parseById".equals(method)) {
promise.complete(JsonObject.mapFrom(PlaygroundTestResp.builder()
.success(false)
.error("方法类型无效,必须是 parse、parseFileList 或 parseById")
.build()));
return promise.future();
}
long startTime = System.currentTimeMillis();
try {
// 创建ShareLinkInfo
ParserCreate parserCreate = ParserCreate.fromShareUrl(shareUrl);
if (StringUtils.isNotBlank(pwd)) {
parserCreate.setShareLinkInfoPwd(pwd);
}
ShareLinkInfo shareLinkInfo = parserCreate.getShareLinkInfo();
// 创建演练场执行器
JsPlaygroundExecutor executor = new JsPlaygroundExecutor(shareLinkInfo, jsCode);
// 根据方法类型选择执行,并异步处理结果
Future<Object> executionFuture;
switch (method) {
case "parse":
executionFuture = executor.executeParseAsync().map(r -> (Object) r);
break;
case "parseFileList":
executionFuture = executor.executeParseFileListAsync().map(r -> (Object) r);
break;
case "parseById":
executionFuture = executor.executeParseByIdAsync().map(r -> (Object) r);
break;
default:
promise.fail(new IllegalArgumentException("未知的方法类型: " + method));
return promise.future();
}
// 异步处理执行结果
executionFuture.onSuccess(result -> {
log.debug("执行成功,结果类型: {}, 结果值: {}",
result != null ? result.getClass().getSimpleName() : "null",
result);
// 获取日志
List<JsPlaygroundLogger.LogEntry> logEntries = executor.getLogs();
log.debug("获取到 {} 条日志记录", logEntries.size());
List<PlaygroundTestResp.LogEntry> respLogs = logEntries.stream()
.map(entry -> PlaygroundTestResp.LogEntry.builder()
.level(entry.getLevel())
.message(entry.getMessage())
.timestamp(entry.getTimestamp())
.source(entry.getSource()) // 使用日志条目的来源标识
.build())
.collect(Collectors.toList());
long executionTime = System.currentTimeMillis() - startTime;
// 构建响应
PlaygroundTestResp response = PlaygroundTestResp.builder()
.success(true)
.result(result)
.logs(respLogs)
.executionTime(executionTime)
.build();
JsonObject jsonResponse = JsonObject.mapFrom(response);
log.debug("测试成功响应: {}", jsonResponse.encodePrettily());
promise.complete(jsonResponse);
}).onFailure(e -> {
long executionTime = System.currentTimeMillis() - startTime;
String errorMessage = e.getMessage();
String stackTrace = getStackTrace(e);
log.error("演练场执行失败", e);
// 尝试获取已有的日志
List<JsPlaygroundLogger.LogEntry> logEntries = executor.getLogs();
List<PlaygroundTestResp.LogEntry> respLogs = logEntries.stream()
.map(entry -> PlaygroundTestResp.LogEntry.builder()
.level(entry.getLevel())
.message(entry.getMessage())
.timestamp(entry.getTimestamp())
.source(entry.getSource()) // 使用日志条目的来源标识
.build())
.collect(Collectors.toList());
PlaygroundTestResp response = PlaygroundTestResp.builder()
.success(false)
.error(errorMessage)
.stackTrace(stackTrace)
.executionTime(executionTime)
.logs(respLogs)
.build();
promise.complete(JsonObject.mapFrom(response));
});
} catch (Exception e) {
long executionTime = System.currentTimeMillis() - startTime;
String errorMessage = e.getMessage();
String stackTrace = getStackTrace(e);
log.error("演练场初始化失败", e);
PlaygroundTestResp response = PlaygroundTestResp.builder()
.success(false)
.error(errorMessage)
.stackTrace(stackTrace)
.executionTime(executionTime)
.logs(new ArrayList<>())
.build();
promise.complete(JsonObject.mapFrom(response));
}
} catch (Exception e) {
log.error("解析请求参数失败", e);
promise.complete(JsonObject.mapFrom(PlaygroundTestResp.builder()
.success(false)
.error("解析请求参数失败: " + e.getMessage())
.stackTrace(getStackTrace(e))
.build()));
}
return promise.future();
}
/**
* 获取types.js文件内容
*
* @param response HTTP响应
*/
@RouteMapping(value = "/types.js", method = RouteMethod.GET)
public void getTypesJs(HttpServerResponse response) {
try (InputStream inputStream = getClass().getClassLoader()
.getResourceAsStream("custom-parsers/types.js")) {
if (inputStream == null) {
ResponseUtil.fireJsonResultResponse(response, JsonResult.error("types.js文件不存在"));
return;
}
String content = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))
.lines()
.collect(Collectors.joining("\n"));
response.putHeader("Content-Type", "text/javascript; charset=utf-8")
.end(content);
} catch (Exception e) {
log.error("读取types.js失败", e);
ResponseUtil.fireJsonResultResponse(response, JsonResult.error("读取types.js失败: " + e.getMessage()));
}
}
/**
* 获取解析器列表
*/
@RouteMapping(value = "/parsers", method = RouteMethod.GET)
public Future<JsonObject> getParserList() {
return dbService.getPlaygroundParserList();
}
/**
* 保存解析器
*/
@RouteMapping(value = "/parsers", method = RouteMethod.POST)
public Future<JsonObject> saveParser(RoutingContext ctx) {
Promise<JsonObject> promise = Promise.promise();
try {
JsonObject body = ctx.body().asJsonObject();
String jsCode = body.getString("jsCode");
if (StringUtils.isBlank(jsCode)) {
promise.complete(JsonResult.error("JavaScript代码不能为空").toJsonObject());
return promise.future();
}
// 解析元数据
try {
var config = JsScriptMetadataParser.parseScript(jsCode);
String type = config.getType();
String displayName = config.getDisplayName();
String name = config.getMetadata().get("name");
String description = config.getMetadata().get("description");
String author = config.getMetadata().get("author");
String version = config.getMetadata().get("version");
String matchPattern = config.getMatchPattern() != null ? config.getMatchPattern().pattern() : null;
// 检查数量限制
dbService.getPlaygroundParserCount().onSuccess(count -> {
if (count >= MAX_PARSER_COUNT) {
promise.complete(JsonResult.error("解析器数量已达到上限(" + MAX_PARSER_COUNT + "个),请先删除不需要的解析器").toJsonObject());
return;
}
// 检查type是否已存在
dbService.getPlaygroundParserList().onSuccess(listResult -> {
var list = listResult.getJsonArray("data");
boolean exists = false;
if (list != null) {
for (int i = 0; i < list.size(); i++) {
var item = list.getJsonObject(i);
if (type.equals(item.getString("type"))) {
exists = true;
break;
}
}
}
if (exists) {
promise.complete(JsonResult.error("解析器类型 " + type + " 已存在,请使用其他类型标识").toJsonObject());
return;
}
// 保存到数据库
JsonObject parser = new JsonObject();
parser.put("name", name);
parser.put("type", type);
parser.put("displayName", displayName);
parser.put("description", description);
parser.put("author", author);
parser.put("version", version);
parser.put("matchPattern", matchPattern);
parser.put("jsCode", jsCode);
parser.put("ip", getClientIp(ctx.request()));
parser.put("enabled", true);
dbService.savePlaygroundParser(parser).onSuccess(result -> {
promise.complete(result);
}).onFailure(e -> {
log.error("保存解析器失败", e);
promise.complete(JsonResult.error("保存失败: " + e.getMessage()).toJsonObject());
});
}).onFailure(e -> {
log.error("获取解析器列表失败", e);
promise.complete(JsonResult.error("检查解析器失败: " + e.getMessage()).toJsonObject());
});
}).onFailure(e -> {
log.error("获取解析器数量失败", e);
promise.complete(JsonResult.error("检查解析器数量失败: " + e.getMessage()).toJsonObject());
});
} catch (Exception e) {
log.error("解析脚本元数据失败", e);
promise.complete(JsonResult.error("解析脚本元数据失败: " + e.getMessage()).toJsonObject());
}
} catch (Exception e) {
log.error("解析请求参数失败", e);
promise.complete(JsonResult.error("解析请求参数失败: " + e.getMessage()).toJsonObject());
}
return promise.future();
}
/**
* 更新解析器
*/
@RouteMapping(value = "/parsers/:id", method = RouteMethod.PUT)
public Future<JsonObject> updateParser(RoutingContext ctx, Long id) {
Promise<JsonObject> promise = Promise.promise();
try {
JsonObject body = ctx.body().asJsonObject();
String jsCode = body.getString("jsCode");
if (StringUtils.isBlank(jsCode)) {
promise.complete(JsonResult.error("JavaScript代码不能为空").toJsonObject());
return promise.future();
}
// 解析元数据
try {
var config = JsScriptMetadataParser.parseScript(jsCode);
String displayName = config.getDisplayName();
String name = config.getMetadata().get("name");
String description = config.getMetadata().get("description");
String author = config.getMetadata().get("author");
String version = config.getMetadata().get("version");
String matchPattern = config.getMatchPattern() != null ? config.getMatchPattern().pattern() : null;
JsonObject parser = new JsonObject();
parser.put("name", name);
parser.put("displayName", displayName);
parser.put("description", description);
parser.put("author", author);
parser.put("version", version);
parser.put("matchPattern", matchPattern);
parser.put("jsCode", jsCode);
parser.put("enabled", body.getBoolean("enabled", true));
dbService.updatePlaygroundParser(id, parser).onSuccess(result -> {
promise.complete(result);
}).onFailure(e -> {
log.error("更新解析器失败", e);
promise.complete(JsonResult.error("更新失败: " + e.getMessage()).toJsonObject());
});
} catch (Exception e) {
log.error("解析脚本元数据失败", e);
promise.complete(JsonResult.error("解析脚本元数据失败: " + e.getMessage()).toJsonObject());
}
} catch (Exception e) {
log.error("解析请求参数失败", e);
promise.complete(JsonResult.error("解析请求参数失败: " + e.getMessage()).toJsonObject());
}
return promise.future();
}
/**
* 删除解析器
*/
@RouteMapping(value = "/parsers/:id", method = RouteMethod.DELETE)
public Future<JsonObject> deleteParser(Long id) {
return dbService.deletePlaygroundParser(id);
}
/**
* 根据ID获取解析器
*/
@RouteMapping(value = "/parsers/:id", method = RouteMethod.GET)
public Future<JsonObject> getParserById(Long id) {
return dbService.getPlaygroundParserById(id);
}
/**
* 获取客户端IP
*/
private String getClientIp(HttpServerRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.remoteAddress().host();
}
return ip;
}
/**
* 获取异常堆栈信息
*/
private String getStackTrace(Throwable throwable) {
if (throwable == null) {
return "";
}
java.io.StringWriter sw = new java.io.StringWriter();
java.io.PrintWriter pw = new java.io.PrintWriter(sw);
throwable.printStackTrace(pw);
return sw.toString();
}
}

View File

@@ -0,0 +1,62 @@
package cn.qaiu.lz.web.model;
import cn.qaiu.db.ddl.Constraint;
import cn.qaiu.db.ddl.Length;
import cn.qaiu.db.ddl.Table;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
/**
* 演练场解析器实体
* 用于保存用户创建的临时JS解析器
*/
@Data
@Table("playground_parser")
public class PlaygroundParser {
private static final long serialVersionUID = 1L;
@Constraint(autoIncrement = true, notNull = true)
private Long id;
@Length(varcharSize = 64)
@Constraint(notNull = true)
private String name; // 解析器名称
@Length(varcharSize = 64)
@Constraint(notNull = true, uniqueKey = "uk_type")
private String type; // 解析器类型标识(唯一)
@Length(varcharSize = 128)
private String displayName; // 显示名称
@Length(varcharSize = 512)
private String description; // 描述
@Length(varcharSize = 64)
private String author; // 作者
@Length(varcharSize = 32)
private String version; // 版本号
@Length(varcharSize = 512)
private String matchPattern; // URL匹配正则
@Length(varcharSize = 65535)
@Constraint(notNull = true)
private String jsCode; // JavaScript代码
@Length(varcharSize = 64)
private String ip; // 创建者IP
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime = new Date(); // 创建时间
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime; // 更新时间
private Boolean enabled = true; // 是否启用
}

View File

@@ -0,0 +1,76 @@
package cn.qaiu.lz.web.model;
import lombok.Builder;
import lombok.Data;
import java.util.List;
/**
* 演练场测试响应模型
*
* @author <a href="https://qaiu.top">QAIU</a>
*/
@Data
@Builder
public class PlaygroundTestResp {
/**
* 是否执行成功
*/
private boolean success;
/**
* 执行结果(根据方法类型返回不同格式)
* - parse: String (下载链接)
* - parseFileList: List<FileInfo>
* - parseById: String (下载链接)
*/
private Object result;
/**
* 执行日志列表
*/
private List<LogEntry> logs;
/**
* 错误信息
*/
private String error;
/**
* 错误堆栈
*/
private String stackTrace;
/**
* 执行时间(毫秒)
*/
private long executionTime;
/**
* 日志条目
*/
@Data
@Builder
public static class LogEntry {
/**
* 日志级别DEBUG, INFO, WARN, ERROR
*/
private String level;
/**
* 日志消息
*/
private String message;
/**
* 日志时间戳
*/
private long timestamp;
/**
* 日志来源JSJavaScript日志或 JAVAJava日志
*/
private String source;
}
}

View File

@@ -20,4 +20,34 @@ public interface DbService extends BaseAsyncService {
Future<StatisticsInfo> getStatisticsInfo();
/**
* 获取演练场解析器列表
*/
Future<JsonObject> getPlaygroundParserList();
/**
* 保存演练场解析器
*/
Future<JsonObject> savePlaygroundParser(JsonObject parser);
/**
* 更新演练场解析器
*/
Future<JsonObject> updatePlaygroundParser(Long id, JsonObject parser);
/**
* 删除演练场解析器
*/
Future<JsonObject> deletePlaygroundParser(Long id);
/**
* 获取演练场解析器数量
*/
Future<Integer> getPlaygroundParserCount();
/**
* 根据ID获取演练场解析器
*/
Future<JsonObject> getPlaygroundParserById(Long id);
}

View File

@@ -10,10 +10,14 @@ import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.json.JsonObject;
import io.vertx.jdbcclient.JDBCPool;
import io.vertx.sqlclient.Row;
import io.vertx.sqlclient.Tuple;
import io.vertx.sqlclient.templates.SqlTemplate;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* lz-web
@@ -66,4 +70,199 @@ public class DbServiceImpl implements DbService {
});
return promise.future();
}
@Override
public Future<JsonObject> getPlaygroundParserList() {
JDBCPool client = JDBCPoolInit.instance().getPool();
Promise<JsonObject> promise = Promise.promise();
String sql = "SELECT * FROM playground_parser ORDER BY create_time DESC";
client.query(sql).execute().onSuccess(rows -> {
List<JsonObject> list = new ArrayList<>();
for (Row row : rows) {
JsonObject parser = new JsonObject();
parser.put("id", row.getLong("id"));
parser.put("name", row.getString("name"));
parser.put("type", row.getString("type"));
parser.put("displayName", row.getString("display_name"));
parser.put("description", row.getString("description"));
parser.put("author", row.getString("author"));
parser.put("version", row.getString("version"));
parser.put("matchPattern", row.getString("match_pattern"));
parser.put("jsCode", row.getString("js_code"));
parser.put("ip", row.getString("ip"));
// 将LocalDateTime转换为字符串格式避免序列化为数组
var createTime = row.getLocalDateTime("create_time");
if (createTime != null) {
parser.put("createTime", createTime.toString().replace("T", " "));
}
var updateTime = row.getLocalDateTime("update_time");
if (updateTime != null) {
parser.put("updateTime", updateTime.toString().replace("T", " "));
}
parser.put("enabled", row.getBoolean("enabled"));
list.add(parser);
}
promise.complete(JsonResult.data(list).toJsonObject());
}).onFailure(e -> {
log.error("getPlaygroundParserList failed", e);
promise.fail(e);
});
return promise.future();
}
@Override
public Future<JsonObject> savePlaygroundParser(JsonObject parser) {
JDBCPool client = JDBCPoolInit.instance().getPool();
Promise<JsonObject> promise = Promise.promise();
String sql = """
INSERT INTO playground_parser
(name, type, display_name, description, author, version, match_pattern, js_code, ip, create_time, enabled)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?)
""";
client.preparedQuery(sql)
.execute(Tuple.of(
parser.getString("name"),
parser.getString("type"),
parser.getString("displayName"),
parser.getString("description"),
parser.getString("author"),
parser.getString("version"),
parser.getString("matchPattern"),
parser.getString("jsCode"),
parser.getString("ip"),
parser.getBoolean("enabled", true)
))
.onSuccess(res -> {
promise.complete(JsonResult.success("保存成功").toJsonObject());
})
.onFailure(e -> {
log.error("savePlaygroundParser failed", e);
promise.fail(e);
});
return promise.future();
}
@Override
public Future<JsonObject> updatePlaygroundParser(Long id, JsonObject parser) {
JDBCPool client = JDBCPoolInit.instance().getPool();
Promise<JsonObject> promise = Promise.promise();
String sql = """
UPDATE playground_parser
SET name = ?, display_name = ?, description = ?, author = ?,
version = ?, match_pattern = ?, js_code = ?, update_time = NOW(), enabled = ?
WHERE id = ?
""";
client.preparedQuery(sql)
.execute(Tuple.of(
parser.getString("name"),
parser.getString("displayName"),
parser.getString("description"),
parser.getString("author"),
parser.getString("version"),
parser.getString("matchPattern"),
parser.getString("jsCode"),
parser.getBoolean("enabled", true),
id
))
.onSuccess(res -> {
promise.complete(JsonResult.success("更新成功").toJsonObject());
})
.onFailure(e -> {
log.error("updatePlaygroundParser failed", e);
promise.fail(e);
});
return promise.future();
}
@Override
public Future<JsonObject> deletePlaygroundParser(Long id) {
JDBCPool client = JDBCPoolInit.instance().getPool();
Promise<JsonObject> promise = Promise.promise();
String sql = "DELETE FROM playground_parser WHERE id = ?";
client.preparedQuery(sql)
.execute(Tuple.of(id))
.onSuccess(res -> {
promise.complete(JsonResult.success("删除成功").toJsonObject());
})
.onFailure(e -> {
log.error("deletePlaygroundParser failed", e);
promise.fail(e);
});
return promise.future();
}
@Override
public Future<Integer> getPlaygroundParserCount() {
JDBCPool client = JDBCPoolInit.instance().getPool();
Promise<Integer> promise = Promise.promise();
String sql = "SELECT COUNT(*) as count FROM playground_parser";
client.query(sql).execute().onSuccess(rows -> {
Integer count = rows.iterator().next().getInteger("count");
promise.complete(count);
}).onFailure(e -> {
log.error("getPlaygroundParserCount failed", e);
promise.fail(e);
});
return promise.future();
}
@Override
public Future<JsonObject> getPlaygroundParserById(Long id) {
JDBCPool client = JDBCPoolInit.instance().getPool();
Promise<JsonObject> promise = Promise.promise();
String sql = "SELECT * FROM playground_parser WHERE id = ?";
client.preparedQuery(sql)
.execute(Tuple.of(id))
.onSuccess(rows -> {
if (rows.size() > 0) {
Row row = rows.iterator().next();
JsonObject parser = new JsonObject();
parser.put("id", row.getLong("id"));
parser.put("name", row.getString("name"));
parser.put("type", row.getString("type"));
parser.put("displayName", row.getString("display_name"));
parser.put("description", row.getString("description"));
parser.put("author", row.getString("author"));
parser.put("version", row.getString("version"));
parser.put("matchPattern", row.getString("match_pattern"));
parser.put("jsCode", row.getString("js_code"));
parser.put("ip", row.getString("ip"));
// 将LocalDateTime转换为字符串格式避免序列化为数组
var createTime = row.getLocalDateTime("create_time");
if (createTime != null) {
parser.put("createTime", createTime.toString().replace("T", " "));
}
var updateTime = row.getLocalDateTime("update_time");
if (updateTime != null) {
parser.put("updateTime", updateTime.toString().replace("T", " "));
}
parser.put("enabled", row.getBoolean("enabled"));
promise.complete(JsonResult.data(parser).toJsonObject());
} else {
promise.fail("解析器不存在");
}
})
.onFailure(e -> {
log.error("getPlaygroundParserById failed", e);
promise.fail(e);
});
return promise.future();
}
}

View File

@@ -0,0 +1,115 @@
### Playground 安全测试用例集合
### 用于验证JavaScript执行环境的安全性
### 测试1: 系统命令执行
POST http://localhost:9000/v2/playground/test
Content-Type: application/json
{
"jsCode": "// ==UserScript==\n// @name 危险测试-系统命令执行\n// @type security_test\n// @match https://test.com/*\n// ==/UserScript==\n\nfunction parse(shareLinkInfo, http, logger) {\n logger.info('尝试执行系统命令...');\n \n try {\n // 尝试1: 直接访问Runtime类执行命令\n var Runtime = Java.type('java.lang.Runtime');\n var runtime = Runtime.getRuntime();\n var process = runtime.exec('whoami');\n var reader = new java.io.BufferedReader(new java.io.InputStreamReader(process.getInputStream()));\n var output = reader.readLine();\n logger.error('【安全漏洞】成功执行系统命令: ' + output);\n return '危险: 系统命令执行成功 - ' + output;\n } catch (e) {\n logger.info('Runtime.exec失败: ' + e.message);\n }\n \n try {\n // 尝试2: 使用ProcessBuilder\n var ProcessBuilder = Java.type('java.lang.ProcessBuilder');\n var pb = new ProcessBuilder(['ls', '-la']);\n var process = pb.start();\n logger.error('【安全漏洞】ProcessBuilder执行成功');\n return '危险: ProcessBuilder执行成功';\n } catch (e) {\n logger.info('ProcessBuilder失败: ' + e.message);\n }\n \n return '✓ 安全: 无法执行系统命令';\n}",
"shareUrl": "https://test.com/share/test123",
"pwd": "",
"method": "parse"
}
### 测试2: 文件系统访问
POST http://localhost:9000/v2/playground/test
Content-Type: application/json
{
"jsCode": "// ==UserScript==\n// @name 危险测试-文件系统访问\n// @type security_test\n// @match https://test.com/*\n// ==/UserScript==\n\nfunction parse(shareLinkInfo, http, logger) {\n logger.info('尝试访问文件系统...');\n \n try {\n var Files = Java.type('java.nio.file.Files');\n var Paths = Java.type('java.nio.file.Paths');\n var path = Paths.get('/etc/passwd');\n var content = Files.readAllLines(path);\n logger.error('【安全漏洞】成功读取文件: ' + content.get(0));\n return '危险: 文件读取成功';\n } catch (e) {\n logger.info('文件读取失败: ' + e.message);\n }\n \n try {\n var FileWriter = Java.type('java.io.FileWriter');\n var writer = new FileWriter('/tmp/security_test.txt');\n writer.write('security test');\n writer.close();\n logger.error('【安全漏洞】成功写入文件');\n return '危险: 文件写入成功';\n } catch (e) {\n logger.info('文件写入失败: ' + e.message);\n }\n \n return '✓ 安全: 无法访问文件系统';\n}",
"shareUrl": "https://test.com/share/test123",
"pwd": "",
"method": "parse"
}
### 测试3: 系统属性和环境变量访问
POST http://localhost:9000/v2/playground/test
Content-Type: application/json
{
"jsCode": "// ==UserScript==\n// @name 危险测试-系统属性访问\n// @type security_test\n// @match https://test.com/*\n// ==/UserScript==\n\nfunction parse(shareLinkInfo, http, logger) {\n logger.info('尝试访问系统属性...');\n \n try {\n var System = Java.type('java.lang.System');\n var userHome = System.getProperty('user.home');\n var userName = System.getProperty('user.name');\n var osName = System.getProperty('os.name');\n logger.error('【安全漏洞】系统属性 - HOME: ' + userHome + ', USER: ' + userName + ', OS: ' + osName);\n return '危险: 系统属性访问成功';\n } catch (e) {\n logger.info('系统属性访问失败: ' + e.message);\n }\n \n try {\n var System = Java.type('java.lang.System');\n var env = System.getenv();\n var path = env.get('PATH');\n logger.error('【安全漏洞】环境变量 PATH: ' + path);\n return '危险: 环境变量访问成功';\n } catch (e) {\n logger.info('环境变量访问失败: ' + e.message);\n }\n \n return '✓ 安全: 无法访问系统属性';\n}",
"shareUrl": "https://test.com/share/test123",
"pwd": "",
"method": "parse"
}
### 测试4: 反射攻击
POST http://localhost:9000/v2/playground/test
Content-Type: application/json
{
"jsCode": "// ==UserScript==\n// @name 危险测试-反射攻击\n// @type security_test\n// @match https://test.com/*\n// ==/UserScript==\n\nfunction parse(shareLinkInfo, http, logger) {\n logger.info('尝试使用反射...');\n \n try {\n var Class = Java.type('java.lang.Class');\n var systemClass = Class.forName('java.lang.System');\n var methods = systemClass.getDeclaredMethods();\n logger.error('【安全漏洞】反射访问成功System类有 ' + methods.length + ' 个方法');\n return '危险: 反射访问成功';\n } catch (e) {\n logger.info('Class.forName失败: ' + e.message);\n }\n \n try {\n var Thread = Java.type('java.lang.Thread');\n var classLoader = Thread.currentThread().getContextClassLoader();\n logger.error('【安全漏洞】获取到ClassLoader: ' + classLoader);\n return '危险: ClassLoader访问成功';\n } catch (e) {\n logger.info('ClassLoader访问失败: ' + e.message);\n }\n \n return '✓ 安全: 无法使用反射';\n}",
"shareUrl": "https://test.com/share/test123",
"pwd": "",
"method": "parse"
}
### 测试5: 网络Socket连接
POST http://localhost:9000/v2/playground/test
Content-Type: application/json
{
"jsCode": "// ==UserScript==\n// @name 危险测试-网络连接\n// @type security_test\n// @match https://test.com/*\n// ==/UserScript==\n\nfunction parse(shareLinkInfo, http, logger) {\n logger.info('尝试创建网络连接...');\n \n try {\n var Socket = Java.type('java.net.Socket');\n var socket = new Socket('127.0.0.1', 9000);\n logger.error('【安全漏洞】Socket连接成功');\n socket.close();\n return '危险: Socket连接成功';\n } catch (e) {\n logger.info('Socket连接失败: ' + e.message);\n }\n \n try {\n var URL = Java.type('java.net.URL');\n var url = new URL('http://localhost:9000');\n var conn = url.openConnection();\n conn.connect();\n logger.error('【安全漏洞】URL连接成功');\n return '危险: URL连接成功';\n } catch (e) {\n logger.info('URL连接失败: ' + e.message);\n }\n \n return '✓ 安全: 无法创建网络连接';\n}",
"shareUrl": "https://test.com/share/test123",
"pwd": "",
"method": "parse"
}
### 测试6: JVM退出攻击
POST http://localhost:9000/v2/playground/test
Content-Type: application/json
{
"jsCode": "// ==UserScript==\n// @name 危险测试-JVM退出\n// @type security_test\n// @match https://test.com/*\n// ==/UserScript==\n\nfunction parse(shareLinkInfo, http, logger) {\n logger.info('尝试退出JVM...');\n \n try {\n var System = Java.type('java.lang.System');\n logger.warn('准备执行 System.exit(1)...');\n System.exit(1);\n return '危险: JVM退出成功';\n } catch (e) {\n logger.info('System.exit失败: ' + e.message);\n }\n \n try {\n var Runtime = Java.type('java.lang.Runtime');\n Runtime.getRuntime().halt(1);\n return '危险: Runtime.halt成功';\n } catch (e) {\n logger.info('Runtime.halt失败: ' + e.message);\n }\n \n return '✓ 安全: 无法退出JVM';\n}",
"shareUrl": "https://test.com/share/test123",
"pwd": "",
"method": "parse"
}
### 测试7: HTTP客户端SSRF攻击
POST http://localhost:9000/v2/playground/test
Content-Type: application/json
{
"jsCode": "// ==UserScript==\n// @name 危险测试-SSRF攻击\n// @type security_test\n// @match https://test.com/*\n// ==/UserScript==\n\nfunction parse(shareLinkInfo, http, logger) {\n logger.info('测试HTTP客户端SSRF风险...');\n \n try {\n // 尝试访问内网地址\n logger.info('尝试访问本地服务...');\n var response = http.get('http://127.0.0.1:9000/v2/health');\n logger.warn('【潜在风险】可以访问内网地址,响应长度: ' + response.length);\n return '⚠ 警告: HTTP客户端可访问内网 (SSRF风险)';\n } catch (e) {\n logger.info('内网访问失败: ' + e.message);\n }\n \n try {\n // 尝试访问云服务元数据API (AWS/阿里云等)\n logger.info('尝试访问云服务元数据API...');\n var response = http.get('http://169.254.169.254/latest/meta-data/');\n logger.error('【严重漏洞】可以访问云服务元数据!');\n return '危险: 可访问云服务元数据';\n } catch (e) {\n logger.info('元数据API访问失败: ' + e.message);\n }\n \n return '✓ 提示: HTTP客户端功能正常';\n}",
"shareUrl": "https://test.com/share/test123",
"pwd": "",
"method": "parse"
}
### 测试8: 尝试访问注入对象的私有方法
POST http://localhost:9000/v2/playground/test
Content-Type: application/json
{
"jsCode": "// ==UserScript==\n// @name 危险测试-对象滥用\n// @type security_test\n// @match https://test.com/*\n// ==/UserScript==\n\nfunction parse(shareLinkInfo, http, logger) {\n logger.info('尝试滥用注入的对象...');\n \n try {\n // 尝试获取http对象的类信息\n var httpClass = http.getClass();\n logger.warn('HTTP客户端类名: ' + httpClass.getName());\n \n var methods = httpClass.getDeclaredMethods();\n logger.warn('HTTP客户端有 ' + methods.length + ' 个方法');\n \n // 列出所有方法\n for (var i = 0; i < Math.min(methods.length, 10); i++) {\n logger.info('方法' + i + ': ' + methods[i].getName());\n }\n \n return '⚠ 警告: 可以通过反射访问注入对象';\n } catch (e) {\n logger.info('对象反射失败: ' + e.message);\n }\n \n try {\n // 尝试获取shareLinkInfo的内部数据\n var infoClass = shareLinkInfo.getClass();\n var fields = infoClass.getDeclaredFields();\n logger.warn('ShareLinkInfo有 ' + fields.length + ' 个字段');\n return '⚠ 警告: 可以访问对象内部结构';\n } catch (e) {\n logger.info('字段访问失败: ' + e.message);\n }\n \n return '✓ 安全: 对象访问受限';\n}",
"shareUrl": "https://test.com/share/test123",
"pwd": "",
"method": "parse"
}
### 测试9: 无限循环DOS攻击
POST http://localhost:9000/v2/playground/test
Content-Type: application/json
{
"jsCode": "// ==UserScript==\n// @name 危险测试-DOS攻击\n// @type security_test\n// @match https://test.com/*\n// ==/UserScript==\n\nfunction parse(shareLinkInfo, http, logger) {\n logger.info('测试DOS防护...');\n \n try {\n logger.warn('准备执行5秒的计算密集操作...');\n var startTime = new Date().getTime();\n var count = 0;\n \n // 执行5秒的计算\n while (new Date().getTime() - startTime < 5000) {\n count++;\n // 每100万次记录一次\n if (count % 1000000 === 0) {\n logger.info('已执行 ' + (count/1000000) + ' 百万次计算');\n }\n }\n \n logger.warn('⚠ 警告: 可执行长时间计算,计数: ' + count);\n return '⚠ 警告: 无超时限制 (DOS风险)';\n } catch (e) {\n logger.info('计算被中断: ' + e.message);\n return '✓ 安全: 存在执行时间限制';\n }\n}",
"shareUrl": "https://test.com/share/test123",
"pwd": "",
"method": "parse"
}
### 测试10: 内存溢出攻击
POST http://localhost:9000/v2/playground/test
Content-Type: application/json
{
"jsCode": "// ==UserScript==\n// @name 危险测试-内存攻击\n// @type security_test\n// @match https://test.com/*\n// ==/UserScript==\n\nfunction parse(shareLinkInfo, http, logger) {\n logger.info('测试内存限制...');\n \n try {\n logger.warn('准备创建大量字符串对象...');\n var arrays = [];\n \n // 尝试创建100个大数组\n for (var i = 0; i < 100; i++) {\n arrays.push(new Array(1000000).fill('x'.repeat(100)));\n if (i % 10 === 0) {\n logger.info('已创建 ' + i + ' 个大数组');\n }\n }\n \n logger.error('【潜在风险】成功创建大量对象,可能导致内存问题');\n return '⚠ 警告: 无内存限制';\n } catch (e) {\n logger.info('内存分配失败: ' + e.message);\n return '✓ 安全: 存在内存限制';\n }\n}",
"shareUrl": "https://test.com/share/test123",
"pwd": "",
"method": "parse"
}
###