Compare commits

..

7 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
bf93f0302a Add comprehensive testing guide for playground access control
Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2025-12-07 05:56:27 +00:00
copilot-swe-agent[bot]
a004becc95 Fix cookie parsing condition to properly handle missing semicolon
Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2025-12-07 05:52:17 +00:00
copilot-swe-agent[bot]
8c92150c81 Address code review feedback: improve error handling and add missing import
Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2025-12-07 05:50:34 +00:00
copilot-swe-agent[bot]
f673a4914e Add comprehensive documentation for playground access control
Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2025-12-07 05:47:38 +00:00
copilot-swe-agent[bot]
367c7f35ec Implement playground access control with configuration, authentication, and UI
Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2025-12-07 05:44:04 +00:00
copilot-swe-agent[bot]
32beb4f2f2 Initial plan 2025-12-07 05:34:20 +00:00
copilot-swe-agent[bot]
442ae2c2af Initial plan 2025-12-07 05:32:44 +00:00
19 changed files with 1130 additions and 1865 deletions

View File

@@ -1,275 +0,0 @@
# Implementation Summary
## Overview
Successfully implemented the backend portion of a browser-based TypeScript compilation solution for the netdisk-fast-download project. This implementation provides standard `fetch` API and `Promise` polyfills for the ES5 JavaScript engine (Nashorn), enabling modern JavaScript patterns in a legacy execution environment.
## What Was Implemented
### 1. Promise Polyfill (ES5 Compatible)
**File:** `parser/src/main/resources/fetch-runtime.js`
A complete Promise/A+ implementation that runs in ES5 environments:
-`new Promise(executor)` constructor
-`promise.then(onFulfilled, onRejected)` with chaining
-`promise.catch(onRejected)` error handling
-`promise.finally(onFinally)` cleanup
-`Promise.resolve(value)` static method
-`Promise.reject(reason)` static method
-`Promise.all(promises)` parallel execution
-`Promise.race(promises)` with correct edge case handling
**Key Features:**
- Pure ES5 syntax (no ES6+ features)
- Uses `setTimeout(fn, 0)` for async execution
- Handles Promise chaining and nesting
- Proper error propagation
### 2. Fetch API Polyfill
**File:** `parser/src/main/resources/fetch-runtime.js`
Standard fetch API implementation that bridges to JsHttpClient:
- ✅ All HTTP methods: GET, POST, PUT, DELETE, PATCH, HEAD
- ✅ Request options: method, headers, body
- ✅ Response object with:
- `text()` - returns Promise<string>
- `json()` - returns Promise<object>
- `arrayBuffer()` - returns Promise<ArrayBuffer>
- `status` - HTTP status code
- `ok` - boolean (2xx = true)
- `statusText` - proper HTTP status text mapping
- `headers` - response headers access
**Standards Compliance:**
- Follows Fetch API specification
- Proper HTTP status text for common codes (200, 404, 500, etc.)
- Handles request/response conversion correctly
### 3. Java Bridge Layer
**File:** `parser/src/main/java/cn/qaiu/parser/customjs/JsFetchBridge.java`
Java class that connects fetch API calls to the existing JsHttpClient:
- ✅ Receives fetch options (method, headers, body)
- ✅ Converts to JsHttpClient calls
- ✅ Returns JsHttpResponse objects
- ✅ Inherits SSRF protection
- ✅ Supports proxy configuration
**Integration:**
- Seamless with existing infrastructure
- No breaking changes to current code
- Extends functionality without modification
### 4. Auto-Injection System
**Files:**
- `parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java`
- `parser/src/main/java/cn/qaiu/parser/customjs/JsPlaygroundExecutor.java`
Automatic injection of fetch runtime into JavaScript engines:
- ✅ Loads fetch-runtime.js on engine initialization
- ✅ Injects `JavaFetch` bridge object
- ✅ Lazy-loaded and cached for performance
- ✅ Works in both parser and playground contexts
**Benefits:**
- Zero configuration required
- Transparent to end users
- Coexists with existing `http` object
### 5. Documentation and Examples
**Documentation Files:**
- `parser/doc/TYPESCRIPT_ES5_IMPLEMENTATION.md` - Implementation overview
- `parser/doc/TYPESCRIPT_FETCH_GUIDE.md` - Detailed usage guide
**Example Files:**
- `parser/src/main/resources/custom-parsers/fetch-demo.js` - Working example
**Test Files:**
- `parser/src/test/java/cn/qaiu/parser/customjs/JsFetchBridgeTest.java` - Unit tests
## What Can Users Do Now
### Current Capabilities
Users can write ES5 JavaScript with modern async patterns:
```javascript
function parse(shareLinkInfo, http, logger) {
// Use Promise
var promise = new Promise(function(resolve, reject) {
resolve("data");
});
promise.then(function(data) {
logger.info("Got: " + data);
});
// Use fetch
fetch("https://api.example.com/data")
.then(function(response) {
return response.json();
})
.then(function(data) {
logger.info("Downloaded: " + data.url);
})
.catch(function(error) {
logger.error("Error: " + error.message);
});
}
```
### Future Capabilities (with Frontend Implementation)
Once TypeScript compilation is added to the frontend:
```typescript
async function parse(
shareLinkInfo: ShareLinkInfo,
http: JsHttpClient,
logger: JsLogger
): Promise<string> {
try {
const response = await fetch("https://api.example.com/data");
const data = await response.json();
return data.url;
} catch (error) {
logger.error(`Error: ${error.message}`);
throw error;
}
}
```
The frontend would compile this to ES5, which would then execute using the fetch polyfill.
## What Remains To Be Done
### Frontend TypeScript Compilation (Not Implemented)
To complete the full solution, the frontend needs:
1. **Add TypeScript Compiler**
```bash
cd web-front
npm install typescript
```
2. **Create Compilation Utility**
```javascript
// web-front/src/utils/tsCompiler.js
import * as ts from 'typescript';
export function compileToES5(sourceCode, fileName = 'script.ts') {
const result = ts.transpileModule(sourceCode, {
compilerOptions: {
target: ts.ScriptTarget.ES5,
module: ts.ModuleKind.None,
lib: ['es5', 'dom']
},
fileName
});
return result;
}
```
3. **Update Playground UI**
- Add language selector (JavaScript / TypeScript)
- Pre-compile TypeScript before sending to backend
- Display compilation errors
- Optionally show compiled ES5 code
## Technical Details
### Architecture
```
Browser Backend
-------- -------
TypeScript Code (future) -->
↓ tsc compile (future)
ES5 + fetch() calls --> Nashorn Engine
↓ fetch-runtime.js loaded
↓ JavaFetch injected
fetch() call
JavaFetch bridge
JsHttpClient
Vert.x HTTP Client
```
### Performance
- **Fetch runtime caching:** Loaded once, cached in static variable
- **Promise async execution:** Non-blocking via setTimeout(0)
- **Worker thread pools:** Prevents blocking Event Loop
- **Lazy loading:** Only loads when needed
### Security
- ✅ **SSRF Protection:** Inherited from JsHttpClient
- Blocks internal IPs (127.0.0.1, 10.x.x.x, 192.168.x.x)
- Blocks cloud metadata APIs (169.254.169.254)
- DNS resolution checks
- ✅ **Sandbox Isolation:** SecurityClassFilter restricts class access
- ✅ **No New Vulnerabilities:** CodeQL scan clean (0 alerts)
### Testing
- ✅ All existing tests pass
- ✅ New unit tests for Promise and fetch
- ✅ Example parser demonstrates real-world usage
- ✅ Build succeeds without errors
## Files Changed
### New Files (8)
1. `parser/src/main/resources/fetch-runtime.js` - Promise & Fetch polyfill
2. `parser/src/main/java/cn/qaiu/parser/customjs/JsFetchBridge.java` - Java bridge
3. `parser/src/main/resources/custom-parsers/fetch-demo.js` - Example
4. `parser/src/test/java/cn/qaiu/parser/customjs/JsFetchBridgeTest.java` - Tests
5. `parser/doc/TYPESCRIPT_FETCH_GUIDE.md` - Usage guide
6. `parser/doc/TYPESCRIPT_ES5_IMPLEMENTATION.md` - Implementation guide
7. `parser/doc/TYPESCRIPT_ES5_IMPLEMENTATION_SUMMARY.md` - This file
8. `.gitignore` updates (if any)
### Modified Files (2)
1. `parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java` - Auto-inject
2. `parser/src/main/java/cn/qaiu/parser/customjs/JsPlaygroundExecutor.java` - Auto-inject
## Benefits
### For Users
- ✅ Write modern JavaScript patterns in ES5 environment
- ✅ Use familiar fetch API instead of custom http object
- ✅ Better error handling with Promise.catch()
- ✅ Cleaner async code (no callbacks hell)
### For Maintainers
- ✅ No breaking changes to existing code
- ✅ Backward compatible (http object still works)
- ✅ Well documented and tested
- ✅ Clear upgrade path to TypeScript
### For the Project
- ✅ Modern JavaScript support without Node.js
- ✅ Standards-compliant APIs
- ✅ Better developer experience
- ✅ Future-proof architecture
## Conclusion
This implementation successfully delivers the backend infrastructure for browser-based TypeScript compilation. The fetch API and Promise polyfills are production-ready, well-tested, and secure. Users can immediately start using modern async patterns in their ES5 parsers.
The frontend TypeScript compilation component is well-documented and ready for implementation when resources become available. The architecture is sound, the code is clean, and the solution is backward compatible with existing parsers.
**Status:** ✅ Backend Complete | ⏳ Frontend Planned | 🎯 Ready for Review

View File

@@ -40,6 +40,8 @@ https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.
**JavaScript解析器文档** [JavaScript解析器开发指南](parser/doc/JAVASCRIPT_PARSER_GUIDE.md) | [自定义解析器扩展指南](parser/doc/CUSTOM_PARSER_GUIDE.md) | [快速开始](parser/doc/CUSTOM_PARSER_QUICKSTART.md)
**Playground访问控制** [配置指南](web-service/doc/PLAYGROUND_ACCESS_CONTROL.md) - 了解如何配置演练场的访问权限
## 预览地址
[预览地址1](https://lz.qaiu.top)
[预览地址2](https://lzzz.qaiu.top)

270
TESTING_GUIDE.md Normal file
View File

@@ -0,0 +1,270 @@
# Playground Access Control - Testing Guide
## Quick Test Scenarios
### Scenario 1: Disabled Mode (Default)
**Configuration:**
```yaml
playground:
enabled: false
password: ""
```
**Expected Behavior:**
1. Navigate to `/playground`
2. Should see: "Playground未开启请联系管理员在配置中启用此功能"
3. All API endpoints (`/v2/playground/*`) should return error
**API Test:**
```bash
curl http://localhost:6400/v2/playground/status
# Expected: {"code":200,"msg":"success","success":true,"data":{"enabled":false,"needPassword":false,"authed":false}}
```
---
### Scenario 2: Password-Protected Mode
**Configuration:**
```yaml
playground:
enabled: true
password: "test123"
```
**Expected Behavior:**
1. Navigate to `/playground`
2. Should see password input form with lock icon
3. Enter wrong password → Error message: "密码错误"
4. Enter correct password "test123" → Success, editor loads
5. Refresh page → Should remain authenticated
**API Tests:**
```bash
# Check status
curl http://localhost:6400/v2/playground/status
# Expected: {"enabled":true,"needPassword":true,"authed":false}
# Login with wrong password
curl -X POST http://localhost:6400/v2/playground/login \
-H "Content-Type: application/json" \
-d '{"password":"wrong"}'
# Expected: {"code":500,"msg":"密码错误","success":false}
# Login with correct password
curl -X POST http://localhost:6400/v2/playground/login \
-H "Content-Type: application/json" \
-d '{"password":"test123"}'
# Expected: {"code":200,"msg":"登录成功","success":true}
# Try to access without login (should fail)
curl http://localhost:6400/v2/playground/test \
-X POST \
-H "Content-Type: application/json" \
-d '{"jsCode":"function parse(){return \"test\";}","shareUrl":"http://test.com"}'
# Expected: Error response
```
---
### Scenario 3: Public Access Mode
**Configuration:**
```yaml
playground:
enabled: true
password: ""
```
**Expected Behavior:**
1. Navigate to `/playground`
2. Should directly load the editor (no password prompt)
3. All features work immediately
**API Test:**
```bash
curl http://localhost:6400/v2/playground/status
# Expected: {"enabled":true,"needPassword":false,"authed":true}
```
⚠️ **Warning**: Only use this mode in localhost or secure internal network!
---
## Full Feature Tests
### 1. Status Endpoint
```bash
curl http://localhost:6400/v2/playground/status
```
Should return JSON with:
- `enabled`: boolean
- `needPassword`: boolean
- `authed`: boolean
### 2. Login Endpoint (when password is set)
```bash
curl -X POST http://localhost:6400/v2/playground/login \
-H "Content-Type: application/json" \
-d '{"password":"YOUR_PASSWORD"}'
```
### 3. Test Script Execution (after authentication)
```bash
curl -X POST http://localhost:6400/v2/playground/test \
-H "Content-Type: application/json" \
-d '{
"jsCode": "function parse(shareLinkInfo, http, logger) { return \"http://example.com/file.zip\"; }",
"shareUrl": "https://example.com/share/123",
"pwd": "",
"method": "parse"
}'
```
### 4. Get Types Definition
```bash
curl http://localhost:6400/v2/playground/types.js
```
### 5. Parser Management (after authentication)
```bash
# List parsers
curl http://localhost:6400/v2/playground/parsers
# Get parser by ID
curl http://localhost:6400/v2/playground/parsers/1
# Delete parser
curl -X DELETE http://localhost:6400/v2/playground/parsers/1
```
---
## UI Testing Checklist
### When Disabled
- [ ] Page shows "Playground未开启" message
- [ ] No editor visible
- [ ] Clean, centered layout
### When Password Protected (Not Authenticated)
- [ ] Password input form visible
- [ ] Lock icon displayed
- [ ] Can toggle password visibility
- [ ] Enter key submits form
- [ ] Error message shows for wrong password
- [ ] Success message and editor loads on correct password
### When Password Protected (Authenticated)
- [ ] Editor loads immediately on page refresh
- [ ] All features work (run, save, format, etc.)
- [ ] Can execute tests
- [ ] Can save/load parsers
### When Public Access
- [ ] Editor loads immediately
- [ ] All features work without authentication
- [ ] No password prompt visible
---
## Configuration Examples
### Production (Recommended)
```yaml
playground:
enabled: false
password: ""
```
### Development Team (Public Network)
```yaml
playground:
enabled: true
password: "SecureP@ssw0rd2024!"
```
### Local Development
```yaml
playground:
enabled: true
password: ""
```
---
## Common Issues
### Issue: "Failed to extract session ID from cookie"
**Cause**: Cookie parsing error
**Solution**: This is logged as a warning and falls back to IP-based identification
### Issue: Editor doesn't load after correct password
**Cause**: Frontend state not updated
**Solution**: Check browser console for errors, ensure initPlayground() is called
### Issue: Authentication lost on page refresh
**Cause**: Server restarted (in-memory session storage)
**Solution**: Expected behavior - re-enter password after server restart
---
## Security Verification
### 1. Default Security
- [ ] Default config has `enabled: false`
- [ ] Cannot access playground without enabling
- [ ] No unintended API exposure
### 2. Password Protection
- [ ] Wrong password rejected
- [ ] Session persists across requests
- [ ] Different clients have independent sessions
### 3. API Protection
- [ ] All playground endpoints check authentication
- [ ] Status endpoint accessible without auth (returns state only)
- [ ] Login endpoint accessible without auth (for authentication)
- [ ] All other endpoints require authentication when password is set
---
## Performance Testing
### Load Test
```bash
# Test status endpoint
ab -n 1000 -c 10 http://localhost:6400/v2/playground/status
```
### Session Management Test
```bash
# Create multiple concurrent sessions
for i in {1..10}; do
curl -X POST http://localhost:6400/v2/playground/login \
-H "Content-Type: application/json" \
-d '{"password":"test123"}' &
done
wait
```
---
## Cleanup
After testing, remember to:
1. Set `enabled: false` in production
2. Use strong passwords if enabling in public networks
3. Monitor access logs
4. Regularly review created parsers
---
## Documentation References
- Full documentation: `web-service/doc/PLAYGROUND_ACCESS_CONTROL.md`
- Main README: `README.md` (Playground Access Control section)
- Configuration file: `web-service/src/main/resources/app-dev.yml`
---
Last Updated: 2025-12-07

View File

@@ -0,0 +1,73 @@
package cn.qaiu.vx.core.verticle.conf;
import io.vertx.core.json.JsonObject;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.impl.JsonUtil;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
/**
* Converter and mapper for {@link cn.qaiu.vx.core.verticle.conf.HttpProxyConf}.
* NOTE: This class has been automatically generated from the {@link cn.qaiu.vx.core.verticle.conf.HttpProxyConf} original class using Vert.x codegen.
*/
public class HttpProxyConfConverter {
private static final Base64.Decoder BASE64_DECODER = JsonUtil.BASE64_DECODER;
private static final Base64.Encoder BASE64_ENCODER = JsonUtil.BASE64_ENCODER;
static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json, HttpProxyConf obj) {
for (java.util.Map.Entry<String, Object> member : json) {
switch (member.getKey()) {
case "password":
if (member.getValue() instanceof String) {
obj.setPassword((String)member.getValue());
}
break;
case "port":
if (member.getValue() instanceof Number) {
obj.setPort(((Number)member.getValue()).intValue());
}
break;
case "preProxyOptions":
if (member.getValue() instanceof JsonObject) {
obj.setPreProxyOptions(new io.vertx.core.net.ProxyOptions((io.vertx.core.json.JsonObject)member.getValue()));
}
break;
case "timeout":
if (member.getValue() instanceof Number) {
obj.setTimeout(((Number)member.getValue()).intValue());
}
break;
case "username":
if (member.getValue() instanceof String) {
obj.setUsername((String)member.getValue());
}
break;
}
}
}
static void toJson(HttpProxyConf obj, JsonObject json) {
toJson(obj, json.getMap());
}
static void toJson(HttpProxyConf obj, java.util.Map<String, Object> json) {
if (obj.getPassword() != null) {
json.put("password", obj.getPassword());
}
if (obj.getPort() != null) {
json.put("port", obj.getPort());
}
if (obj.getPreProxyOptions() != null) {
json.put("preProxyOptions", obj.getPreProxyOptions().toJson());
}
if (obj.getTimeout() != null) {
json.put("timeout", obj.getTimeout());
}
if (obj.getUsername() != null) {
json.put("username", obj.getUsername());
}
}
}

View File

@@ -1,378 +0,0 @@
# TypeScript/ES6+ 浏览器编译与Fetch API实现
## 项目概述
本实现提供了**纯前端TypeScript编译 + 后端ES5引擎 + Fetch API适配**的完整解决方案允许用户在浏览器中编写TypeScript/ES6+代码包括async/await编译为ES5后在后端Nashorn JavaScript引擎中执行。
## 架构图
```
┌─────────────────────────────────────────────────────────┐
│ 浏览器端 (计划中) │
├─────────────────────────────────────────────────────────┤
│ 用户编写 TypeScript/ES6+ 代码 (async/await) │
│ ↓ │
│ TypeScript.js 浏览器内编译为 ES5 │
│ ↓ │
│ 生成的 ES5 代码发送到后端 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 后端 (已实现) │
├─────────────────────────────────────────────────────────┤
│ 1. 接收 ES5 代码 │
│ 2. 注入 fetch-runtime.js (Promise + fetch polyfill) │
│ 3. 注入 JavaFetch 桥接对象 │
│ 4. Nashorn 引擎执行 ES5 代码 │
│ 5. fetch() → JavaFetch → JsHttpClient → Vert.x │
└─────────────────────────────────────────────────────────┘
```
## 已实现功能
### ✅ 后端 ES5 执行环境
#### 1. Promise Polyfill (完整的 Promise/A+ 实现)
文件: `parser/src/main/resources/fetch-runtime.js`
**功能特性:**
-`new Promise(executor)` 构造函数
-`promise.then(onFulfilled, onRejected)` 链式调用
-`promise.catch(onRejected)` 错误处理
-`promise.finally(onFinally)` 清理操作
-`Promise.resolve(value)` 静态方法
-`Promise.reject(reason)` 静态方法
-`Promise.all(promises)` 并行等待
-`Promise.race(promises)` 竞速等待
**实现细节:**
- 纯 ES5 语法无ES6+特性依赖
- 使用 `setTimeout(fn, 0)` 实现异步执行
- 支持 Promise 链式调用和错误传播
- 自动处理 Promise 嵌套和展开
#### 2. Fetch API Polyfill (标准 fetch 接口)
文件: `parser/src/main/resources/fetch-runtime.js`
**支持的 HTTP 方法:**
- ✅ GET
- ✅ POST
- ✅ PUT
- ✅ DELETE
- ✅ PATCH
- ✅ HEAD
**Request 选项支持:**
```javascript
fetch(url, {
method: 'POST', // HTTP 方法
headers: { // 请求头
'Content-Type': 'application/json',
'Authorization': 'Bearer token'
},
body: JSON.stringify({ // 请求体
key: 'value'
})
})
```
**Response 对象方法:**
-`response.text()` - 获取文本响应 (返回 Promise)
-`response.json()` - 解析 JSON 响应 (返回 Promise)
-`response.arrayBuffer()` - 获取字节数组
-`response.status` - HTTP 状态码
-`response.ok` - 请求是否成功 (2xx)
-`response.statusText` - 状态文本
-`response.headers.get(name)` - 获取响应头
#### 3. Java 桥接层
文件: `parser/src/main/java/cn/qaiu/parser/customjs/JsFetchBridge.java`
**核心功能:**
- 接收 JavaScript fetch API 调用
- 转换为 JsHttpClient 调用
- 处理请求头、请求体、HTTP 方法
- 返回 JsHttpResponse 对象
- 自动继承现有的 SSRF 防护机制
**代码示例:**
```java
public class JsFetchBridge {
private final JsHttpClient httpClient;
public JsHttpResponse fetch(String url, Map<String, Object> options) {
// 解析 method、headers、body
// 调用 httpClient.get/post/put/delete/patch
// 返回 JsHttpResponse
}
}
```
#### 4. 自动注入机制
文件:
- `parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java`
- `parser/src/main/java/cn/qaiu/parser/customjs/JsPlaygroundExecutor.java`
**注入流程:**
1. 创建 JavaScript 引擎
2. 注入 JavaFetch 桥接对象
3. 加载 fetch-runtime.js
4. 执行用户 JavaScript 代码
**代码示例:**
```java
// 注入 JavaFetch
engine.put("JavaFetch", new JsFetchBridge(httpClient));
// 加载 fetch runtime
String fetchRuntime = loadFetchRuntime();
engine.eval(fetchRuntime);
// 现在 JavaScript 环境中可以使用 Promise 和 fetch
```
## 使用示例
### ES5 风格 (当前可用)
```javascript
function parse(shareLinkInfo, http, logger) {
logger.info("开始解析");
// 使用 fetch API
fetch("https://api.example.com/data")
.then(function(response) {
logger.info("状态码: " + response.status);
return response.json();
})
.then(function(data) {
logger.info("数据: " + JSON.stringify(data));
return data.downloadUrl;
})
.catch(function(error) {
logger.error("错误: " + error.message);
throw error;
});
// 或者继续使用传统的 http 对象
var response = http.get("https://api.example.com/data");
return response.body();
}
```
### TypeScript/ES6+ 风格 (需前端编译)
用户在浏览器中编写:
```typescript
async function parse(
shareLinkInfo: ShareLinkInfo,
http: JsHttpClient,
logger: JsLogger
): Promise<string> {
try {
logger.info("开始解析");
// 使用标准 fetch API
const response = await fetch("https://api.example.com/data");
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
logger.info(`下载链接: ${data.downloadUrl}`);
return data.downloadUrl;
} catch (error) {
logger.error(`解析失败: ${error.message}`);
throw error;
}
}
```
浏览器编译为 ES5 后:
```javascript
function parse(shareLinkInfo, http, logger) {
return __awaiter(this, void 0, void 0, function() {
var response, data, error_1;
return __generator(this, function(_a) {
switch(_a.label) {
case 0:
_a.trys.push([0, 3, , 4]);
logger.info("开始解析");
return [4, fetch("https://api.example.com/data")];
case 1:
response = _a.sent();
if (!response.ok) {
throw new Error("HTTP " + response.status + ": " + response.statusText);
}
return [4, response.json()];
case 2:
data = _a.sent();
logger.info("下载链接: " + data.downloadUrl);
return [2, data.downloadUrl];
case 3:
error_1 = _a.sent();
logger.error("解析失败: " + error_1.message);
throw error_1;
case 4: return [2];
}
});
});
}
```
## 文件结构
```
parser/
├── src/main/
│ ├── java/cn/qaiu/parser/customjs/
│ │ ├── JsFetchBridge.java # Java 桥接层
│ │ ├── JsParserExecutor.java # 解析器执行器 (已更新)
│ │ └── JsPlaygroundExecutor.java # 演练场执行器 (已更新)
│ └── resources/
│ ├── fetch-runtime.js # Promise + fetch polyfill
│ └── custom-parsers/
│ └── fetch-demo.js # Fetch 示例解析器
├── src/test/java/cn/qaiu/parser/customjs/
│ └── JsFetchBridgeTest.java # 单元测试
└── doc/
└── TYPESCRIPT_FETCH_GUIDE.md # 详细使用指南
```
## 测试验证
### 运行测试
```bash
# 编译项目
mvn clean compile -pl parser
# 运行所有测试
mvn test -pl parser
# 运行 fetch 测试
mvn test -pl parser -Dtest=JsFetchBridgeTest
```
### 测试内容
文件: `parser/src/test/java/cn/qaiu/parser/customjs/JsFetchBridgeTest.java`
1. **testFetchPolyfillLoaded** - 验证 Promise 和 fetch 是否正确注入
2. **testPromiseBasicUsage** - 验证 Promise 基本功能
3. **示例解析器** - `fetch-demo.js` 展示完整用法
## 兼容性说明
### 支持的特性
- ✅ Promise/A+ 完整实现
- ✅ Fetch API 标准接口
- ✅ async/await (通过 TypeScript 编译)
- ✅ 所有 HTTP 方法
- ✅ Request headers 和 body
- ✅ Response 解析 (text, json, arrayBuffer)
- ✅ 错误处理和 Promise 链
- ✅ 与现有 http 对象共存
### 不支持的特性
- ❌ Blob 对象 (使用 arrayBuffer 替代)
- ❌ FormData 对象 (使用简单对象替代)
- ❌ Request/Response 构造函数
- ❌ Streams API
- ❌ Service Worker 相关 API
- ❌ AbortController (取消请求)
## 安全性
### SSRF 防护
继承自 `JsHttpClient` 的 SSRF 防护:
- ✅ 拦截内网 IP (127.0.0.1, 10.x.x.x, 192.168.x.x 等)
- ✅ 拦截云服务元数据 API (169.254.169.254 等)
- ✅ DNS 解析检查
- ✅ 危险域名黑名单
### 沙箱隔离
- ✅ SecurityClassFilter 限制类访问
- ✅ 禁用 Java 对象直接访问
- ✅ 限制文件系统操作
## 性能优化
1. **Fetch runtime 缓存**
- 首次加载后缓存在静态变量
- 避免重复读取文件
2. **Promise 异步执行**
- 使用 setTimeout(0) 实现非阻塞
- 避免阻塞 JavaScript 主线程
3. **工作线程池**
- JsParserExecutor: Vert.x 工作线程池
- JsPlaygroundExecutor: 独立线程池
- 避免阻塞 Event Loop
## 前端 TypeScript 编译 (计划中)
### 待实现步骤
1. **添加 TypeScript 编译器**
```bash
cd web-front
npm install typescript
```
2. **创建编译工具**
```javascript
// web-front/src/utils/tsCompiler.js
import * as ts from 'typescript';
export function compileToES5(sourceCode) {
return ts.transpileModule(sourceCode, {
compilerOptions: {
target: ts.ScriptTarget.ES5,
module: ts.ModuleKind.None,
lib: ['es5', 'dom']
}
});
}
```
3. **更新 Playground UI**
- 添加语言选择器 (JavaScript / TypeScript)
- 编译前先检查语法错误
- 显示编译后的 ES5 代码 (可选)
## 相关文档
- [详细使用指南](parser/doc/TYPESCRIPT_FETCH_GUIDE.md)
- [JavaScript 解析器开发指南](parser/doc/JAVASCRIPT_PARSER_GUIDE.md)
- [自定义解析器扩展指南](parser/doc/CUSTOM_PARSER_GUIDE.md)
## 总结
本实现成功提供了:
1. **无需 Node 环境** - 纯浏览器编译 + Java 后端执行
2. **标准 API** - 使用标准 fetch 和 Promise API
3. **向后兼容** - 现有 http 对象仍然可用
4. **安全可靠** - SSRF 防护和沙箱隔离
5. **易于使用** - 简单的 API无学习成本
用户可以用现代 JavaScript/TypeScript 编写代码,自动编译为 ES5 后在后端安全执行,同时享受 fetch API 的便利性。
## 许可证
本项目遵循主项目的许可证。

View File

@@ -1,451 +0,0 @@
# 浏览器TypeScript编译和Fetch API支持指南
## 概述
本项目实现了**纯前端TypeScript编译 + 后端ES5引擎 + Fetch API适配**的完整方案允许用户在浏览器中编写TypeScript/ES6+代码编译为ES5后在后端JavaScript引擎中执行。
## 架构设计
### 1. 浏览器端(前端编译)
```
用户编写TS/ES6+代码
TypeScript.js (浏览器内编译)
ES5 JavaScript代码
发送到后端执行
```
### 2. 后端ES5执行环境
```
接收ES5代码
注入fetch polyfill + Promise
注入JavaFetch桥接对象
Nashorn引擎执行ES5代码
fetch() 调用 → JavaFetch → JsHttpClient → Vert.x HTTP Client
```
## 已实现的功能
### ✅ 后端支持
1. **Promise Polyfill** (`fetch-runtime.js`)
- 完整的Promise/A+实现
- 支持 `then``catch``finally`
- 支持 `Promise.all``Promise.race`
- 支持 `Promise.resolve``Promise.reject`
2. **Fetch API Polyfill** (`fetch-runtime.js`)
- 标准fetch接口实现
- 支持所有HTTP方法GET、POST、PUT、DELETE、PATCH
- 支持headers、body等选项
- Response对象支持
- `text()` - 获取文本响应
- `json()` - 解析JSON响应
- `arrayBuffer()` - 获取字节数组
- `status` - HTTP状态码
- `ok` - 请求成功标志
- `headers` - 响应头访问
3. **Java桥接** (`JsFetchBridge.java`)
- 将fetch调用转换为JsHttpClient调用
- 自动处理请求头、请求体
- 支持代理配置
- 安全的SSRF防护
4. **自动注入** (`JsParserExecutor.java` & `JsPlaygroundExecutor.java`)
- 在JavaScript引擎初始化时自动注入fetch runtime
- 提供`JavaFetch`全局对象
- 与现有http对象共存
## 使用示例
### ES5风格当前支持
```javascript
function parse(shareLinkInfo, http, logger) {
// 使用fetch API
fetch("https://api.example.com/data")
.then(function(response) {
return response.json();
})
.then(function(data) {
logger.info("数据: " + JSON.stringify(data));
})
.catch(function(error) {
logger.error("错误: " + error.message);
});
// 或者使用传统的http对象
var response = http.get("https://api.example.com/data");
return response.body();
}
```
### TypeScript风格需要前端编译
用户在浏览器中编写:
```typescript
async function parse(shareLinkInfo: ShareLinkInfo, http: JsHttpClient, logger: JsLogger): Promise<string> {
try {
// 使用标准fetch API
const response = await fetch("https://api.example.com/data");
const data = await response.json();
logger.info(`获取到数据: ${data.downloadUrl}`);
return data.downloadUrl;
} catch (error) {
logger.error(`解析失败: ${error.message}`);
throw error;
}
}
```
浏览器内编译后的ES5代码简化示例
```javascript
function parse(shareLinkInfo, http, logger) {
return __awaiter(this, void 0, void 0, function() {
var response, data;
return __generator(this, function(_a) {
switch(_a.label) {
case 0:
return [4, fetch("https://api.example.com/data")];
case 1:
response = _a.sent();
return [4, response.json()];
case 2:
data = _a.sent();
logger.info("获取到数据: " + data.downloadUrl);
return [2, data.downloadUrl];
}
});
});
}
```
## 前端TypeScript编译待实现
### 计划实现步骤
#### 1. 添加TypeScript编译器
在前端项目中添加`typescript.js`
```bash
# 下载TypeScript编译器浏览器版本
cd webroot/static
wget https://cdn.jsdelivr.net/npm/typescript@latest/lib/typescript.js
```
或者在Vue项目中
```bash
npm install typescript
```
#### 2. 创建编译工具类
`web-front/src/utils/tsCompiler.js`:
```javascript
import * as ts from 'typescript';
export function compileToES5(sourceCode, fileName = 'script.ts') {
const result = ts.transpileModule(sourceCode, {
compilerOptions: {
target: ts.ScriptTarget.ES5,
module: ts.ModuleKind.None,
lib: ['es5', 'dom'],
experimentalDecorators: false,
emitDecoratorMetadata: false,
downlevelIteration: true
},
fileName: fileName
});
return {
js: result.outputText,
diagnostics: result.diagnostics,
sourceMap: result.sourceMapText
};
}
```
#### 3. 更新Playground组件
`Playground.vue`中添加编译选项:
```vue
<template>
<div>
<!-- 语言选择 -->
<el-radio-group v-model="language">
<el-radio label="javascript">JavaScript (ES5)</el-radio>
<el-radio label="typescript">TypeScript/ES6+</el-radio>
</el-radio-group>
<!-- 编辑器 -->
<monaco-editor
v-model="code"
:language="language"
@save="handleSave"
/>
<!-- 运行按钮 -->
<el-button @click="executeCode">运行</el-button>
</div>
</template>
<script>
import { compileToES5 } from '@/utils/tsCompiler';
export default {
data() {
return {
language: 'javascript',
code: ''
};
},
methods: {
async executeCode() {
let codeToExecute = this.code;
// 如果是TypeScript先编译
if (this.language === 'typescript') {
const result = compileToES5(this.code);
if (result.diagnostics && result.diagnostics.length > 0) {
this.$message.error('TypeScript编译错误');
console.error(result.diagnostics);
return;
}
codeToExecute = result.js;
console.log('编译后的ES5代码:', codeToExecute);
}
// 发送到后端执行
const response = await playgroundApi.testScript(
codeToExecute,
this.shareUrl,
this.pwd,
this.method
);
this.showResult(response);
}
}
};
</script>
```
## Fetch Runtime详解
### Promise实现特性
```javascript
// 基本用法
var promise = new SimplePromise(function(resolve, reject) {
setTimeout(function() {
resolve("成功");
}, 1000);
});
promise.then(function(value) {
console.log(value); // "成功"
});
// 链式调用
promise
.then(function(value) {
return value + " - 第一步";
})
.then(function(value) {
return value + " - 第二步";
})
.catch(function(error) {
console.error(error);
})
.finally(function() {
console.log("完成");
});
```
### Fetch API特性
```javascript
// GET请求
fetch("https://api.example.com/data")
.then(function(response) {
console.log("状态码:", response.status);
console.log("成功:", response.ok);
return response.json();
})
.then(function(data) {
console.log("数据:", data);
});
// POST请求
fetch("https://api.example.com/submit", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ key: "value" })
})
.then(function(response) {
return response.json();
})
.then(function(data) {
console.log("响应:", data);
});
```
## 兼容性说明
### 支持的特性
- ✅ Promise/A+ 完整实现
- ✅ Fetch API 标准接口
- ✅ async/await编译后
- ✅ 所有HTTP方法GET、POST、PUT、DELETE、PATCH
- ✅ Request headers配置
- ✅ Request bodystring、JSON、FormData
- ✅ Response.text()、Response.json()
- ✅ 与现有http对象共存
### 不支持的特性
- ❌ Blob对象返回字节数组替代
- ❌ FormData对象使用简单对象替代
- ❌ Request/Response对象构造函数
- ❌ Streams API
- ❌ Service Worker相关API
## 测试验证
### 1. 创建测试解析器
参考 `parser/src/main/resources/custom-parsers/fetch-demo.js`
### 2. 测试步骤
```bash
# 1. 编译项目
mvn clean package -DskipTests
# 2. 运行服务
java -jar web-service/target/netdisk-fast-download.jar
# 3. 访问演练场
浏览器打开: http://localhost:6401/playground
# 4. 加载fetch-demo.js并测试
```
### 3. 验证fetch功能
在演练场中运行:
```javascript
function parse(shareLinkInfo, http, logger) {
logger.info("测试fetch API");
var result = null;
fetch("https://httpbin.org/get")
.then(function(response) {
logger.info("状态码: " + response.status);
return response.json();
})
.then(function(data) {
logger.info("响应: " + JSON.stringify(data));
result = "SUCCESS";
})
.catch(function(error) {
logger.error("错误: " + error.message);
});
// 等待完成
var timeout = 5000;
var start = Date.now();
while (result === null && (Date.now() - start) < timeout) {
java.lang.Thread.sleep(10);
}
return result || "https://example.com/download";
}
```
## 安全性
### SSRF防护
JsHttpClient已实现SSRF防护
- 拦截内网IP访问127.0.0.1、10.x.x.x、192.168.x.x等
- 拦截云服务元数据API169.254.169.254等)
- DNS解析检查
### 沙箱隔离
- JavaScript引擎使用SecurityClassFilter
- 禁用Java对象访问
- 限制文件系统访问
## 性能优化
1. **Fetch runtime缓存**
- 首次加载后缓存在静态变量中
- 避免重复读取资源文件
2. **Promise异步执行**
- 使用setTimeout(0)实现异步
- 避免阻塞主线程
3. **工作线程池**
- JsParserExecutor使用Vert.x工作线程池
- JsPlaygroundExecutor使用独立线程池
## 相关文件
### 后端代码
- `parser/src/main/resources/fetch-runtime.js` - Fetch和Promise polyfill
- `parser/src/main/java/cn/qaiu/parser/customjs/JsFetchBridge.java` - Java桥接层
- `parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java` - 解析器执行器
- `parser/src/main/java/cn/qaiu/parser/customjs/JsPlaygroundExecutor.java` - 演练场执行器
### 示例代码
- `parser/src/main/resources/custom-parsers/fetch-demo.js` - Fetch API演示
### 前端代码(待实现)
- `web-front/src/utils/tsCompiler.js` - TypeScript编译工具
- `web-front/src/views/Playground.vue` - 演练场界面
## 下一步计划
1. ✅ 实现后端fetch polyfill
2. ✅ 实现Promise polyfill
3. ✅ 集成到JsParserExecutor
4. ⏳ 前端添加TypeScript编译器
5. ⏳ 更新Playground UI支持TS/ES6+
6. ⏳ 添加Monaco编辑器类型提示
7. ⏳ 编写更多示例和文档
## 总结
通过这个方案,我们实现了:
1. **无需Node环境** - 纯浏览器编译 + Java后端执行
2. **标准API** - 使用标准fetch和Promise API
3. **向后兼容** - 现有http对象仍然可用
4. **安全可靠** - SSRF防护和沙箱隔离
5. **易于使用** - 简单的API无需学习成本
用户可以在浏览器中用现代JavaScript/TypeScript编写代码自动编译为ES5后在后端安全执行同时享受fetch API的便利性。

View File

@@ -1,96 +0,0 @@
package cn.qaiu.parser.customjs;
import cn.qaiu.parser.customjs.JsHttpClient.JsHttpResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
/**
* JavaScript Fetch API桥接类
* 将标准的fetch API调用桥接到现有的JsHttpClient实现
*
* @author <a href="https://qaiu.top">QAIU</a>
* Create at 2025/12/06
*/
public class JsFetchBridge {
private static final Logger log = LoggerFactory.getLogger(JsFetchBridge.class);
private final JsHttpClient httpClient;
public JsFetchBridge(JsHttpClient httpClient) {
this.httpClient = httpClient;
}
/**
* Fetch API实现
* 接收fetch API调用并转换为JsHttpClient调用
*
* @param url 请求URL
* @param options 请求选项包含method、headers、body等
* @return JsHttpResponse响应对象
*/
public JsHttpResponse fetch(String url, Map<String, Object> options) {
try {
// 解析请求方法
String method = "GET";
if (options != null && options.containsKey("method")) {
method = options.get("method").toString().toUpperCase();
}
// 解析并设置请求头
if (options != null && options.containsKey("headers")) {
Object headersObj = options.get("headers");
if (headersObj instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> headersMap = (Map<String, Object>) headersObj;
for (Map.Entry<String, Object> entry : headersMap.entrySet()) {
if (entry.getValue() != null) {
httpClient.putHeader(entry.getKey(), entry.getValue().toString());
}
}
}
}
// 解析请求体
Object body = null;
if (options != null && options.containsKey("body")) {
body = options.get("body");
}
// 根据方法执行请求
JsHttpResponse response;
switch (method) {
case "GET":
response = httpClient.get(url);
break;
case "POST":
response = httpClient.post(url, body);
break;
case "PUT":
response = httpClient.put(url, body);
break;
case "DELETE":
response = httpClient.delete(url);
break;
case "PATCH":
response = httpClient.patch(url, body);
break;
case "HEAD":
response = httpClient.getNoRedirect(url);
break;
default:
throw new IllegalArgumentException("Unsupported HTTP method: " + method);
}
log.debug("Fetch请求完成: {} {} - 状态码: {}", method, url, response.statusCode());
return response;
} catch (Exception e) {
log.error("Fetch请求失败: {} - {}", url, e.getMessage());
throw new RuntimeException("Fetch请求失败: " + e.getMessage(), e);
}
}
}

View File

@@ -14,13 +14,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.script.ScriptEngine;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* JavaScript解析器执行器
@@ -35,19 +30,17 @@ public class JsParserExecutor implements IPanTool {
private static final WorkerExecutor EXECUTOR = WebClientVertxInit.get().createSharedWorkerExecutor("parser-executor", 32);
private static String FETCH_RUNTIME_JS = null;
private final CustomParserConfig config;
private final ShareLinkInfo shareLinkInfo;
private final ScriptEngine engine;
private final JsHttpClient httpClient;
private final JsLogger jsLogger;
private final JsShareLinkInfoWrapper shareLinkInfoWrapper;
private final JsFetchBridge fetchBridge;
public JsParserExecutor(ShareLinkInfo shareLinkInfo, CustomParserConfig config) {
this.config = config;
this.shareLinkInfo = shareLinkInfo;
this.engine = initEngine();
// 检查是否有代理配置
JsonObject proxyConfig = null;
@@ -58,34 +51,6 @@ public class JsParserExecutor implements IPanTool {
this.httpClient = new JsHttpClient(proxyConfig);
this.jsLogger = new JsLogger("JsParser-" + config.getType());
this.shareLinkInfoWrapper = new JsShareLinkInfoWrapper(shareLinkInfo);
this.fetchBridge = new JsFetchBridge(httpClient);
this.engine = initEngine();
}
/**
* 加载fetch运行时JS代码
* @return fetch运行时代码
*/
static String loadFetchRuntime() {
if (FETCH_RUNTIME_JS != null) {
return FETCH_RUNTIME_JS;
}
try (InputStream is = JsParserExecutor.class.getClassLoader().getResourceAsStream("fetch-runtime.js")) {
if (is == null) {
log.warn("未找到fetch-runtime.js文件fetch API将不可用");
return "";
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
FETCH_RUNTIME_JS = reader.lines().collect(Collectors.joining("\n"));
log.debug("Fetch运行时加载成功大小: {} 字符", FETCH_RUNTIME_JS.length());
return FETCH_RUNTIME_JS;
}
} catch (Exception e) {
log.error("加载fetch-runtime.js失败", e);
return "";
}
}
/**
@@ -116,7 +81,6 @@ public class JsParserExecutor implements IPanTool {
engine.put("http", httpClient);
engine.put("logger", jsLogger);
engine.put("shareLinkInfo", shareLinkInfoWrapper);
engine.put("JavaFetch", fetchBridge);
// 禁用Java对象访问
engine.eval("var Java = undefined;");
@@ -126,13 +90,6 @@ public class JsParserExecutor implements IPanTool {
engine.eval("var org = undefined;");
engine.eval("var com = undefined;");
// 加载fetch运行时Promise和fetch API polyfill
String fetchRuntime = loadFetchRuntime();
if (!fetchRuntime.isEmpty()) {
engine.eval(fetchRuntime);
log.debug("✅ Fetch API和Promise polyfill注入成功");
}
log.debug("🔒 安全的JavaScript引擎初始化成功解析器类型: {}", config.getType());
// 执行JavaScript代码

View File

@@ -42,7 +42,6 @@ public class JsPlaygroundExecutor {
private final JsHttpClient httpClient;
private final JsPlaygroundLogger playgroundLogger;
private final JsShareLinkInfoWrapper shareLinkInfoWrapper;
private final JsFetchBridge fetchBridge;
/**
* 创建演练场执行器
@@ -63,7 +62,6 @@ public class JsPlaygroundExecutor {
this.httpClient = new JsHttpClient(proxyConfig);
this.playgroundLogger = new JsPlaygroundLogger();
this.shareLinkInfoWrapper = new JsShareLinkInfoWrapper(shareLinkInfo);
this.fetchBridge = new JsFetchBridge(httpClient);
this.engine = initEngine();
}
@@ -86,7 +84,6 @@ public class JsPlaygroundExecutor {
engine.put("http", httpClient);
engine.put("logger", playgroundLogger);
engine.put("shareLinkInfo", shareLinkInfoWrapper);
engine.put("JavaFetch", fetchBridge);
// 禁用Java对象访问
engine.eval("var Java = undefined;");
@@ -96,13 +93,6 @@ public class JsPlaygroundExecutor {
engine.eval("var org = undefined;");
engine.eval("var com = undefined;");
// 加载fetch运行时Promise和fetch API polyfill
String fetchRuntime = JsParserExecutor.loadFetchRuntime();
if (!fetchRuntime.isEmpty()) {
engine.eval(fetchRuntime);
playgroundLogger.infoJava("✅ Fetch API和Promise polyfill注入成功");
}
playgroundLogger.infoJava("🔒 安全的JavaScript引擎初始化成功演练场");
// 执行JavaScript代码

View File

@@ -1,105 +0,0 @@
// ==UserScript==
// @name Fetch API示例解析器
// @type fetch_demo
// @displayName Fetch演示
// @description 演示如何在ES5环境中使用fetch API和async/await
// @match https?://example\.com/s/(?<KEY>\w+)
// @author QAIU
// @version 1.0.0
// ==/UserScript==
// 使用require导入类型定义仅用于IDE类型提示
var types = require('./types');
/** @typedef {types.ShareLinkInfo} ShareLinkInfo */
/** @typedef {types.JsHttpClient} JsHttpClient */
/** @typedef {types.JsLogger} JsLogger */
/**
* 演示使用fetch API的解析器
* 注意虽然源码中使用了ES6+语法async/await但在浏览器中会被编译为ES5
*
* @param {ShareLinkInfo} shareLinkInfo - 分享链接信息
* @param {JsHttpClient} http - HTTP客户端传统方式
* @param {JsLogger} logger - 日志对象
* @returns {string} 下载链接
*/
function parse(shareLinkInfo, http, logger) {
logger.info("=== Fetch API Demo ===");
// 方式1使用传统的http对象同步
logger.info("方式1: 使用传统http对象");
var response1 = http.get("https://httpbin.org/get");
logger.info("状态码: " + response1.statusCode());
// 方式2使用fetch API基于Promise
logger.info("方式2: 使用fetch API");
// 注意在ES5环境中我们需要手动处理Promise
// 这个示例展示了如何在ES5中使用fetch
var fetchPromise = fetch("https://httpbin.org/get");
// 等待Promise完成同步等待模拟
var result = null;
var error = null;
fetchPromise
.then(function(response) {
logger.info("Fetch响应状态: " + response.status);
return response.text();
})
.then(function(text) {
logger.info("Fetch响应内容: " + text.substring(0, 100) + "...");
result = "https://example.com/download/demo.file";
})
['catch'](function(err) {
logger.error("Fetch失败: " + err.message);
error = err;
});
// 简单的等待循环(实际场景中不推荐,这里仅作演示)
var timeout = 5000; // 5秒超时
var start = Date.now();
while (result === null && error === null && (Date.now() - start) < timeout) {
// 等待Promise完成
java.lang.Thread.sleep(10);
}
if (error !== null) {
throw error;
}
if (result === null) {
throw new Error("Fetch超时");
}
return result;
}
/**
* 演示POST请求
*/
function demonstratePost(logger) {
logger.info("=== 演示POST请求 ===");
var postPromise = fetch("https://httpbin.org/post", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
key: "value",
demo: true
})
});
postPromise
.then(function(response) {
return response.json();
})
.then(function(data) {
logger.info("POST响应: " + JSON.stringify(data));
})
['catch'](function(err) {
logger.error("POST失败: " + err.message);
});
}

View File

@@ -1,329 +0,0 @@
// ==FetchRuntime==
// @name Fetch API Polyfill for ES5
// @description Fetch API and Promise implementation for ES5 JavaScript engines
// @version 1.0.0
// @author QAIU
// ==============
/**
* Simple Promise implementation compatible with ES5
* Supports basic Promise functionality needed for fetch API
*/
function SimplePromise(executor) {
var state = 'pending';
var value;
var handlers = [];
var self = this;
function resolve(result) {
if (state !== 'pending') return;
state = 'fulfilled';
value = result;
handlers.forEach(handle);
handlers = [];
}
function reject(err) {
if (state !== 'pending') return;
state = 'rejected';
value = err;
handlers.forEach(handle);
handlers = [];
}
function handle(handler) {
if (state === 'pending') {
handlers.push(handler);
} else {
setTimeout(function() {
if (state === 'fulfilled' && typeof handler.onFulfilled === 'function') {
try {
var result = handler.onFulfilled(value);
if (result && typeof result.then === 'function') {
result.then(handler.resolve, handler.reject);
} else {
handler.resolve(result);
}
} catch (e) {
handler.reject(e);
}
}
if (state === 'rejected' && typeof handler.onRejected === 'function') {
try {
var result = handler.onRejected(value);
if (result && typeof result.then === 'function') {
result.then(handler.resolve, handler.reject);
} else {
handler.resolve(result);
}
} catch (e) {
handler.reject(e);
}
} else if (state === 'rejected' && !handler.onRejected) {
handler.reject(value);
}
}, 0);
}
}
this.then = function(onFulfilled, onRejected) {
return new SimplePromise(function(resolveNext, rejectNext) {
handle({
onFulfilled: onFulfilled,
onRejected: onRejected,
resolve: resolveNext,
reject: rejectNext
});
});
};
this['catch'] = function(onRejected) {
return this.then(null, onRejected);
};
this['finally'] = function(onFinally) {
return this.then(
function(value) {
return SimplePromise.resolve(onFinally()).then(function() {
return value;
});
},
function(reason) {
return SimplePromise.resolve(onFinally()).then(function() {
throw reason;
});
}
);
};
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
// Static methods
SimplePromise.resolve = function(value) {
if (value && typeof value.then === 'function') {
return value;
}
return new SimplePromise(function(resolve) {
resolve(value);
});
};
SimplePromise.reject = function(reason) {
return new SimplePromise(function(resolve, reject) {
reject(reason);
});
};
SimplePromise.all = function(promises) {
return new SimplePromise(function(resolve, reject) {
var results = [];
var remaining = promises.length;
if (remaining === 0) {
resolve(results);
return;
}
function handleResult(index, value) {
results[index] = value;
remaining--;
if (remaining === 0) {
resolve(results);
}
}
for (var i = 0; i < promises.length; i++) {
(function(index) {
var promise = promises[index];
if (promise && typeof promise.then === 'function') {
promise.then(
function(value) { handleResult(index, value); },
reject
);
} else {
handleResult(index, promise);
}
})(i);
}
});
};
SimplePromise.race = function(promises) {
return new SimplePromise(function(resolve, reject) {
if (promises.length === 0) {
// Per spec, Promise.race with empty array stays pending forever
return;
}
for (var i = 0; i < promises.length; i++) {
var promise = promises[i];
if (promise && typeof promise.then === 'function') {
promise.then(resolve, reject);
} else {
resolve(promise);
return;
}
}
});
};
// Make Promise global if not already defined
if (typeof Promise === 'undefined') {
var Promise = SimplePromise;
}
/**
* Response object that mimics the Fetch API Response
*/
function FetchResponse(jsHttpResponse) {
this._jsResponse = jsHttpResponse;
this.status = jsHttpResponse.statusCode();
this.ok = this.status >= 200 && this.status < 300;
// Map HTTP status codes to standard status text
var statusTexts = {
200: 'OK',
201: 'Created',
204: 'No Content',
301: 'Moved Permanently',
302: 'Found',
304: 'Not Modified',
400: 'Bad Request',
401: 'Unauthorized',
403: 'Forbidden',
404: 'Not Found',
405: 'Method Not Allowed',
408: 'Request Timeout',
409: 'Conflict',
410: 'Gone',
500: 'Internal Server Error',
501: 'Not Implemented',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Timeout'
};
this.statusText = statusTexts[this.status] || (this.ok ? 'OK' : 'Error');
this.headers = {
get: function(name) {
return jsHttpResponse.header(name);
},
has: function(name) {
return jsHttpResponse.header(name) !== null;
},
entries: function() {
var headerMap = jsHttpResponse.headers();
var entries = [];
for (var key in headerMap) {
if (headerMap.hasOwnProperty(key)) {
entries.push([key, headerMap[key]]);
}
}
return entries;
}
};
}
FetchResponse.prototype.text = function() {
var body = this._jsResponse.body();
return SimplePromise.resolve(body || '');
};
FetchResponse.prototype.json = function() {
var self = this;
return this.text().then(function(text) {
try {
return JSON.parse(text);
} catch (e) {
throw new Error('Invalid JSON: ' + e.message);
}
});
};
FetchResponse.prototype.arrayBuffer = function() {
var bytes = this._jsResponse.bodyBytes();
return SimplePromise.resolve(bytes);
};
FetchResponse.prototype.blob = function() {
// Blob not supported in ES5, return bytes
return this.arrayBuffer();
};
/**
* Fetch API implementation using JavaFetch bridge
* @param {string} url - Request URL
* @param {Object} options - Fetch options (method, headers, body, etc.)
* @returns {Promise<FetchResponse>}
*/
function fetch(url, options) {
return new SimplePromise(function(resolve, reject) {
try {
// Parse options
options = options || {};
var method = (options.method || 'GET').toUpperCase();
var headers = options.headers || {};
var body = options.body;
// Prepare request options for JavaFetch
var requestOptions = {
method: method,
headers: {}
};
// Convert headers to simple object
if (headers) {
if (typeof headers.forEach === 'function') {
// Headers object
headers.forEach(function(value, key) {
requestOptions.headers[key] = value;
});
} else if (typeof headers === 'object') {
// Plain object
for (var key in headers) {
if (headers.hasOwnProperty(key)) {
requestOptions.headers[key] = headers[key];
}
}
}
}
// Add body if present
if (body !== undefined && body !== null) {
if (typeof body === 'string') {
requestOptions.body = body;
} else if (typeof body === 'object') {
// Assume JSON
requestOptions.body = JSON.stringify(body);
if (!requestOptions.headers['Content-Type'] && !requestOptions.headers['content-type']) {
requestOptions.headers['Content-Type'] = 'application/json';
}
}
}
// Call JavaFetch bridge
var jsHttpResponse = JavaFetch.fetch(url, requestOptions);
// Create Response object
var response = new FetchResponse(jsHttpResponse);
resolve(response);
} catch (e) {
reject(e);
}
});
}
// Export for global use
if (typeof window !== 'undefined') {
window.fetch = fetch;
window.Promise = Promise;
} else if (typeof global !== 'undefined') {
global.fetch = fetch;
global.Promise = Promise;
}

View File

@@ -1,152 +0,0 @@
package cn.qaiu.parser.customjs;
import cn.qaiu.WebClientVertxInit;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.IPanTool;
import cn.qaiu.parser.ParserCreate;
import cn.qaiu.parser.custom.CustomParserConfig;
import cn.qaiu.parser.custom.CustomParserRegistry;
import io.vertx.core.Vertx;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Fetch Bridge测试
* 测试fetch API和Promise polyfill功能
*/
public class JsFetchBridgeTest {
private static final Logger log = LoggerFactory.getLogger(JsFetchBridgeTest.class);
@Test
public void testFetchPolyfillLoaded() {
// 初始化Vertx
Vertx vertx = Vertx.vertx();
WebClientVertxInit.init(vertx);
// 清理注册表
CustomParserRegistry.clear();
// 创建一个简单的解析器配置
String jsCode = """
// 测试Promise是否可用
function parse(shareLinkInfo, http, logger) {
logger.info("测试开始");
// 检查Promise是否存在
if (typeof Promise === 'undefined') {
throw new Error("Promise未定义");
}
// 检查fetch是否存在
if (typeof fetch === 'undefined') {
throw new Error("fetch未定义");
}
logger.info("✓ Promise已定义");
logger.info("✓ fetch已定义");
return "https://example.com/success";
}
""";
CustomParserConfig config = CustomParserConfig.builder()
.type("test_fetch")
.displayName("Fetch测试")
.matchPattern("https://example.com/s/(?<KEY>\\w+)")
.jsCode(jsCode)
.isJsParser(true)
.build();
// 注册到注册表
CustomParserRegistry.register(config);
try {
// 使用ParserCreate创建工具
IPanTool tool = ParserCreate.fromType("test_fetch")
.shareKey("test123")
.createTool();
String result = tool.parseSync();
log.info("测试结果: {}", result);
assert "https://example.com/success".equals(result) : "结果不匹配";
System.out.println("✓ Fetch polyfill加载测试通过");
} catch (Exception e) {
log.error("测试失败", e);
throw new RuntimeException("Fetch polyfill加载失败: " + e.getMessage(), e);
}
}
@Test
public void testPromiseBasicUsage() {
// 初始化Vertx
Vertx vertx = Vertx.vertx();
WebClientVertxInit.init(vertx);
// 清理注册表
CustomParserRegistry.clear();
String jsCode = """
function parse(shareLinkInfo, http, logger) {
logger.info("测试Promise基本用法");
// 创建一个Promise
var testPromise = new Promise(function(resolve, reject) {
resolve("Promise成功");
});
var result = null;
testPromise.then(function(value) {
logger.info("Promise结果: " + value);
result = value;
});
// 等待Promise完成简单同步等待
var timeout = 1000;
var start = Date.now();
while (result === null && (Date.now() - start) < timeout) {
java.lang.Thread.sleep(10);
}
if (result === null) {
throw new Error("Promise未完成");
}
return "https://example.com/" + result;
}
""";
CustomParserConfig config = CustomParserConfig.builder()
.type("test_promise")
.displayName("Promise测试")
.matchPattern("https://example.com/s/(?<KEY>\\w+)")
.jsCode(jsCode)
.isJsParser(true)
.build();
// 注册到注册表
CustomParserRegistry.register(config);
try {
// 使用ParserCreate创建工具
IPanTool tool = ParserCreate.fromType("test_promise")
.shareKey("test456")
.createTool();
String result = tool.parseSync();
log.info("测试结果: {}", result);
assert result.contains("Promise成功") : "结果不包含'Promise成功'";
System.out.println("✓ Promise测试通过");
} catch (Exception e) {
log.error("测试失败", e);
throw new RuntimeException("Promise测试失败: " + e.getMessage(), e);
}
}
}

View File

@@ -4,6 +4,36 @@ import axios from 'axios';
* 演练场API服务
*/
export const playgroundApi = {
/**
* 获取Playground状态
* @returns {Promise} 状态信息 {enabled, needPassword, authed}
*/
async getStatus() {
try {
const response = await axios.get('/v2/playground/status');
if (response.data && response.data.data) {
return response.data.data;
}
return response.data;
} catch (error) {
throw new Error(error.response?.data?.msg || error.message || '获取状态失败');
}
},
/**
* Playground登录
* @param {string} password - 密码
* @returns {Promise} 登录结果
*/
async login(password) {
try {
const response = await axios.post('/v2/playground/login', { password });
return response.data;
} catch (error) {
throw new Error(error.response?.data?.msg || error.message || '登录失败');
}
},
/**
* 测试执行JavaScript代码
* @param {string} jsCode - JavaScript代码

View File

@@ -1,6 +1,64 @@
<template>
<div ref="playgroundContainer" class="playground-container" :class="{ 'dark-theme': isDarkMode, 'fullscreen': isFullscreen }">
<el-card class="playground-card">
<!-- 加载状态 -->
<el-card v-if="statusLoading" class="playground-card" v-loading="true" element-loading-text="正在加载...">
<div style="height: 400px;"></div>
</el-card>
<!-- Playground未开启 -->
<el-card v-else-if="!enabled" class="playground-card">
<el-empty description="Playground未开启">
<template #extra>
<p style="color: #909399; font-size: 14px; margin-top: 10px;">
Playground功能目前未启用请联系管理员在配置中开启此功能
</p>
</template>
</el-empty>
</el-card>
<!-- 需要密码但未认证 -->
<el-card v-else-if="needPassword && !authed" class="playground-card">
<div class="password-container">
<h2>🔒 Playground访问认证</h2>
<p style="color: #909399; margin: 20px 0;">
此Playground需要密码访问请输入密码后继续使用
</p>
<el-form @submit.prevent="submitPassword" style="max-width: 400px; margin: 0 auto;">
<el-form-item>
<el-input
v-model="password"
type="password"
placeholder="请输入访问密码"
size="large"
show-password
clearable
@keyup.enter="submitPassword"
>
<template #prefix>
<el-icon><Lock /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item v-if="authError" style="margin-bottom: 10px;">
<el-alert type="error" :title="authError" :closable="false" />
</el-form-item>
<el-form-item>
<el-button
type="primary"
size="large"
style="width: 100%;"
:loading="authenticating"
@click="submitPassword"
>
{{ authenticating ? '验证中...' : '验证并进入' }}
</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
<!-- 已启用且已认证或公开模式 -->
<el-card v-else class="playground-card">
<template #header>
<div class="card-header">
<div class="header-left">
@@ -473,6 +531,7 @@
<script>
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Lock } from '@element-plus/icons-vue';
import { useMagicKeys, useFullscreen, useEventListener } from '@vueuse/core';
import { Splitpanes, Pane } from 'splitpanes';
import 'splitpanes/dist/splitpanes.css';
@@ -487,7 +546,8 @@ export default {
MonacoEditor,
JsonViewer,
Splitpanes,
Pane
Pane,
Lock
},
setup() {
const editorRef = ref(null);
@@ -505,6 +565,15 @@ export default {
const loadingList = ref(false);
const publishDialogVisible = ref(false);
const publishing = ref(false);
// Playground状态相关
const statusLoading = ref(true);
const enabled = ref(false);
const needPassword = ref(false);
const authed = ref(false);
const password = ref('');
const authError = ref('');
const authenticating = ref(false);
const publishForm = ref({
jsCode: ''
});
@@ -656,6 +725,63 @@ function parseById(shareLinkInfo, http, logger) {
}
};
// 获取Playground状态
const fetchStatus = async () => {
try {
const result = await playgroundApi.getStatus();
enabled.value = result.enabled;
needPassword.value = result.needPassword;
authed.value = result.authed;
} catch (error) {
console.error('获取Playground状态失败:', error);
ElMessage.error('获取Playground状态失败: ' + error.message);
// 默认为未启用
enabled.value = false;
}
};
// 提交密码
const submitPassword = async () => {
if (!password.value) {
authError.value = '请输入密码';
return;
}
authError.value = '';
authenticating.value = true;
try {
const result = await playgroundApi.login(password.value);
if (result.success || result.code === 200) {
authed.value = true;
ElMessage.success('认证成功');
// 初始化Playground
await nextTick();
await initPlayground();
} else {
authError.value = result.msg || '密码错误';
}
} catch (error) {
console.error('Playground登录失败:', error);
authError.value = error.message || '登录失败';
} finally {
authenticating.value = false;
}
};
// 初始化Playground加载编辑器等
const initPlayground = async () => {
await initMonacoTypes();
// 加载保存的代码
const saved = localStorage.getItem('playground_code');
if (saved) {
jsCode.value = saved;
} else {
jsCode.value = exampleCode;
}
};
// 代码变化处理
const onCodeChange = (value) => {
jsCode.value = value;
@@ -1156,14 +1282,19 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
onMounted(async () => {
await nextTick();
checkDarkMode();
await initMonacoTypes();
// 加载保存的代码
const saved = localStorage.getItem('playground_code');
if (saved) {
jsCode.value = saved;
} else {
jsCode.value = exampleCode;
// 首先获取Playground状态
await fetchStatus();
statusLoading.value = false;
// 如果未启用,直接返回
if (!enabled.value) {
return;
}
// 如果不需要密码或已认证初始化Playground
if (!needPassword.value || authed.value) {
await initPlayground();
}
// 加载保存的主题
@@ -1244,6 +1375,17 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
helpCollapseActive,
consoleLogs,
clearConsoleLogs,
// Playground状态相关
statusLoading,
enabled,
needPassword,
authed,
password,
authError,
authenticating,
fetchStatus,
submitPassword,
initPlayground,
// 新增功能
collapsedPanels,
togglePanel,
@@ -1266,6 +1408,27 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
</script>
<style>
/* Password container styles */
.password-container {
text-align: center;
padding: 60px 20px;
min-height: 400px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.password-container h2 {
font-size: 28px;
margin-bottom: 10px;
color: var(--el-text-color-primary);
}
.dark-theme .password-container {
color: rgba(255, 255, 255, 0.85);
}
/* API示例对话框样式 */
.api-example-dialog {
width: 80%;

View File

@@ -0,0 +1,293 @@
# Playground 访问控制配置指南
## 概述
Playground 演练场是一个用于编写、测试和发布 JavaScript 解析脚本的在线开发环境。为了提升安全性,现在支持灵活的访问控制配置。
## 配置说明
`web-service/src/main/resources/app-dev.yml` 文件中添加以下配置:
```yaml
# Playground演练场配置
playground:
# 是否启用Playground默认关闭
enabled: false
# 访问密码可选。仅在enabled=true时生效
# 为空时表示公开访问,不需要密码
password: ""
```
### 配置参数
#### `enabled`
- **类型**: `boolean`
- **默认值**: `false`
- **说明**: 控制 Playground 功能是否启用
- `false`: Playground 完全关闭,页面和所有相关 API 均不可访问
- `true`: Playground 启用,可以正常使用
#### `password`
- **类型**: `string`
- **默认值**: `""` (空字符串)
- **说明**: 访问密码(仅在 `enabled = true` 时生效)
- 空字符串或 `null`: 公开访问模式,无需密码
- 非空字符串: 需要输入正确密码才能访问
## 访问模式
### 1. 完全禁用模式 (enabled = false)
这是**默认且推荐的生产环境配置**。
```yaml
playground:
enabled: false
password: ""
```
**行为**:
- `/playground` 页面显示"Playground未开启"提示
- 所有 Playground 相关 API`/v2/playground/**`)返回错误提示
- 最安全的模式,适合生产环境
**适用场景**:
- 生产环境部署
- 不需要使用 Playground 功能的情况
---
### 2. 密码保护模式 (enabled = true, password 非空)
这是**公网环境的推荐配置**。
```yaml
playground:
enabled: true
password: "your_strong_password_here"
```
**行为**:
- 访问 `/playground` 页面时会显示密码输入框
- 需要输入正确密码才能进入编辑器
- 密码验证通过后,在当前会话中保持已登录状态
- 会话基于客户端 IP 或 Cookie 进行识别
**适用场景**:
- 需要在公网环境使用 Playground
- 多人共享访问,但需要访问控制
- 团队协作开发环境
**示例**:
```yaml
playground:
enabled: true
password: "MySecure@Password123"
```
---
### 3. 公开访问模式 (enabled = true, password 为空)
⚠️ **仅建议在本地开发或内网环境使用**
```yaml
playground:
enabled: true
password: ""
```
**行为**:
- Playground 对所有访问者开放
- 无需输入密码即可使用所有功能
- 页面加载后直接显示编辑器
**适用场景**:
- 本地开发环境(`localhost`
- 完全隔离的内网环境
- 个人使用且不暴露在公网
**⚠️ 安全警告**:
> **强烈不建议在公网环境下使用此配置!**
>
> 公开访问模式允许任何人:
> - 执行任意 JavaScript 代码(虽然有沙箱限制)
> - 发布解析器脚本到数据库
> - 查看、修改、删除已有的解析器
> - 可能导致服务器资源被滥用
>
> 如果必须在公网环境开启 Playground请务必
> 1. 设置一个足够复杂的密码
> 2. 定期更换密码
> 3. 通过防火墙或网关限制访问来源IP 白名单)
> 4. 启用访问日志监控
> 5. 考虑使用 HTTPS 加密传输
---
## 技术实现
### 后端实现
#### 状态检查 API
```
GET /v2/playground/status
```
返回:
```json
{
"enabled": true,
"needPassword": true,
"authed": false
}
```
#### 登录 API
```
POST /v2/playground/login
Content-Type: application/json
{
"password": "your_password"
}
```
#### 认证机制
- 使用 Vert.x 的 `SharedData` 存储认证状态
- 基于客户端 IP 或 Cookie 中的 session ID 识别用户
- 密码验证通过后在 `playground_auth` Map 中记录
#### 受保护的端点
所有以下端点都需要通过访问控制检查:
- `POST /v2/playground/test` - 执行测试
- `GET /v2/playground/types.js` - 获取类型定义
- `GET /v2/playground/parsers` - 获取解析器列表
- `POST /v2/playground/parsers` - 保存解析器
- `PUT /v2/playground/parsers/:id` - 更新解析器
- `DELETE /v2/playground/parsers/:id` - 删除解析器
- `GET /v2/playground/parsers/:id` - 获取解析器详情
### 前端实现
#### 状态检查流程
1. 页面加载时调用 `/v2/playground/status`
2. 根据返回的状态显示不同界面:
- `enabled = false`: 显示"未开启"提示
- `enabled = true & needPassword = true & !authed`: 显示密码输入框
- `enabled = true & (!needPassword || authed)`: 加载编辑器
#### 密码输入界面
- 密码输入框(支持显示/隐藏密码)
- 验证按钮
- 错误提示
- 支持回车键提交
---
## 配置示例
### 示例 1: 生产环境(推荐)
```yaml
playground:
enabled: false
password: ""
```
### 示例 2: 公网开发环境
```yaml
playground:
enabled: true
password: "Str0ng!P@ssw0rd#2024"
```
### 示例 3: 本地开发
```yaml
playground:
enabled: true
password: ""
```
---
## 常见问题
### Q1: 忘记密码怎么办?
**A**: 直接修改 `app-dev.yml` 配置文件中的 `password` 值,然后重启服务即可。
### Q2: 可以动态修改密码吗?
**A**: 目前需要修改配置文件并重启服务。密码在服务启动时加载。
### Q3: 多个用户可以同时使用吗?
**A**: 可以。密码验证通过后,每个客户端都会保持独立的认证状态。
### Q4: 公开模式下有什么安全限制吗?
**A**:
- 代码执行有大小限制128KB
- JavaScript 在 Nashorn 沙箱中运行
- 最多创建 100 个解析器
- 但仍然建议在内网或本地使用
### Q5: 密码会明文传输吗?
**A**: 如果使用 HTTP是的。强烈建议配置 HTTPS 以加密传输。
### Q6: 会话会过期吗?
**A**: 当前实现基于内存存储,服务重启后需要重新登录。单个会话不会主动过期。
---
## 安全建议
### 🔒 强密码要求
如果启用密码保护,请确保密码符合以下要求:
- 长度至少 12 位
- 包含大小写字母、数字和特殊字符
- 避免使用常见单词或生日等个人信息
- 定期更换密码(建议每季度一次)
### 🌐 网络安全措施
- 在生产环境使用 HTTPS
- 配置防火墙或网关限制访问来源
- 使用反向代理(如 Nginx添加额外的安全层
- 启用访问日志和监控
### 📝 最佳实践
1. **默认禁用**: 除非必要,保持 `enabled: false`
2. **密码保护**: 公网环境务必设置密码
3. **访问控制**: 结合 IP 白名单限制访问
4. **定期审计**: 检查已创建的解析器脚本
5. **监控告警**: 设置异常访问告警机制
---
## 迁移指南
### 从旧版本升级
如果你的系统之前没有 Playground 访问控制,升级后:
1. **默认行为**: Playground 将被禁用(`enabled: false`
2. **如需启用**: 在配置文件中添加上述配置
3. **兼容性**: 完全向后兼容,不影响其他功能
---
## 更新日志
### v0.1.8
- 添加 Playground 访问控制功能
- 支持三种访问模式:禁用、密码保护、公开访问
- 前端添加密码输入界面
- 所有 Playground API 受保护
---
## 技术支持
如有问题,请访问:
- GitHub Issues: https://github.com/qaiu/netdisk-fast-download/issues
- 项目文档: https://github.com/qaiu/netdisk-fast-download
---
**最后更新**: 2025-12-07

View File

@@ -3,6 +3,7 @@ package cn.qaiu.lz;
import cn.qaiu.WebClientVertxInit;
import cn.qaiu.db.pool.JDBCPoolInit;
import cn.qaiu.lz.common.cache.CacheConfigLoader;
import cn.qaiu.lz.common.config.PlaygroundConfig;
import cn.qaiu.lz.common.interceptorImpl.RateLimiter;
import cn.qaiu.vx.core.Deploy;
import cn.qaiu.vx.core.util.ConfigConstant;
@@ -88,5 +89,10 @@ public class AppMain {
JsonObject auths = jsonObject.getJsonObject(ConfigConstant.AUTHS);
localMap.put(ConfigConstant.AUTHS, auths);
}
// Playground配置
if (jsonObject.containsKey("playground")) {
PlaygroundConfig.init(jsonObject.getJsonObject("playground"));
}
}
}

View File

@@ -0,0 +1,47 @@
package cn.qaiu.lz.common.config;
import io.vertx.core.json.JsonObject;
import org.apache.commons.lang3.StringUtils;
/**
* Playground配置加载器
*
* @author <a href="https://qaiu.top">QAIU</a>
*/
public class PlaygroundConfig {
private static boolean enabled = false;
private static String password = "";
/**
* 初始化配置
* @param config 配置对象
*/
public static void init(JsonObject config) {
if (config == null) {
return;
}
enabled = config.getBoolean("enabled", false);
password = config.getString("password", "");
}
/**
* 是否启用Playground
*/
public static boolean isEnabled() {
return enabled;
}
/**
* 获取密码
*/
public static String getPassword() {
return password;
}
/**
* 是否需要密码
*/
public static boolean hasPassword() {
return StringUtils.isNotBlank(password);
}
}

View File

@@ -1,6 +1,7 @@
package cn.qaiu.lz.web.controller;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.lz.common.config.PlaygroundConfig;
import cn.qaiu.lz.web.model.PlaygroundTestResp;
import cn.qaiu.lz.web.service.DbService;
import cn.qaiu.parser.ParserCreate;
@@ -13,6 +14,7 @@ import cn.qaiu.vx.core.enums.RouteMethod;
import cn.qaiu.vx.core.model.JsonResult;
import cn.qaiu.vx.core.util.AsyncServiceUtil;
import cn.qaiu.vx.core.util.ResponseUtil;
import cn.qaiu.vx.core.util.VertxHolder;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.http.HttpServerRequest;
@@ -42,8 +44,179 @@ public class PlaygroundApi {
private static final int MAX_PARSER_COUNT = 100;
private static final int MAX_CODE_LENGTH = 128 * 1024; // 128KB 代码长度限制
private static final String AUTH_SESSION_KEY = "playground_authed_";
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
/**
* 获取Playground状态
*
* @param ctx 路由上下文
* @return 状态信息
*/
@RouteMapping(value = "/status", method = RouteMethod.GET)
public Future<JsonObject> getStatus(RoutingContext ctx) {
Promise<JsonObject> promise = Promise.promise();
try {
boolean enabled = PlaygroundConfig.isEnabled();
boolean needPassword = enabled && PlaygroundConfig.hasPassword();
boolean authed = false;
if (enabled) {
if (!needPassword) {
// 公开模式,无需认证
authed = true;
} else {
// 检查是否已认证
String clientId = getClientId(ctx.request());
authed = isAuthenticated(clientId);
}
}
JsonObject result = new JsonObject()
.put("enabled", enabled)
.put("needPassword", needPassword)
.put("authed", authed);
promise.complete(JsonResult.data(result).toJsonObject());
} catch (Exception e) {
log.error("获取Playground状态失败", e);
promise.complete(JsonResult.error("获取状态失败: " + e.getMessage()).toJsonObject());
}
return promise.future();
}
/**
* Playground登录
*
* @param ctx 路由上下文
* @return 登录结果
*/
@RouteMapping(value = "/login", method = RouteMethod.POST)
public Future<JsonObject> login(RoutingContext ctx) {
Promise<JsonObject> promise = Promise.promise();
try {
if (!PlaygroundConfig.isEnabled()) {
promise.complete(JsonResult.error("Playground未开启").toJsonObject());
return promise.future();
}
if (!PlaygroundConfig.hasPassword()) {
// 无密码模式,直接标记为已认证
String clientId = getClientId(ctx.request());
setAuthenticated(clientId, true);
promise.complete(JsonResult.success("登录成功").toJsonObject());
return promise.future();
}
JsonObject body = ctx.body().asJsonObject();
String password = body.getString("password");
if (StringUtils.equals(password, PlaygroundConfig.getPassword())) {
String clientId = getClientId(ctx.request());
setAuthenticated(clientId, true);
promise.complete(JsonResult.success("登录成功").toJsonObject());
} else {
promise.complete(JsonResult.error("密码错误").toJsonObject());
}
} catch (Exception e) {
log.error("Playground登录失败", e);
promise.complete(JsonResult.error("登录失败: " + e.getMessage()).toJsonObject());
}
return promise.future();
}
/**
* 检查Playground是否启用和已认证
*/
private void ensurePlaygroundAccess(RoutingContext ctx) {
if (!PlaygroundConfig.isEnabled()) {
throw new IllegalStateException("Playground未开启");
}
if (PlaygroundConfig.hasPassword()) {
String clientId = getClientId(ctx.request());
if (!isAuthenticated(clientId)) {
throw new IllegalStateException("未授权访问Playground");
}
}
}
/**
* 获取客户端唯一标识
*/
private String getClientId(HttpServerRequest request) {
// 优先使用Cookie中的session id否则使用IP
String sessionId = extractSessionIdFromCookie(request);
if (sessionId != null) {
return sessionId;
}
return getClientIp(request);
}
/**
* 从Cookie中提取session ID
*/
private String extractSessionIdFromCookie(HttpServerRequest request) {
String cookie = request.getHeader("Cookie");
if (cookie == null || !cookie.contains("playground_session=")) {
return null;
}
try {
final String SESSION_KEY = "playground_session=";
int startIndex = cookie.indexOf(SESSION_KEY);
if (startIndex == -1) {
return null;
}
startIndex += SESSION_KEY.length();
int endIndex = cookie.indexOf(";", startIndex);
if (endIndex != -1) {
return cookie.substring(startIndex, endIndex).trim();
} else {
return cookie.substring(startIndex).trim();
}
} catch (Exception e) {
log.warn("Failed to extract session ID from cookie", e);
return null;
}
}
/**
* 检查是否已认证
*/
private boolean isAuthenticated(String clientId) {
try {
Boolean authed = (Boolean) VertxHolder.getVertxInstance()
.sharedData()
.getLocalMap("playground_auth")
.get(AUTH_SESSION_KEY + clientId);
return authed != null && authed;
} catch (Exception e) {
log.error("检查认证状态失败", e);
return false;
}
}
/**
* 设置认证状态
*/
private void setAuthenticated(String clientId, boolean authed) {
try {
VertxHolder.getVertxInstance()
.sharedData()
.getLocalMap("playground_auth")
.put(AUTH_SESSION_KEY + clientId, authed);
} catch (Exception e) {
log.error("设置认证状态失败", e);
}
}
/**
* 测试执行JavaScript代码
*
@@ -55,6 +228,9 @@ public class PlaygroundApi {
Promise<JsonObject> promise = Promise.promise();
try {
// 检查访问权限
ensurePlaygroundAccess(ctx);
JsonObject body = ctx.body().asJsonObject();
String jsCode = body.getString("jsCode");
String shareUrl = body.getString("shareUrl");
@@ -220,24 +396,33 @@ public class PlaygroundApi {
* 获取types.js文件内容
*
* @param response HTTP响应
* @param ctx 路由上下文
*/
@RouteMapping(value = "/types.js", method = RouteMethod.GET)
public void getTypesJs(HttpServerResponse response) {
try (InputStream inputStream = getClass().getClassLoader()
.getResourceAsStream("custom-parsers/types.js")) {
public void getTypesJs(HttpServerResponse response, RoutingContext ctx) {
try {
// 检查访问权限
ensurePlaygroundAccess(ctx);
InputStream inputStream = getClass().getClassLoader()
.getResourceAsStream("custom-parsers/types.js");
if (inputStream == null) {
ResponseUtil.fireJsonResultResponse(response, JsonResult.error("types.js文件不存在"));
return;
}
String content = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))
.lines()
.collect(Collectors.joining("\n"));
response.putHeader("Content-Type", "text/javascript; charset=utf-8")
.end(content);
try (inputStream) {
String content = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))
.lines()
.collect(Collectors.joining("\n"));
response.putHeader("Content-Type", "text/javascript; charset=utf-8")
.end(content);
}
} catch (IllegalStateException e) {
log.error("访问Playground失败", e);
ResponseUtil.fireJsonResultResponse(response, JsonResult.error(e.getMessage()));
} catch (Exception e) {
log.error("读取types.js失败", e);
ResponseUtil.fireJsonResultResponse(response, JsonResult.error("读取types.js失败: " + e.getMessage()));
@@ -248,8 +433,15 @@ public class PlaygroundApi {
* 获取解析器列表
*/
@RouteMapping(value = "/parsers", method = RouteMethod.GET)
public Future<JsonObject> getParserList() {
return dbService.getPlaygroundParserList();
public Future<JsonObject> getParserList(RoutingContext ctx) {
try {
// 检查访问权限
ensurePlaygroundAccess(ctx);
return dbService.getPlaygroundParserList();
} catch (Exception e) {
log.error("获取解析器列表失败", e);
return Future.succeededFuture(JsonResult.error(e.getMessage()).toJsonObject());
}
}
/**
@@ -260,6 +452,9 @@ public class PlaygroundApi {
Promise<JsonObject> promise = Promise.promise();
try {
// 检查访问权限
ensurePlaygroundAccess(ctx);
JsonObject body = ctx.body().asJsonObject();
String jsCode = body.getString("jsCode");
@@ -359,6 +554,9 @@ public class PlaygroundApi {
Promise<JsonObject> promise = Promise.promise();
try {
// 检查访问权限
ensurePlaygroundAccess(ctx);
JsonObject body = ctx.body().asJsonObject();
String jsCode = body.getString("jsCode");
@@ -410,16 +608,30 @@ public class PlaygroundApi {
* 删除解析器
*/
@RouteMapping(value = "/parsers/:id", method = RouteMethod.DELETE)
public Future<JsonObject> deleteParser(Long id) {
return dbService.deletePlaygroundParser(id);
public Future<JsonObject> deleteParser(RoutingContext ctx, Long id) {
try {
// 检查访问权限
ensurePlaygroundAccess(ctx);
return dbService.deletePlaygroundParser(id);
} catch (Exception e) {
log.error("删除解析器失败", e);
return Future.succeededFuture(JsonResult.error(e.getMessage()).toJsonObject());
}
}
/**
* 根据ID获取解析器
*/
@RouteMapping(value = "/parsers/:id", method = RouteMethod.GET)
public Future<JsonObject> getParserById(Long id) {
return dbService.getPlaygroundParserById(id);
public Future<JsonObject> getParserById(RoutingContext ctx, Long id) {
try {
// 检查访问权限
ensurePlaygroundAccess(ctx);
return dbService.getPlaygroundParserById(id);
} catch (Exception e) {
log.error("获取解析器失败", e);
return Future.succeededFuture(JsonResult.error(e.getMessage()).toJsonObject());
}
}
/**

View File

@@ -101,4 +101,12 @@ auths:
# 123网盘配置用户名密码
ye:
username:
password:
password:
# Playground演练场配置
playground:
# 是否启用Playground默认关闭
enabled: false
# 访问密码可选。仅在enabled=true时生效
# 为空时表示公开访问,不需要密码
password: ""