mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2025-12-16 12:23:03 +00:00
js演练场
This commit is contained in:
370
parser/doc/API_USAGE.md
Normal file
370
parser/doc/API_USAGE.md
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
# 自定义解析器API使用指南
|
||||||
|
|
||||||
|
## 📡 API端点
|
||||||
|
|
||||||
|
当你在演练场发布自定义解析器后,可以通过以下API端点使用:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1️⃣ 302重定向(直接下载)
|
||||||
|
|
||||||
|
**端点**: `/parser`
|
||||||
|
|
||||||
|
**方法**: `GET`
|
||||||
|
|
||||||
|
**描述**: 返回302重定向到实际下载地址,适合浏览器直接访问下载
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| url | string | ✅ 是 | 分享链接(需URL编码) |
|
||||||
|
| pwd | string | ❌ 否 | 分享密码 |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 基本请求
|
||||||
|
GET http://localhost:6400/parser?url=https://lanzoui.com/i7Aq12ab3cd
|
||||||
|
|
||||||
|
# 带密码
|
||||||
|
GET http://localhost:6400/parser?url=https://lanzoui.com/i7Aq12ab3cd&pwd=1234
|
||||||
|
|
||||||
|
# curl命令
|
||||||
|
curl -L "http://localhost:6400/parser?url=https://lanzoui.com/i7Aq12ab3cd"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应
|
||||||
|
|
||||||
|
```http
|
||||||
|
HTTP/1.1 302 Found
|
||||||
|
Location: https://download-server.com/file/xxx
|
||||||
|
```
|
||||||
|
|
||||||
|
浏览器会自动跳转到下载地址。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2️⃣ JSON响应(获取解析结果)
|
||||||
|
|
||||||
|
**端点**: `/json/parser`
|
||||||
|
|
||||||
|
**方法**: `GET`
|
||||||
|
|
||||||
|
**描述**: 返回JSON格式的解析结果,包含下载链接等详细信息
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| url | string | ✅ 是 | 分享链接(需URL编码) |
|
||||||
|
| pwd | string | ❌ 否 | 分享密码 |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 基本请求
|
||||||
|
GET http://localhost:6400/json/parser?url=https://lanzoui.com/i7Aq12ab3cd
|
||||||
|
|
||||||
|
# 带密码
|
||||||
|
GET http://localhost:6400/json/parser?url=https://lanzoui.com/i7Aq12ab3cd&pwd=1234
|
||||||
|
|
||||||
|
# curl命令
|
||||||
|
curl "http://localhost:6400/json/parser?url=https://lanzoui.com/i7Aq12ab3cd"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应格式
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"url": "https://download-server.com/file/xxx",
|
||||||
|
"fileName": "example.zip",
|
||||||
|
"fileSize": "10MB",
|
||||||
|
"parseTime": 1234
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 使用场景
|
||||||
|
|
||||||
|
### 场景1: 浏览器直接下载
|
||||||
|
|
||||||
|
用户点击链接直接下载:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<a href="http://localhost:6400/parser?url=https://lanzoui.com/i7Aq12ab3cd">
|
||||||
|
点击下载
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景2: 获取下载信息
|
||||||
|
|
||||||
|
JavaScript获取下载链接:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
fetch('http://localhost:6400/json/parser?url=https://lanzoui.com/i7Aq12ab3cd')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
console.log('下载链接:', data.data.url);
|
||||||
|
console.log('文件名:', data.data.fileName);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景3: 命令行下载
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 方式1: 直接下载
|
||||||
|
curl -L -O "http://localhost:6400/parser?url=https://lanzoui.com/i7Aq12ab3cd"
|
||||||
|
|
||||||
|
# 方式2: 先获取链接再下载
|
||||||
|
DOWNLOAD_URL=$(curl -s "http://localhost:6400/json/parser?url=https://lanzoui.com/i7Aq12ab3cd" | jq -r '.data.url')
|
||||||
|
curl -L -O "$DOWNLOAD_URL"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景4: Python脚本
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# 获取解析结果
|
||||||
|
response = requests.get(
|
||||||
|
'http://localhost:6400/json/parser',
|
||||||
|
params={
|
||||||
|
'url': 'https://lanzoui.com/i7Aq12ab3cd',
|
||||||
|
'pwd': '1234'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
if result['code'] == 200:
|
||||||
|
download_url = result['data']['url']
|
||||||
|
print(f'下载链接: {download_url}')
|
||||||
|
|
||||||
|
# 下载文件
|
||||||
|
file_response = requests.get(download_url)
|
||||||
|
with open('download.file', 'wb') as f:
|
||||||
|
f.write(file_response.content)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 解析器匹配规则
|
||||||
|
|
||||||
|
系统会根据分享链接的URL自动选择合适的解析器:
|
||||||
|
|
||||||
|
1. **优先匹配自定义解析器**
|
||||||
|
- 检查演练场发布的解析器
|
||||||
|
- 使用 `@match` 正则表达式匹配
|
||||||
|
|
||||||
|
2. **内置解析器**
|
||||||
|
- 如果没有匹配的自定义解析器
|
||||||
|
- 使用系统内置的解析器
|
||||||
|
|
||||||
|
### 示例
|
||||||
|
|
||||||
|
假设你发布了蓝奏云解析器:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// @match https?://lanzou[a-z]{1,2}\.com/(?<KEY>[a-zA-Z0-9]+)
|
||||||
|
```
|
||||||
|
|
||||||
|
当请求以下链接时会使用你的解析器:
|
||||||
|
- ✅ `https://lanzoui.com/i7Aq12ab3cd`
|
||||||
|
- ✅ `https://lanzoux.com/i7Aq12ab3cd`
|
||||||
|
- ✅ `http://lanzouy.com/i7Aq12ab3cd`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ 高级用法
|
||||||
|
|
||||||
|
### 1. 指定解析器类型
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 通过type参数指定解析器
|
||||||
|
GET http://localhost:6400/parser?url=https://example.com/s/abc&type=custom_parser
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 获取文件列表
|
||||||
|
|
||||||
|
对于支持文件夹的网盘:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 获取文件列表
|
||||||
|
GET http://localhost:6400/json/parser/list?url=https://example.com/s/abc
|
||||||
|
|
||||||
|
# 按文件ID获取下载链接
|
||||||
|
GET http://localhost:6400/json/parser/file?url=https://example.com/s/abc&fileId=123
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 批量解析
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const urls = [
|
||||||
|
'https://lanzoui.com/i7Aq12ab3cd',
|
||||||
|
'https://lanzoui.com/i8Bq34ef5gh'
|
||||||
|
];
|
||||||
|
|
||||||
|
const results = await Promise.all(
|
||||||
|
urls.map(url =>
|
||||||
|
fetch(`http://localhost:6400/json/parser?url=${encodeURIComponent(url)}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 安全注意事项
|
||||||
|
|
||||||
|
### 1. SSRF防护
|
||||||
|
|
||||||
|
系统已实施SSRF防护,以下请求会被拦截:
|
||||||
|
|
||||||
|
❌ 内网地址:
|
||||||
|
```bash
|
||||||
|
# 这些会被拦截
|
||||||
|
http://127.0.0.1:8080/admin
|
||||||
|
http://192.168.1.1/config
|
||||||
|
http://169.254.169.254/latest/meta-data/
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ 公网地址:
|
||||||
|
```bash
|
||||||
|
# 这些是允许的
|
||||||
|
https://lanzoui.com/xxx
|
||||||
|
https://pan.baidu.com/s/xxx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 速率限制
|
||||||
|
|
||||||
|
建议添加速率限制,避免滥用:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 使用节流
|
||||||
|
import { throttle } from 'lodash';
|
||||||
|
|
||||||
|
const parseUrl = throttle((url) => {
|
||||||
|
return fetch(`/json/parser?url=${encodeURIComponent(url)}`);
|
||||||
|
}, 1000); // 每秒最多1次请求
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 错误处理
|
||||||
|
|
||||||
|
### 常见错误码
|
||||||
|
|
||||||
|
| 错误码 | 说明 | 解决方法 |
|
||||||
|
|--------|------|----------|
|
||||||
|
| 400 | 参数错误 | 检查url参数是否正确编码 |
|
||||||
|
| 404 | 未找到解析器 | 确认链接格式是否匹配解析器规则 |
|
||||||
|
| 500 | 解析失败 | 查看日志,可能是解析器代码错误 |
|
||||||
|
| 503 | 服务不可用 | 稍后重试 |
|
||||||
|
|
||||||
|
### 错误响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 500,
|
||||||
|
"msg": "解析失败: 无法提取下载参数",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误处理示例
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
fetch('/json/parser?url=' + encodeURIComponent(shareUrl))
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.code === 200) {
|
||||||
|
console.log('成功:', data.data.url);
|
||||||
|
} else {
|
||||||
|
console.error('失败:', data.msg);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('请求失败:', error.message);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 最佳实践
|
||||||
|
|
||||||
|
### 1. URL编码
|
||||||
|
|
||||||
|
始终对分享链接进行URL编码:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ✅ 正确
|
||||||
|
const encodedUrl = encodeURIComponent('https://lanzoui.com/i7Aq12ab3cd');
|
||||||
|
fetch(`/json/parser?url=${encodedUrl}`);
|
||||||
|
|
||||||
|
// ❌ 错误
|
||||||
|
fetch('/json/parser?url=https://lanzoui.com/i7Aq12ab3cd');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 错误重试
|
||||||
|
|
||||||
|
实现指数退避重试:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function parseWithRetry(url, maxRetries = 3) {
|
||||||
|
for (let i = 0; i < maxRetries; i++) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/json/parser?url=${encodeURIComponent(url)}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.code === 200) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是服务器错误,重试
|
||||||
|
if (data.code >= 500 && i < maxRetries - 1) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(data.msg);
|
||||||
|
} catch (error) {
|
||||||
|
if (i === maxRetries - 1) throw error;
|
||||||
|
await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 超时处理
|
||||||
|
|
||||||
|
设置请求超时:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeout = setTimeout(() => controller.abort(), 30000); // 30秒超时
|
||||||
|
|
||||||
|
fetch('/json/parser?url=' + encodeURIComponent(url), {
|
||||||
|
signal: controller.signal
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.finally(() => clearTimeout(timeout));
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 更多资源
|
||||||
|
|
||||||
|
- **演练场文档**: `/parser/doc/JAVASCRIPT_PARSER_GUIDE.md`
|
||||||
|
- **自定义解析器**: `/parser/doc/CUSTOM_PARSER_GUIDE.md`
|
||||||
|
- **安全指南**: `/parser/doc/security/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**最后更新**: 2025-11-29
|
||||||
|
**版本**: v1.0
|
||||||
|
|
||||||
464
parser/doc/SECURITY_TESTING_GUIDE.md
Normal file
464
parser/doc/SECURITY_TESTING_GUIDE.md
Normal file
@@ -0,0 +1,464 @@
|
|||||||
|
# JavaScript执行器安全测试指南
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本文档提供了一套完整的安全测试用例,用于验证JavaScript演练场执行器的安全性。这些测试旨在检测潜在的安全漏洞,包括但不限于:
|
||||||
|
|
||||||
|
- 系统命令执行
|
||||||
|
- 文件系统访问
|
||||||
|
- 反射攻击
|
||||||
|
- 网络攻击 (SSRF)
|
||||||
|
- JVM退出
|
||||||
|
- DOS攻击
|
||||||
|
- 内存溢出
|
||||||
|
|
||||||
|
## ⚠️ 重要警告
|
||||||
|
|
||||||
|
**这些测试用例包含危险代码,仅用于安全测试目的!**
|
||||||
|
|
||||||
|
- ❌ 不要在生产环境执行这些测试
|
||||||
|
- ❌ 不要将这些代码暴露给未授权用户
|
||||||
|
- ✅ 仅在隔离的测试环境中执行
|
||||||
|
- ✅ 执行前确保有完整的系统备份
|
||||||
|
|
||||||
|
## 测试方式
|
||||||
|
|
||||||
|
### 方式1: JUnit单元测试
|
||||||
|
|
||||||
|
使用提供的JUnit测试类 `SecurityTest.java`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd parser
|
||||||
|
mvn test -Dtest=SecurityTest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式2: HTTP接口测试
|
||||||
|
|
||||||
|
使用提供的HTTP测试文件 `playground-security-tests.http`:
|
||||||
|
|
||||||
|
1. 启动应用服务器
|
||||||
|
2. 在IDE中打开 `web-service/src/test/resources/playground-security-tests.http`
|
||||||
|
3. 逐个执行测试用例
|
||||||
|
|
||||||
|
或使用curl命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:9000/v2/playground/test \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d @test-case.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试用例说明
|
||||||
|
|
||||||
|
### 1. 系统命令执行测试 🔴 高危
|
||||||
|
|
||||||
|
**测试目标**: 验证是否能通过Java的Runtime或ProcessBuilder执行系统命令
|
||||||
|
|
||||||
|
**危险级别**: ⚠️⚠️⚠️ 极高
|
||||||
|
|
||||||
|
**测试内容**:
|
||||||
|
- 尝试使用 `Runtime.getRuntime().exec()` 执行shell命令
|
||||||
|
- 尝试使用 `ProcessBuilder` 执行系统命令
|
||||||
|
- 尝试读取命令执行结果
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- ✅ **安全**: 无法访问 `Java.type()` 或相关类
|
||||||
|
- ❌ **危险**: 成功执行系统命令
|
||||||
|
|
||||||
|
**示例攻击**:
|
||||||
|
```javascript
|
||||||
|
var Runtime = Java.type('java.lang.Runtime');
|
||||||
|
var process = Runtime.getRuntime().exec('whoami');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 文件系统访问测试 🔴 高危
|
||||||
|
|
||||||
|
**测试目标**: 验证是否能读写本地文件系统
|
||||||
|
|
||||||
|
**危险级别**: ⚠️⚠️⚠️ 极高
|
||||||
|
|
||||||
|
**测试内容**:
|
||||||
|
- 尝试读取敏感文件 (`/etc/passwd`, 数据库文件等)
|
||||||
|
- 尝试写入文件到系统目录
|
||||||
|
- 尝试删除文件
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- ✅ **安全**: 无法访问文件系统API
|
||||||
|
- ❌ **危险**: 成功读写文件
|
||||||
|
|
||||||
|
**示例攻击**:
|
||||||
|
```javascript
|
||||||
|
var Files = Java.type('java.nio.file.Files');
|
||||||
|
var content = Files.readAllLines(Paths.get('/etc/passwd'));
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 系统属性访问测试 🟡 中危
|
||||||
|
|
||||||
|
**测试目标**: 验证是否能访问系统属性和环境变量
|
||||||
|
|
||||||
|
**危险级别**: ⚠️⚠️ 高
|
||||||
|
|
||||||
|
**测试内容**:
|
||||||
|
- 读取系统属性 (`user.home`, `user.name`, `java.version`)
|
||||||
|
- 读取环境变量 (`PATH`, `JAVA_HOME`, API密钥等)
|
||||||
|
- 修改系统属性
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- ✅ **安全**: 无法访问System类
|
||||||
|
- ❌ **危险**: 成功获取敏感信息
|
||||||
|
|
||||||
|
**潜在风险**: 可能泄露系统配置、用户信息、API密钥等敏感数据
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 反射攻击测试 🔴 高危
|
||||||
|
|
||||||
|
**测试目标**: 验证是否能通过反射绕过访问控制
|
||||||
|
|
||||||
|
**危险级别**: ⚠️⚠️⚠️ 极高
|
||||||
|
|
||||||
|
**测试内容**:
|
||||||
|
- 使用 `Class.forName()` 加载任意类
|
||||||
|
- 通过反射调用私有方法
|
||||||
|
- 修改final字段
|
||||||
|
- 获取ClassLoader
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- ✅ **安全**: 无法使用反射API
|
||||||
|
- ❌ **危险**: 成功绕过访问控制
|
||||||
|
|
||||||
|
**示例攻击**:
|
||||||
|
```javascript
|
||||||
|
var Class = Java.type('java.lang.Class');
|
||||||
|
var systemClass = Class.forName('java.lang.System');
|
||||||
|
var methods = systemClass.getDeclaredMethods();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. 网络Socket攻击测试 🔴 高危
|
||||||
|
|
||||||
|
**测试目标**: 验证是否能创建任意网络连接
|
||||||
|
|
||||||
|
**危险级别**: ⚠️⚠️⚠️ 极高
|
||||||
|
|
||||||
|
**测试内容**:
|
||||||
|
- 创建Socket连接到任意主机
|
||||||
|
- 使用URL/URLConnection访问任意地址
|
||||||
|
- 端口扫描
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- ✅ **安全**: 无法创建网络连接
|
||||||
|
- ❌ **危险**: 可以连接任意主机端口
|
||||||
|
|
||||||
|
**潜在风险**: 可用于端口扫描、内网渗透、绕过防火墙
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. JVM退出攻击测试 🔴 高危
|
||||||
|
|
||||||
|
**测试目标**: 验证是否能终止JVM进程
|
||||||
|
|
||||||
|
**危险级别**: ⚠️⚠️⚠️ 极高
|
||||||
|
|
||||||
|
**测试内容**:
|
||||||
|
- 调用 `System.exit()`
|
||||||
|
- 调用 `Runtime.halt()`
|
||||||
|
- 触发致命错误
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- ✅ **安全**: 无法退出JVM
|
||||||
|
- ❌ **危险**: 成功终止应用
|
||||||
|
|
||||||
|
**影响**: 导致整个应用崩溃,拒绝服务
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. HTTP客户端SSRF测试 🟡 中危
|
||||||
|
|
||||||
|
**测试目标**: 验证注入的httpClient是否可被滥用
|
||||||
|
|
||||||
|
**危险级别**: ⚠️⚠️ 高
|
||||||
|
|
||||||
|
**测试内容**:
|
||||||
|
- 访问内网地址 (127.0.0.1, 192.168.x.x, 10.x.x.x)
|
||||||
|
- 访问云服务元数据API (169.254.169.254)
|
||||||
|
- 访问本地服务端口
|
||||||
|
- 访问管理后台
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- ✅ **最佳**: HTTP客户端有白名单限制
|
||||||
|
- ⚠️ **可接受**: 可以访问外网但不能访问内网
|
||||||
|
- ❌ **危险**: 可以访问任意地址包括内网
|
||||||
|
|
||||||
|
**潜在风险**: SSRF攻击、内网信息泄露、云服务凭证窃取
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. 对象滥用测试 🟡 中危
|
||||||
|
|
||||||
|
**测试目标**: 验证注入的Java对象是否可被反射访问
|
||||||
|
|
||||||
|
**危险级别**: ⚠️⚠️ 高
|
||||||
|
|
||||||
|
**测试内容**:
|
||||||
|
- 通过反射访问注入对象的私有字段
|
||||||
|
- 调用对象的非公开方法
|
||||||
|
- 修改对象内部状态
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- ✅ **安全**: 无法通过反射访问对象
|
||||||
|
- ⚠️ **可接受**: 只能访问公开API
|
||||||
|
- ❌ **危险**: 可以访问和修改内部状态
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. DOS攻击测试 🟡 中危
|
||||||
|
|
||||||
|
**测试目标**: 验证是否存在执行时间限制
|
||||||
|
|
||||||
|
**危险级别**: ⚠️⚠️ 高
|
||||||
|
|
||||||
|
**测试内容**:
|
||||||
|
- 无限循环
|
||||||
|
- 长时间计算
|
||||||
|
- 递归调用
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- ✅ **安全**: 有超时机制,自动中断执行
|
||||||
|
- ❌ **危险**: 可以无限执行
|
||||||
|
|
||||||
|
**影响**: 消耗CPU资源,导致服务响应缓慢或拒绝服务
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. 内存溢出测试 🟡 中危
|
||||||
|
|
||||||
|
**测试目标**: 验证是否存在内存使用限制
|
||||||
|
|
||||||
|
**危险级别**: ⚠️⚠️ 高
|
||||||
|
|
||||||
|
**测试内容**:
|
||||||
|
- 创建大量对象
|
||||||
|
- 分配大数组
|
||||||
|
- 递归创建深层对象
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- ✅ **安全**: 有内存限制,防止OOM
|
||||||
|
- ❌ **危险**: 可以无限分配内存
|
||||||
|
|
||||||
|
**影响**: 导致内存溢出,应用崩溃
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 安全建议
|
||||||
|
|
||||||
|
### 当前Nashorn引擎的安全问题
|
||||||
|
|
||||||
|
Nashorn引擎默认允许JavaScript访问所有Java类,这是一个严重的安全隐患。以下是建议的安全措施:
|
||||||
|
|
||||||
|
### 1. 使用ClassFilter限制类访问 🔒 必须
|
||||||
|
|
||||||
|
```java
|
||||||
|
import jdk.nashorn.api.scripting.ClassFilter;
|
||||||
|
import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
|
||||||
|
|
||||||
|
public class SecurityClassFilter implements ClassFilter {
|
||||||
|
@Override
|
||||||
|
public boolean exposeToScripts(String className) {
|
||||||
|
// 黑名单:禁止访问危险类
|
||||||
|
if (className.startsWith("java.lang.Runtime") ||
|
||||||
|
className.startsWith("java.lang.ProcessBuilder") ||
|
||||||
|
className.startsWith("java.io.File") ||
|
||||||
|
className.startsWith("java.nio.file") ||
|
||||||
|
className.startsWith("java.lang.System") ||
|
||||||
|
className.startsWith("java.lang.Class") ||
|
||||||
|
className.startsWith("java.lang.reflect") ||
|
||||||
|
className.startsWith("java.net.Socket") ||
|
||||||
|
className.startsWith("java.net.URL")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 白名单:只允许特定的类
|
||||||
|
// return className.startsWith("允许的包名");
|
||||||
|
|
||||||
|
return false; // 默认拒绝所有
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用ClassFilter创建引擎
|
||||||
|
NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
|
||||||
|
ScriptEngine engine = factory.getScriptEngine(new SecurityClassFilter());
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 设置执行超时 ⏱️ 强烈推荐
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 使用Future + timeout
|
||||||
|
Future<?> future = executor.submit(() -> {
|
||||||
|
engine.eval(jsCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
future.get(30, TimeUnit.SECONDS); // 30秒超时
|
||||||
|
} catch (TimeoutException e) {
|
||||||
|
future.cancel(true);
|
||||||
|
throw new RuntimeException("脚本执行超时");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 限制内存使用 💾 推荐
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 在Worker线程中执行,限制堆大小
|
||||||
|
// 启动参数: -Xmx512m
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 沙箱隔离 🏝️ 强烈推荐
|
||||||
|
|
||||||
|
考虑使用以下方案:
|
||||||
|
|
||||||
|
- **GraalVM JavaScript**: 更安全的JavaScript引擎,支持沙箱
|
||||||
|
- **Docker容器隔离**: 在容器中执行不信任的代码
|
||||||
|
- **Java SecurityManager**: 配置安全策略文件
|
||||||
|
|
||||||
|
### 5. HTTP客户端访问控制 🌐 必须
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 在JsHttpClient中添加URL验证
|
||||||
|
private boolean isAllowedUrl(String url) {
|
||||||
|
// 禁止访问内网地址
|
||||||
|
if (url.matches(".*\\b(127\\.0\\.0\\.1|localhost|192\\.168\\.|10\\.|172\\.(1[6-9]|2[0-9]|3[01])\\.).*")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 禁止访问云服务元数据
|
||||||
|
if (url.contains("169.254.169.254")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 白名单检查
|
||||||
|
// return allowedDomains.contains(getDomain(url));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 输入验证 ✅ 必须
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 验证JavaScript代码
|
||||||
|
private void validateJsCode(String jsCode) {
|
||||||
|
// 检查代码长度
|
||||||
|
if (jsCode.length() > 100000) {
|
||||||
|
throw new IllegalArgumentException("代码过长");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查危险关键词
|
||||||
|
List<String> dangerousKeywords = Arrays.asList(
|
||||||
|
"Java.type",
|
||||||
|
"getClass",
|
||||||
|
"getRuntime",
|
||||||
|
"exec(",
|
||||||
|
"ProcessBuilder",
|
||||||
|
"System.exit",
|
||||||
|
"Runtime.halt"
|
||||||
|
);
|
||||||
|
|
||||||
|
for (String keyword : dangerousKeywords) {
|
||||||
|
if (jsCode.contains(keyword)) {
|
||||||
|
throw new SecurityException("代码包含危险操作: " + keyword);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. 监控和日志 📊 必须
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 记录所有执行的脚本
|
||||||
|
log.info("执行脚本 - 用户: {}, IP: {}, 代码哈希: {}",
|
||||||
|
userId, clientIp, DigestUtils.md5Hex(jsCode));
|
||||||
|
|
||||||
|
// 监控异常行为
|
||||||
|
if (executionTime > 10000) {
|
||||||
|
log.warn("脚本执行时间过长: {}ms", executionTime);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. 迁移到GraalVM 🚀 长期建议
|
||||||
|
|
||||||
|
Nashorn已在JDK 15中废弃,建议迁移到GraalVM JavaScript:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.graalvm.js</groupId>
|
||||||
|
<artifactId>js</artifactId>
|
||||||
|
<version>23.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
```
|
||||||
|
|
||||||
|
GraalVM提供更好的安全性和性能:
|
||||||
|
- 默认沙箱隔离
|
||||||
|
- 无法访问Java类(除非显式允许)
|
||||||
|
- 更好的性能
|
||||||
|
- 活跃维护
|
||||||
|
|
||||||
|
## 测试检查清单
|
||||||
|
|
||||||
|
执行安全测试时,请确认以下检查项:
|
||||||
|
|
||||||
|
- [ ] 测试1: 系统命令执行 - 应该**失败**
|
||||||
|
- [ ] 测试2: 文件系统访问 - 应该**失败**
|
||||||
|
- [ ] 测试3: 系统属性访问 - 应该**失败**
|
||||||
|
- [ ] 测试4: 反射攻击 - 应该**失败**
|
||||||
|
- [ ] 测试5: 网络Socket - 应该**失败**
|
||||||
|
- [ ] 测试6: JVM退出 - 应该**失败**
|
||||||
|
- [ ] 测试7: SSRF攻击 - 应该**部分失败**(禁止内网访问)
|
||||||
|
- [ ] 测试8: 对象滥用 - 应该**部分失败**(只能访问公开API)
|
||||||
|
- [ ] 测试9: DOS攻击 - 应该**超时中断**
|
||||||
|
- [ ] 测试10: 内存溢出 - 应该**抛出OOM或限制**
|
||||||
|
|
||||||
|
## 安全评估标准
|
||||||
|
|
||||||
|
### 🟢 安全 (A级)
|
||||||
|
- 所有高危测试都失败
|
||||||
|
- 有完善的ClassFilter
|
||||||
|
- 有超时和内存限制
|
||||||
|
- HTTP客户端有访问控制
|
||||||
|
|
||||||
|
### 🟡 基本安全 (B级)
|
||||||
|
- 大部分高危测试失败
|
||||||
|
- 无法执行系统命令和文件操作
|
||||||
|
- 有部分访问控制
|
||||||
|
|
||||||
|
### 🟠 存在风险 (C级)
|
||||||
|
- 某些中危测试通过
|
||||||
|
- 缺少超时或内存限制
|
||||||
|
- HTTP客户端无限制
|
||||||
|
|
||||||
|
### 🔴 严重不安全 (D级)
|
||||||
|
- 高危测试通过
|
||||||
|
- 可以执行系统命令
|
||||||
|
- 可以读写文件系统
|
||||||
|
- **不应在生产环境使用**
|
||||||
|
|
||||||
|
## 参考资料
|
||||||
|
|
||||||
|
- [OWASP - Server Side Request Forgery](https://owasp.org/www-community/attacks/Server_Side_Request_Forgery)
|
||||||
|
- [Nashorn Security Guide](https://docs.oracle.com/javase/8/docs/technotes/guides/scripting/nashorn/security.html)
|
||||||
|
- [GraalVM JavaScript Security](https://www.graalvm.org/latest/security-guide/polyglot-sandbox/)
|
||||||
|
- [Java SecurityManager Documentation](https://docs.oracle.com/javase/tutorial/essential/environment/security.html)
|
||||||
|
|
||||||
|
## 联系方式
|
||||||
|
|
||||||
|
如果发现新的安全漏洞,请通过安全渠道报告,不要公开披露。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**免责声明**: 本文档仅用于安全测试和教育目的。任何人使用这些测试用例造成的损害,作者概不负责。
|
||||||
|
|
||||||
174
parser/doc/security/CHANGELOG_SECURITY.md
Normal file
174
parser/doc/security/CHANGELOG_SECURITY.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# 安全修复更新日志
|
||||||
|
|
||||||
|
## [2025-11-29] - 优化SSRF防护策略
|
||||||
|
|
||||||
|
### 🔄 变更内容
|
||||||
|
|
||||||
|
#### 调整SSRF防护为宽松模式
|
||||||
|
- **问题**: 原有SSRF防护过于严格,导致正常外网请求也被拦截
|
||||||
|
- **症状**: `Error: 请求失败: 404` 或其他网络错误
|
||||||
|
- **修复**: 调整验证逻辑,只拦截明确的危险请求
|
||||||
|
|
||||||
|
#### 具体改进
|
||||||
|
|
||||||
|
1. ✅ **允许DNS解析失败的请求**
|
||||||
|
- 之前:DNS解析失败 → 抛出异常
|
||||||
|
- 现在:DNS解析失败 → 允许继续(可能是外网域名)
|
||||||
|
|
||||||
|
2. ✅ **允许格式异常的URL**
|
||||||
|
- 之前:URL解析异常 → 抛出异常
|
||||||
|
- 现在:URL解析异常 → 只记录日志,允许继续
|
||||||
|
|
||||||
|
3. ✅ **优化IP检测逻辑**
|
||||||
|
- 先检查是否为IP地址格式
|
||||||
|
- 对域名才进行DNS解析
|
||||||
|
- 减少不必要的网络请求
|
||||||
|
|
||||||
|
### 🛡️ 保留的安全防护
|
||||||
|
|
||||||
|
以下危险请求仍然会被拦截:
|
||||||
|
|
||||||
|
- ❌ 本地回环:`127.0.0.1`, `localhost`, `::1`
|
||||||
|
- ❌ 内网IP:`192.168.x.x`, `10.x.x.x`, `172.16-31.x.x`
|
||||||
|
- ❌ 云服务元数据:`169.254.169.254`, `metadata.google.internal`
|
||||||
|
- ❌ 解析到内网的域名
|
||||||
|
|
||||||
|
### 📊 影响范围
|
||||||
|
|
||||||
|
**修改文件**:
|
||||||
|
- `parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java`
|
||||||
|
|
||||||
|
**新增文档**:
|
||||||
|
- `parser/SSRF_PROTECTION.md` - SSRF防护策略说明
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [2025-11-28] - 修复JavaScript远程代码执行漏洞
|
||||||
|
|
||||||
|
### 🚨 严重安全漏洞修复
|
||||||
|
|
||||||
|
#### 漏洞描述
|
||||||
|
- **类型**: 远程代码执行 (RCE)
|
||||||
|
- **危险级别**: 🔴 极高
|
||||||
|
- **影响**: JavaScript可以访问所有Java类,执行任意系统命令
|
||||||
|
|
||||||
|
#### 修复措施
|
||||||
|
|
||||||
|
1. ✅ **实现ClassFilter类过滤器**
|
||||||
|
- 文件:`SecurityClassFilter.java`
|
||||||
|
- 功能:拦截JavaScript对危险Java类的访问
|
||||||
|
- 黑名单包括:Runtime, File, System, Class, Socket等
|
||||||
|
|
||||||
|
2. ✅ **禁用Java内置对象**
|
||||||
|
- 禁用:`Java`, `JavaImporter`, `Packages`
|
||||||
|
- 位置:`JsPlaygroundExecutor`, `JsParserExecutor`
|
||||||
|
|
||||||
|
3. ✅ **添加SSRF防护**
|
||||||
|
- 文件:`JsHttpClient.java`
|
||||||
|
- 功能:防止访问内网地址和云服务元数据
|
||||||
|
|
||||||
|
4. ✅ **修复ArrayIndexOutOfBoundsException**
|
||||||
|
- 问题:`getScriptEngine()` 方法参数错误
|
||||||
|
- 修复:使用正确的方法签名 `getScriptEngine(new String[0], null, classFilter)`
|
||||||
|
|
||||||
|
### 📦 新增文件
|
||||||
|
|
||||||
|
**安全组件**:
|
||||||
|
- `parser/src/main/java/cn/qaiu/parser/customjs/SecurityClassFilter.java`
|
||||||
|
|
||||||
|
**测试套件**:
|
||||||
|
- `parser/src/test/java/cn/qaiu/parser/SecurityTest.java` (7个测试用例)
|
||||||
|
- `web-service/src/test/resources/playground-security-tests.http` (10个测试用例)
|
||||||
|
|
||||||
|
**文档**:
|
||||||
|
- `parser/doc/SECURITY_TESTING_GUIDE.md` - 详细安全测试指南
|
||||||
|
- `parser/SECURITY_TEST_README.md` - 快速开始指南
|
||||||
|
- `parser/SECURITY_FIX_SUMMARY.md` - 修复总结
|
||||||
|
- `parser/test-security.sh` - 自动化测试脚本
|
||||||
|
- `SECURITY_URGENT_FIX.md` - 紧急修复通知
|
||||||
|
- `QUICK_TEST.md` - 快速验证指南
|
||||||
|
|
||||||
|
### 🔧 修改文件
|
||||||
|
|
||||||
|
1. `JsPlaygroundExecutor.java`
|
||||||
|
- 使用安全的ScriptEngine
|
||||||
|
- 禁用Java对象访问
|
||||||
|
|
||||||
|
2. `JsParserExecutor.java`
|
||||||
|
- 使用安全的ScriptEngine
|
||||||
|
- 禁用Java对象访问
|
||||||
|
|
||||||
|
3. `JsHttpClient.java`
|
||||||
|
- 添加URL安全验证
|
||||||
|
- 实现SSRF防护
|
||||||
|
|
||||||
|
### 📊 修复效果
|
||||||
|
|
||||||
|
| 测试项目 | 修复前 | 修复后 |
|
||||||
|
|---------|--------|--------|
|
||||||
|
| 系统命令执行 | ❌ 成功 | ✅ 被拦截 |
|
||||||
|
| 文件系统访问 | ❌ 成功 | ✅ 被拦截 |
|
||||||
|
| 系统属性访问 | ❌ 成功 | ✅ 被拦截 |
|
||||||
|
| 反射攻击 | ❌ 成功 | ✅ 被拦截 |
|
||||||
|
| 网络Socket | ❌ 成功 | ✅ 被拦截 |
|
||||||
|
| JVM退出 | ❌ 成功 | ✅ 被拦截 |
|
||||||
|
| SSRF攻击 | ❌ 成功 | ✅ 被拦截 |
|
||||||
|
|
||||||
|
### 📈 安全评级提升
|
||||||
|
|
||||||
|
- **修复前**: 🔴 D级(严重不安全)
|
||||||
|
- **修复后**: 🟢 A级(安全)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 部署建议
|
||||||
|
|
||||||
|
### 立即部署步骤
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 拉取最新代码
|
||||||
|
git pull
|
||||||
|
|
||||||
|
# 2. 重新编译
|
||||||
|
mvn clean install
|
||||||
|
|
||||||
|
# 3. 重启服务
|
||||||
|
./bin/stop.sh
|
||||||
|
./bin/run.sh
|
||||||
|
|
||||||
|
# 4. 验证修复
|
||||||
|
cd parser
|
||||||
|
mvn test -Dtest=SecurityTest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 验证清单
|
||||||
|
|
||||||
|
- [ ] 服务启动成功
|
||||||
|
- [ ] 日志显示"🔒 安全的JavaScript引擎初始化成功"
|
||||||
|
- [ ] Java.type() 被禁用(返回undefined)
|
||||||
|
- [ ] 内网访问被拦截
|
||||||
|
- [ ] 外网访问正常工作
|
||||||
|
- [ ] 安全测试全部通过
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关资源
|
||||||
|
|
||||||
|
- **快速验证**: `QUICK_TEST.md`
|
||||||
|
- **SSRF策略**: `parser/SSRF_PROTECTION.md`
|
||||||
|
- **详细修复**: `parser/SECURITY_FIX_SUMMARY.md`
|
||||||
|
- **测试指南**: `parser/doc/SECURITY_TESTING_GUIDE.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 联系方式
|
||||||
|
|
||||||
|
如发现新的安全问题或有改进建议,请通过以下方式反馈:
|
||||||
|
- 提交Issue
|
||||||
|
- 安全邮件:qaiu00@gmail.com
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**维护者**: QAIU
|
||||||
|
**许可**: MIT License
|
||||||
|
|
||||||
309
parser/doc/security/FAQ.md
Normal file
309
parser/doc/security/FAQ.md
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
# 安全修复常见问题 FAQ
|
||||||
|
|
||||||
|
## ❓ 常见问题解答
|
||||||
|
|
||||||
|
### Q1: 为什么还是显示"请求失败: 404"?
|
||||||
|
|
||||||
|
**答**: 这是**正常现象**!404是HTTP响应状态码,说明:
|
||||||
|
|
||||||
|
✅ **安全检查已通过** - 你的请求没有被SSRF防护拦截
|
||||||
|
✅ **请求已发出** - HTTP客户端工作正常
|
||||||
|
❌ **目标资源不存在** - 目标服务器返回404错误
|
||||||
|
|
||||||
|
#### 如何区分安全拦截 vs 正常404?
|
||||||
|
|
||||||
|
| 错误类型 | 错误消息 | 原因 |
|
||||||
|
|---------|---------|------|
|
||||||
|
| **安全拦截** | `SecurityException: 🔒 安全拦截: 禁止访问内网IP地址` | SSRF防护拦截 |
|
||||||
|
| **安全拦截** | `SecurityException: 🔒 安全拦截: 禁止访问云服务元数据API` | 危险域名拦截 |
|
||||||
|
| **正常404** | `Error: 请求失败: 404` | 目标URL不存在 |
|
||||||
|
| **正常错误** | `HTTP请求超时` | 网络超时 |
|
||||||
|
| **正常错误** | `Connection refused` | 目标服务器拒绝连接 |
|
||||||
|
|
||||||
|
#### 示例对比
|
||||||
|
|
||||||
|
**❌ 被安全拦截(内网攻击)**:
|
||||||
|
```javascript
|
||||||
|
try {
|
||||||
|
var response = http.get('http://127.0.0.1:6400/admin');
|
||||||
|
} catch (e) {
|
||||||
|
// 错误消息: SecurityException: 🔒 安全拦截: 禁止访问内网IP地址
|
||||||
|
logger.error(e.message);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ 正常404(资源不存在)**:
|
||||||
|
```javascript
|
||||||
|
try {
|
||||||
|
var response = http.get('https://httpbin.org/not-exist');
|
||||||
|
if (response.statusCode() !== 200) {
|
||||||
|
// 404是正常的HTTP响应,不是安全拦截
|
||||||
|
throw new Error("请求失败: " + response.statusCode());
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 错误消息: Error: 请求失败: 404
|
||||||
|
logger.error(e.message);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 解决方法
|
||||||
|
|
||||||
|
如果你的代码中有这样的检查:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ❌ 不好的做法:对所有非200状态码都抛出异常
|
||||||
|
if (response.statusCode() !== 200) {
|
||||||
|
throw new Error("请求失败: " + response.statusCode());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
建议改为:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ✅ 更好的做法:区分不同的状态码
|
||||||
|
var statusCode = response.statusCode();
|
||||||
|
|
||||||
|
if (statusCode === 404) {
|
||||||
|
logger.warn("资源不存在: " + url);
|
||||||
|
return null; // 或者其他默认值
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusCode < 200 || statusCode >= 300) {
|
||||||
|
throw new Error("请求失败: " + statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.body();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Q2: 如何确认安全修复已生效?
|
||||||
|
|
||||||
|
**答**: 执行以下测试:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 测试1: 尝试访问内网(应该被拦截)
|
||||||
|
try {
|
||||||
|
http.get('http://127.0.0.1:6400/');
|
||||||
|
logger.error('❌ 失败: 内网访问成功(不应该)');
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message.includes('安全拦截')) {
|
||||||
|
logger.info('✅ 通过: 内网访问被拦截');
|
||||||
|
} else {
|
||||||
|
logger.warn('⚠️ 警告: 错误但非安全拦截 - ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试2: 访问外网(应该正常工作,可能返回404但不会被拦截)
|
||||||
|
try {
|
||||||
|
var response = http.get('https://httpbin.org/status/200');
|
||||||
|
logger.info('✅ 通过: 外网访问正常');
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('❌ 失败: 外网访问被拦截(不应该) - ' + e.message);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Q3: Java.type() 相关错误
|
||||||
|
|
||||||
|
**错误消息**: `ReferenceError: "Java" is not defined`
|
||||||
|
|
||||||
|
**答**: 这是**正确的行为**!说明安全修复生效了。
|
||||||
|
|
||||||
|
之前(不安全):
|
||||||
|
```javascript
|
||||||
|
var System = Java.type('java.lang.System'); // ❌ 可以执行
|
||||||
|
```
|
||||||
|
|
||||||
|
现在(安全):
|
||||||
|
```javascript
|
||||||
|
var System = Java.type('java.lang.System'); // ✅ 抛出错误
|
||||||
|
// ReferenceError: "Java" is not defined
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Q4: 如何测试SSRF防护?
|
||||||
|
|
||||||
|
**答**: 使用以下测试用例:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function testSSRF() {
|
||||||
|
var tests = [
|
||||||
|
// 应该被拦截的
|
||||||
|
{url: 'http://127.0.0.1:6400/', shouldBlock: true},
|
||||||
|
{url: 'http://localhost/', shouldBlock: true},
|
||||||
|
{url: 'http://192.168.1.1/', shouldBlock: true},
|
||||||
|
{url: 'http://169.254.169.254/latest/meta-data/', shouldBlock: true},
|
||||||
|
|
||||||
|
// 应该允许的
|
||||||
|
{url: 'https://httpbin.org/get', shouldBlock: false},
|
||||||
|
{url: 'https://www.example.com/', shouldBlock: false}
|
||||||
|
];
|
||||||
|
|
||||||
|
tests.forEach(function(test) {
|
||||||
|
try {
|
||||||
|
var response = http.get(test.url);
|
||||||
|
if (test.shouldBlock) {
|
||||||
|
logger.error('❌ 失败: ' + test.url + ' 应该被拦截但没有');
|
||||||
|
} else {
|
||||||
|
logger.info('✅ 通过: ' + test.url + ' 正确允许');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (test.shouldBlock && e.message.includes('安全拦截')) {
|
||||||
|
logger.info('✅ 通过: ' + test.url + ' 正确拦截');
|
||||||
|
} else if (!test.shouldBlock) {
|
||||||
|
logger.error('❌ 失败: ' + test.url + ' 不应该被拦截 - ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Q5: 服务启动时出现 ArrayIndexOutOfBoundsException
|
||||||
|
|
||||||
|
**答**: 说明代码未更新或未重新编译。
|
||||||
|
|
||||||
|
**解决方法**:
|
||||||
|
```bash
|
||||||
|
# 1. 确认代码已更新
|
||||||
|
grep -n "new String\[0\]" parser/src/main/java/cn/qaiu/parser/customjs/JsPlaygroundExecutor.java
|
||||||
|
|
||||||
|
# 应该看到类似:
|
||||||
|
# 68: ScriptEngine engine = factory.getScriptEngine(new String[0], null, new SecurityClassFilter());
|
||||||
|
|
||||||
|
# 2. 重新编译
|
||||||
|
mvn clean install
|
||||||
|
|
||||||
|
# 3. 重启服务
|
||||||
|
./bin/stop.sh && ./bin/run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Q6: 如何关闭SSRF防护?(不推荐)
|
||||||
|
|
||||||
|
**⚠️ 警告**: 关闭SSRF防护会带来严重的安全风险!
|
||||||
|
|
||||||
|
如果确实需要(仅用于开发环境),可以修改 `JsHttpClient.java`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
private void validateUrlSecurity(String url) {
|
||||||
|
// 注释掉所有验证逻辑
|
||||||
|
log.debug("SSRF防护已禁用(仅开发环境)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**强烈建议**: 保持SSRF防护开启,使用白名单策略代替完全关闭。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Q7: 如何添加域名白名单?
|
||||||
|
|
||||||
|
**答**: 当前策略是黑名单模式。如需白名单,修改 `validateUrlSecurity`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
private static final String[] ALLOWED_DOMAINS = {
|
||||||
|
"api.example.com",
|
||||||
|
"cdn.example.com"
|
||||||
|
};
|
||||||
|
|
||||||
|
private void validateUrlSecurity(String url) {
|
||||||
|
URI uri = new URI(url);
|
||||||
|
String host = uri.getHost();
|
||||||
|
|
||||||
|
// 白名单检查
|
||||||
|
boolean allowed = false;
|
||||||
|
for (String domain : ALLOWED_DOMAINS) {
|
||||||
|
if (host.equals(domain) || host.endsWith("." + domain)) {
|
||||||
|
allowed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allowed) {
|
||||||
|
throw new SecurityException("域名不在白名单中: " + host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Q8: 性能影响
|
||||||
|
|
||||||
|
**Q**: 安全检查会影响性能吗?
|
||||||
|
|
||||||
|
**A**: 影响很小:
|
||||||
|
- ClassFilter: 在引擎初始化时执行一次,几乎无性能影响
|
||||||
|
- SSRF检查: 每次HTTP请求前执行,主要是DNS解析(已有缓存)
|
||||||
|
- 预计性能影响: < 5ms/请求
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Q9: 如何查看安全日志?
|
||||||
|
|
||||||
|
**答**:
|
||||||
|
```bash
|
||||||
|
# 查看安全拦截日志
|
||||||
|
tail -f logs/*/run.log | grep "安全拦截"
|
||||||
|
|
||||||
|
# 查看JavaScript引擎初始化日志
|
||||||
|
tail -f logs/*/run.log | grep "JavaScript引擎"
|
||||||
|
|
||||||
|
# 应该看到:
|
||||||
|
# 🔒 安全的JavaScript引擎初始化成功(演练场)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Q10: 迁移到GraalVM
|
||||||
|
|
||||||
|
**Q**: 如何迁移到更安全的GraalVM JavaScript?
|
||||||
|
|
||||||
|
**A**:
|
||||||
|
|
||||||
|
1. 添加依赖(`pom.xml`):
|
||||||
|
```xml
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.graalvm.js</groupId>
|
||||||
|
<artifactId>js</artifactId>
|
||||||
|
<version>23.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 修改代码:
|
||||||
|
```java
|
||||||
|
import org.graalvm.polyglot.*;
|
||||||
|
|
||||||
|
Context context = Context.newBuilder("js")
|
||||||
|
.allowHostAccess(HostAccess.NONE) // 禁止访问Java
|
||||||
|
.allowIO(IOAccess.NONE) // 禁止IO
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Value result = context.eval("js", jsCode);
|
||||||
|
```
|
||||||
|
|
||||||
|
GraalVM优势:
|
||||||
|
- ✅ 默认沙箱隔离
|
||||||
|
- ✅ 更好的安全性
|
||||||
|
- ✅ 更好的性能
|
||||||
|
- ✅ 活跃维护
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 获取帮助
|
||||||
|
|
||||||
|
如果以上FAQ没有解决你的问题:
|
||||||
|
|
||||||
|
1. 查看详细文档: `parser/doc/security/`
|
||||||
|
2. 运行安全测试: `./parser/doc/security/test-security.sh`
|
||||||
|
3. 查看测试指南: `SECURITY_TESTING_GUIDE.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**最后更新**: 2025-11-29
|
||||||
|
|
||||||
293
parser/doc/security/QUICK_TEST.md
Normal file
293
parser/doc/security/QUICK_TEST.md
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
# 🧪 安全修复快速验证指南
|
||||||
|
|
||||||
|
## 修复内容
|
||||||
|
✅ JavaScript远程代码执行漏洞已修复
|
||||||
|
✅ SSRF攻击防护已添加
|
||||||
|
✅ 方法调用错误已修复(`ArrayIndexOutOfBoundsException`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快速测试步骤
|
||||||
|
|
||||||
|
### 1. 重新编译(必须)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/q/IdeaProjects/mycode/netdisk-fast-download
|
||||||
|
mvn clean install -DskipTests
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 重启服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 停止旧服务
|
||||||
|
./bin/stop.sh
|
||||||
|
|
||||||
|
# 启动新服务
|
||||||
|
./bin/run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 执行安全测试
|
||||||
|
|
||||||
|
#### 方式A: 使用HTTP测试文件(推荐)
|
||||||
|
|
||||||
|
1. 确保服务已启动(默认端口 6400)
|
||||||
|
2. 使用IDE打开: `web-service/src/test/resources/playground-security-tests.http`
|
||||||
|
3. 执行"测试3: 系统属性和环境变量访问"
|
||||||
|
|
||||||
|
**期望结果**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"result": "✓ 安全: 无法访问系统属性",
|
||||||
|
"logs": [
|
||||||
|
{
|
||||||
|
"level": "INFO",
|
||||||
|
"message": "尝试访问系统属性..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": "INFO",
|
||||||
|
"message": "系统属性访问失败: ReferenceError: \"Java\" is not defined"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 方式B: 使用JUnit测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd parser
|
||||||
|
mvn test -Dtest=SecurityTest#testSystemPropertiesAccess
|
||||||
|
```
|
||||||
|
|
||||||
|
**期望输出**:
|
||||||
|
```
|
||||||
|
[INFO] 尝试访问系统属性...
|
||||||
|
[INFO] 方法1失败: ReferenceError: "Java" is not defined
|
||||||
|
✓ 安全: 无法访问系统属性
|
||||||
|
测试完成: 系统属性访问测试
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 验证清单
|
||||||
|
|
||||||
|
运行测试后,确认以下几点:
|
||||||
|
|
||||||
|
### ✅ 必须通过的检查
|
||||||
|
|
||||||
|
- [ ] 服务启动成功,没有 `ArrayIndexOutOfBoundsException`
|
||||||
|
- [ ] 日志中出现:`🔒 安全的JavaScript引擎初始化成功`
|
||||||
|
- [ ] JavaScript代码执行正常(parse函数可以调用)
|
||||||
|
- [ ] 尝试访问 `Java.type()` 时返回错误:`ReferenceError: "Java" is not defined`
|
||||||
|
- [ ] 尝试访问 `System.getProperty()` 时失败
|
||||||
|
- [ ] HTTP请求内网地址(如 127.0.0.1)时被拦截
|
||||||
|
|
||||||
|
### ⚠️ 如果出现以下情况说明修复失败
|
||||||
|
|
||||||
|
- [ ] 服务启动时抛出异常
|
||||||
|
- [ ] JavaScript可以成功调用 `Java.type()`
|
||||||
|
- [ ] 可以获取到系统属性(如用户名、HOME目录)
|
||||||
|
- [ ] 可以访问内网地址(127.0.0.1, 192.168.x.x)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快速测试用例
|
||||||
|
|
||||||
|
### 测试1: 验证Java访问被禁用 ✅
|
||||||
|
|
||||||
|
在演练场输入以下代码:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ==UserScript==
|
||||||
|
// @name 快速安全测试
|
||||||
|
// @type test
|
||||||
|
// @match https://test.com/*
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
function parse(shareLinkInfo, http, logger) {
|
||||||
|
logger.info('开始安全测试...');
|
||||||
|
|
||||||
|
// 测试1: Java对象
|
||||||
|
try {
|
||||||
|
if (typeof Java !== 'undefined') {
|
||||||
|
logger.error('❌ 失败: Java对象仍然可用');
|
||||||
|
return 'FAILED: Java可用';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.info('✅ 通过: Java对象未定义');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试2: JavaImporter
|
||||||
|
try {
|
||||||
|
if (typeof JavaImporter !== 'undefined') {
|
||||||
|
logger.error('❌ 失败: JavaImporter仍然可用');
|
||||||
|
return 'FAILED: JavaImporter可用';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.info('✅ 通过: JavaImporter未定义');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试3: Packages
|
||||||
|
try {
|
||||||
|
if (typeof Packages !== 'undefined') {
|
||||||
|
logger.error('❌ 失败: Packages仍然可用');
|
||||||
|
return 'FAILED: Packages可用';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.info('✅ 通过: Packages未定义');
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('✅ 所有测试通过!系统安全!');
|
||||||
|
return 'SUCCESS: 安全修复生效';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**期望输出**:
|
||||||
|
```
|
||||||
|
[INFO] 开始安全测试...
|
||||||
|
[INFO] ✅ 通过: Java对象未定义
|
||||||
|
[INFO] ✅ 通过: JavaImporter未定义
|
||||||
|
[INFO] ✅ 通过: Packages未定义
|
||||||
|
[INFO] ✅ 所有测试通过!系统安全!
|
||||||
|
SUCCESS: 安全修复生效
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试2: 验证SSRF防护 ✅
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function parse(shareLinkInfo, http, logger) {
|
||||||
|
logger.info('测试SSRF防护...');
|
||||||
|
|
||||||
|
// 测试访问内网
|
||||||
|
try {
|
||||||
|
http.get('http://127.0.0.1:6400/');
|
||||||
|
logger.error('❌ 失败: 可以访问内网');
|
||||||
|
return 'FAILED: SSRF防护无效';
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message && e.message.includes('安全拦截')) {
|
||||||
|
logger.info('✅ 通过: 内网访问被阻止 - ' + e.message);
|
||||||
|
return 'SUCCESS: SSRF防护有效';
|
||||||
|
} else {
|
||||||
|
logger.warn('⚠️ 警告: 错误但非安全拦截 - ' + e.message);
|
||||||
|
return 'WARNING: 未知错误';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**期望输出**:
|
||||||
|
```
|
||||||
|
[INFO] 测试SSRF防护...
|
||||||
|
[INFO] ✅ 通过: 内网访问被阻止 - SecurityException: 🔒 安全拦截: 禁止访问内网地址
|
||||||
|
SUCCESS: SSRF防护有效
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 故障排查
|
||||||
|
|
||||||
|
### 问题1: 服务启动失败
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查编译是否成功
|
||||||
|
ls -la parser/target/parser-*.jar
|
||||||
|
ls -la web-service/target/*.jar
|
||||||
|
|
||||||
|
# 如果没有jar文件,重新编译
|
||||||
|
mvn clean install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题2: ArrayIndexOutOfBoundsException 仍然出现
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 确认代码已更新
|
||||||
|
grep -n "new String\[0\]" parser/src/main/java/cn/qaiu/parser/customjs/JsPlaygroundExecutor.java
|
||||||
|
|
||||||
|
# 应该看到类似:
|
||||||
|
# 68: ScriptEngine engine = factory.getScriptEngine(new String[0], null, new SecurityClassFilter());
|
||||||
|
|
||||||
|
# 如果没有,说明代码未更新,重新拉取
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题3: 测试显示"Java仍然可用"
|
||||||
|
|
||||||
|
这是**严重问题**,说明修复未生效:
|
||||||
|
|
||||||
|
1. 确认代码已更新
|
||||||
|
2. 确认重新编译
|
||||||
|
3. 确认重启服务
|
||||||
|
4. 检查日志是否有"安全的JavaScript引擎初始化成功"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查日志
|
||||||
|
tail -f logs/*/run.log | grep "JavaScript引擎"
|
||||||
|
|
||||||
|
# 应该看到:
|
||||||
|
# 🔒 安全的JavaScript引擎初始化成功(演练场)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一键测试脚本
|
||||||
|
|
||||||
|
创建并运行快速测试:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/q/IdeaProjects/mycode/netdisk-fast-download
|
||||||
|
|
||||||
|
# 重新编译
|
||||||
|
echo "📦 重新编译..."
|
||||||
|
mvn clean install -DskipTests
|
||||||
|
|
||||||
|
# 重启服务
|
||||||
|
echo "🔄 重启服务..."
|
||||||
|
./bin/stop.sh
|
||||||
|
sleep 2
|
||||||
|
./bin/run.sh
|
||||||
|
|
||||||
|
# 等待服务启动
|
||||||
|
echo "⏳ 等待服务启动..."
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# 运行安全测试
|
||||||
|
echo "🧪 运行安全测试..."
|
||||||
|
cd parser
|
||||||
|
mvn test -Dtest=SecurityTest#testSystemPropertiesAccess
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ 测试完成!请检查上方输出确认安全修复是否生效。"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 成功标志
|
||||||
|
|
||||||
|
如果看到以下输出,说明修复成功:
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ 服务启动成功
|
||||||
|
✅ 日志: 🔒 安全的JavaScript引擎初始化成功
|
||||||
|
✅ 测试: ReferenceError: "Java" is not defined
|
||||||
|
✅ 测试: ✓ 安全: 无法访问系统属性
|
||||||
|
✅ 测试: 🔒 安全拦截: 禁止访问内网地址
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
测试通过后:
|
||||||
|
1. ✅ 标记漏洞为"已修复"
|
||||||
|
2. ✅ 部署到生产环境(如果适用)
|
||||||
|
3. ✅ 更新安全文档
|
||||||
|
4. ✅ 通知团队成员
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文档**:
|
||||||
|
- 详细修复说明: `parser/SECURITY_FIX_SUMMARY.md`
|
||||||
|
- 紧急修复指南: `SECURITY_URGENT_FIX.md`
|
||||||
|
- 完整测试指南: `parser/doc/SECURITY_TESTING_GUIDE.md`
|
||||||
|
|
||||||
|
**最后更新**: 2025-11-29
|
||||||
|
|
||||||
42
parser/doc/security/README.md
Normal file
42
parser/doc/security/README.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# 安全相关文档索引
|
||||||
|
|
||||||
|
本目录包含JavaScript执行器的安全修复和测试相关文档。
|
||||||
|
|
||||||
|
## 📚 文档列表
|
||||||
|
|
||||||
|
### 🚀 快速开始
|
||||||
|
- **[QUICK_TEST.md](QUICK_TEST.md)** - 快速验证指南(5分钟)
|
||||||
|
- **[FAQ.md](FAQ.md)** - 常见问题解答 ⭐ **推荐先看这个!**
|
||||||
|
- **[test-security.sh](test-security.sh)** - 一键测试脚本
|
||||||
|
|
||||||
|
### 📋 安全修复说明
|
||||||
|
- **[SECURITY_FIX_SUMMARY.md](SECURITY_FIX_SUMMARY.md)** - 完整修复总结
|
||||||
|
- **[SECURITY_URGENT_FIX.md](SECURITY_URGENT_FIX.md)** - 紧急修复通知
|
||||||
|
- **[CHANGELOG_SECURITY.md](CHANGELOG_SECURITY.md)** - 安全更新日志
|
||||||
|
|
||||||
|
### 🧪 测试指南
|
||||||
|
- **[SECURITY_TEST_README.md](SECURITY_TEST_README.md)** - 安全测试快速入门
|
||||||
|
- **[SECURITY_TESTING_GUIDE.md](../SECURITY_TESTING_GUIDE.md)** - 详细测试指南
|
||||||
|
|
||||||
|
### 🛡️ 防护策略
|
||||||
|
- **[SSRF_PROTECTION.md](SSRF_PROTECTION.md)** - SSRF防护策略说明
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚨 重要提醒
|
||||||
|
|
||||||
|
如果你看到这些文档,说明系统曾经存在严重的安全漏洞。请务必:
|
||||||
|
|
||||||
|
1. ✅ 确认已应用最新的安全修复
|
||||||
|
2. ✅ 运行安全测试验证修复效果
|
||||||
|
3. ✅ 重新部署到生产环境
|
||||||
|
|
||||||
|
## ❓ 遇到问题?
|
||||||
|
|
||||||
|
- **看到"请求失败: 404"?** → 这是正常的HTTP响应,不是安全拦截!查看 [FAQ.md](FAQ.md#q1-为什么还是显示请求失败-404)
|
||||||
|
- **Java.type() 报错?** → 这说明安全修复生效了!查看 [FAQ.md](FAQ.md#q3-javatype-相关错误)
|
||||||
|
- **服务启动失败?** → 检查是否重新编译,查看 [FAQ.md](FAQ.md#q5-服务启动时出现-arrayindexoutofboundsexception)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
最后更新: 2025-11-29
|
||||||
323
parser/doc/security/SECURITY_FIX_SUMMARY.md
Normal file
323
parser/doc/security/SECURITY_FIX_SUMMARY.md
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
# JavaScript远程代码执行漏洞修复总结
|
||||||
|
|
||||||
|
## 🔴 严重安全漏洞已修复
|
||||||
|
|
||||||
|
**修复日期**: 2025-11-28
|
||||||
|
**漏洞类型**: 远程代码执行 (RCE)
|
||||||
|
**危险等级**: 🔴 极高
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 漏洞描述
|
||||||
|
|
||||||
|
### 原始问题
|
||||||
|
|
||||||
|
JavaScript执行器使用 Nashorn 引擎,但**没有任何安全限制**,允许JavaScript代码:
|
||||||
|
|
||||||
|
1. ❌ 访问所有Java类 (通过 `Java.type()`)
|
||||||
|
2. ❌ 执行系统命令 (`Runtime.exec()`)
|
||||||
|
3. ❌ 读写文件系统 (`java.io.File`)
|
||||||
|
4. ❌ 访问系统属性 (`System.getProperty()`)
|
||||||
|
5. ❌ 使用反射绕过限制 (`Class.forName()`)
|
||||||
|
6. ❌ 创建任意网络连接 (`Socket`)
|
||||||
|
7. ❌ 访问内网服务 (SSRF攻击)
|
||||||
|
|
||||||
|
### 测试结果(修复前)
|
||||||
|
|
||||||
|
```
|
||||||
|
[ERROR] [JS] 【安全漏洞】获取到系统属性 - HOME: /Users/q, USER: q
|
||||||
|
结果: 危险: 系统属性访问成功 - q
|
||||||
|
```
|
||||||
|
|
||||||
|
**这意味着任何用户提供的JavaScript代码都可以完全控制服务器!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 已实施的安全措施
|
||||||
|
|
||||||
|
### 1. ClassFilter 类过滤器 🔒
|
||||||
|
|
||||||
|
**文件**: `parser/src/main/java/cn/qaiu/parser/customjs/SecurityClassFilter.java`
|
||||||
|
|
||||||
|
**功能**: 拦截JavaScript对危险Java类的访问
|
||||||
|
|
||||||
|
**黑名单包括**:
|
||||||
|
- 系统命令执行: `Runtime`, `ProcessBuilder`
|
||||||
|
- 文件系统访问: `File`, `Files`, `Paths`, `FileInputStream/OutputStream`
|
||||||
|
- 系统访问: `System`, `SecurityManager`
|
||||||
|
- 反射: `Class`, `Method`, `Field`, `ClassLoader`
|
||||||
|
- 网络: `Socket`, `URL`, `URLConnection`
|
||||||
|
- 线程: `Thread`, `ExecutorService`
|
||||||
|
- 数据库: `Connection`, `Statement`
|
||||||
|
- 脚本引擎: `ScriptEngine`
|
||||||
|
|
||||||
|
**效果**:
|
||||||
|
```java
|
||||||
|
public boolean exposeToScripts(String className) {
|
||||||
|
// 检查黑名单
|
||||||
|
if (className.startsWith("java.lang.System")) {
|
||||||
|
log.warn("🔒 安全拦截: JavaScript尝试访问危险类 - {}", className);
|
||||||
|
return false; // 拒绝访问
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 禁用Java内置对象 🚫
|
||||||
|
|
||||||
|
**修改位置**: `JsPlaygroundExecutor.initEngine()` 和 `JsParserExecutor.initEngine()`
|
||||||
|
|
||||||
|
**实施方法**:
|
||||||
|
```java
|
||||||
|
// 创建带ClassFilter的安全引擎
|
||||||
|
NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
|
||||||
|
ScriptEngine engine = factory.getScriptEngine(new SecurityClassFilter());
|
||||||
|
|
||||||
|
// 禁用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;");
|
||||||
|
```
|
||||||
|
|
||||||
|
**效果**: JavaScript无法使用 `Java.type()` 等方法访问Java类
|
||||||
|
|
||||||
|
### 3. SSRF防护 🌐
|
||||||
|
|
||||||
|
**文件**: `parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java`
|
||||||
|
|
||||||
|
**功能**: 防止JavaScript通过HTTP客户端访问内网资源
|
||||||
|
|
||||||
|
**防护措施**:
|
||||||
|
```java
|
||||||
|
private void validateUrlSecurity(String url) {
|
||||||
|
// 1. 检查危险域名黑名单
|
||||||
|
// - localhost
|
||||||
|
// - 169.254.169.254 (云服务元数据API)
|
||||||
|
// - metadata.google.internal
|
||||||
|
|
||||||
|
// 2. 检查内网IP
|
||||||
|
// - 127.x.x.x (本地回环)
|
||||||
|
// - 10.x.x.x (内网A类)
|
||||||
|
// - 172.16-31.x.x (内网B类)
|
||||||
|
// - 192.168.x.x (内网C类)
|
||||||
|
// - 169.254.x.x (链路本地)
|
||||||
|
|
||||||
|
// 3. 检查协议
|
||||||
|
// - 仅允许 HTTP/HTTPS
|
||||||
|
|
||||||
|
if (PRIVATE_IP_PATTERN.matcher(ip).find()) {
|
||||||
|
throw new SecurityException("🔒 安全拦截: 禁止访问内网地址");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**应用位置**: 所有HTTP请求方法
|
||||||
|
- `get()`
|
||||||
|
- `getWithRedirect()`
|
||||||
|
- `getNoRedirect()`
|
||||||
|
- `post()`
|
||||||
|
- `put()`
|
||||||
|
|
||||||
|
### 4. 超时保护 ⏱️
|
||||||
|
|
||||||
|
**已有机制**: Worker线程池限制
|
||||||
|
|
||||||
|
**位置**:
|
||||||
|
- `JsPlaygroundExecutor`: 16个worker线程
|
||||||
|
- `JsParserExecutor`: 32个worker线程
|
||||||
|
|
||||||
|
**超时**: HTTP请求默认30秒超时
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 安全验证
|
||||||
|
|
||||||
|
### 测试方法
|
||||||
|
|
||||||
|
使用提供的安全测试套件:
|
||||||
|
|
||||||
|
#### 方式1: JUnit测试
|
||||||
|
```bash
|
||||||
|
cd parser
|
||||||
|
mvn test -Dtest=SecurityTest
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 方式2: HTTP接口测试
|
||||||
|
```bash
|
||||||
|
# 启动服务器后执行
|
||||||
|
# 使用 web-service/src/test/resources/playground-security-tests.http
|
||||||
|
```
|
||||||
|
|
||||||
|
### 预期结果(修复后)
|
||||||
|
|
||||||
|
所有危险操作应该被拦截:
|
||||||
|
|
||||||
|
```
|
||||||
|
[INFO] [JS] 尝试访问系统属性...
|
||||||
|
[INFO] [JS] 系统属性访问失败: ReferenceError: "Java" is not defined
|
||||||
|
✓ 安全: 无法访问系统属性
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 修复效果对比
|
||||||
|
|
||||||
|
| 测试项目 | 修复前 | 修复后 |
|
||||||
|
|---------|--------|--------|
|
||||||
|
| 系统命令执行 | ❌ 成功执行 | ✅ 被拦截 |
|
||||||
|
| 文件系统访问 | ❌ 可读写文件 | ✅ 被拦截 |
|
||||||
|
| 系统属性访问 | ❌ 获取成功 | ✅ 被拦截 |
|
||||||
|
| 反射攻击 | ❌ 可使用反射 | ✅ 被拦截 |
|
||||||
|
| 网络Socket | ❌ 可创建连接 | ✅ 被拦截 |
|
||||||
|
| JVM退出 | ❌ 可终止进程 | ✅ 被拦截 |
|
||||||
|
| SSRF内网访问 | ❌ 可访问内网 | ✅ 被拦截 |
|
||||||
|
| SSRF元数据API | ❌ 可访问 | ✅ 被拦截 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 修改的文件列表
|
||||||
|
|
||||||
|
### 新增文件
|
||||||
|
|
||||||
|
1. ✅ `parser/src/main/java/cn/qaiu/parser/customjs/SecurityClassFilter.java`
|
||||||
|
- ClassFilter实现,拦截危险类访问
|
||||||
|
|
||||||
|
2. ✅ `parser/src/test/java/cn/qaiu/parser/SecurityTest.java`
|
||||||
|
- 7个安全测试用例
|
||||||
|
|
||||||
|
3. ✅ `web-service/src/test/resources/playground-security-tests.http`
|
||||||
|
- 10个HTTP安全测试用例
|
||||||
|
|
||||||
|
4. ✅ `parser/doc/SECURITY_TESTING_GUIDE.md`
|
||||||
|
- 完整的安全测试和修复指南
|
||||||
|
|
||||||
|
5. ✅ `parser/SECURITY_TEST_README.md`
|
||||||
|
- 快速开始指南
|
||||||
|
|
||||||
|
6. ✅ `parser/test-security.sh`
|
||||||
|
- 自动化测试脚本
|
||||||
|
|
||||||
|
7. ✅ `parser/SECURITY_FIX_SUMMARY.md`
|
||||||
|
- 本文件(修复总结)
|
||||||
|
|
||||||
|
### 修改的文件
|
||||||
|
|
||||||
|
1. ✅ `parser/src/main/java/cn/qaiu/parser/customjs/JsPlaygroundExecutor.java`
|
||||||
|
- 修改 `initEngine()` 方法使用 SecurityClassFilter
|
||||||
|
- 禁用 Java 内置对象
|
||||||
|
|
||||||
|
2. ✅ `parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java`
|
||||||
|
- 修改 `initEngine()` 方法使用 SecurityClassFilter
|
||||||
|
- 禁用 Java 内置对象
|
||||||
|
|
||||||
|
3. ✅ `parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java`
|
||||||
|
- 添加 `validateUrlSecurity()` 方法
|
||||||
|
- 在所有HTTP请求方法中添加SSRF检查
|
||||||
|
- 添加内网IP检测和危险域名黑名单
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 重要提示
|
||||||
|
|
||||||
|
### 1. 立即部署
|
||||||
|
|
||||||
|
这是一个**严重的安全漏洞**,请尽快部署修复:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 重新编译
|
||||||
|
mvn clean install
|
||||||
|
|
||||||
|
# 重启服务
|
||||||
|
./bin/stop.sh
|
||||||
|
./bin/run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 验证修复
|
||||||
|
|
||||||
|
部署后**必须**执行安全测试:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd parser
|
||||||
|
./test-security.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
确认所有高危测试都被拦截!
|
||||||
|
|
||||||
|
### 3. 监控日志
|
||||||
|
|
||||||
|
留意日志中的安全拦截记录:
|
||||||
|
|
||||||
|
```
|
||||||
|
[WARN] 🔒 安全拦截: JavaScript尝试访问危险类 - java.lang.System
|
||||||
|
[WARN] 🔒 安全拦截: 尝试访问内网地址 - 127.0.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
如果看到大量拦截日志,可能有人在尝试攻击。
|
||||||
|
|
||||||
|
### 4. 后续改进
|
||||||
|
|
||||||
|
**长期建议**: 迁移到 GraalVM JavaScript
|
||||||
|
|
||||||
|
Nashorn已废弃,建议迁移到更安全、更现代的引擎:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.graalvm.js</groupId>
|
||||||
|
<artifactId>js</artifactId>
|
||||||
|
<version>23.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
```
|
||||||
|
|
||||||
|
GraalVM优势:
|
||||||
|
- 默认沙箱隔离
|
||||||
|
- 无法访问Java类(除非显式允许)
|
||||||
|
- 更好的性能
|
||||||
|
- 活跃维护
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 相关文档
|
||||||
|
|
||||||
|
- **详细测试指南**: `parser/doc/SECURITY_TESTING_GUIDE.md`
|
||||||
|
- **快速开始**: `parser/SECURITY_TEST_README.md`
|
||||||
|
- **测试用例**:
|
||||||
|
- JUnit: `parser/src/test/java/cn/qaiu/parser/SecurityTest.java`
|
||||||
|
- HTTP: `web-service/src/test/resources/playground-security-tests.http`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 结论
|
||||||
|
|
||||||
|
### 修复前(极度危险 🔴)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 攻击者可以执行任意代码
|
||||||
|
var Runtime = Java.type('java.lang.Runtime');
|
||||||
|
Runtime.getRuntime().exec('rm -rf /'); // 删除所有文件!
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修复后(安全 ✅)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 所有危险操作被拦截
|
||||||
|
var Runtime = Java.type('java.lang.Runtime');
|
||||||
|
// ReferenceError: "Java" is not defined
|
||||||
|
```
|
||||||
|
|
||||||
|
**安全级别**: 🔴 D级(严重不安全) → 🟢 A级(安全)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**免责声明**: 虽然已实施多层安全防护,但没有系统是100%安全的。建议定期审计代码,关注安全更新,并考虑迁移到更现代的JavaScript引擎(如GraalVM)。
|
||||||
|
|
||||||
|
**联系方式**: 如发现新的安全问题,请通过安全渠道私密报告。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**修复完成** ✅
|
||||||
|
**审核状态**: 待用户验证
|
||||||
|
**下一步**: 执行安全测试套件,确认所有漏洞已修复
|
||||||
|
|
||||||
180
parser/doc/security/SECURITY_TEST_README.md
Normal file
180
parser/doc/security/SECURITY_TEST_README.md
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
# JavaScript执行器安全测试
|
||||||
|
|
||||||
|
## 📋 概述
|
||||||
|
|
||||||
|
本目录提供了完整的JavaScript执行器安全测试工具和文档,用于验证演练场执行器是否存在安全漏洞。
|
||||||
|
|
||||||
|
## 🎯 测试目标
|
||||||
|
|
||||||
|
验证以下安全风险:
|
||||||
|
|
||||||
|
| 测试项目 | 危险级别 | 说明 |
|
||||||
|
|---------|---------|------|
|
||||||
|
| 系统命令执行 | 🔴 极高 | 验证是否能执行shell命令 |
|
||||||
|
| 文件系统访问 | 🔴 极高 | 验证是否能读写本地文件 |
|
||||||
|
| 系统属性访问 | 🟡 高 | 验证是否能获取系统信息 |
|
||||||
|
| 反射攻击 | 🔴 极高 | 验证是否能通过反射绕过限制 |
|
||||||
|
| 网络Socket | 🔴 极高 | 验证是否能创建任意网络连接 |
|
||||||
|
| JVM退出 | 🔴 极高 | 验证是否能终止应用 |
|
||||||
|
| SSRF攻击 | 🟡 高 | 验证HTTP客户端访问控制 |
|
||||||
|
|
||||||
|
## 📂 测试资源
|
||||||
|
|
||||||
|
```
|
||||||
|
parser/
|
||||||
|
├── src/test/java/cn/qaiu/parser/
|
||||||
|
│ └── SecurityTest.java # JUnit测试用例(7个测试方法)
|
||||||
|
├── doc/
|
||||||
|
│ └── SECURITY_TESTING_GUIDE.md # 详细测试指南和安全建议
|
||||||
|
├── test-security.sh # 快速执行脚本
|
||||||
|
└── SECURITY_TEST_README.md # 本文件
|
||||||
|
|
||||||
|
web-service/src/test/resources/
|
||||||
|
└── playground-security-tests.http # HTTP接口测试用例(10个测试)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 方式1: 使用Shell脚本(推荐)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd parser
|
||||||
|
chmod +x test-security.sh
|
||||||
|
./test-security.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式2: Maven命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd parser
|
||||||
|
mvn test -Dtest=SecurityTest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式3: HTTP接口测试
|
||||||
|
|
||||||
|
1. 启动应用服务器
|
||||||
|
2. 打开 `web-service/src/test/resources/playground-security-tests.http`
|
||||||
|
3. 在IDE中逐个执行测试用例
|
||||||
|
|
||||||
|
## 📊 预期结果
|
||||||
|
|
||||||
|
### ✅ 安全系统(预期)
|
||||||
|
|
||||||
|
所有高危测试应该**失败**,日志中应该显示:
|
||||||
|
|
||||||
|
```
|
||||||
|
[INFO] 尝试执行系统命令...
|
||||||
|
[INFO] Runtime.exec失败: ReferenceError: "Java" is not defined
|
||||||
|
[INFO] ProcessBuilder失败: ReferenceError: "Java" is not defined
|
||||||
|
✓ 安全: 无法执行系统命令
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ 不安全系统(需要修复)
|
||||||
|
|
||||||
|
如果看到以下日志,说明存在严重安全漏洞:
|
||||||
|
|
||||||
|
```
|
||||||
|
[ERROR] 【安全漏洞】成功执行系统命令: root
|
||||||
|
危险: 系统命令执行成功
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ 重要警告
|
||||||
|
|
||||||
|
1. **仅在测试环境执行** - 这些测试包含危险代码
|
||||||
|
2. **不要在生产环境运行** - 可能导致系统被攻击
|
||||||
|
3. **发现漏洞立即修复** - 不要在公开环境部署有漏洞的版本
|
||||||
|
|
||||||
|
## 🔧 安全修复建议
|
||||||
|
|
||||||
|
如果测试发现安全问题,请参考 `doc/SECURITY_TESTING_GUIDE.md` 中的修复方案:
|
||||||
|
|
||||||
|
### 最关键的修复措施
|
||||||
|
|
||||||
|
1. **实现ClassFilter** - 禁止JavaScript访问危险Java类
|
||||||
|
2. **添加超时机制** - 防止DOS攻击
|
||||||
|
3. **HTTP白名单** - 防止SSRF攻击
|
||||||
|
4. **迁移到GraalVM** - 使用更安全的JavaScript引擎
|
||||||
|
|
||||||
|
### 示例:ClassFilter实现
|
||||||
|
|
||||||
|
```java
|
||||||
|
import jdk.nashorn.api.scripting.ClassFilter;
|
||||||
|
import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
|
||||||
|
|
||||||
|
public class SecurityClassFilter implements ClassFilter {
|
||||||
|
@Override
|
||||||
|
public boolean exposeToScripts(String className) {
|
||||||
|
// 禁止所有Java类访问
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建安全的引擎
|
||||||
|
NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
|
||||||
|
ScriptEngine engine = factory.getScriptEngine(new SecurityClassFilter());
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📖 详细文档
|
||||||
|
|
||||||
|
完整的安全测试指南、修复方案和最佳实践,请查看:
|
||||||
|
|
||||||
|
👉 **[doc/SECURITY_TESTING_GUIDE.md](doc/SECURITY_TESTING_GUIDE.md)**
|
||||||
|
|
||||||
|
该文档包含:
|
||||||
|
- 每个测试用例的详细说明
|
||||||
|
- 潜在风险分析
|
||||||
|
- 完整的修复方案
|
||||||
|
- 安全配置最佳实践
|
||||||
|
- GraalVM迁移指南
|
||||||
|
|
||||||
|
## 🔍 测试检查清单
|
||||||
|
|
||||||
|
执行测试后,请确认:
|
||||||
|
|
||||||
|
- [ ] ✅ 测试1: 系统命令执行 - **失败**(安全)
|
||||||
|
- [ ] ✅ 测试2: 文件系统访问 - **失败**(安全)
|
||||||
|
- [ ] ✅ 测试3: 系统属性访问 - **失败**(安全)
|
||||||
|
- [ ] ✅ 测试4: 反射攻击 - **失败**(安全)
|
||||||
|
- [ ] ✅ 测试5: 网络Socket - **失败**(安全)
|
||||||
|
- [ ] ✅ 测试6: JVM退出 - **失败**(安全)
|
||||||
|
- [ ] ⚠️ 测试7: SSRF攻击 - **部分失败**(禁止内网访问)
|
||||||
|
|
||||||
|
## 💡 常见问题
|
||||||
|
|
||||||
|
### Q: 为什么要进行这些测试?
|
||||||
|
|
||||||
|
A: JavaScript执行器允许运行用户提供的代码,如果不加限制,恶意用户可能:
|
||||||
|
- 执行系统命令窃取数据
|
||||||
|
- 读取敏感文件
|
||||||
|
- 攻击内网服务器
|
||||||
|
- 导致服务器崩溃
|
||||||
|
|
||||||
|
### Q: 测试失败是好事还是坏事?
|
||||||
|
|
||||||
|
A: **测试失败是好事!** 这意味着危险操作被成功阻止了。如果测试通过(返回"危险"),说明存在安全漏洞。
|
||||||
|
|
||||||
|
### Q: 可以跳过这些测试吗?
|
||||||
|
|
||||||
|
A: **强烈不建议!** 如果系统对外提供JavaScript执行功能,必须进行安全测试。否则可能导致严重的安全事故。
|
||||||
|
|
||||||
|
### Q: Nashorn已经废弃了,应该怎么办?
|
||||||
|
|
||||||
|
A: 建议迁移到 **GraalVM JavaScript**,它提供:
|
||||||
|
- 更好的安全性(默认沙箱)
|
||||||
|
- 更好的性能
|
||||||
|
- 活跃的维护和更新
|
||||||
|
|
||||||
|
## 🆘 需要帮助?
|
||||||
|
|
||||||
|
如果测试发现安全问题或需要修复建议:
|
||||||
|
|
||||||
|
1. 查看详细文档:`doc/SECURITY_TESTING_GUIDE.md`
|
||||||
|
2. 参考HTTP测试用例:`web-service/src/test/resources/playground-security-tests.http`
|
||||||
|
3. 检查JUnit测试代码:`src/test/java/cn/qaiu/parser/SecurityTest.java`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**最后更新**: 2025-11-28
|
||||||
|
**作者**: QAIU
|
||||||
|
**许可**: MIT License
|
||||||
|
|
||||||
@@ -0,0 +1,323 @@
|
|||||||
|
package cn.qaiu.parser.customjs;
|
||||||
|
|
||||||
|
import cn.qaiu.WebClientVertxInit;
|
||||||
|
import cn.qaiu.entity.FileInfo;
|
||||||
|
import cn.qaiu.entity.ShareLinkInfo;
|
||||||
|
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 java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JavaScript演练场执行器
|
||||||
|
* 用于临时执行JavaScript代码,不注册到解析器注册表
|
||||||
|
*
|
||||||
|
* @author <a href="https://qaiu.top">QAIU</a>
|
||||||
|
*/
|
||||||
|
public class JsPlaygroundExecutor {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(JsPlaygroundExecutor.class);
|
||||||
|
|
||||||
|
private static final WorkerExecutor EXECUTOR = WebClientVertxInit.get().createSharedWorkerExecutor("playground-executor", 16);
|
||||||
|
|
||||||
|
private final ShareLinkInfo shareLinkInfo;
|
||||||
|
private final String jsCode;
|
||||||
|
private final ScriptEngine engine;
|
||||||
|
private final JsHttpClient httpClient;
|
||||||
|
private final JsPlaygroundLogger playgroundLogger;
|
||||||
|
private final JsShareLinkInfoWrapper shareLinkInfoWrapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建演练场执行器
|
||||||
|
*
|
||||||
|
* @param shareLinkInfo 分享链接信息
|
||||||
|
* @param jsCode JavaScript代码
|
||||||
|
*/
|
||||||
|
public JsPlaygroundExecutor(ShareLinkInfo shareLinkInfo, String jsCode) {
|
||||||
|
this.shareLinkInfo = shareLinkInfo;
|
||||||
|
this.jsCode = jsCode;
|
||||||
|
|
||||||
|
// 检查是否有代理配置
|
||||||
|
JsonObject proxyConfig = null;
|
||||||
|
if (shareLinkInfo.getOtherParam().containsKey("proxy")) {
|
||||||
|
proxyConfig = (JsonObject) shareLinkInfo.getOtherParam().get("proxy");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.httpClient = new JsHttpClient(proxyConfig);
|
||||||
|
this.playgroundLogger = new JsPlaygroundLogger();
|
||||||
|
this.shareLinkInfoWrapper = new JsShareLinkInfoWrapper(shareLinkInfo);
|
||||||
|
this.engine = initEngine();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化JavaScript引擎(带安全限制)
|
||||||
|
*/
|
||||||
|
private ScriptEngine initEngine() {
|
||||||
|
try {
|
||||||
|
// 使用安全的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可用");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注入Java对象到JavaScript环境
|
||||||
|
engine.put("http", httpClient);
|
||||||
|
engine.put("logger", playgroundLogger);
|
||||||
|
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;");
|
||||||
|
|
||||||
|
playgroundLogger.infoJava("🔒 安全的JavaScript引擎初始化成功(演练场)");
|
||||||
|
|
||||||
|
// 执行JavaScript代码
|
||||||
|
engine.eval(jsCode);
|
||||||
|
|
||||||
|
log.debug("JavaScript引擎初始化成功(演练场)");
|
||||||
|
return engine;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("JavaScript引擎初始化失败(演练场)", e);
|
||||||
|
throw new RuntimeException("JavaScript引擎初始化失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行parse方法(异步)
|
||||||
|
*
|
||||||
|
* @return Future包装的执行结果
|
||||||
|
*/
|
||||||
|
public Future<String> executeParseAsync() {
|
||||||
|
// 在worker线程中执行,避免阻塞事件循环
|
||||||
|
return EXECUTOR.executeBlocking(() -> {
|
||||||
|
playgroundLogger.infoJava("开始执行parse方法");
|
||||||
|
try {
|
||||||
|
Object parseFunction = engine.get("parse");
|
||||||
|
if (parseFunction == null) {
|
||||||
|
playgroundLogger.errorJava("JavaScript代码中未找到parse函数");
|
||||||
|
throw new RuntimeException("JavaScript代码中未找到parse函数");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parseFunction instanceof ScriptObjectMirror parseMirror) {
|
||||||
|
playgroundLogger.debugJava("调用parse函数");
|
||||||
|
log.debug("[JsPlaygroundExecutor] 调用parse函数,当前日志数量: {}", playgroundLogger.size());
|
||||||
|
Object result = parseMirror.call(null, shareLinkInfoWrapper, httpClient, playgroundLogger);
|
||||||
|
log.debug("[JsPlaygroundExecutor] parse函数执行完成,当前日志数量: {}", playgroundLogger.size());
|
||||||
|
|
||||||
|
if (result instanceof String) {
|
||||||
|
playgroundLogger.infoJava("解析成功,返回结果: " + result);
|
||||||
|
return (String) result;
|
||||||
|
} else {
|
||||||
|
String errorMsg = "parse方法返回值类型错误,期望String,实际: " +
|
||||||
|
(result != null ? result.getClass().getSimpleName() : "null");
|
||||||
|
playgroundLogger.errorJava(errorMsg);
|
||||||
|
throw new RuntimeException(errorMsg);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
playgroundLogger.errorJava("parse函数类型错误");
|
||||||
|
throw new RuntimeException("parse函数类型错误");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
playgroundLogger.errorJava("执行parse方法失败: " + e.getMessage(), e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行parseFileList方法(异步)
|
||||||
|
*
|
||||||
|
* @return Future包装的文件列表
|
||||||
|
*/
|
||||||
|
public Future<List<FileInfo>> executeParseFileListAsync() {
|
||||||
|
// 在worker线程中执行,避免阻塞事件循环
|
||||||
|
return EXECUTOR.executeBlocking(() -> {
|
||||||
|
playgroundLogger.infoJava("开始执行parseFileList方法");
|
||||||
|
try {
|
||||||
|
Object parseFileListFunction = engine.get("parseFileList");
|
||||||
|
if (parseFileListFunction == null) {
|
||||||
|
playgroundLogger.errorJava("JavaScript代码中未找到parseFileList函数");
|
||||||
|
throw new RuntimeException("JavaScript代码中未找到parseFileList函数");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parseFileListFunction instanceof ScriptObjectMirror parseFileListMirror) {
|
||||||
|
playgroundLogger.debugJava("调用parseFileList函数");
|
||||||
|
Object result = parseFileListMirror.call(null, shareLinkInfoWrapper, httpClient, playgroundLogger);
|
||||||
|
|
||||||
|
if (result instanceof ScriptObjectMirror resultMirror) {
|
||||||
|
List<FileInfo> fileList = convertToFileInfoList(resultMirror);
|
||||||
|
playgroundLogger.infoJava("文件列表解析成功,共 " + fileList.size() + " 个文件");
|
||||||
|
return fileList;
|
||||||
|
} else {
|
||||||
|
String errorMsg = "parseFileList方法返回值类型错误,期望数组,实际: " +
|
||||||
|
(result != null ? result.getClass().getSimpleName() : "null");
|
||||||
|
playgroundLogger.errorJava(errorMsg);
|
||||||
|
throw new RuntimeException(errorMsg);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
playgroundLogger.errorJava("parseFileList函数类型错误");
|
||||||
|
throw new RuntimeException("parseFileList函数类型错误");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
playgroundLogger.errorJava("执行parseFileList方法失败: " + e.getMessage(), e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行parseById方法(异步)
|
||||||
|
*
|
||||||
|
* @return Future包装的执行结果
|
||||||
|
*/
|
||||||
|
public Future<String> executeParseByIdAsync() {
|
||||||
|
// 在worker线程中执行,避免阻塞事件循环
|
||||||
|
return EXECUTOR.executeBlocking(() -> {
|
||||||
|
playgroundLogger.infoJava("开始执行parseById方法");
|
||||||
|
try {
|
||||||
|
Object parseByIdFunction = engine.get("parseById");
|
||||||
|
if (parseByIdFunction == null) {
|
||||||
|
playgroundLogger.errorJava("JavaScript代码中未找到parseById函数");
|
||||||
|
throw new RuntimeException("JavaScript代码中未找到parseById函数");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parseByIdFunction instanceof ScriptObjectMirror parseByIdMirror) {
|
||||||
|
playgroundLogger.debugJava("调用parseById函数");
|
||||||
|
Object result = parseByIdMirror.call(null, shareLinkInfoWrapper, httpClient, playgroundLogger);
|
||||||
|
|
||||||
|
if (result instanceof String) {
|
||||||
|
playgroundLogger.infoJava("按ID解析成功: " + result);
|
||||||
|
return (String) result;
|
||||||
|
} else {
|
||||||
|
String errorMsg = "parseById方法返回值类型错误,期望String,实际: " +
|
||||||
|
(result != null ? result.getClass().getSimpleName() : "null");
|
||||||
|
playgroundLogger.errorJava(errorMsg);
|
||||||
|
throw new RuntimeException(errorMsg);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
playgroundLogger.errorJava("parseById函数类型错误");
|
||||||
|
throw new RuntimeException("parseById函数类型错误");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
playgroundLogger.errorJava("执行parseById方法失败: " + e.getMessage(), e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取日志列表
|
||||||
|
*/
|
||||||
|
public List<JsPlaygroundLogger.LogEntry> getLogs() {
|
||||||
|
List<JsPlaygroundLogger.LogEntry> logs = playgroundLogger.getLogs();
|
||||||
|
System.out.println("[JsPlaygroundExecutor] 获取日志,数量: " + logs.size());
|
||||||
|
return logs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取ShareLinkInfo对象
|
||||||
|
*/
|
||||||
|
public ShareLinkInfo getShareLinkInfo() {
|
||||||
|
return shareLinkInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将JavaScript对象数组转换为FileInfo列表
|
||||||
|
*/
|
||||||
|
private List<FileInfo> convertToFileInfoList(ScriptObjectMirror resultMirror) {
|
||||||
|
List<FileInfo> fileList = new ArrayList<>();
|
||||||
|
|
||||||
|
if (resultMirror.isArray()) {
|
||||||
|
for (int i = 0; i < resultMirror.size(); i++) {
|
||||||
|
Object item = resultMirror.get(String.valueOf(i));
|
||||||
|
if (item instanceof ScriptObjectMirror) {
|
||||||
|
FileInfo fileInfo = convertToFileInfo((ScriptObjectMirror) item);
|
||||||
|
if (fileInfo != null) {
|
||||||
|
fileList.add(fileInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将JavaScript对象转换为FileInfo
|
||||||
|
*/
|
||||||
|
private FileInfo convertToFileInfo(ScriptObjectMirror itemMirror) {
|
||||||
|
try {
|
||||||
|
FileInfo fileInfo = new FileInfo();
|
||||||
|
|
||||||
|
// 设置基本字段
|
||||||
|
if (itemMirror.hasMember("fileName")) {
|
||||||
|
fileInfo.setFileName(itemMirror.getMember("fileName").toString());
|
||||||
|
}
|
||||||
|
if (itemMirror.hasMember("fileId")) {
|
||||||
|
fileInfo.setFileId(itemMirror.getMember("fileId").toString());
|
||||||
|
}
|
||||||
|
if (itemMirror.hasMember("fileType")) {
|
||||||
|
fileInfo.setFileType(itemMirror.getMember("fileType").toString());
|
||||||
|
}
|
||||||
|
if (itemMirror.hasMember("size")) {
|
||||||
|
Object size = itemMirror.getMember("size");
|
||||||
|
if (size instanceof Number) {
|
||||||
|
fileInfo.setSize(((Number) size).longValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (itemMirror.hasMember("sizeStr")) {
|
||||||
|
fileInfo.setSizeStr(itemMirror.getMember("sizeStr").toString());
|
||||||
|
}
|
||||||
|
if (itemMirror.hasMember("createTime")) {
|
||||||
|
fileInfo.setCreateTime(itemMirror.getMember("createTime").toString());
|
||||||
|
}
|
||||||
|
if (itemMirror.hasMember("updateTime")) {
|
||||||
|
fileInfo.setUpdateTime(itemMirror.getMember("updateTime").toString());
|
||||||
|
}
|
||||||
|
if (itemMirror.hasMember("createBy")) {
|
||||||
|
fileInfo.setCreateBy(itemMirror.getMember("createBy").toString());
|
||||||
|
}
|
||||||
|
if (itemMirror.hasMember("downloadCount")) {
|
||||||
|
Object downloadCount = itemMirror.getMember("downloadCount");
|
||||||
|
if (downloadCount instanceof Number) {
|
||||||
|
fileInfo.setDownloadCount(((Number) downloadCount).intValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (itemMirror.hasMember("fileIcon")) {
|
||||||
|
fileInfo.setFileIcon(itemMirror.getMember("fileIcon").toString());
|
||||||
|
}
|
||||||
|
if (itemMirror.hasMember("panType")) {
|
||||||
|
fileInfo.setPanType(itemMirror.getMember("panType").toString());
|
||||||
|
}
|
||||||
|
if (itemMirror.hasMember("parserUrl")) {
|
||||||
|
fileInfo.setParserUrl(itemMirror.getMember("parserUrl").toString());
|
||||||
|
}
|
||||||
|
if (itemMirror.hasMember("previewUrl")) {
|
||||||
|
fileInfo.setPreviewUrl(itemMirror.getMember("previewUrl").toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileInfo;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
playgroundLogger.errorJava("转换FileInfo对象失败", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
package cn.qaiu.parser.customjs;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 演练场日志收集器
|
||||||
|
* 收集JavaScript执行过程中的日志信息
|
||||||
|
* 注意:为避免Nashorn对Java重载方法的选择问题,所有日志方法都使用Object参数
|
||||||
|
*
|
||||||
|
* @author <a href="https://qaiu.top">QAIU</a>
|
||||||
|
*/
|
||||||
|
public class JsPlaygroundLogger {
|
||||||
|
|
||||||
|
// 使用线程安全的列表
|
||||||
|
private final List<LogEntry> logs = Collections.synchronizedList(new ArrayList<>());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志条目
|
||||||
|
*/
|
||||||
|
public static class LogEntry {
|
||||||
|
private final String level;
|
||||||
|
private final String message;
|
||||||
|
private final long timestamp;
|
||||||
|
private final String source; // "JS" 或 "JAVA"
|
||||||
|
|
||||||
|
public LogEntry(String level, String message, String source) {
|
||||||
|
this.level = level;
|
||||||
|
this.message = message;
|
||||||
|
this.timestamp = System.currentTimeMillis();
|
||||||
|
this.source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLevel() {
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSource() {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将任意对象转为字符串
|
||||||
|
*/
|
||||||
|
private String toString(Object obj) {
|
||||||
|
if (obj == null) {
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
return obj.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录日志(内部方法)
|
||||||
|
* @param level 日志级别
|
||||||
|
* @param message 日志消息
|
||||||
|
* @param source 日志来源:"JS" 或 "JAVA"
|
||||||
|
*/
|
||||||
|
private void log(String level, Object message, String source) {
|
||||||
|
String msg = toString(message);
|
||||||
|
logs.add(new LogEntry(level, msg, source));
|
||||||
|
System.out.println("[" + source + "PlaygroundLogger] " + level + ": " + msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调试日志(供JavaScript调用)
|
||||||
|
* 使用Object参数避免Nashorn重载选择问题
|
||||||
|
*/
|
||||||
|
public void debug(Object message) {
|
||||||
|
log("DEBUG", message, "JS");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 信息日志(供JavaScript调用)
|
||||||
|
* 使用Object参数避免Nashorn重载选择问题
|
||||||
|
*/
|
||||||
|
public void info(Object message) {
|
||||||
|
log("INFO", message, "JS");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 警告日志(供JavaScript调用)
|
||||||
|
* 使用Object参数避免Nashorn重载选择问题
|
||||||
|
*/
|
||||||
|
public void warn(Object message) {
|
||||||
|
log("WARN", message, "JS");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误日志(供JavaScript调用)
|
||||||
|
* 使用Object参数避免Nashorn重载选择问题
|
||||||
|
*/
|
||||||
|
public void error(Object message) {
|
||||||
|
log("ERROR", message, "JS");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误日志(带异常,供JavaScript调用)
|
||||||
|
*/
|
||||||
|
public void error(Object message, Throwable throwable) {
|
||||||
|
String msg = toString(message);
|
||||||
|
if (throwable != null) {
|
||||||
|
msg = msg + ": " + throwable.getMessage();
|
||||||
|
}
|
||||||
|
logs.add(new LogEntry("ERROR", msg, "JS"));
|
||||||
|
System.out.println("[JSPlaygroundLogger] ERROR: " + msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 以下是供Java层调用的内部方法 =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调试日志(供Java层调用)
|
||||||
|
*/
|
||||||
|
public void debugJava(String message) {
|
||||||
|
log("DEBUG", message, "JAVA");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 信息日志(供Java层调用)
|
||||||
|
*/
|
||||||
|
public void infoJava(String message) {
|
||||||
|
log("INFO", message, "JAVA");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 警告日志(供Java层调用)
|
||||||
|
*/
|
||||||
|
public void warnJava(String message) {
|
||||||
|
log("WARN", message, "JAVA");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误日志(供Java层调用)
|
||||||
|
*/
|
||||||
|
public void errorJava(String message) {
|
||||||
|
log("ERROR", message, "JAVA");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误日志(带异常,供Java层调用)
|
||||||
|
*/
|
||||||
|
public void errorJava(String message, Throwable throwable) {
|
||||||
|
String msg = message;
|
||||||
|
if (throwable != null) {
|
||||||
|
msg = msg + ": " + throwable.getMessage();
|
||||||
|
}
|
||||||
|
logs.add(new LogEntry("ERROR", msg, "JAVA"));
|
||||||
|
System.out.println("[JAVAPlaygroundLogger] ERROR: " + msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有日志
|
||||||
|
*/
|
||||||
|
public List<LogEntry> getLogs() {
|
||||||
|
synchronized (logs) {
|
||||||
|
return new ArrayList<>(logs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取日志数量
|
||||||
|
*/
|
||||||
|
public int size() {
|
||||||
|
return logs.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空日志
|
||||||
|
*/
|
||||||
|
public void clear() {
|
||||||
|
logs.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
309
web-front/PLAYGROUND_UI_UPGRADE.md
Normal file
309
web-front/PLAYGROUND_UI_UPGRADE.md
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
# 演练场界面升级完成
|
||||||
|
|
||||||
|
## ✅ 已完成的功能
|
||||||
|
|
||||||
|
### 1. IDE风格工具栏
|
||||||
|
|
||||||
|
**新的工具栏布局**:
|
||||||
|
- 运行按钮(带loading动画)+ 快捷键提示
|
||||||
|
- 保存、格式化按钮组
|
||||||
|
- 主题切换下拉菜单(3种主题)
|
||||||
|
- 全屏按钮
|
||||||
|
- 更多操作下拉菜单
|
||||||
|
|
||||||
|
**改进点**:
|
||||||
|
- 更清晰的视觉层次
|
||||||
|
- 图标 + 文字组合
|
||||||
|
- 快捷键提示(tooltip)
|
||||||
|
- 响应式布局适配
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 全局快捷键系统
|
||||||
|
|
||||||
|
使用 `@vueuse/core` 的 `useMagicKeys` 实现:
|
||||||
|
|
||||||
|
| 快捷键 | 功能 | 实现方式 |
|
||||||
|
|--------|------|---------|
|
||||||
|
| `Ctrl/Cmd + Enter` | 运行测试 | executeTest() |
|
||||||
|
| `Ctrl/Cmd + S` | 保存代码 | saveCode() |
|
||||||
|
| `Shift + Alt + F` | 格式化代码 | formatCode() |
|
||||||
|
| `F11` | 全屏模式 | toggleFullscreen() |
|
||||||
|
| `Ctrl/Cmd + L` | 清空控制台 | clearConsoleLogs() |
|
||||||
|
| `Ctrl/Cmd + R` | 重置代码 | loadTemplate() |
|
||||||
|
| `Ctrl/Cmd + /` | 快捷键帮助 | showShortcutsHelp() |
|
||||||
|
|
||||||
|
**特点**:
|
||||||
|
- 自动阻止浏览器默认行为(Ctrl+S保存、Ctrl+R刷新等)
|
||||||
|
- Mac和Windows都支持
|
||||||
|
- 实时响应,无延迟
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 主题切换系统
|
||||||
|
|
||||||
|
**三种主题**:
|
||||||
|
1. **Light** - 明亮主题(vs编辑器 + 浅色页面)
|
||||||
|
2. **Dark** - 暗色主题(vs-dark编辑器 + 暗色页面)
|
||||||
|
3. **High Contrast** - 高对比度(hc-black编辑器 + 暗色页面)
|
||||||
|
|
||||||
|
**同步切换**:
|
||||||
|
- Monaco编辑器主题
|
||||||
|
- Element Plus页面主题
|
||||||
|
- 自动保存到localStorage
|
||||||
|
|
||||||
|
**切换方式**:
|
||||||
|
- 点击工具栏主题下拉菜单
|
||||||
|
- 图标随主题变化(Sunny/Moon/MostlyCloudy)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 可拖拽分栏布局
|
||||||
|
|
||||||
|
使用 `splitpanes` 库实现:
|
||||||
|
|
||||||
|
**布局结构**:
|
||||||
|
```
|
||||||
|
+------------------------------------------+
|
||||||
|
| [代码编辑器] | [测试参数 + 结果] |
|
||||||
|
| | |
|
||||||
|
| 70% | 30% |
|
||||||
|
| 可拖拽调整 ← → | |
|
||||||
|
+------------------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
**特点**:
|
||||||
|
- 左右分栏可拖拽调整大小
|
||||||
|
- 最小宽度限制(30% - 20%)
|
||||||
|
- 平滑过渡动画
|
||||||
|
- 响应式适配
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. 区域折叠功能
|
||||||
|
|
||||||
|
**可折叠的区域**:
|
||||||
|
1. ✅ 右侧整体面板 - 折叠后编辑器占满全屏
|
||||||
|
2. ✅ 测试参数卡片 - 独立折叠
|
||||||
|
3. ✅ 测试结果卡片 - 独立折叠
|
||||||
|
4. ✅ 控制台日志卡片 - 独立折叠
|
||||||
|
5. ✅ 使用说明卡片 - 默认折叠
|
||||||
|
|
||||||
|
**折叠按钮**:
|
||||||
|
- 卡片header右侧的箭头按钮
|
||||||
|
- 右侧整体面板:左侧边缘的折叠按钮
|
||||||
|
- 折叠后:固定的展开按钮
|
||||||
|
|
||||||
|
**状态持久化**:
|
||||||
|
- 自动保存到localStorage
|
||||||
|
- 页面刷新后保持折叠状态
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. 全屏模式
|
||||||
|
|
||||||
|
**实现方式**:
|
||||||
|
- 使用 `@vueuse/core` 的 `useFullscreen`
|
||||||
|
- 支持浏览器原生全屏API
|
||||||
|
|
||||||
|
**触发方式**:
|
||||||
|
- F11快捷键
|
||||||
|
- 工具栏全屏按钮
|
||||||
|
- 图标随状态变化
|
||||||
|
|
||||||
|
**效果**:
|
||||||
|
- 容器填充整个屏幕
|
||||||
|
- 自动调整padding为0
|
||||||
|
- z-index提升到最高层
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. 快捷键帮助弹窗
|
||||||
|
|
||||||
|
**内容**:
|
||||||
|
- 表格形式展示所有快捷键
|
||||||
|
- 功能名称 + 快捷键标签
|
||||||
|
|
||||||
|
**触发方式**:
|
||||||
|
- Ctrl/Cmd + / 快捷键
|
||||||
|
- 工具栏"更多"菜单中的"快捷键"选项
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. UI/UX改进
|
||||||
|
|
||||||
|
**视觉优化**:
|
||||||
|
- 使用CSS变量适配明暗主题
|
||||||
|
- 平滑的过渡动画(0.3s cubic-bezier)
|
||||||
|
- 悬停效果优化
|
||||||
|
- 按钮点击缩放反馈
|
||||||
|
- 改进的滚动条样式
|
||||||
|
|
||||||
|
**交互优化**:
|
||||||
|
- 控制台显示日志数量标签
|
||||||
|
- JS日志特殊样式(绿色主题)
|
||||||
|
- 卡片悬停阴影效果
|
||||||
|
- 更好的视觉层次
|
||||||
|
|
||||||
|
**响应式设计**:
|
||||||
|
- 移动端自动调整布局
|
||||||
|
- 小屏幕优化
|
||||||
|
- 触摸设备友好
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 新增的UI元素
|
||||||
|
|
||||||
|
### 工具栏
|
||||||
|
- 运行按钮(CaretRight图标 + loading状态)
|
||||||
|
- 按钮组(视觉分组)
|
||||||
|
- 主题切换下拉菜单(带图标)
|
||||||
|
- 全屏按钮
|
||||||
|
- 更多操作菜单
|
||||||
|
|
||||||
|
### 折叠按钮
|
||||||
|
- 右侧面板折叠按钮(蓝色浮动按钮)
|
||||||
|
- 卡片折叠箭头(ArrowUp/ArrowDown)
|
||||||
|
- 展开按钮(固定在右侧边缘)
|
||||||
|
|
||||||
|
### 状态指示
|
||||||
|
- 控制台日志数量标签
|
||||||
|
- 主题名称显示
|
||||||
|
- 加载状态动画
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 技术实现
|
||||||
|
|
||||||
|
### 依赖库
|
||||||
|
- `@vueuse/core` - 快捷键、全屏API
|
||||||
|
- `splitpanes` - 可拖拽分栏
|
||||||
|
- `element-plus` - UI组件库
|
||||||
|
- `vue3-json-viewer` - JSON查看器
|
||||||
|
|
||||||
|
### 核心代码
|
||||||
|
|
||||||
|
**快捷键系统**:
|
||||||
|
```javascript
|
||||||
|
import { useMagicKeys, useFullscreen, useEventListener } from '@vueuse/core';
|
||||||
|
|
||||||
|
const keys = useMagicKeys();
|
||||||
|
const ctrlEnter = keys['Ctrl+Enter'];
|
||||||
|
|
||||||
|
watch(ctrlEnter, (pressed) => {
|
||||||
|
if (pressed) executeTest();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**折叠功能**:
|
||||||
|
```javascript
|
||||||
|
const collapsedPanels = ref({
|
||||||
|
rightPanel: false,
|
||||||
|
testParams: false,
|
||||||
|
testResult: false,
|
||||||
|
console: false,
|
||||||
|
help: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const togglePanel = (panelName) => {
|
||||||
|
collapsedPanels.value[panelName] = !collapsedPanels.value[panelName];
|
||||||
|
localStorage.setItem('playground_collapsed_panels', JSON.stringify(collapsedPanels.value));
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**主题切换**:
|
||||||
|
```javascript
|
||||||
|
const changeTheme = (themeName) => {
|
||||||
|
const theme = themes.find(t => t.name === themeName);
|
||||||
|
if (theme.page === 'dark') {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
localStorage.setItem('playground_theme', themeName);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 改进对比
|
||||||
|
|
||||||
|
| 特性 | 改进前 | 改进后 |
|
||||||
|
|------|--------|--------|
|
||||||
|
| 工具栏 | 简单按钮排列 | IDE风格分组工具栏 |
|
||||||
|
| 布局 | 固定16:8比例 | 可拖拽调整Splitpanes |
|
||||||
|
| 折叠 | 仅使用说明可折叠 | 所有区域可独立折叠 |
|
||||||
|
| 快捷键 | 无 | 7个常用快捷键 |
|
||||||
|
| 主题 | 跟随系统 | 3种主题自由切换 |
|
||||||
|
| 全屏 | 无 | 支持F11全屏模式 |
|
||||||
|
| 响应式 | 基础 | 完整的移动端适配 |
|
||||||
|
| 动画 | 无 | 平滑的折叠/展开动画 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 如何使用新功能
|
||||||
|
|
||||||
|
### 主题切换
|
||||||
|
1. 点击工具栏的主题按钮
|
||||||
|
2. 选择Light/Dark/High Contrast
|
||||||
|
3. 编辑器和页面同步切换
|
||||||
|
|
||||||
|
### 折叠面板
|
||||||
|
1. 点击卡片header的箭头按钮折叠该卡片
|
||||||
|
2. 点击右侧边缘的按钮折叠整个右侧面板
|
||||||
|
3. 折叠后点击浮动按钮展开
|
||||||
|
|
||||||
|
### 调整布局
|
||||||
|
1. 拖拽中间的分隔线调整左右比例
|
||||||
|
2. 右侧面板折叠后编辑器自动占满
|
||||||
|
|
||||||
|
### 使用快捷键
|
||||||
|
1. 按 `Ctrl+/` 查看所有快捷键
|
||||||
|
2. 使用快捷键快速操作
|
||||||
|
3. 工具提示会显示对应的快捷键
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 下一步
|
||||||
|
|
||||||
|
1. **重新编译前端**:
|
||||||
|
```bash
|
||||||
|
cd web-front
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **复制到部署目录**:
|
||||||
|
```bash
|
||||||
|
cp -r nfd-front/* ../webroot/nfd-front/
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **测试功能**:
|
||||||
|
- 打开演练场页面
|
||||||
|
- 测试所有快捷键
|
||||||
|
- 测试主题切换
|
||||||
|
- 测试折叠功能
|
||||||
|
- 测试全屏模式
|
||||||
|
- 测试拖拽调整布局
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 已知问题
|
||||||
|
|
||||||
|
无
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 使用提示
|
||||||
|
|
||||||
|
1. **首次使用**: 点击"快捷键"按钮查看所有可用快捷键
|
||||||
|
2. **调整布局**: 拖拽分隔线找到最适合你的布局
|
||||||
|
3. **专注编码**: 折叠右侧面板获得更大编辑空间
|
||||||
|
4. **保护眼睛**: 使用暗色主题减少疲劳
|
||||||
|
5. **快速测试**: Ctrl+Enter直接运行,无需鼠标
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**升级日期**: 2025-11-29
|
||||||
|
**版本**: v2.0
|
||||||
|
**状态**: ✅ 完成
|
||||||
|
|
||||||
196
web-front/src/components/MonacoEditor.vue
Normal file
196
web-front/src/components/MonacoEditor.vue
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="editorContainer" class="monaco-editor-container"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MonacoEditor',
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
language: {
|
||||||
|
type: String,
|
||||||
|
default: 'javascript'
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
type: String,
|
||||||
|
default: 'vs'
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: '500px'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue', 'change'],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const editorContainer = ref(null);
|
||||||
|
let editor = null;
|
||||||
|
let monaco = null;
|
||||||
|
|
||||||
|
const defaultOptions = {
|
||||||
|
value: props.modelValue,
|
||||||
|
language: props.language,
|
||||||
|
theme: props.theme,
|
||||||
|
automaticLayout: true,
|
||||||
|
fontSize: 14,
|
||||||
|
minimap: {
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
wordWrap: 'on',
|
||||||
|
lineNumbers: 'on',
|
||||||
|
roundedSelection: false,
|
||||||
|
readOnly: false,
|
||||||
|
cursorStyle: 'line',
|
||||||
|
formatOnPaste: true,
|
||||||
|
formatOnType: true,
|
||||||
|
tabSize: 2,
|
||||||
|
insertSpaces: true,
|
||||||
|
...props.options
|
||||||
|
};
|
||||||
|
|
||||||
|
const initEditor = async () => {
|
||||||
|
try {
|
||||||
|
if (!editorContainer.value) {
|
||||||
|
console.error('编辑器容器未找到');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动态导入monaco-editor loader
|
||||||
|
let loaderModule;
|
||||||
|
try {
|
||||||
|
loaderModule = await import('@monaco-editor/loader');
|
||||||
|
} catch (importError) {
|
||||||
|
console.error('导入@monaco-editor/loader失败:', importError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取loader对象
|
||||||
|
// @monaco-editor/loader可能使用default导出或named导出
|
||||||
|
let loader;
|
||||||
|
if (loaderModule.default) {
|
||||||
|
loader = loaderModule.default;
|
||||||
|
} else if (loaderModule.loader) {
|
||||||
|
loader = loaderModule.loader;
|
||||||
|
} else {
|
||||||
|
loader = loaderModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loader) {
|
||||||
|
console.error('Monaco Editor loader未找到,loaderModule:', loaderModule);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof loader.init !== 'function') {
|
||||||
|
console.error('loader.init不是函数,loader对象:', loader);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化Monaco Editor
|
||||||
|
monaco = await loader.init();
|
||||||
|
|
||||||
|
if (!monaco) {
|
||||||
|
console.error('loader.init返回null或undefined');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!monaco.editor) {
|
||||||
|
console.error('monaco.editor不存在,monaco对象:', monaco);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
editor = monaco.editor.create(editorContainer.value, {
|
||||||
|
...defaultOptions,
|
||||||
|
value: props.modelValue
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听内容变化
|
||||||
|
editor.onDidChangeModelContent(() => {
|
||||||
|
const value = editor.getValue();
|
||||||
|
emit('update:modelValue', value);
|
||||||
|
emit('change', value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置容器高度
|
||||||
|
if (editorContainer.value) {
|
||||||
|
editorContainer.value.style.height = props.height;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Monaco Editor初始化失败:', error);
|
||||||
|
console.error('错误详情:', error.stack);
|
||||||
|
console.error('错误对象:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTheme = (newTheme) => {
|
||||||
|
if (editor) {
|
||||||
|
monaco.editor.setTheme(newTheme);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDocument = () => {
|
||||||
|
if (editor) {
|
||||||
|
editor.getAction('editor.action.formatDocument').run();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (newValue) => {
|
||||||
|
if (editor && editor.getValue() !== newValue) {
|
||||||
|
editor.setValue(newValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.theme, (newTheme) => {
|
||||||
|
updateTheme(newTheme);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.height, (newHeight) => {
|
||||||
|
if (editorContainer.value) {
|
||||||
|
editorContainer.value.style.height = newHeight;
|
||||||
|
if (editor) {
|
||||||
|
editor.layout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initEditor();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (editor) {
|
||||||
|
editor.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
editorContainer,
|
||||||
|
formatDocument,
|
||||||
|
getEditor: () => editor,
|
||||||
|
getMonaco: () => monaco
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.monaco-editor-container {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monaco-editor-container :deep(.monaco-editor) {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
359
web-front/src/utils/monacoTypes.js
Normal file
359
web-front/src/utils/monacoTypes.js
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
/**
|
||||||
|
* Monaco Editor 代码补全配置工具
|
||||||
|
* 基于 types.js 提供完整的代码补全支持
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置Monaco Editor的类型定义和代码补全
|
||||||
|
* @param {monaco} monaco - Monaco Editor实例
|
||||||
|
*/
|
||||||
|
export async function configureMonacoTypes(monaco) {
|
||||||
|
if (!monaco) {
|
||||||
|
console.warn('Monaco Editor未初始化');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册JavaScript语言特性
|
||||||
|
monaco.languages.setLanguageConfiguration('javascript', {
|
||||||
|
comments: {
|
||||||
|
lineComment: '//',
|
||||||
|
blockComment: ['/*', '*/']
|
||||||
|
},
|
||||||
|
brackets: [
|
||||||
|
['{', '}'],
|
||||||
|
['[', ']'],
|
||||||
|
['(', ')']
|
||||||
|
],
|
||||||
|
autoClosingPairs: [
|
||||||
|
{ open: '{', close: '}' },
|
||||||
|
{ open: '[', close: ']' },
|
||||||
|
{ open: '(', close: ')' },
|
||||||
|
{ open: '"', close: '"' },
|
||||||
|
{ open: "'", close: "'" }
|
||||||
|
],
|
||||||
|
surroundingPairs: [
|
||||||
|
{ open: '{', close: '}' },
|
||||||
|
{ open: '[', close: ']' },
|
||||||
|
{ open: '(', close: ')' },
|
||||||
|
{ open: '"', close: '"' },
|
||||||
|
{ open: "'", close: "'" }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册类型定义
|
||||||
|
registerTypeDefinitions(monaco);
|
||||||
|
|
||||||
|
// 注册代码补全提供者
|
||||||
|
registerCompletionProvider(monaco);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册类型定义
|
||||||
|
*/
|
||||||
|
function registerTypeDefinitions(monaco) {
|
||||||
|
// ShareLinkInfo类型定义
|
||||||
|
const shareLinkInfoType = `
|
||||||
|
interface ShareLinkInfo {
|
||||||
|
getShareUrl(): string;
|
||||||
|
getShareKey(): string;
|
||||||
|
getSharePassword(): string;
|
||||||
|
getType(): string;
|
||||||
|
getPanName(): string;
|
||||||
|
getOtherParam(key: string): any;
|
||||||
|
hasOtherParam(key: string): boolean;
|
||||||
|
getOtherParamAsString(key: string): string | null;
|
||||||
|
getOtherParamAsInteger(key: string): number | null;
|
||||||
|
getOtherParamAsBoolean(key: string): boolean | null;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// JsHttpClient类型定义
|
||||||
|
const httpClientType = `
|
||||||
|
interface JsHttpClient {
|
||||||
|
get(url: string): JsHttpResponse;
|
||||||
|
getWithRedirect(url: string): JsHttpResponse;
|
||||||
|
getNoRedirect(url: string): JsHttpResponse;
|
||||||
|
post(url: string, data?: any): JsHttpResponse;
|
||||||
|
put(url: string, data?: any): JsHttpResponse;
|
||||||
|
delete(url: string): JsHttpResponse;
|
||||||
|
patch(url: string, data?: any): JsHttpResponse;
|
||||||
|
putHeader(name: string, value: string): JsHttpClient;
|
||||||
|
putHeaders(headers: Record<string, string>): JsHttpClient;
|
||||||
|
removeHeader(name: string): JsHttpClient;
|
||||||
|
clearHeaders(): JsHttpClient;
|
||||||
|
getHeaders(): Record<string, string>;
|
||||||
|
setTimeout(seconds: number): JsHttpClient;
|
||||||
|
sendForm(data: Record<string, any>): JsHttpResponse;
|
||||||
|
sendMultipartForm(url: string, data: Record<string, any>): JsHttpResponse;
|
||||||
|
sendJson(data: any): JsHttpResponse;
|
||||||
|
urlEncode(str: string): string;
|
||||||
|
urlDecode(str: string): string;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// JsHttpResponse类型定义
|
||||||
|
const httpResponseType = `
|
||||||
|
interface JsHttpResponse {
|
||||||
|
body(): string;
|
||||||
|
json(): any;
|
||||||
|
statusCode(): number;
|
||||||
|
header(name: string): string | null;
|
||||||
|
headers(): Record<string, string>;
|
||||||
|
isSuccess(): boolean;
|
||||||
|
bodyBytes(): number[];
|
||||||
|
bodySize(): number;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// JsLogger类型定义
|
||||||
|
const loggerType = `
|
||||||
|
interface JsLogger {
|
||||||
|
debug(message: string, ...args: any[]): void;
|
||||||
|
info(message: string, ...args: any[]): void;
|
||||||
|
warn(message: string, ...args: any[]): void;
|
||||||
|
error(message: string, ...args: any[]): void;
|
||||||
|
isDebugEnabled(): boolean;
|
||||||
|
isInfoEnabled(): boolean;
|
||||||
|
isWarnEnabled(): boolean;
|
||||||
|
isErrorEnabled(): boolean;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// FileInfo类型定义
|
||||||
|
const fileInfoType = `
|
||||||
|
interface FileInfo {
|
||||||
|
fileName: string;
|
||||||
|
fileId: string;
|
||||||
|
fileType: 'file' | 'folder';
|
||||||
|
size: number;
|
||||||
|
sizeStr: string;
|
||||||
|
createTime: string;
|
||||||
|
updateTime?: string;
|
||||||
|
createBy?: string;
|
||||||
|
downloadCount?: number;
|
||||||
|
fileIcon?: string;
|
||||||
|
panType?: string;
|
||||||
|
parserUrl?: string;
|
||||||
|
previewUrl?: string;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 合并所有类型定义
|
||||||
|
const allTypes = `
|
||||||
|
${shareLinkInfoType}
|
||||||
|
${httpClientType}
|
||||||
|
${httpResponseType}
|
||||||
|
${loggerType}
|
||||||
|
${fileInfoType}
|
||||||
|
|
||||||
|
// 全局变量声明
|
||||||
|
declare var shareLinkInfo: ShareLinkInfo;
|
||||||
|
declare var http: JsHttpClient;
|
||||||
|
declare var logger: JsLogger;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 注册类型定义到Monaco
|
||||||
|
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||||
|
allTypes,
|
||||||
|
'file:///types.d.ts'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册代码补全提供者
|
||||||
|
*/
|
||||||
|
function registerCompletionProvider(monaco) {
|
||||||
|
monaco.languages.registerCompletionItemProvider('javascript', {
|
||||||
|
provideCompletionItems: (model, position) => {
|
||||||
|
const word = model.getWordUntilPosition(position);
|
||||||
|
const range = {
|
||||||
|
startLineNumber: position.lineNumber,
|
||||||
|
endLineNumber: position.lineNumber,
|
||||||
|
startColumn: word.startColumn,
|
||||||
|
endColumn: word.endColumn
|
||||||
|
};
|
||||||
|
|
||||||
|
const suggestions = [
|
||||||
|
// ShareLinkInfo方法
|
||||||
|
{
|
||||||
|
label: 'shareLinkInfo.getShareUrl()',
|
||||||
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
|
insertText: 'shareLinkInfo.getShareUrl()',
|
||||||
|
documentation: '获取分享URL',
|
||||||
|
range
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'shareLinkInfo.getShareKey()',
|
||||||
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
|
insertText: 'shareLinkInfo.getShareKey()',
|
||||||
|
documentation: '获取分享Key',
|
||||||
|
range
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'shareLinkInfo.getSharePassword()',
|
||||||
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
|
insertText: 'shareLinkInfo.getSharePassword()',
|
||||||
|
documentation: '获取分享密码',
|
||||||
|
range
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'shareLinkInfo.getType()',
|
||||||
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
|
insertText: 'shareLinkInfo.getType()',
|
||||||
|
documentation: '获取网盘类型',
|
||||||
|
range
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'shareLinkInfo.getPanName()',
|
||||||
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
|
insertText: 'shareLinkInfo.getPanName()',
|
||||||
|
documentation: '获取网盘名称',
|
||||||
|
range
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'shareLinkInfo.getOtherParam(key)',
|
||||||
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
|
insertText: 'shareLinkInfo.getOtherParam(${1:key})',
|
||||||
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
|
documentation: '获取其他参数',
|
||||||
|
range
|
||||||
|
},
|
||||||
|
// JsHttpClient方法
|
||||||
|
{
|
||||||
|
label: 'http.get(url)',
|
||||||
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
|
insertText: 'http.get(${1:url})',
|
||||||
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
|
documentation: '发起GET请求',
|
||||||
|
range
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'http.post(url, data)',
|
||||||
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
|
insertText: 'http.post(${1:url}, ${2:data})',
|
||||||
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
|
documentation: '发起POST请求',
|
||||||
|
range
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'http.putHeader(name, value)',
|
||||||
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
|
insertText: 'http.putHeader(${1:name}, ${2:value})',
|
||||||
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
|
documentation: '设置请求头',
|
||||||
|
range
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'http.sendForm(data)',
|
||||||
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
|
insertText: 'http.sendForm(${1:data})',
|
||||||
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
|
documentation: '发送表单数据',
|
||||||
|
range
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'http.sendJson(data)',
|
||||||
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
|
insertText: 'http.sendJson(${1:data})',
|
||||||
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
|
documentation: '发送JSON数据',
|
||||||
|
range
|
||||||
|
},
|
||||||
|
// JsLogger方法
|
||||||
|
{
|
||||||
|
label: 'logger.info(message)',
|
||||||
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
|
insertText: 'logger.info(${1:message})',
|
||||||
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
|
documentation: '记录信息日志',
|
||||||
|
range
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'logger.debug(message)',
|
||||||
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
|
insertText: 'logger.debug(${1:message})',
|
||||||
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
|
documentation: '记录调试日志',
|
||||||
|
range
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'logger.warn(message)',
|
||||||
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
|
insertText: 'logger.warn(${1:message})',
|
||||||
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
|
documentation: '记录警告日志',
|
||||||
|
range
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'logger.error(message)',
|
||||||
|
kind: monaco.languages.CompletionItemKind.Method,
|
||||||
|
insertText: 'logger.error(${1:message})',
|
||||||
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
|
documentation: '记录错误日志',
|
||||||
|
range
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return { suggestions };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从API获取types.js内容并配置
|
||||||
|
*/
|
||||||
|
export async function loadTypesFromApi(monaco) {
|
||||||
|
try {
|
||||||
|
// 先尝试从缓存加载
|
||||||
|
const cacheKey = 'playground_types_js';
|
||||||
|
const cachedContent = localStorage.getItem(cacheKey);
|
||||||
|
if (cachedContent) {
|
||||||
|
try {
|
||||||
|
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||||
|
cachedContent,
|
||||||
|
'file:///types.js'
|
||||||
|
);
|
||||||
|
console.log('从缓存加载types.js成功');
|
||||||
|
// 异步更新缓存
|
||||||
|
updateTypesJsCache();
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('使用缓存的types.js失败,重新加载:', error);
|
||||||
|
localStorage.removeItem(cacheKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从API加载
|
||||||
|
const response = await fetch('/v2/playground/types.js');
|
||||||
|
if (response.ok) {
|
||||||
|
const typesJsContent = await response.text();
|
||||||
|
// 缓存到localStorage
|
||||||
|
localStorage.setItem(cacheKey, typesJsContent);
|
||||||
|
// 添加到类型定义中
|
||||||
|
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||||
|
typesJsContent,
|
||||||
|
'file:///types.js'
|
||||||
|
);
|
||||||
|
console.log('加载types.js成功并已缓存');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('加载types.js失败,使用内置类型定义:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步更新types.js缓存
|
||||||
|
*/
|
||||||
|
async function updateTypesJsCache() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/v2/playground/types.js');
|
||||||
|
if (response.ok) {
|
||||||
|
const typesJsContent = await response.text();
|
||||||
|
localStorage.setItem('playground_types_js', typesJsContent);
|
||||||
|
console.log('types.js缓存已更新');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('更新types.js缓存失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
146
web-front/src/utils/playgroundApi.js
Normal file
146
web-front/src/utils/playgroundApi.js
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 演练场API服务
|
||||||
|
*/
|
||||||
|
export const playgroundApi = {
|
||||||
|
/**
|
||||||
|
* 测试执行JavaScript代码
|
||||||
|
* @param {string} jsCode - JavaScript代码
|
||||||
|
* @param {string} shareUrl - 分享链接
|
||||||
|
* @param {string} pwd - 密码(可选)
|
||||||
|
* @param {string} method - 测试方法:parse/parseFileList/parseById
|
||||||
|
* @returns {Promise} 测试结果
|
||||||
|
*/
|
||||||
|
async testScript(jsCode, shareUrl, pwd = '', method = 'parse') {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/v2/playground/test', {
|
||||||
|
jsCode,
|
||||||
|
shareUrl,
|
||||||
|
pwd,
|
||||||
|
method
|
||||||
|
});
|
||||||
|
// 框架会自动包装成JsonResult,需要从data字段获取
|
||||||
|
if (response.data && response.data.data) {
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
// 如果没有包装,直接返回
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = error.response?.data?.data?.error ||
|
||||||
|
error.response?.data?.error ||
|
||||||
|
error.response?.data?.msg ||
|
||||||
|
error.message ||
|
||||||
|
'测试执行失败';
|
||||||
|
throw new Error(errorMsg);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取types.js文件内容
|
||||||
|
* @returns {Promise<string>} types.js内容
|
||||||
|
*/
|
||||||
|
async getTypesJs() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/v2/playground/types.js', {
|
||||||
|
responseType: 'text'
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error.response?.data?.error || error.message || '获取types.js失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取解析器列表
|
||||||
|
*/
|
||||||
|
async getParserList() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/v2/playground/parsers');
|
||||||
|
// 框架会自动包装成JsonResult,需要从data字段获取
|
||||||
|
if (response.data && response.data.data) {
|
||||||
|
return {
|
||||||
|
code: response.data.code || 200,
|
||||||
|
data: response.data.data,
|
||||||
|
msg: response.data.msg,
|
||||||
|
success: response.data.success
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error.response?.data?.error || error.response?.data?.msg || error.message || '获取解析器列表失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存解析器
|
||||||
|
*/
|
||||||
|
async saveParser(jsCode) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/v2/playground/parsers', { jsCode });
|
||||||
|
// 框架会自动包装成JsonResult
|
||||||
|
if (response.data && response.data.data) {
|
||||||
|
return {
|
||||||
|
code: response.data.code || 200,
|
||||||
|
data: response.data.data,
|
||||||
|
msg: response.data.msg,
|
||||||
|
success: response.data.success
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = error.response?.data?.data?.error ||
|
||||||
|
error.response?.data?.error ||
|
||||||
|
error.response?.data?.msg ||
|
||||||
|
error.message ||
|
||||||
|
'保存解析器失败';
|
||||||
|
throw new Error(errorMsg);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新解析器
|
||||||
|
*/
|
||||||
|
async updateParser(id, jsCode, enabled = true) {
|
||||||
|
try {
|
||||||
|
const response = await axios.put(`/v2/playground/parsers/${id}`, { jsCode, enabled });
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error.response?.data?.error || error.message || '更新解析器失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除解析器
|
||||||
|
*/
|
||||||
|
async deleteParser(id) {
|
||||||
|
try {
|
||||||
|
const response = await axios.delete(`/v2/playground/parsers/${id}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error.response?.data?.error || error.message || '删除解析器失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID获取解析器
|
||||||
|
*/
|
||||||
|
async getParserById(id) {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/v2/playground/parsers/${id}`);
|
||||||
|
// 框架会自动包装成JsonResult
|
||||||
|
if (response.data && response.data.data) {
|
||||||
|
return {
|
||||||
|
code: response.data.code || 200,
|
||||||
|
data: response.data.data,
|
||||||
|
msg: response.data.msg,
|
||||||
|
success: response.data.success
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error.response?.data?.error || error.response?.data?.msg || error.message || '获取解析器失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Reference in New Issue
Block a user