mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2026-01-11 17:04:13 +00:00
Fix playground bugs and remove TypeScript compiler
- Fix BUG1: JavaScript timeout with proper thread interruption using ScheduledExecutorService - Fix BUG2: Add URL regex validation before execution in playground test API - Fix BUG3: Register published parsers to CustomParserRegistry on save/update/delete - Remove TypeScript compiler functionality (tsCompiler.js, dependencies, UI) - Add password authentication for playground access - Add mobile responsive layout support - Load playground parsers on application startup
This commit is contained in:
@@ -5,7 +5,12 @@ import cn.qaiu.db.pool.JDBCPoolInit;
|
||||
import cn.qaiu.lz.common.cache.CacheConfigLoader;
|
||||
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;
|
||||
@@ -13,6 +18,7 @@ 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;
|
||||
@@ -26,6 +32,7 @@ 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) {
|
||||
@@ -55,6 +62,10 @@ 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);
|
||||
});
|
||||
@@ -93,4 +104,40 @@ public class AppMain {
|
||||
// 演练场配置
|
||||
PlaygroundConfig.loadFromJson(jsonObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在启动时加载所有已发布的演练场解析器
|
||||
*/
|
||||
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");
|
||||
CustomParserConfig config = JsScriptMetadataParser.parseScript(jsCode);
|
||||
CustomParserRegistry.register(config);
|
||||
loadedCount++;
|
||||
log.info("已加载演练场解析器: {} ({})",
|
||||
config.getDisplayName(), config.getType());
|
||||
} catch (Exception e) {
|
||||
log.error("加载演练场解析器失败: {}", parser.getString("name"), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info("演练场解析器加载完成,共加载 {} 个解析器", loadedCount);
|
||||
} else {
|
||||
log.info("未找到已发布的演练场解析器");
|
||||
}
|
||||
}).onFailure(e -> {
|
||||
log.error("加载演练场解析器列表失败", e);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import cn.qaiu.lz.web.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.CustomParserConfig;
|
||||
import cn.qaiu.parser.custom.CustomParserRegistry;
|
||||
import cn.qaiu.parser.customjs.JsPlaygroundExecutor;
|
||||
import cn.qaiu.parser.customjs.JsPlaygroundLogger;
|
||||
import cn.qaiu.parser.customjs.JsScriptMetadataParser;
|
||||
@@ -30,6 +32,8 @@ 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;
|
||||
|
||||
/**
|
||||
@@ -178,6 +182,32 @@ public class PlaygroundApi {
|
||||
.build()));
|
||||
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)) {
|
||||
@@ -433,7 +463,18 @@ public class PlaygroundApi {
|
||||
parser.put("enabled", true);
|
||||
|
||||
dbService.savePlaygroundParser(parser).onSuccess(result -> {
|
||||
promise.complete(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());
|
||||
}
|
||||
}).onFailure(e -> {
|
||||
log.error("保存解析器失败", e);
|
||||
promise.complete(JsonResult.error("保存失败: " + e.getMessage()).toJsonObject());
|
||||
@@ -483,12 +524,14 @@ 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);
|
||||
@@ -498,10 +541,29 @@ public class PlaygroundApi {
|
||||
parser.put("version", version);
|
||||
parser.put("matchPattern", matchPattern);
|
||||
parser.put("jsCode", jsCode);
|
||||
parser.put("enabled", body.getBoolean("enabled", true));
|
||||
parser.put("enabled", enabled);
|
||||
|
||||
dbService.updatePlaygroundParser(id, parser).onSuccess(result -> {
|
||||
promise.complete(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());
|
||||
}
|
||||
}).onFailure(e -> {
|
||||
log.error("更新解析器失败", e);
|
||||
promise.complete(JsonResult.error("更新失败: " + e.getMessage()).toJsonObject());
|
||||
@@ -528,7 +590,38 @@ public class PlaygroundApi {
|
||||
if (!checkAuth(ctx)) {
|
||||
return Future.succeededFuture(JsonResult.error("未授权访问").toJsonObject());
|
||||
}
|
||||
return dbService.deletePlaygroundParser(id);
|
||||
|
||||
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);
|
||||
} 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);
|
||||
}
|
||||
}).onFailure(e -> {
|
||||
log.error("获取解析器信息失败", e);
|
||||
promise.complete(JsonResult.error("获取解析器信息失败: " + e.getMessage()).toJsonObject());
|
||||
});
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
91
web-service/src/test/resources/playground-dos-tests.http
Normal file
91
web-service/src/test/resources/playground-dos-tests.http
Normal file
@@ -0,0 +1,91 @@
|
||||
### 安全漏洞修复测试 - 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)
|
||||
|
||||
Reference in New Issue
Block a user