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
45 changed files with 1218 additions and 4814 deletions

View File

@@ -1,17 +0,0 @@
version: 2
updates:
- package-ecosystem: "maven"
directory: "/"
open-pull-requests-limit: 10
ignore:
# 忽略通过 BOM 管理的 Vert.x 依赖
# 这些依赖的版本通过 vertx-dependencies BOM 统一管理
# 应该通过更新 pom.xml 中的 vertx.version 属性来更新这些依赖
- dependency-name: "io.vertx:vertx-web"
- dependency-name: "io.vertx:vertx-codegen"
- dependency-name: "io.vertx:vertx-config"
- dependency-name: "io.vertx:vertx-config-yaml"
- dependency-name: "io.vertx:vertx-service-proxy"
- dependency-name: "io.vertx:vertx-web-proxy"
- dependency-name: "io.vertx:vertx-web-client"

2
.gitignore vendored
View File

@@ -29,8 +29,6 @@ target/
/src/logs/
*.zip
sdkTest.log
app.yml
app-local.yml
#some local files

View File

@@ -40,11 +40,11 @@ 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功能** [JS解析器演练场密码保护说明](PLAYGROUND_PASSWORD_PROTECTION.md)
**Playground访问控制** [配置指南](web-service/doc/PLAYGROUND_ACCESS_CONTROL.md) - 了解如何配置演练场的访问权限
## 预览地址
[预览地址1](https://lz.qaiu.top)
[预览地址2](https://lz0.qaiu.top)
[预览地址2](https://lzzz.qaiu.top)
[移动/联通/天翼云盘大文件试用版](https://189.qaiu.top)
main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/netdisk-fast-download/tree/main-jdk11)
@@ -61,7 +61,7 @@ main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/
- [蓝奏云-lz](https://pc.woozooo.com/)
- [蓝奏云优享-iz](https://www.ilanzou.com/)
- [奶牛快传-cow](https://cowtransfer.com/)
- ~[奶牛快传-cow(即将停服)](https://cowtransfer.com/)~
- [移动云云空间-ec](https://www.ecpan.cn/web)
- [小飞机网盘-fj](https://www.feijipan.com/)
- [亿方云-fc](https://www.fangcloud.com/)
@@ -473,11 +473,10 @@ Core模块集成Vert.x实现类似spring的注解式路由API
</p>
### 关于赞助定制专属版
1. 专属版提供对小飞机,蓝奏优享大文件解析的支持, 提供天翼云盘/移动云盘/联通云盘的解析支持
2. 可提供托管服务:包含部署服务和云服务器环境
3. 可提供功能定制开发
您可能需要提供一定的资金赞助支持定制专属版, 请添加以下任意一个联系方式详谈赞助模式:
### 关于专属版
99元, 提供对小飞机,蓝奏优享大文件解析的支持, 提供天翼云盘,移动云盘,联通云盘的解析支持
199元, 包含部署服务, 需提供宝塔环境
提供功能定制开发, 添加以下任意一个联系方式详谈:
<p>qq: 197575894</p>
<p>wechat: imcoding_</p>

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

@@ -23,8 +23,6 @@ import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.*;
import io.vertx.ext.web.handler.sockjs.SockJSHandler;
import io.vertx.ext.web.handler.sockjs.SockJSHandlerOptions;
import io.vertx.ext.web.sstore.LocalSessionStore;
import io.vertx.ext.web.sstore.SessionStore;
import javassist.CtClass;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
@@ -100,16 +98,6 @@ public class RouterHandlerFactory implements BaseHttpApi {
// 配置文件上传路径
mainRouter.route().handler(BodyHandler.create().setUploadsDirectory("uploads"));
// 配置Session管理 - 用于演练场登录状态持久化
// 30天过期时间毫秒
SessionStore sessionStore = LocalSessionStore.create(VertxHolder.getVertxInstance());
SessionHandler sessionHandler = SessionHandler.create(sessionStore)
.setSessionTimeout(30L * 24 * 60 * 60 * 1000) // 30天
.setSessionCookieName("SESSIONID") // Cookie名称
.setCookieHttpOnlyFlag(true) // 防止XSS攻击
.setCookieSecureFlag(false); // 非HTTPS环境设置为false
mainRouter.route().handler(sessionHandler);
// 拦截器
Set<Handler<RoutingContext>> interceptorSet = getInterceptorSet();
Route route0 = mainRouter.route("/*");

View File

@@ -128,9 +128,7 @@ public class ReverseProxyVerticle extends AbstractVerticle {
}
private HttpServer getHttpsServer(JsonObject proxyConf) {
HttpServerOptions httpServerOptions = new HttpServerOptions()
.setCompressionSupported(true);
HttpServerOptions httpServerOptions = new HttpServerOptions();
if (proxyConf.containsKey("ssl")) {
JsonObject sslConfig = proxyConf.getJsonObject("ssl");
@@ -184,7 +182,6 @@ public class ReverseProxyVerticle extends AbstractVerticle {
} else {
staticHandler = StaticHandler.create();
}
if (staticConf.containsKey("directory-listing")) {
staticHandler.setDirectoryListing(staticConf.getBoolean("directory-listing"));
} else if (staticConf.containsKey("index")) {

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

@@ -244,17 +244,19 @@ public class JsHttpClient {
if (data != null) {
if (data instanceof String) {
return request.sendBuffer(Buffer.buffer((String) data));
request.sendBuffer(Buffer.buffer((String) data));
} else if (data instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, String> mapData = (Map<String, String>) data;
return request.sendForm(MultiMap.caseInsensitiveMultiMap().addAll(mapData));
request.sendForm(MultiMap.caseInsensitiveMultiMap().addAll(mapData));
} else {
return request.sendJson(data);
request.sendJson(data);
}
} else {
request.send();
}
return request.send();
}
});
}
@@ -274,17 +276,19 @@ public class JsHttpClient {
if (data != null) {
if (data instanceof String) {
return request.sendBuffer(Buffer.buffer((String) data));
request.sendBuffer(Buffer.buffer((String) data));
} else if (data instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, String> mapData = (Map<String, String>) data;
return request.sendForm(MultiMap.caseInsensitiveMultiMap().addAll(mapData));
request.sendForm(MultiMap.caseInsensitiveMultiMap().addAll(mapData));
} else {
return request.sendJson(data);
request.sendJson(data);
}
} else {
request.send();
}
return request.send();
}
});
}
@@ -318,17 +322,19 @@ public class JsHttpClient {
if (data != null) {
if (data instanceof String) {
return request.sendBuffer(Buffer.buffer((String) data));
request.sendBuffer(Buffer.buffer((String) data));
} else if (data instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, String> mapData = (Map<String, String>) data;
return request.sendForm(MultiMap.caseInsensitiveMultiMap().addAll(mapData));
request.sendForm(MultiMap.caseInsensitiveMultiMap().addAll(mapData));
} else {
return request.sendJson(data);
request.sendJson(data);
}
} else {
request.send();
}
return request.send();
}
});
}

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

@@ -36,21 +36,12 @@ public class JsPlaygroundExecutor {
return thread;
});
// 超时调度线程池,用于处理超时中断
private static final ScheduledExecutorService TIMEOUT_SCHEDULER = Executors.newScheduledThreadPool(2, r -> {
Thread thread = new Thread(r);
thread.setName("playground-timeout-scheduler-" + System.currentTimeMillis());
thread.setDaemon(true);
return thread;
});
private final ShareLinkInfo shareLinkInfo;
private final String jsCode;
private final ScriptEngine engine;
private final JsHttpClient httpClient;
private final JsPlaygroundLogger playgroundLogger;
private final JsShareLinkInfoWrapper shareLinkInfoWrapper;
private final JsFetchBridge fetchBridge;
/**
* 创建演练场执行器
@@ -71,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();
}
@@ -94,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;");
@@ -104,14 +93,7 @@ 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("初始化成功");
playgroundLogger.infoJava("🔒 安全的JavaScript引擎初始化成功演练场");
// 执行JavaScript代码
engine.eval(jsCode);
@@ -169,23 +151,12 @@ public class JsPlaygroundExecutor {
}
}, INDEPENDENT_EXECUTOR);
// 创建超时任务,强制取消执行
ScheduledFuture<?> timeoutTask = TIMEOUT_SCHEDULER.schedule(() -> {
if (!executionFuture.isDone()) {
executionFuture.cancel(true); // 强制中断执行线程
playgroundLogger.errorJava("执行超时,已强制中断");
log.warn("JavaScript执行超时已强制取消");
}
}, EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
// 处理执行结果
executionFuture.whenComplete((result, error) -> {
// 取消超时任务
timeoutTask.cancel(false);
// 添加超时处理
executionFuture.orTimeout(EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.whenComplete((result, error) -> {
if (error != null) {
if (error instanceof CancellationException) {
String timeoutMsg = "JavaScript执行超时超过" + EXECUTION_TIMEOUT_SECONDS + "秒),已强制中断";
if (error instanceof TimeoutException) {
String timeoutMsg = "JavaScript执行超时超过" + EXECUTION_TIMEOUT_SECONDS + "秒),可能存在无限循环";
playgroundLogger.errorJava(timeoutMsg);
log.error(timeoutMsg);
promise.fail(new RuntimeException(timeoutMsg));
@@ -244,23 +215,12 @@ public class JsPlaygroundExecutor {
}
}, INDEPENDENT_EXECUTOR);
// 创建超时任务,强制取消执行
ScheduledFuture<?> timeoutTask = TIMEOUT_SCHEDULER.schedule(() -> {
if (!executionFuture.isDone()) {
executionFuture.cancel(true); // 强制中断执行线程
playgroundLogger.errorJava("执行超时,已强制中断");
log.warn("JavaScript执行超时已强制取消");
}
}, EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
// 处理执行结果
executionFuture.whenComplete((result, error) -> {
// 取消超时任务
timeoutTask.cancel(false);
// 添加超时处理
executionFuture.orTimeout(EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.whenComplete((result, error) -> {
if (error != null) {
if (error instanceof CancellationException) {
String timeoutMsg = "JavaScript执行超时超过" + EXECUTION_TIMEOUT_SECONDS + "秒),已强制中断";
if (error instanceof TimeoutException) {
String timeoutMsg = "JavaScript执行超时超过" + EXECUTION_TIMEOUT_SECONDS + "秒),可能存在无限循环";
playgroundLogger.errorJava(timeoutMsg);
log.error(timeoutMsg);
promise.fail(new RuntimeException(timeoutMsg));
@@ -318,23 +278,12 @@ public class JsPlaygroundExecutor {
}
}, INDEPENDENT_EXECUTOR);
// 创建超时任务,强制取消执行
ScheduledFuture<?> timeoutTask = TIMEOUT_SCHEDULER.schedule(() -> {
if (!executionFuture.isDone()) {
executionFuture.cancel(true); // 强制中断执行线程
playgroundLogger.errorJava("执行超时,已强制中断");
log.warn("JavaScript执行超时已强制取消");
}
}, EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
// 处理执行结果
executionFuture.whenComplete((result, error) -> {
// 取消超时任务
timeoutTask.cancel(false);
// 添加超时处理
executionFuture.orTimeout(EXECUTION_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.whenComplete((result, error) -> {
if (error != null) {
if (error instanceof CancellationException) {
String timeoutMsg = "JavaScript执行超时超过" + EXECUTION_TIMEOUT_SECONDS + "秒),已强制中断";
if (error instanceof TimeoutException) {
String timeoutMsg = "JavaScript执行超时超过" + EXECUTION_TIMEOUT_SECONDS + "秒),可能存在无限循环";
playgroundLogger.errorJava(timeoutMsg);
log.error(timeoutMsg);
promise.fail(new RuntimeException(timeoutMsg));

View File

@@ -29,7 +29,7 @@ import java.util.regex.Pattern;
*/
public class LzTool extends PanBase {
public static final String SHARE_URL_PREFIX = "https://wwwwp.lanzoup.com";
public static final String SHARE_URL_PREFIX = "https://wwww.lanzoum.com";
MultiMap headers0 = HeaderUtils.parseHeaders("""
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate

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,362 +0,0 @@
import requests
import re
import sys
import json
import time
import random
import zlib
def get_timestamp():
"""获取当前时间戳(毫秒)"""
return str(int(time.time() * 1000))
def crc32(data):
"""计算CRC32并转换为16进制"""
crc = zlib.crc32(data.encode()) & 0xffffffff
return format(crc, '08x')
def hex_to_int(hex_str):
"""16进制转10进制"""
return int(hex_str, 16)
def encode123(url, way, version, timestamp):
"""
123盘的URL加密算法
参考C++代码中的encode123函数
"""
# 生成随机数
a = int(10000000 * random.randint(1, 10000000) / 10000)
# 字符映射表
u = "adefghlmyijnopkqrstubcvwsz"
# 将时间戳转换为时间格式
time_long = int(timestamp) // 1000
time_struct = time.localtime(time_long)
time_str = time.strftime("%Y%m%d%H%M", time_struct)
# 根据时间字符串生成g
g = ""
for char in time_str:
digit = int(char)
if digit == 0:
g += u[0]
else:
# 修正数字1对应索引0数字2对应索引1以此类推
g += u[digit - 1]
# 计算y值CRC32的十进制
y = str(hex_to_int(crc32(g)))
# 计算最终的CRC32
final_crc_input = f"{time_long}|{a}|{url}|{way}|{version}|{y}"
final_crc = str(hex_to_int(crc32(final_crc_input)))
# 返回加密后的URL参数
return f"?{y}={time_long}-{a}-{final_crc}"
def login_123pan(username, password):
"""登录123盘获取token"""
print(f"🔐 正在登录账号: {username}")
login_data = {
"passport": username,
"password": password,
"remember": True
}
try:
response = requests.post(
"https://login.123pan.com/api/user/sign_in",
json=login_data,
timeout=30
)
result = response.json()
if result.get('code') == 200:
token = result.get('data', {}).get('token', '')
print(f"✅ 登录成功!")
return token
else:
error_msg = result.get('message', '未知错误')
print(f"❌ 登录失败: {error_msg}")
return None
except Exception as e:
print(f"❌ 登录请求失败: {e}")
return None
def get_share_info(share_key, password=''):
"""获取分享信息(不需要登录)"""
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Referer': 'https://www.123pan.com/',
'Origin': 'https://www.123pan.com',
}
api_url = f"https://www.123pan.com/b/api/share/get?limit=100&next=1&orderBy=share_id&orderDirection=desc&shareKey={share_key}&SharePwd={password}&ParentFileId=0&Page=1"
try:
response = requests.get(api_url, headers=headers, timeout=30)
return response.json()
except Exception as e:
print(f"❌ 获取分享信息失败: {e}")
return None
def get_download_url_android(file_info, token):
"""
使用Android平台API获取下载链接关键方法
参考C++代码中的逻辑
"""
# 🔥 关键使用Android平台的请求头
headers = {
'App-Version': '55',
'platform': 'android',
'Authorization': f'Bearer {token}',
'User-Agent': 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36',
'Content-Type': 'application/json',
}
# 构建请求数据
post_data = {
'driveId': 0,
'etag': file_info.get('Etag', ''),
'fileId': file_info.get('FileId'),
'fileName': file_info.get('FileName', ''),
's3keyFlag': file_info.get('S3KeyFlag', ''),
'size': file_info.get('Size'),
'type': 0
}
# 🔥 关键使用encode123加密URL参数
timestamp = get_timestamp()
encrypted_params = encode123('/b/api/file/download_info', 'android', '55', timestamp)
api_url = f"https://www.123pan.com/b/api/file/download_info{encrypted_params}"
print(f" 📡 API URL: {api_url[:80]}...")
try:
response = requests.post(api_url, json=post_data, headers=headers, timeout=30)
result = response.json()
print(f" 📥 API响应: code={result.get('code')}, message={result.get('message', 'N/A')}")
if result.get('code') == 0 and 'data' in result:
download_url = result['data'].get('DownloadUrl') or result['data'].get('DownloadURL')
return download_url
else:
error_msg = result.get('message', '未知错误')
print(f" ✗ API返回错误: {error_msg}")
return None
except Exception as e:
print(f" ✗ 请求失败: {e}")
import traceback
traceback.print_exc()
return None
def start(link, password='', username='', user_password=''):
"""主函数解析123盘分享链接"""
result = {
'code': 200,
'data': [],
'need_login': False
}
# 提取 Share_Key
patterns = [
r'/s/(.*?)\.html',
r'/s/([^/\s]+)',
]
share_key = None
for pattern in patterns:
matches = re.findall(pattern, link)
if matches:
share_key = matches[0]
break
if not share_key:
return {
"code": 201,
"message": "分享地址错误,无法提取分享密钥"
}
print(f"📌 分享密钥: {share_key}")
# 如果提供了账号密码,先登录
token = None
if username and user_password:
token = login_123pan(username, user_password)
if not token:
return {
"code": 201,
"message": "登录失败"
}
else:
print("⚠️ 未提供登录信息,某些文件可能无法下载")
# 获取分享信息
print(f"\n📂 正在获取文件列表...")
share_data = get_share_info(share_key, password)
if not share_data or share_data.get('code') != 0:
error_msg = share_data.get('message', '未知错误') if share_data else '请求失败'
return {
"code": 201,
"message": f"获取分享信息失败: {error_msg}"
}
# 获取文件列表
if 'data' not in share_data or 'InfoList' not in share_data['data']:
return {
"code": 201,
"message": "返回数据格式错误"
}
info_list = share_data['data']['InfoList']
length = len(info_list)
print(f"📁 找到 {length} 个项目\n")
# 遍历文件列表
for i, file_info in enumerate(info_list):
file_type = file_info.get('Type', 0)
file_name = file_info.get('FileName', '')
# 跳过文件夹
if file_type != 0:
print(f"[{i+1}/{length}] 跳过文件夹: {file_name}")
continue
print(f"[{i+1}/{length}] 正在解析: {file_name}")
if not token:
print(f" ⚠️ 需要登录才能获取下载链接")
result['need_login'] = True
continue
# 🔥 使用Android平台API获取下载链接
print(f" 🤖 使用Android平台API...")
download_url = get_download_url_android(file_info, token)
if download_url:
result['data'].append({
"Name": file_name,
"Size": file_info.get('Size', 0),
"DownloadURL": download_url
})
print(f" ✓ 成功获取直链\n")
else:
print(f" ✗ 获取失败\n")
return result
def format_size(size_bytes):
"""格式化文件大小"""
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if size_bytes < 1024.0:
return f"{size_bytes:.2f} {unit}"
size_bytes /= 1024.0
return f"{size_bytes:.2f} PB"
def main():
"""主程序入口"""
if len(sys.argv) < 2:
print("=" * 80)
print(" 123盘直链解析工具 v3.0")
print("=" * 80)
print("\n📖 使用方法:")
print(" python 123.py <分享链接> [选项]")
print("\n⚙️ 选项:")
print(" --pwd <密码> 分享密码(如果有)")
print(" --user <账号> 123盘账号")
print(" --pass <密码> 123盘密码")
print("\n💡 示例:")
print(' # 需要登录的分享(推荐)')
print(' python 123.py "https://www.123pan.com/s/xxxxx" --user "账号" --pass "密码"')
print()
print(' # 有分享密码')
print(' python 123.py "https://www.123pan.com/s/xxxxx" --pwd "分享密码" --user "账号" --pass "密码"')
print("\n✨ 特性:")
print(" • 使用Android平台API完全绕过限制")
print(" • 使用123盘加密算法encode123")
print(" • 支持账号密码登录")
print(" • 无地区限制,无流量限制")
print("=" * 80)
sys.exit(1)
link = sys.argv[1]
password = ''
username = ''
user_password = ''
# 解析参数
i = 2
while i < len(sys.argv):
if sys.argv[i] == '--pwd' and i + 1 < len(sys.argv):
password = sys.argv[i + 1]
i += 2
elif sys.argv[i] == '--user' and i + 1 < len(sys.argv):
username = sys.argv[i + 1]
i += 2
elif sys.argv[i] == '--pass' and i + 1 < len(sys.argv):
user_password = sys.argv[i + 1]
i += 2
else:
i += 1
print("\n" + "=" * 80)
print(" 开始解析分享链接")
print("=" * 80)
print(f"🔗 链接: {link}")
if password:
print(f"🔐 分享密码: {password}")
if username:
print(f"👤 登录账号: {username}")
print("=" * 80)
print()
result = start(link, password, username, user_password)
if result['code'] != 200:
print(f"\n❌ 错误: {result['message']}")
sys.exit(1)
if not result['data']:
print("\n⚠️ 没有成功获取到任何文件的直链")
if result.get('need_login'):
print("\n🔒 该分享需要登录才能下载")
print("\n请使用以下命令:")
print(f' python 123.py "{link}" --user "你的账号" --pass "你的密码"')
sys.exit(1)
print("\n" + "=" * 80)
print(" ✅ 解析成功!")
print("=" * 80)
for idx, file in enumerate(result['data'], 1):
print(f"\n📄 文件 {idx}:")
print(f" 名称: {file['Name']}")
print(f" 大小: {format_size(file['Size'])} ({file['Size']:,} 字节)")
print(f" 直链: {file['DownloadURL']}")
print("-" * 80)
print("\n💾 下载方法:")
print("\n 使用curl命令:")
for file in result['data']:
safe_name = file['Name'].replace('"', '\\"')
print(f' curl -L -o "{safe_name}" "{file["DownloadURL"]}"')
print("\n 使用aria2c命令推荐多线程:")
for file in result['data']:
safe_name = file['Name'].replace('"', '\\"')
print(f' aria2c -x 16 -s 16 -o "{safe_name}" "{file["DownloadURL"]}"')
print("\n💡 提示:")
print(" • 使用Android平台API无地区限制")
print(" • 直链有效期通常为几小时")
print(" • 推荐使用 aria2c 下载(速度最快)")
print()
if __name__ == "__main__":
main()

View File

@@ -549,176 +549,6 @@ public class JsHttpClientTest {
}
}
@Test
public void testPostWithJsonString() {
System.out.println("\n[测试16] POST请求JSON字符串 - httpbin.org/post");
System.out.println("测试修复POST请求发送JSON字符串时请求体是否正确发送");
try {
String url = "https://httpbin.org/post";
System.out.println("请求URL: " + url);
// 模拟阿里云盘登录请求格式
String jsonData = "{\"grant_type\":\"refresh_token\",\"refresh_token\":\"test_token_123\"}";
System.out.println("POST数据JSON字符串: " + jsonData);
// 设置Content-Type为application/json
httpClient.putHeader("Content-Type", "application/json");
System.out.println("设置Content-Type: application/json");
System.out.println("开始请求...");
long startTime = System.currentTimeMillis();
JsHttpClient.JsHttpResponse response = httpClient.post(url, jsonData);
long endTime = System.currentTimeMillis();
System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms");
System.out.println("状态码: " + response.statusCode());
String body = response.body();
System.out.println("响应体前500字符: " + (body != null && body.length() > 500 ? body.substring(0, 500) + "..." : body));
// 验证结果
assertNotNull("响应不能为null", response);
assertEquals("状态码应该是200", 200, response.statusCode());
assertNotNull("响应体不能为null", body);
// 验证请求体是否正确发送httpbin会回显请求数据
assertTrue("响应体应该包含发送的JSON数据",
body.contains("grant_type") || body.contains("refresh_token"));
System.out.println("✓ 测试通过 - POST请求体已正确发送");
} catch (Exception e) {
System.err.println("✗ 测试失败: " + e.getMessage());
e.printStackTrace();
fail("POST JSON字符串请求测试失败: " + e.getMessage());
}
}
@Test
public void testAlipanTokenApi() {
System.out.println("\n[测试20] 阿里云盘Token接口测试 - auth.aliyundrive.com/v2/account/token");
System.out.println("参考 alipan.js 中的登录逻辑,测试请求格式是否正确");
try {
String tokenUrl = "https://auth.aliyundrive.com/v2/account/token";
System.out.println("请求URL: " + tokenUrl);
// 参考 alipan.js 中的请求格式
// setJsonHeaders(http) 设置 Content-Type: application/json 和 User-Agent
httpClient.putHeader("Content-Type", "application/json");
httpClient.putHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
// 参考 alipan.js: JSON.stringify({grant_type: "refresh_token", refresh_token: REFRESH_TOKEN})
String jsonData = "{\"grant_type\":\"refresh_token\",\"refresh_token\":\"\"}";
System.out.println("POST数据JSON字符串: " + jsonData);
System.out.println("注意使用无效token测试错误响应格式");
System.out.println("开始请求...");
long startTime = System.currentTimeMillis();
JsHttpClient.JsHttpResponse response = httpClient.post(tokenUrl, jsonData);
long endTime = System.currentTimeMillis();
System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms");
System.out.println("状态码: " + response.statusCode());
String body = response.body();
System.out.println("响应体: " + body);
// 验证结果
assertNotNull("响应不能为null", response);
// 使用无效token应该返回400或401等错误状态码但请求格式应该是正确的
assertTrue("状态码应该是4xx无效token或200如果token有效",
response.statusCode() >= 200 && response.statusCode() < 500);
assertNotNull("响应体不能为null", body);
// 验证响应格式阿里云盘API通常返回JSON
try {
Object jsonResponse = response.json();
System.out.println("响应JSON解析成功: " + jsonResponse);
assertNotNull("JSON响应不能为null", jsonResponse);
} catch (Exception e) {
System.out.println("警告响应不是有效的JSON格式");
}
// 验证请求头是否正确设置
System.out.println("验证请求头设置...");
Map<String, String> headers = httpClient.getHeaders();
assertTrue("应该设置了Content-Type", headers.containsKey("Content-Type"));
assertEquals("Content-Type应该是application/json",
"application/json", headers.get("Content-Type"));
System.out.println("✓ 测试通过 - 请求格式正确已成功发送到阿里云盘API");
} catch (Exception e) {
System.err.println("✗ 测试失败: " + e.getMessage());
e.printStackTrace();
// 如果是超时或其他网络错误,说明请求格式可能有问题
if (e.getMessage() != null && e.getMessage().contains("超时")) {
fail("请求超时,可能是请求格式问题或网络问题: " + e.getMessage());
} else {
fail("阿里云盘Token接口测试失败: " + e.getMessage());
}
}
}
@Test
public void testAlipanTokenApiWithValidFormat() {
System.out.println("\n[测试21] 阿里云盘Token接口格式验证 - 使用httpbin验证请求格式");
System.out.println("通过httpbin回显验证请求格式是否与alipan.js中的格式一致");
try {
// 使用httpbin来验证请求格式
String testUrl = "https://httpbin.org/post";
System.out.println("测试URL: " + testUrl);
// 参考 alipan.js 中的请求格式
httpClient.clearHeaders(); // 清空之前的头
httpClient.putHeader("Content-Type", "application/json");
httpClient.putHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
// 完全模拟 alipan.js 中的请求体格式
String jsonData = "{\"grant_type\":\"refresh_token\",\"refresh_token\":\"test_refresh_token_12345\"}";
System.out.println("POST数据JSON字符串: " + jsonData);
System.out.println("开始请求...");
long startTime = System.currentTimeMillis();
JsHttpClient.JsHttpResponse response = httpClient.post(testUrl, jsonData);
long endTime = System.currentTimeMillis();
System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms");
System.out.println("状态码: " + response.statusCode());
String body = response.body();
System.out.println("响应体前800字符: " + (body != null && body.length() > 800 ? body.substring(0, 800) + "..." : body));
// 验证结果
assertNotNull("响应不能为null", response);
assertEquals("状态码应该是200", 200, response.statusCode());
assertNotNull("响应体不能为null", body);
// httpbin会回显请求数据验证请求体是否正确发送
assertTrue("响应体应该包含grant_type字段", body.contains("grant_type"));
assertTrue("响应体应该包含refresh_token字段", body.contains("refresh_token"));
assertTrue("响应体应该包含发送的refresh_token值", body.contains("test_refresh_token_12345"));
// 验证Content-Type是否正确
assertTrue("响应体应该包含Content-Type信息", body.contains("application/json"));
// 验证User-Agent是否正确
assertTrue("响应体应该包含User-Agent信息", body.contains("Mozilla"));
System.out.println("✓ 测试通过 - 请求格式与alipan.js中的格式完全一致");
System.out.println(" - JSON请求体正确发送");
System.out.println(" - Content-Type正确设置");
System.out.println(" - User-Agent正确设置");
} catch (Exception e) {
System.err.println("✗ 测试失败: " + e.getMessage());
e.printStackTrace();
fail("阿里云盘Token接口格式验证失败: " + e.getMessage());
}
}
@Test
public void testSetTimeout() {
System.out.println("\n[测试15] 设置超时时间 - setTimeout方法");

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

@@ -3,7 +3,6 @@ module.exports = {
'@vue/cli-plugin-babel/preset'
],
plugins: [
'@vue/babel-plugin-transform-vue-jsx',
'@babel/plugin-transform-class-static-block'
'@vue/babel-plugin-transform-vue-jsx'
]
}

View File

@@ -1,176 +0,0 @@
# Monaco Editor NPM包配置说明
## ✅ 已完成的配置
### 1. NPM包安装
已在 `package.json` 中安装:
- `monaco-editor`: ^0.55.1 - Monaco Editor核心包
- `@monaco-editor/loader`: ^1.4.0 - Monaco Editor加载器
- `monaco-editor-webpack-plugin`: ^7.1.1 - Webpack打包插件devDependencies
### 2. Webpack配置
`vue.config.js` 中已配置:
```javascript
new MonacoEditorPlugin({
languages: ['javascript', 'typescript', 'json'],
features: ['coreCommands', 'find'],
publicPath: process.env.NODE_ENV === 'production' ? './' : '/'
})
```
### 3. 组件配置
`MonacoEditor.vue``Playground.vue` 中已配置:
```javascript
// 配置loader使用本地打包的文件而不是CDN
if (loader.config) {
const vsPath = process.env.NODE_ENV === 'production'
? './js/vs' // 生产环境使用相对路径
: '/js/vs'; // 开发环境使用绝对路径
loader.config({
paths: {
vs: vsPath
}
});
}
```
---
## 🔍 工作原理
### 打包流程
1. `monaco-editor-webpack-plugin` 在构建时将 Monaco Editor 文件打包到 `js/vs/` 目录
2. `@monaco-editor/loader` 通过配置的路径加载本地文件
3. 不再从 CDN`https://cdn.jsdelivr.net`)加载
### 文件结构(构建后)
```
nfd-front/
├── js/
│ └── vs/
│ ├── editor/
│ ├── loader/
│ ├── base/
│ └── ...
└── index.html
```
---
## 🧪 验证方法
### 1. 检查网络请求
打开浏览器开发者工具 → Network标签
- ✅ 应该看到请求 `/js/vs/...``./js/vs/...`
- ❌ 不应该看到请求 `cdn.jsdelivr.net` 或其他CDN域名
### 2. 检查构建产物
```bash
cd web-front
npm run build
ls -la nfd-front/js/vs/
```
应该看到 Monaco Editor 的文件被打包到本地。
### 3. 离线测试
1. 断开网络连接
2. 访问 Playground 页面
3. ✅ 编辑器应该正常加载(因为使用本地文件)
4. ❌ 如果使用CDN编辑器会加载失败
---
## 📝 修改的文件
1.`web-front/src/components/MonacoEditor.vue`
- 添加了 `loader.config()` 配置,明确使用本地路径
2.`web-front/src/views/Playground.vue`
-`initMonacoTypes()` 中添加了相同的配置
3.`web-front/vue.config.js`
- 添加了 `publicPath` 配置,确保路径正确
---
## 🚀 部署
### 开发环境
```bash
cd web-front
npm install # 确保依赖已安装
npm run serve
```
访问 `http://127.0.0.1:6444/playground`,编辑器应该从本地加载。
### 生产环境
```bash
cd web-front
npm run build
```
构建后Monaco Editor 文件会打包到 `nfd-front/js/vs/` 目录。
---
## ⚠️ 注意事项
### 1. 文件大小
Monaco Editor 打包后会增加构建产物大小约2-3MB但这是正常的。
### 2. 首次加载
- 开发环境:文件从 webpack dev server 加载
- 生产环境:文件从本地 `js/vs/` 目录加载
### 3. 缓存
浏览器会缓存 Monaco Editor 文件,更新后可能需要清除缓存。
---
## 🔧 故障排查
### 问题:编辑器无法加载
**检查**
1. 确认 `npm install` 已执行
2. 检查浏览器控制台是否有错误
3. 检查 Network 标签,确认文件路径是否正确
### 问题仍然从CDN加载
**解决**
1. 清除浏览器缓存
2. 确认 `loader.config()` 已正确配置
3. 检查 `vue.config.js` 中的 `publicPath` 配置
### 问题:构建后路径错误
**解决**
- 检查 `publicPath` 配置
- 确认生产环境的相对路径 `./js/vs` 正确
---
## ✅ 优势
1. **离线可用** - 不依赖外部CDN
2. **加载速度** - 本地文件通常比CDN更快
3. **版本控制** - 使用固定版本的Monaco Editor
4. **安全性** - 不依赖第三方CDN服务
5. **稳定性** - CDN故障不影响使用
---
**配置状态**: ✅ 已完成
**验证状态**: ⚠️ 待测试
**建议**: 运行 `npm run build` 并检查构建产物

View File

@@ -5,8 +5,7 @@
"scripts": {
"serve": "vue-cli-service serve",
"dev": "vue-cli-service serve",
"build": "vue-cli-service build && node scripts/compress-vs.js",
"build:no-compress": "vue-cli-service build",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
@@ -17,7 +16,7 @@
"clipboard": "^2.0.11",
"core-js": "^3.8.3",
"element-plus": "2.11.3",
"monaco-editor": "^0.55.1",
"monaco-editor": "^0.45.0",
"qrcode": "^1.5.4",
"splitpanes": "^4.0.4",
"vue": "^3.5.12",
@@ -29,7 +28,6 @@
"@babel/core": "^7.26.0",
"@babel/eslint-parser": "^7.25.9",
"@babel/plugin-transform-class-properties": "^7.26.0",
"@babel/plugin-transform-class-static-block": "^7.26.0",
"@vue/babel-plugin-transform-vue-jsx": "^1.4.0",
"@vue/cli-plugin-babel": "~5.0.8",
"@vue/cli-plugin-eslint": "~5.0.8",
@@ -37,8 +35,7 @@
"compression-webpack-plugin": "^11.1.0",
"eslint": "^9.0.0",
"eslint-plugin-vue": "^9.30.0",
"filemanager-webpack-plugin": "8.0.0",
"monaco-editor-webpack-plugin": "^7.1.1"
"filemanager-webpack-plugin": "8.0.0"
},
"eslintConfig": {
"root": true,

View File

@@ -9,8 +9,8 @@
content="Netdisk fast download,网盘直链解析工具">
<meta name="description"
content="Netdisk fast download 网盘直链解析工具">
<!-- Font Awesome 图标库 - 使用国内CDN -->
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Font Awesome 图标库 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.page-loading-wrap {
padding: 120px;
@@ -154,26 +154,11 @@
}
</style>
<script>
// 等待DOM加载完成后再操作
(function() {
function applyDarkTheme() {
const body = document.body;
if (body && body.classList) {
// 只在用户明确选择暗色模式时才应用,不自动检测系统偏好
if (localStorage.getItem('isDarkMode') === 'true') {
body.classList.add('dark-theme')
const saved = localStorage.getItem('isDarkMode') === 'true'
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches
if (saved || (!saved && systemDark)) {
document.body.classList.add('dark-theme')
}
}
}
// 如果DOM已加载立即执行
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', applyDarkTheme);
} else {
// DOM已加载立即执行
applyDarkTheme();
}
})();
</script>
</head>
<body>

View File

@@ -1,124 +0,0 @@
#!/usr/bin/env node
const path = require("path");
const fs = require("fs");
const zlib = require("zlib");
const { promisify } = require("util");
const gzip = promisify(zlib.gzip);
const readdir = promisify(fs.readdir);
const stat = promisify(fs.stat);
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
// 递归压缩目录下的所有文件
async function compressDirectory(dirPath, threshold = 1024) {
if (!fs.existsSync(dirPath)) {
console.warn(`目录不存在: ${dirPath}`);
return;
}
const files = await readdir(dirPath, { withFileTypes: true });
let compressedCount = 0;
let totalOriginalSize = 0;
let totalCompressedSize = 0;
for (const file of files) {
const filePath = path.join(dirPath, file.name);
if (file.isDirectory()) {
await compressDirectory(filePath, threshold);
} else if (file.isFile()) {
const stats = await stat(filePath);
// 只压缩超过阈值且不是已压缩的文件
if (stats.size > threshold && !filePath.endsWith('.gz') && !filePath.endsWith('.map')) {
try {
const content = await readFile(filePath);
const compressed = await gzip(content);
await writeFile(filePath + '.gz', compressed);
compressedCount++;
totalOriginalSize += stats.size;
totalCompressedSize += compressed.length;
console.log(`${file.name} (${(stats.size / 1024).toFixed(2)}KB -> ${(compressed.length / 1024).toFixed(2)}KB)`);
} catch (error) {
console.warn(`⚠ 压缩失败: ${filePath}`, error.message);
}
}
}
}
if (compressedCount > 0) {
console.log(`\n压缩完成: ${compressedCount} 个文件`);
console.log(`原始大小: ${(totalOriginalSize / 1024 / 1024).toFixed(2)}MB`);
console.log(`压缩后大小: ${(totalCompressedSize / 1024 / 1024).toFixed(2)}MB`);
console.log(`压缩率: ${((1 - totalCompressedSize / totalOriginalSize) * 100).toFixed(1)}%`);
}
}
// 删除未使用的 worker 文件
function deleteUnusedWorkers() {
const jsDir = path.join(__dirname, '../nfd-front/js');
const workers = ['editor.worker.js', 'editor.worker.js.gz', 'json.worker.js', 'json.worker.js.gz', 'ts.worker.js', 'ts.worker.js.gz'];
let deletedCount = 0;
for (const worker of workers) {
const filePath = path.join(jsDir, worker);
if (fs.existsSync(filePath)) {
try {
fs.unlinkSync(filePath);
deletedCount++;
console.log(`✓ 已删除未使用的文件: ${worker}`);
} catch (error) {
console.warn(`⚠ 删除失败: ${worker}`, error.message);
}
}
}
if (deletedCount > 0) {
console.log(`\n已删除 ${deletedCount} 个未使用的 worker 文件\n`);
}
}
// 复制到 webroot
function copyToWebroot() {
const source = path.join(__dirname, '../nfd-front');
const dest = path.join(__dirname, '../../webroot/nfd-front');
// 使用 FileManagerPlugin 的方式,这里用简单的复制
const { execSync } = require('child_process');
try {
// 删除目标目录
if (fs.existsSync(dest)) {
execSync(`rm -rf "${dest}"`, { stdio: 'inherit' });
}
// 复制整个目录
execSync(`cp -R "${source}" "${dest}"`, { stdio: 'inherit' });
console.log('\n✓ 已复制到 webroot');
} catch (error) {
console.error('\n✗ 复制到 webroot 失败:', error.message);
process.exit(1);
}
}
// 主函数
async function main() {
// 先删除未使用的 worker 文件
deleteUnusedWorkers();
// 然后压缩 vs 目录
const vsPath = path.join(__dirname, '../nfd-front/js/vs');
console.log('开始压缩 vs 目录下的文件...\n');
try {
await compressDirectory(vsPath, 1024); // 只压缩超过1KB的文件
console.log('\n✓ vs 目录压缩完成');
} catch (error) {
console.error('\n✗ vs 目录压缩失败:', error);
process.exit(1);
}
// 最后复制到 webroot
copyToWebroot();
}
main();

View File

@@ -43,16 +43,12 @@ watch(darkMode, (newValue) => {
emit('theme-change', newValue)
// 应用主题到body
const html = document.documentElement;
const body = document.body;
if (html && body && html.classList && body.classList) {
if (newValue) {
body.classList.add('dark-theme')
html.classList.add('dark-theme')
document.body.classList.add('dark-theme')
document.documentElement.classList.add('dark-theme')
} else {
body.classList.remove('dark-theme')
html.classList.remove('dark-theme')
}
document.body.classList.remove('dark-theme')
document.documentElement.classList.remove('dark-theme')
}
})
@@ -61,11 +57,9 @@ onMounted(() => {
emit('theme-change', darkMode.value)
// 应用初始主题
const html = document.documentElement;
const body = document.body;
if (html && body && html.classList && body.classList && darkMode.value) {
body.classList.add('dark-theme')
html.classList.add('dark-theme')
if (darkMode.value) {
document.body.classList.add('dark-theme')
document.documentElement.classList.add('dark-theme')
}
})
</script>

View File

@@ -388,14 +388,8 @@ export default {
return date.toLocaleString('zh-CN')
},
checkTheme() {
const html = document.documentElement;
const body = document.body;
if (html && body && html.classList && body.classList) {
this.isDarkTheme = body.classList.contains('dark-theme') ||
html.classList.contains('dark-theme')
} else {
this.isDarkTheme = false;
}
this.isDarkTheme = document.body.classList.contains('dark-theme') ||
document.documentElement.classList.contains('dark-theme')
},
renderContent(h, { node, data, store }) {
const isFolder = data.fileType === 'folder'

View File

@@ -34,7 +34,6 @@ export default {
const editorContainer = ref(null);
let editor = null;
let monaco = null;
let touchHandlers = { start: null, move: null };
const defaultOptions = {
value: props.modelValue,
@@ -95,19 +94,6 @@ export default {
return;
}
// 配置Monaco Editor使用本地打包的文件而不是CDN
if (loader.config) {
const vsPath = process.env.NODE_ENV === 'production'
? './js/vs' // 生产环境使用相对路径
: '/js/vs'; // 开发环境使用绝对路径
loader.config({
paths: {
vs: vsPath
}
});
}
// 初始化Monaco Editor
monaco = await loader.init();
@@ -137,46 +123,6 @@ export default {
if (editorContainer.value) {
editorContainer.value.style.height = props.height;
}
// 移动端:添加触摸缩放来调整字体大小
if (window.innerWidth <= 768 && editorContainer.value) {
let initialDistance = 0;
let initialFontSize = defaultOptions.fontSize || 14;
const minFontSize = 8;
const maxFontSize = 24;
const getTouchDistance = (touch1, touch2) => {
const dx = touch1.clientX - touch2.clientX;
const dy = touch1.clientY - touch2.clientY;
return Math.sqrt(dx * dx + dy * dy);
};
touchHandlers.start = (e) => {
if (e.touches.length === 2 && editor) {
initialDistance = getTouchDistance(e.touches[0], e.touches[1]);
initialFontSize = editor.getOption(monaco.editor.EditorOption.fontSize);
}
};
touchHandlers.move = (e) => {
if (e.touches.length === 2 && editor) {
e.preventDefault(); // 防止页面缩放
const currentDistance = getTouchDistance(e.touches[0], e.touches[1]);
const scale = currentDistance / initialDistance;
const newFontSize = Math.round(initialFontSize * scale);
// 限制字体大小范围
const clampedFontSize = Math.max(minFontSize, Math.min(maxFontSize, newFontSize));
if (clampedFontSize !== editor.getOption(monaco.editor.EditorOption.fontSize)) {
editor.updateOptions({ fontSize: clampedFontSize });
}
}
};
editorContainer.value.addEventListener('touchstart', touchHandlers.start, { passive: false });
editorContainer.value.addEventListener('touchmove', touchHandlers.move, { passive: false });
}
} catch (error) {
console.error('Monaco Editor初始化失败:', error);
console.error('错误详情:', error.stack);
@@ -220,11 +166,6 @@ export default {
});
onBeforeUnmount(() => {
// 清理触摸事件监听器
if (editorContainer.value && touchHandlers.start && touchHandlers.move) {
editorContainer.value.removeEventListener('touchstart', touchHandlers.start);
editorContainer.value.removeEventListener('touchmove', touchHandlers.move);
}
if (editor) {
editor.dispose();
}
@@ -246,26 +187,10 @@ export default {
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
/* 允许用户选择文本 */
-webkit-user-select: text;
user-select: text;
}
.monaco-editor-container :deep(.monaco-editor) {
border-radius: 4px;
}
/* 移动端:禁用页面缩放,只允许编辑器字体缩放 */
@media (max-width: 768px) {
.monaco-editor-container {
/* 禁用页面级别的缩放,只允许编辑器内部字体缩放 */
touch-action: pan-x pan-y;
}
.monaco-editor-container :deep(.monaco-editor) {
/* 禁用页面级别的缩放 */
touch-action: pan-x pan-y;
}
}
</style>

View File

@@ -10,13 +10,7 @@ const routes = [
{ path: '/showFile', component: ShowFile },
{ path: '/showList', component: ShowList },
{ path: '/clientLinks', component: ClientLinks },
{ path: '/playground', component: Playground },
// 404页面 - 必须放在最后
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFound.vue')
}
{ path: '/playground', component: Playground }
]
const router = createRouter({

View File

@@ -1,38 +1,36 @@
import axios from 'axios';
// 创建axios实例配置携带cookie
const axiosInstance = axios.create({
withCredentials: true // 重要允许跨域请求携带cookie
});
/**
* 演练场API服务
*/
export const playgroundApi = {
/**
* 获取Playground状态(是否需要认证)
* @returns {Promise} 状态信息
* 获取Playground状态
* @returns {Promise} 状态信息 {enabled, needPassword, authed}
*/
async getStatus() {
try {
const response = await axiosInstance.get('/v2/playground/status');
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?.error || error.message || '获取状态失败');
throw new Error(error.response?.data?.msg || error.message || '获取状态失败');
}
},
/**
* Playground登录
* @param {string} password - 访问密码
* @param {string} password - 密码
* @returns {Promise} 登录结果
*/
async login(password) {
try {
const response = await axiosInstance.post('/v2/playground/login', { password });
const response = await axios.post('/v2/playground/login', { password });
return response.data;
} catch (error) {
throw new Error(error.response?.data?.error || error.message || '登录失败');
throw new Error(error.response?.data?.msg || error.message || '登录失败');
}
},
@@ -46,7 +44,7 @@ export const playgroundApi = {
*/
async testScript(jsCode, shareUrl, pwd = '', method = 'parse') {
try {
const response = await axiosInstance.post('/v2/playground/test', {
const response = await axios.post('/v2/playground/test', {
jsCode,
shareUrl,
pwd,
@@ -74,7 +72,7 @@ export const playgroundApi = {
*/
async getTypesJs() {
try {
const response = await axiosInstance.get('/v2/playground/types.js', {
const response = await axios.get('/v2/playground/types.js', {
responseType: 'text'
});
return response.data;
@@ -88,7 +86,7 @@ export const playgroundApi = {
*/
async getParserList() {
try {
const response = await axiosInstance.get('/v2/playground/parsers');
const response = await axios.get('/v2/playground/parsers');
// 框架会自动包装成JsonResult需要从data字段获取
if (response.data && response.data.data) {
return {
@@ -109,7 +107,7 @@ export const playgroundApi = {
*/
async saveParser(jsCode) {
try {
const response = await axiosInstance.post('/v2/playground/parsers', { jsCode });
const response = await axios.post('/v2/playground/parsers', { jsCode });
// 框架会自动包装成JsonResult
if (response.data && response.data.data) {
return {
@@ -135,7 +133,7 @@ export const playgroundApi = {
*/
async updateParser(id, jsCode, enabled = true) {
try {
const response = await axiosInstance.put(`/v2/playground/parsers/${id}`, { jsCode, enabled });
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 || '更新解析器失败');
@@ -147,7 +145,7 @@ export const playgroundApi = {
*/
async deleteParser(id) {
try {
const response = await axiosInstance.delete(`/v2/playground/parsers/${id}`);
const response = await axios.delete(`/v2/playground/parsers/${id}`);
return response.data;
} catch (error) {
throw new Error(error.response?.data?.error || error.message || '删除解析器失败');
@@ -159,7 +157,7 @@ export const playgroundApi = {
*/
async getParserById(id) {
try {
const response = await axiosInstance.get(`/v2/playground/parsers/${id}`);
const response = await axios.get(`/v2/playground/parsers/${id}`);
// 框架会自动包装成JsonResult
if (response.data && response.data.data) {
return {
@@ -173,6 +171,6 @@ export const playgroundApi = {
} catch (error) {
throw new Error(error.response?.data?.error || error.response?.data?.msg || error.message || '获取解析器失败');
}
},
}
};

View File

@@ -48,7 +48,7 @@
</div>
<!-- 项目简介移到卡片内 -->
<div class="project-intro">
<div class="intro-title">NFD网盘直链解析0.1.9_b15</div>
<div class="intro-title">NFD网盘直链解析0.1.9_b12</div>
<div class="intro-desc">
<div>支持网盘蓝奏云蓝奏云优享小飞机盘123云盘奶牛快传移动云空间QQ邮箱云盘QQ闪传等 <el-link style="color:#606cf5" href="https://github.com/qaiu/netdisk-fast-download?tab=readme-ov-file#%E7%BD%91%E7%9B%98%E6%94%AF%E6%8C%81%E6%83%85%E5%86%B5" target="_blank"> &gt;&gt; </el-link></div>
<div>文件夹解析支持蓝奏云蓝奏云优享小飞机盘123云盘</div>
@@ -218,7 +218,7 @@
<!-- 版本号显示 -->
<div class="version-info">
<span class="version-text">内部版本: {{ buildVersion }}</span>
<el-link v-if="playgroundEnabled" :href="'/playground'" class="playground-link">脚本演练场</el-link>
<!-- <el-link :href="'/playground'" class="playground-link">JS演练场</el-link>-->
</div>
<!-- 文件解析结果区下方加分享按钮 -->
@@ -248,7 +248,6 @@ import DirectoryTree from '@/components/DirectoryTree'
import parserUrl from '../parserUrl1'
import fileTypeUtils from '@/utils/fileTypeUtils'
import { ElMessage } from 'element-plus'
import { playgroundApi } from '@/utils/playgroundApi'
export const previewBaseUrl = 'https://nfd-parser.github.io/nfd-preview/preview.html?src=';
@@ -298,10 +297,7 @@ export default {
errorButtonVisible: false,
// 版本信息
buildVersion: '',
// 演练场启用状态
playgroundEnabled: false
buildVersion: ''
}
},
methods: {
@@ -320,9 +316,7 @@ export default {
// 主题切换
handleThemeChange(isDark) {
this.isDarkMode = isDark
if (document.body && document.body.classList) {
document.body.classList.toggle('dark-theme', isDark)
}
window.localStorage.setItem('isDarkMode', isDark)
},
@@ -558,19 +552,6 @@ export default {
}
},
// 检查演练场是否启用
async checkPlaygroundEnabled() {
try {
const result = await playgroundApi.getStatus()
if (result && result.data) {
this.playgroundEnabled = result.data.enabled === true
}
} catch (error) {
console.error('检查演练场状态失败:', error)
this.playgroundEnabled = false
}
},
// 新增切换目录树展示模式方法
setDirectoryViewMode(mode) {
this.directoryViewMode = mode
@@ -675,9 +656,6 @@ export default {
// 获取版本号
this.getBuildVersion()
// 检查演练场是否启用
this.checkPlaygroundEnabled()
// 自动读取剪切板
if (this.autoReadClipboard) {
this.getPaste()

View File

@@ -1,135 +0,0 @@
<template>
<div class="not-found-container">
<div class="not-found-content">
<div class="not-found-icon">
<el-icon :size="120"><DocumentDelete /></el-icon>
</div>
<h1 class="not-found-title">404</h1>
<p class="not-found-message">抱歉您访问的页面不存在</p>
<div class="not-found-actions">
<el-button type="primary" @click="goHome">返回首页</el-button>
<el-button @click="goBack">返回上一页</el-button>
</div>
</div>
</div>
</template>
<script>
import { useRouter } from 'vue-router'
import { DocumentDelete } from '@element-plus/icons-vue'
export default {
name: 'NotFound',
components: {
DocumentDelete
},
setup() {
const router = useRouter()
const goHome = () => {
router.push('/')
}
const goBack = () => {
if (window.history.length > 1) {
router.go(-1)
} else {
router.push('/')
}
}
return {
goHome,
goBack
}
}
}
</script>
<style scoped>
.not-found-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
/* background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); */
padding: 20px;
}
.not-found-content {
text-align: center;
background: white;
padding: 60px 40px;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 500px;
width: 100%;
}
.not-found-icon {
color: #909399;
margin-bottom: 20px;
}
.not-found-title {
font-size: 72px;
font-weight: bold;
color: #303133;
margin: 0 0 20px 0;
line-height: 1;
}
.not-found-message {
font-size: 18px;
color: #606266;
margin: 0 0 40px 0;
}
.not-found-actions {
display: flex;
gap: 15px;
justify-content: center;
}
/* 暗色主题支持 */
.dark-theme .not-found-content {
background: #1d1e1f;
}
.dark-theme .not-found-title {
color: #e5eaf3;
}
.dark-theme .not-found-message {
color: #a3a6ad;
}
.dark-theme .not-found-icon {
color: #6c6e72;
}
/* 移动端适配 */
@media (max-width: 768px) {
.not-found-content {
padding: 40px 20px;
}
.not-found-title {
font-size: 48px;
}
.not-found-message {
font-size: 16px;
}
.not-found-actions {
flex-direction: column;
}
.not-found-actions .el-button {
width: 100%;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -61,16 +61,12 @@ export default {
}
},
toggleTheme(isDark) {
const html = document.documentElement;
const body = document.body;
if (html && body && html.classList && body.classList) {
if (isDark) {
body.classList.add('dark-theme')
html.classList.add('dark-theme')
document.body.classList.add('dark-theme')
document.documentElement.classList.add('dark-theme')
} else {
body.classList.remove('dark-theme')
html.classList.remove('dark-theme')
}
document.body.classList.remove('dark-theme')
document.documentElement.classList.remove('dark-theme')
}
}
},

View File

@@ -4,10 +4,8 @@ const path = require("path");
function resolve(dir) {
return path.join(__dirname, dir)
}
const CompressionPlugin = require('compression-webpack-plugin');
const FileManagerPlugin = require('filemanager-webpack-plugin');
const MonacoEditorPlugin = require('monaco-editor-webpack-plugin');
const FileManagerPlugin = require('filemanager-webpack-plugin')
module.exports = {
productionSourceMap: false, // 是否在构建生产包时生成sourceMap文件false将提高构建速度
@@ -45,7 +43,7 @@ module.exports = {
'@': resolve('src')
}
},
// Monaco Editor配置 - 使用本地打包
// Monaco Editor配置
module: {
rules: [
{
@@ -55,18 +53,9 @@ module.exports = {
]
},
plugins: [
new MonacoEditorPlugin({
languages: ['javascript', 'typescript', 'json'],
features: ['coreCommands', 'find', 'format', 'suggest', 'quickCommand'],
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
// Worker 文件输出路径
filename: 'js/[name].worker.js'
}),
new CompressionPlugin({
test: /\.js$|\.html$|\.css/, // 匹配文件
threshold: 10240, // 对超过10k文件压缩
// 排除 js 目录下的 worker 文件Monaco Editor 使用 vs/assets 下的)
exclude: /js\/.*\.worker\.js$/
threshold: 10240 // 对超过10k文件压缩
}),
new FileManagerPlugin({ //初始化 filemanager-webpack-plugin 插件实例
events: {
@@ -81,11 +70,7 @@ module.exports = {
{ source: '../webroot/nfd-front/view/.gitignore', options: { force: true } },
],
copy: [
// 复制 Monaco Editor 的 vs 目录到 js/vs
{
source: './node_modules/monaco-editor/min/vs',
destination: './nfd-front/js/vs'
}
{ source: './nfd-front', destination: '../webroot/nfd-front' }
],
archive: [ //然后我们选择dist文件夹将之打包成dist.zip并放在根目录
{

View File

@@ -1,148 +0,0 @@
# 脚本演练场功能测试报告
## 测试时间
2026-01-02 19:29
## 测试环境
- 服务地址: http://localhost:6401
- 后端版本: 0.1.8
- 前端版本: 0.1.9
## 测试结果总结
### ✅ 1. 服务启动测试
- **状态**: 通过
- **结果**: 服务成功启动监听端口6401
- **日志**:
```
演练场解析器加载完成,共加载 0 个解析器
数据库连接成功
启动成功: 本地服务地址: http://127.0.0.1:6401
```
### ✅ 2. 密码认证功能测试
- **状态**: 通过
- **测试项**:
- ✅ `/v2/playground/status` API正常响应
- ✅ `/v2/playground/login` 登录API正常响应
- ✅ 密码验证机制正常工作
- **结果**:
```json
{
"code": 200,
"msg": "登录成功",
"success": true
}
```
### ✅ 3. BUG1修复验证JS超时机制
- **状态**: 已修复
- **修复内容**:
- 在`JsPlaygroundExecutor`中实现了线程中断机制
- 使用`ScheduledExecutorService`和`Future.cancel(true)`确保超时后强制中断
- 超时时间设置为30秒
- **代码位置**: `parser/src/main/java/cn/qaiu/parser/customjs/JsPlaygroundExecutor.java`
- **验证**: 代码已编译通过,超时机制已实现
### ✅ 4. BUG2修复验证URL正则匹配验证
- **状态**: 已修复
- **修复内容**:
- 在`PlaygroundApi.test()`方法中添加了URL匹配验证
- 执行前检查分享链接是否匹配脚本的`@match`规则
- 不匹配时返回明确的错误提示
- **代码位置**: `web-service/src/main/java/cn/qaiu/lz/web/controller/PlaygroundApi.java:185-209`
- **验证**: 代码已编译通过,验证逻辑已实现
### ✅ 5. BUG3修复验证脚本注册功能
- **状态**: 已修复
- **修复内容**:
- 在`PlaygroundApi.saveParser()`中保存后立即注册到`CustomParserRegistry`
- 在`PlaygroundApi.updateParser()`中更新后重新注册
- 在`PlaygroundApi.deleteParser()`中删除时注销
- 在`AppMain`启动时加载所有已发布的解析器
- **代码位置**:
- `web-service/src/main/java/cn/qaiu/lz/web/controller/PlaygroundApi.java`
- `web-service/src/main/java/cn/qaiu/lz/AppMain.java`
- **验证**: 代码已编译通过,注册机制已实现
### ✅ 6. TypeScript功能移除
- **状态**: 已完成
- **移除内容**:
- ✅ 删除`web-front/src/utils/tsCompiler.js`
- ✅ 从`package.json`移除`typescript`依赖
- ✅ 从`Playground.vue`移除TypeScript相关UI和逻辑
- ✅ 删除后端TypeScript API端点
- ✅ 删除`PlaygroundTypeScriptCode`模型类
- ✅ 删除TypeScript相关文档文件
- **验证**: 代码已编译通过无TypeScript相关代码残留
### ✅ 7. 文本更新JS演练场 → 脚本演练场
- **状态**: 已完成
- **更新位置**:
- ✅ `Home.vue`: "JS演练场" → "脚本演练场"
- ✅ `Playground.vue`: "JS解析器演练场" → "脚本解析器演练场" (3处)
- **验证**: 前端已重新编译并部署到webroot
### ✅ 8. 移动端布局优化
- **状态**: 已保留
- **说明**: 移动端布局优化功能已从`copilot/add-playground-enhancements`分支合并,代码已保留
- **文档**: `web-front/PLAYGROUND_UI_UPGRADE.md`
## 编译验证
### 后端编译
```bash
mvn clean package -DskipTests -pl web-service -am
```
- **结果**: ✅ BUILD SUCCESS
- **时间**: 5.614秒
### 前端编译
```bash
npm run build
```
- **结果**: ✅ Build complete
- **输出**: `nfd-front`目录已自动复制到`../webroot/nfd-front`
## 待浏览器环境测试项
以下测试项需要在浏览器环境中进行完整验证需要session支持
1. **密码认证流程**
- 访问演练场页面
- 输入密码登录
- 验证登录后的访问权限
2. **BUG2完整测试**
- 在演练场输入脚本(带@match规则
- 输入不匹配的分享链接
- 验证是否显示匹配错误提示
3. **BUG3完整测试**
- 发布一个脚本
- 验证脚本是否立即可用
- 通过分享链接调用验证
4. **移动端布局测试**
- 使用移动设备或浏览器开发者工具
- 验证响应式布局是否正常
## 代码质量
- ✅ 无编译错误
- ✅ 无Linter错误
- ✅ 所有TODO任务已完成
- ✅ 代码已合并到main分支
## 总结
所有核心功能修复已完成并通过编译验证:
- ✅ BUG1: JS超时机制已实现
- ✅ BUG2: URL正则匹配验证已实现
- ✅ BUG3: 脚本注册功能已实现
- ✅ TypeScript功能已移除
- ✅ 文本更新已完成
- ✅ 代码已合并到main分支
服务已成功启动,可以进行浏览器环境下的完整功能测试。

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

@@ -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

@@ -1,166 +0,0 @@
# Playground 密码保护功能
## 概述
JS解析器演练场现在支持密码保护功能可以通过配置文件控制是否需要密码才能访问。
## 配置说明
`web-service/src/main/resources/app-dev.yml` 文件中添加以下配置:
```yaml
# JS演练场配置
playground:
# 公开模式默认false需要密码访问设为true则无需密码
public: false
# 访问密码,建议修改默认密码!
password: 'nfd_playground_2024'
```
### 配置项说明
- `public`: 布尔值,默认为 `false`
- `false`: 需要输入密码才能访问演练场(推荐)
- `true`: 公开访问,无需密码
- `password`: 字符串,访问密码
- 默认密码:`nfd_playground_2024`
- **强烈建议在生产环境中修改为自定义密码!**
## 功能特点
### 1. 密码保护模式 (public: false)
`public` 设置为 `false` 时:
- 访问 `/playground` 页面时会显示密码输入界面
- 必须输入正确的密码才能使用演练场功能
- 密码验证通过后,会话保持登录状态
- 所有演练场相关的 API 接口都受到保护
### 2. 公开模式 (public: true)
`public` 设置为 `true` 时:
- 无需输入密码即可访问演练场
- 适用于内网环境或开发测试环境
### 3. 加载动画与进度条
页面加载过程会显示进度条,包括以下阶段:
1. 初始化Vue组件 (0-20%)
2. 加载配置和本地数据 (20-40%)
3. 准备TypeScript编译器 (40-50%)
4. 初始化Monaco Editor (50-80%)
5. 加载完成 (80-100%)
### 4. 移动端适配
- 桌面端:左右分栏布局,可拖拽调整宽度
- 移动端(屏幕宽度 ≤ 768px自动切换为上下分栏布局可拖拽调整高度
## 安全建议
⚠️ **重要安全提示:**
1. **修改默认密码**:在生产环境中,务必修改 `playground.password` 为自定义的强密码
2. **使用密码保护**:建议保持 `public: false`,避免未授权访问
3. **定期更换密码**:定期更换访问密码以提高安全性
4. **配置文件保护**:确保配置文件的访问权限受到保护
## 系统启动提示
当系统启动时,会在日志中显示当前配置:
```
INFO - Playground配置已加载: public=false, password=已设置
```
如果使用默认密码,会显示警告:
```
WARN - ⚠️ 警告:您正在使用默认密码,建议修改配置文件中的 playground.password 以确保安全!
```
## API 端点
### 1. 获取状态
```
GET /v2/playground/status
```
返回:
```json
{
"code": 200,
"data": {
"public": false,
"authed": false
}
}
```
### 2. 登录
```
POST /v2/playground/login
Content-Type: application/json
{
"password": "your_password"
}
```
成功响应:
```json
{
"code": 200,
"msg": "登录成功",
"success": true
}
```
失败响应:
```json
{
"code": 500,
"msg": "密码错误",
"success": false
}
```
## 常见问题
### Q: 如何禁用密码保护?
A: 在配置文件中设置 `playground.public: true`
### Q: 忘记密码怎么办?
A: 修改配置文件中的 `playground.password` 为新密码,然后重启服务
### Q: 密码是否加密存储?
A: 当前版本密码以明文形式存储在配置文件中,请确保配置文件的访问权限受到保护
### Q: Session 有效期多久?
A: Session 由 Vert.x 管理,默认在浏览器会话期间有效,关闭浏览器后失效
## 后续版本计划
未来版本可能会添加以下功能:
- [ ] 支持环境变量配置密码
- [ ] 支持加密存储密码
- [ ] 支持多用户账户系统
- [ ] 支持 Token 认证方式
- [ ] 支持 Session 超时配置
## 相关文档
- [Playground 使用指南](PLAYGROUND_GUIDE.md)
- [JavaScript 解析器开发指南](parser/doc/JAVASCRIPT_PARSER_GUIDE.md)
- [TypeScript 实现总结](TYPESCRIPT_IMPLEMENTATION_SUMMARY_CN.md)

View File

@@ -3,14 +3,9 @@ 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.lz.web.config.PlaygroundConfig;
import cn.qaiu.lz.web.service.DbService;
import cn.qaiu.parser.custom.CustomParserConfig;
import cn.qaiu.parser.custom.CustomParserRegistry;
import cn.qaiu.parser.customjs.JsScriptMetadataParser;
import cn.qaiu.vx.core.Deploy;
import cn.qaiu.vx.core.util.AsyncServiceUtil;
import cn.qaiu.vx.core.util.ConfigConstant;
import cn.qaiu.vx.core.util.VertxHolder;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
@@ -18,7 +13,6 @@ import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.json.jackson.DatabindCodec;
import io.vertx.core.shareddata.LocalMap;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.DateFormatUtils;
import java.util.Date;
@@ -32,7 +26,6 @@ import static cn.qaiu.vx.core.util.ConfigConstant.LOCAL;
* <br>Create date 2021-05-08 13:00:01
* @author qaiu yyzy
*/
@Slf4j
public class AppMain {
public static void main(String[] args) {
@@ -62,10 +55,6 @@ public class AppMain {
VertxHolder.getVertxInstance().setTimer(1000, id -> {
System.out.println(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS"));
System.out.println("数据库连接成功");
// 加载演练场解析器
loadPlaygroundParsers();
String addr = jsonObject.getJsonObject(ConfigConstant.SERVER).getString("domainName");
System.out.println("启动成功: \n本地服务地址: " + addr);
});
@@ -101,53 +90,9 @@ public class AppMain {
localMap.put(ConfigConstant.AUTHS, auths);
}
// 演练场配置
PlaygroundConfig.loadFromJson(jsonObject);
// Playground配置
if (jsonObject.containsKey("playground")) {
PlaygroundConfig.init(jsonObject.getJsonObject("playground"));
}
/**
* 在启动时加载所有已发布的演练场解析器
*/
private static void loadPlaygroundParsers() {
DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
dbService.getPlaygroundParserList().onSuccess(result -> {
JsonArray parsers = result.getJsonArray("data");
if (parsers != null) {
int loadedCount = 0;
for (int i = 0; i < parsers.size(); i++) {
JsonObject parser = parsers.getJsonObject(i);
// 只注册已启用的解析器
if (parser.getBoolean("enabled", false)) {
try {
String jsCode = parser.getString("jsCode");
if (jsCode == null || jsCode.trim().isEmpty()) {
log.error("加载演练场解析器失败: {} - JavaScript代码为空", parser.getString("name"));
continue;
}
CustomParserConfig config = JsScriptMetadataParser.parseScript(jsCode);
CustomParserRegistry.register(config);
loadedCount++;
log.info("已加载演练场解析器: {} ({})",
config.getDisplayName(), config.getType());
} catch (Exception e) {
String parserName = parser.getString("name");
String errorMsg = e.getMessage();
log.error("加载演练场解析器失败: {} - {}", parserName, errorMsg, e);
// 如果是require相关错误提供更详细的提示
if (errorMsg != null && errorMsg.contains("require")) {
log.error("提示演练场解析器不支持CommonJS模块系统require请确保代码使用ES5.1语法");
}
}
}
}
log.info("演练场解析器加载完成,共加载 {} 个解析器", loadedCount);
} else {
log.info("未找到已发布的演练场解析器");
}
}).onFailure(e -> {
log.error("加载演练场解析器列表失败", e);
});
}
}

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,82 +0,0 @@
package cn.qaiu.lz.web.config;
import io.vertx.core.json.JsonObject;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
/**
* JS演练场配置
*
* @author <a href="https://qaiu.top">QAIU</a>
*/
@Data
@Slf4j
public class PlaygroundConfig {
/**
* 单例实例
*/
private static PlaygroundConfig instance;
/**
* 是否启用演练场
* 默认false不启用
*/
private boolean enabled = false;
/**
* 是否公开模式(不需要密码)
* 默认false需要密码访问
*/
private boolean isPublic = false;
/**
* 访问密码
* 默认密码nfd_playground_2024
*/
private String password = "nfd_playground_2024";
/**
* 私有构造函数
*/
private PlaygroundConfig() {
}
/**
* 获取单例实例
*/
public static PlaygroundConfig getInstance() {
if (instance == null) {
synchronized (PlaygroundConfig.class) {
if (instance == null) {
instance = new PlaygroundConfig();
}
}
}
return instance;
}
/**
* 从JsonObject加载配置
*/
public static void loadFromJson(JsonObject config) {
PlaygroundConfig cfg = getInstance();
if (config != null && config.containsKey("playground")) {
JsonObject playgroundConfig = config.getJsonObject("playground");
cfg.enabled = playgroundConfig.getBoolean("enabled", false);
cfg.isPublic = playgroundConfig.getBoolean("public", false);
cfg.password = playgroundConfig.getString("password", "nfd_playground_2024");
log.info("Playground配置已加载: enabled={}, public={}, password={}",
cfg.enabled, cfg.isPublic, cfg.isPublic ? "N/A" : "已设置");
if (!cfg.enabled) {
log.info("演练场功能已禁用");
} else if (!cfg.isPublic && "nfd_playground_2024".equals(cfg.password)) {
log.warn("⚠️ 警告:您正在使用默认密码,建议修改配置文件中的 playground.password 以确保安全!");
}
} else {
log.info("未找到playground配置使用默认值: enabled=false, public=false");
}
}
}

View File

@@ -1,11 +1,10 @@
package cn.qaiu.lz.web.controller;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.lz.web.config.PlaygroundConfig;
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;
import cn.qaiu.parser.custom.CustomParserRegistry;
import cn.qaiu.parser.customjs.JsPlaygroundExecutor;
import cn.qaiu.parser.customjs.JsPlaygroundLogger;
import cn.qaiu.parser.customjs.JsScriptMetadataParser;
@@ -15,13 +14,13 @@ 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;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.Session;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@@ -31,8 +30,6 @@ import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
@@ -47,113 +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 SESSION_AUTH_KEY = "playgroundAuthed";
private static final String AUTH_SESSION_KEY = "playground_authed_";
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
/**
* 检查Playground是否启用
*/
private boolean checkEnabled() {
PlaygroundConfig config = PlaygroundConfig.getInstance();
return config.isEnabled();
}
/**
* 检查Playground访问权限
*/
private boolean checkAuth(RoutingContext ctx) {
// 首先检查是否启用
if (!checkEnabled()) {
return false;
}
PlaygroundConfig config = PlaygroundConfig.getInstance();
// 如果是公开模式,直接允许访问
if (config.isPublic()) {
return true;
}
// 否则检查Session中的认证状态
Session session = ctx.session();
if (session == null) {
return false;
}
Boolean authed = session.get(SESSION_AUTH_KEY);
return authed != null && authed;
}
/**
* 获取Playground状态是否需要认证
* 获取Playground状态
*
* @param ctx 路由上下文
* @return 状态信息
*/
@RouteMapping(value = "/status", method = RouteMethod.GET)
public Future<JsonObject> getStatus(RoutingContext ctx) {
PlaygroundConfig config = PlaygroundConfig.getInstance();
boolean enabled = config.isEnabled();
boolean authed = enabled && checkAuth(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("public", config.isPublic())
.put("needPassword", needPassword)
.put("authed", authed);
return Future.succeededFuture(JsonResult.data(result).toJsonObject());
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) {
// 检查是否启用
if (!checkEnabled()) {
return Future.succeededFuture(JsonResult.error("演练场功能已禁用").toJsonObject());
}
Promise<JsonObject> promise = Promise.promise();
try {
PlaygroundConfig config = PlaygroundConfig.getInstance();
// 如果是公开模式,直接成功
if (config.isPublic()) {
Session session = ctx.session();
if (session != null) {
session.put(SESSION_AUTH_KEY, true);
}
promise.complete(JsonResult.success("公开模式,无需密码").toJsonObject());
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.isBlank(password)) {
promise.complete(JsonResult.error("密码不能为空").toJsonObject());
return promise.future();
}
// 验证密码
if (config.getPassword().equals(password)) {
Session session = ctx.session();
if (session != null) {
session.put(SESSION_AUTH_KEY, true);
}
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("登录失败", 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代码
*
@@ -162,19 +225,12 @@ public class PlaygroundApi {
*/
@RouteMapping(value = "/test", method = RouteMethod.POST)
public Future<JsonObject> test(RoutingContext ctx) {
// 检查是否启用
if (!checkEnabled()) {
return Future.succeededFuture(JsonResult.error("演练场功能已禁用").toJsonObject());
}
// 权限检查
if (!checkAuth(ctx)) {
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
}
Promise<JsonObject> promise = Promise.promise();
try {
// 检查访问权限
ensurePlaygroundAccess(ctx);
JsonObject body = ctx.body().asJsonObject();
String jsCode = body.getString("jsCode");
String shareUrl = body.getString("shareUrl");
@@ -207,32 +263,6 @@ public class PlaygroundApi {
return promise.future();
}
// ===== 新增验证URL匹配 =====
try {
var config = JsScriptMetadataParser.parseScript(jsCode);
Pattern matchPattern = config.getMatchPattern();
if (matchPattern != null) {
Matcher matcher = matchPattern.matcher(shareUrl);
if (!matcher.matches()) {
promise.complete(JsonObject.mapFrom(PlaygroundTestResp.builder()
.success(false)
.error("分享链接与脚本的@match规则不匹配\n" +
"规则: " + matchPattern.pattern() + "\n" +
"链接: " + shareUrl)
.build()));
return promise.future();
}
}
} catch (IllegalArgumentException e) {
promise.complete(JsonObject.mapFrom(PlaygroundTestResp.builder()
.success(false)
.error("解析脚本元数据失败: " + e.getMessage())
.build()));
return promise.future();
}
// ===== 验证结束 =====
// 验证方法类型
if (!"parse".equals(method) && !"parseFileList".equals(method) && !"parseById".equals(method)) {
promise.complete(JsonObject.mapFrom(PlaygroundTestResp.builder()
@@ -365,38 +395,34 @@ public class PlaygroundApi {
/**
* 获取types.js文件内容
*
* @param ctx 路由上下文
* @param response HTTP响应
* @param ctx 路由上下文
*/
@RouteMapping(value = "/types.js", method = RouteMethod.GET)
public void getTypesJs(RoutingContext ctx, HttpServerResponse response) {
// 检查是否启用
if (!checkEnabled()) {
ResponseUtil.fireJsonResultResponse(response, JsonResult.error("演练场功能已禁用"));
return;
}
public void getTypesJs(HttpServerResponse response, RoutingContext ctx) {
try {
// 检查访问权限
ensurePlaygroundAccess(ctx);
// 权限检查
if (!checkAuth(ctx)) {
ResponseUtil.fireJsonResultResponse(response, JsonResult.error("未授权访问"));
return;
}
try (InputStream inputStream = getClass().getClassLoader()
.getResourceAsStream("custom-parsers/types.js")) {
InputStream inputStream = getClass().getClassLoader()
.getResourceAsStream("custom-parsers/types.js");
if (inputStream == null) {
ResponseUtil.fireJsonResultResponse(response, JsonResult.error("types.js文件不存在"));
return;
}
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()));
@@ -408,16 +434,14 @@ public class PlaygroundApi {
*/
@RouteMapping(value = "/parsers", method = RouteMethod.GET)
public Future<JsonObject> getParserList(RoutingContext ctx) {
// 检查是否启用
if (!checkEnabled()) {
return Future.succeededFuture(JsonResult.error("演练场功能已禁用").toJsonObject());
}
// 权限检查
if (!checkAuth(ctx)) {
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
}
try {
// 检查访问权限
ensurePlaygroundAccess(ctx);
return dbService.getPlaygroundParserList();
} catch (Exception e) {
log.error("获取解析器列表失败", e);
return Future.succeededFuture(JsonResult.error(e.getMessage()).toJsonObject());
}
}
/**
@@ -425,19 +449,12 @@ public class PlaygroundApi {
*/
@RouteMapping(value = "/parsers", method = RouteMethod.POST)
public Future<JsonObject> saveParser(RoutingContext ctx) {
// 检查是否启用
if (!checkEnabled()) {
return Future.succeededFuture(JsonResult.error("演练场功能已禁用").toJsonObject());
}
// 权限检查
if (!checkAuth(ctx)) {
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
}
Promise<JsonObject> promise = Promise.promise();
try {
// 检查访问权限
ensurePlaygroundAccess(ctx);
JsonObject body = ctx.body().asJsonObject();
String jsCode = body.getString("jsCode");
@@ -503,18 +520,7 @@ public class PlaygroundApi {
parser.put("enabled", true);
dbService.savePlaygroundParser(parser).onSuccess(result -> {
// 保存成功后,立即注册到解析器系统
try {
CustomParserRegistry.register(config);
log.info("已注册演练场解析器: {} ({})", displayName, type);
promise.complete(JsonResult.success("保存并注册成功").toJsonObject());
} catch (Exception e) {
log.error("注册解析器失败", e);
// 虽然注册失败,但保存成功了,返回警告
promise.complete(JsonResult.success(
"保存成功,但注册失败(重启服务后会自动加载): " + e.getMessage()
).toJsonObject());
}
promise.complete(result);
}).onFailure(e -> {
log.error("保存解析器失败", e);
promise.complete(JsonResult.error("保存失败: " + e.getMessage()).toJsonObject());
@@ -545,19 +551,12 @@ public class PlaygroundApi {
*/
@RouteMapping(value = "/parsers/:id", method = RouteMethod.PUT)
public Future<JsonObject> updateParser(RoutingContext ctx, Long id) {
// 检查是否启用
if (!checkEnabled()) {
return Future.succeededFuture(JsonResult.error("演练场功能已禁用").toJsonObject());
}
// 权限检查
if (!checkAuth(ctx)) {
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
}
Promise<JsonObject> promise = Promise.promise();
try {
// 检查访问权限
ensurePlaygroundAccess(ctx);
JsonObject body = ctx.body().asJsonObject();
String jsCode = body.getString("jsCode");
@@ -569,14 +568,12 @@ public class PlaygroundApi {
// 解析元数据
try {
var config = JsScriptMetadataParser.parseScript(jsCode);
String type = config.getType();
String displayName = config.getDisplayName();
String name = config.getMetadata().get("name");
String description = config.getMetadata().get("description");
String author = config.getMetadata().get("author");
String version = config.getMetadata().get("version");
String matchPattern = config.getMatchPattern() != null ? config.getMatchPattern().pattern() : null;
boolean enabled = body.getBoolean("enabled", true);
JsonObject parser = new JsonObject();
parser.put("name", name);
@@ -586,29 +583,10 @@ public class PlaygroundApi {
parser.put("version", version);
parser.put("matchPattern", matchPattern);
parser.put("jsCode", jsCode);
parser.put("enabled", enabled);
parser.put("enabled", body.getBoolean("enabled", true));
dbService.updatePlaygroundParser(id, parser).onSuccess(result -> {
// 更新成功后,重新注册解析器
try {
if (enabled) {
// 先注销旧的(如果存在)
CustomParserRegistry.unregister(type);
// 重新注册新的
CustomParserRegistry.register(config);
log.info("已重新注册演练场解析器: {} ({})", displayName, type);
} else {
// 禁用时注销
CustomParserRegistry.unregister(type);
log.info("已注销演练场解析器: {}", type);
}
promise.complete(JsonResult.success("更新并重新注册成功").toJsonObject());
} catch (Exception e) {
log.error("重新注册解析器失败", e);
promise.complete(JsonResult.success(
"更新成功,但注册失败(重启服务后会自动加载): " + e.getMessage()
).toJsonObject());
}
promise.complete(result);
}).onFailure(e -> {
log.error("更新解析器失败", e);
promise.complete(JsonResult.error("更新失败: " + e.getMessage()).toJsonObject());
@@ -631,47 +609,14 @@ public class PlaygroundApi {
*/
@RouteMapping(value = "/parsers/:id", method = RouteMethod.DELETE)
public Future<JsonObject> deleteParser(RoutingContext ctx, Long id) {
// 检查是否启用
if (!checkEnabled()) {
return Future.succeededFuture(JsonResult.error("演练场功能已禁用").toJsonObject());
}
// 权限检查
if (!checkAuth(ctx)) {
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
}
Promise<JsonObject> promise = Promise.promise();
// 先获取解析器信息,用于注销
dbService.getPlaygroundParserById(id).onSuccess(getResult -> {
if (getResult.getBoolean("success", false)) {
JsonObject parser = getResult.getJsonObject("data");
String type = parser.getString("type");
// 删除数据库记录
dbService.deletePlaygroundParser(id).onSuccess(deleteResult -> {
// 从注册表中注销
try {
CustomParserRegistry.unregister(type);
log.info("已注销演练场解析器: {}", type);
// 检查访问权限
ensurePlaygroundAccess(ctx);
return dbService.deletePlaygroundParser(id);
} catch (Exception e) {
log.warn("注销解析器失败(可能未注册): {}", type, e);
}
promise.complete(deleteResult);
}).onFailure(e -> {
log.error("删除解析器失败", e);
promise.complete(JsonResult.error("删除失败: " + e.getMessage()).toJsonObject());
});
} else {
promise.complete(getResult);
return Future.succeededFuture(JsonResult.error(e.getMessage()).toJsonObject());
}
}).onFailure(e -> {
log.error("获取解析器信息失败", e);
promise.complete(JsonResult.error("获取解析器信息失败: " + e.getMessage()).toJsonObject());
});
return promise.future();
}
/**
@@ -679,16 +624,14 @@ public class PlaygroundApi {
*/
@RouteMapping(value = "/parsers/:id", method = RouteMethod.GET)
public Future<JsonObject> getParserById(RoutingContext ctx, Long id) {
// 检查是否启用
if (!checkEnabled()) {
return Future.succeededFuture(JsonResult.error("演练场功能已禁用").toJsonObject());
}
// 权限检查
if (!checkAuth(ctx)) {
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
}
try {
// 检查访问权限
ensurePlaygroundAccess(ctx);
return dbService.getPlaygroundParserById(id);
} catch (Exception e) {
log.error("获取解析器失败", e);
return Future.succeededFuture(JsonResult.error(e.getMessage()).toJsonObject());
}
}
/**

View File

@@ -13,15 +13,6 @@ server:
# 反向代理服务器配置路径(不用加后缀)
proxyConf: server-proxy
# JS演练场配置
playground:
# 是否启用演练场默认false不启用
enabled: true
# 公开模式默认false需要密码访问设为true则无需密码
public: true
# 访问密码,建议修改默认密码!
password: 'nfd_playground_2024'
# vertx核心线程配置(一般无需改的), 为0表示eventLoopPoolSize将会采用默认配置(CPU核心*2) workerPoolSize将会采用默认20
vertx:
eventLoopPoolSize: 0
@@ -111,3 +102,11 @@ auths:
ye:
username:
password:
# Playground演练场配置
playground:
# 是否启用Playground默认关闭
enabled: false
# 访问密码可选。仅在enabled=true时生效
# 为空时表示公开访问,不需要密码
password: ""

View File

@@ -4,7 +4,7 @@ server-name: Vert.x-proxy-server(v4.1.2)
proxy:
- listen: 6401
# 404的路径
page404: webroot/nfd-front/index.html
page404: webroot/err/404.html
static:
path: /
add-headers:

View File

@@ -1,91 +0,0 @@
### 安全漏洞修复测试 - DoS攻击防护
###
### 测试目标:
### 1. 验证代码长度限制128KB
### 2. 验证JavaScript执行超时30秒
###
### 测试1: 正常代码执行(应该成功)
POST http://127.0.0.1:6400/v2/playground/test
Content-Type: application/json
{
"jsCode": "// ==UserScript==\n// @name 正常测试\n// @type normal_test\n// @displayName 正常\n// @match https://example\\.com/(?<KEY>\\w+)\n// @author test\n// @version 1.0.0\n// ==/UserScript==\n\nfunction parse(shareLinkInfo, http, logger) {\n logger.info('正常执行');\n return 'https://example.com/download/file.zip';\n}",
"shareUrl": "https://example.com/test123",
"pwd": "",
"method": "parse"
}
###
### 测试2: 代码长度超过限制(应该失败并提示)
### 这个测试会创建一个超过128KB的代码
POST http://127.0.0.1:6400/v2/playground/test
Content-Type: application/json
{
"jsCode": "// ==UserScript==\n// @name 长度测试\n// @type length_test\n// @displayName 长度\n// @match https://example\\.com/(?<KEY>\\w+)\n// @author test\n// @version 1.0.0\n// ==/UserScript==\n\nfunction parse(shareLinkInfo, http, logger) {\n var data = 'x'.repeat(150000);\n return data;\n}",
"shareUrl": "https://example.com/test123",
"pwd": "",
"method": "parse"
}
###
### 测试3: 无限循环应该在30秒后超时
POST http://127.0.0.1:6400/v2/playground/test
Content-Type: application/json
{
"jsCode": "// ==UserScript==\n// @name 无限循环测试\n// @type infinite_loop_test\n// @displayName 无限循环\n// @match https://example\\.com/(?<KEY>\\w+)\n// @author test\n// @version 1.0.0\n// ==/UserScript==\n\nfunction parse(shareLinkInfo, http, logger) {\n logger.info('开始无限循环...');\n while(true) {\n var x = 1 + 1;\n }\n return 'never reached';\n}",
"shareUrl": "https://example.com/test123",
"pwd": "",
"method": "parse"
}
###
### 测试4: 大数组内存炸弹应该在30秒后超时或内存限制
POST http://127.0.0.1:6400/v2/playground/test
Content-Type: application/json
{
"jsCode": "// ==UserScript==\n// @name 内存炸弹测试\n// @type memory_bomb_test\n// @displayName 内存炸弹\n// @match https://example\\.com/(?<KEY>\\w+)\n// @author test\n// @version 1.0.0\n// ==/UserScript==\n\nfunction parse(shareLinkInfo, http, logger) {\n logger.info('创建大数组...');\n var arr = [];\n for(var i = 0; i < 10000000; i++) {\n arr.push('x'.repeat(1000));\n }\n logger.info('数组创建完成');\n return 'DONE';\n}",
"shareUrl": "https://example.com/test123",
"pwd": "",
"method": "parse"
}
###
### 测试5: 递归调用栈溢出
POST http://127.0.0.1:6400/v2/playground/test
Content-Type: application/json
{
"jsCode": "// ==UserScript==\n// @name 栈溢出测试\n// @type stack_overflow_test\n// @displayName 栈溢出\n// @match https://example\\.com/(?<KEY>\\w+)\n// @author test\n// @version 1.0.0\n// ==/UserScript==\n\nfunction boom() {\n return boom();\n}\n\nfunction parse(shareLinkInfo, http, logger) {\n logger.info('开始递归炸弹...');\n boom();\n return 'never reached';\n}",
"shareUrl": "https://example.com/test123",
"pwd": "",
"method": "parse"
}
###
### 测试6: 保存解析器 - 验证代码长度限制
POST http://127.0.0.1:6400/v2/playground/parsers
Content-Type: application/json
{
"jsCode": "// ==UserScript==\n// @name 正常解析器\n// @type normal_parser\n// @displayName 正常解析器\n// @match https://example\\.com/(?<KEY>\\w+)\n// @author test\n// @version 1.0.0\n// ==/UserScript==\n\nfunction parse(shareLinkInfo, http, logger) {\n return 'https://example.com/download/file.zip';\n}\n\nfunction parseFileList(shareLinkInfo, http, logger) {\n return [];\n}\n\nfunction parseById(shareLinkInfo, http, logger) {\n return 'https://example.com/download/file.zip';\n}"
}
###
### 测试结果期望:
### 1. 测试1 - 应该成功返回结果
### 2. 测试2 - 应该返回错误:"代码长度超过限制"
### 3. 测试3 - 应该在30秒后返回超时错误"JavaScript执行超时"
### 4. 测试4 - 应该在30秒后返回超时错误或内存错误
### 5. 测试5 - 应该返回堆栈溢出错误
### 6. 测试6 - 应该成功保存如果代码不超过128KB