Compare commits

...

6 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
29b59d8450 Remove 奶牛快传(cowtransfer) from README.md
Agent-Logs-Url: https://github.com/qaiu/netdisk-fast-download/sessions/63030ac7-158c-43ca-9dcd-254e03c6f8f4

Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2026-04-23 11:27:04 +00:00
qaiu
97b7e2f86e Merge pull request #181 from qaiu/copilot/check-authentication-mechanism
fix: 演练场密码登录后所有API返回"未授权访问"
2026-04-23 09:34:18 +08:00
copilot-swe-agent[bot]
2f55294b58 fix: 修复演练场输入密码后提示未授权访问的问题
根本原因:框架 RouterHandlerFactory 未注册 SessionHandler,
导致 ctx.session() 始终返回 null。登录时密码校验通过但认证
状态被静默丢弃,后续所有请求均返回"未授权访问"。

修复方案:将 Session 鉴权改为 Token(Bearer)鉴权:
- PlaygroundConfig: 新增 generateToken()/validateToken(),
  使用 SecureRandom 生成密码学安全 Token,并在生成时
  清理过期 Token 防止内存泄漏
- PlaygroundApi: login() 返回 Token;checkAuth() 从
  Authorization 请求头中读取并校验 Token
- playgroundApi.js: 添加请求拦截器自动携带 Token;
  login() 从响应中提取并保存 Token 到 localStorage
- Playground.vue: 后端报告未认证时同步清除 playground_token

Agent-Logs-Url: https://github.com/qaiu/netdisk-fast-download/sessions/52144d13-cd49-4a3d-b279-9b8d6cbad757

Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2026-04-23 01:22:09 +00:00
q
2161190d9a config: switch active profile to dev 2026-04-22 16:10:22 +08:00
q
aaae301cbc release v3.0.0: core refactoring, new AppRun/PostExecVerticle, proxy and router improvements 2026-04-22 15:57:35 +08:00
qaiu
9ca6511235 Merge pull request #180 from qaiu/copilot/fix-filemanagerplugin-copy-back
fix: restore missing copy of build output to webroot/nfd-front in vue.config.js
2026-04-22 12:57:06 +08:00
35 changed files with 1383 additions and 445 deletions

View File

@@ -1,4 +1,5 @@
{ {
"java.compile.nullAnalysis.mode": "automatic", "java.compile.nullAnalysis.mode": "automatic",
"java.configuration.updateBuildConfiguration": "interactive" "java.configuration.updateBuildConfiguration": "interactive",
"java.debug.settings.onBuildFailureProceed": true
} }

View File

@@ -23,7 +23,7 @@ QQ交流群1017480890
> >
</div> </div>
> netdisk-fast-download网盘直链解析可以把云盘分享链接转为直链可广泛应用于各类下载站资源站个人博客图床APP下载更新视频点播等领域。支持市面各大主流云盘的文件分享以及文件夹分享链接已支持蓝奏云/蓝奏云优享/奶牛快传/移动云云空间/小飞机盘/亿方云/123云盘/Cloudreve等支持加密分享以及部分网盘文件夹分享。 > netdisk-fast-download网盘直链解析可以把云盘分享链接转为直链可广泛应用于各类下载站资源站个人博客图床APP下载更新视频点播等领域。支持市面各大主流云盘的文件分享以及文件夹分享链接已支持蓝奏云/蓝奏云优享/移动云云空间/小飞机盘/亿方云/123云盘/Cloudreve等支持加密分享以及部分网盘文件夹分享。
[官方文档](https://nfd-parser.github.io/) [官方文档](https://nfd-parser.github.io/)
[API接入](https://nfdparser.apifox.cn/) [API接入](https://nfdparser.apifox.cn/)
@@ -62,7 +62,6 @@ https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.
**注意⚠️请不要过度依赖 lz.qaiu.top建议本地搭建或者云服务器自行搭建。请求量过多的话服务器可能会被云盘厂商限制遇到解析失败的分享链接不要着急提issues请先检查分享是否有效。** **注意⚠️请不要过度依赖 lz.qaiu.top建议本地搭建或者云服务器自行搭建。请求量过多的话服务器可能会被云盘厂商限制遇到解析失败的分享链接不要着急提issues请先检查分享是否有效。**
## 网盘支持情况: ## 网盘支持情况:
> 20230905 奶牛云直链做了防盗链需加入请求头Referer: https://cowtransfer.com/
> 20230824 123云盘解析大文件(>100MB)失效,需要登录 > 20230824 123云盘解析大文件(>100MB)失效,需要登录
> 20230722 UC网盘解析失效需要登录 > 20230722 UC网盘解析失效需要登录

View File

@@ -73,6 +73,12 @@
<version>${jackson.version}</version> <version>${jackson.version}</version>
</dependency> </dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>

View File

@@ -3,12 +3,13 @@ package cn.qaiu.vx.core;
import cn.qaiu.vx.core.util.CommonUtil; import cn.qaiu.vx.core.util.CommonUtil;
import cn.qaiu.vx.core.util.ConfigUtil; import cn.qaiu.vx.core.util.ConfigUtil;
import cn.qaiu.vx.core.util.VertxHolder; import cn.qaiu.vx.core.util.VertxHolder;
import cn.qaiu.vx.core.verticle.HttpProxyVerticle;
import cn.qaiu.vx.core.verticle.PostExecVerticle;
import cn.qaiu.vx.core.verticle.ReverseProxyVerticle; import cn.qaiu.vx.core.verticle.ReverseProxyVerticle;
import cn.qaiu.vx.core.verticle.RouterVerticle; import cn.qaiu.vx.core.verticle.RouterVerticle;
import cn.qaiu.vx.core.verticle.ServiceVerticle; import cn.qaiu.vx.core.verticle.ServiceVerticle;
import io.vertx.core.*; import io.vertx.core.*;
import io.vertx.core.dns.AddressResolverOptions; import io.vertx.core.dns.AddressResolverOptions;
import io.vertx.core.impl.launcher.commands.VersionCommand;
import io.vertx.core.json.JsonObject; import io.vertx.core.json.JsonObject;
import io.vertx.core.shareddata.LocalMap; import io.vertx.core.shareddata.LocalMap;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -17,6 +18,7 @@ import org.slf4j.LoggerFactory;
import java.lang.management.ManagementFactory; import java.lang.management.ManagementFactory;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.UUID;
import java.util.concurrent.locks.LockSupport; import java.util.concurrent.locks.LockSupport;
import static cn.qaiu.vx.core.util.ConfigConstant.*; import static cn.qaiu.vx.core.util.ConfigConstant.*;
@@ -54,6 +56,7 @@ public final class Deploy {
public void start(String[] args, Handler<JsonObject> handle) { public void start(String[] args, Handler<JsonObject> handle) {
this.mainThread = Thread.currentThread(); this.mainThread = Thread.currentThread();
this.handle = handle; this.handle = handle;
if (args.length > 0 && args[0].startsWith("app-")) { if (args.length > 0 && args[0].startsWith("app-")) {
// 启动参数dev或者prod // 启动参数dev或者prod
path.append("-").append(args[0].replace("app-","")); path.append("-").append(args[0].replace("app-",""));
@@ -104,7 +107,7 @@ public final class Deploy {
System.out.printf(logoTemplate, System.out.printf(logoTemplate,
CommonUtil.getAppVersion(), CommonUtil.getAppVersion(),
VersionCommand.getVersion(), "4x",
conf.getString("copyright"), conf.getString("copyright"),
year year
); );
@@ -123,12 +126,12 @@ public final class Deploy {
var vertxOptions = vertxConfigELPS == 0 ? var vertxOptions = vertxConfigELPS == 0 ?
new VertxOptions() : new VertxOptions(vertxConfig); new VertxOptions() : new VertxOptions(vertxConfig);
vertxOptions.setAddressResolverOptions( // vertxOptions.setAddressResolverOptions(
new AddressResolverOptions(). // new AddressResolverOptions().
addServer("114.114.114.114"). // addServer("114.114.114.114").
addServer("114.114.115.115"). // addServer("114.114.115.115").
addServer("8.8.8.8"). // addServer("8.8.8.8").
addServer("8.8.4.4")); // addServer("8.8.4.4"));
LOGGER.info("vertxConfigEventLoopPoolSize: {}, eventLoopPoolSize: {}, workerPoolSize: {}", vertxConfigELPS, LOGGER.info("vertxConfigEventLoopPoolSize: {}, eventLoopPoolSize: {}, workerPoolSize: {}", vertxConfigELPS,
vertxOptions.getEventLoopPoolSize(), vertxOptions.getEventLoopPoolSize(),
vertxOptions.getWorkerPoolSize()); vertxOptions.getWorkerPoolSize());
@@ -153,12 +156,39 @@ public final class Deploy {
var future2 = vertx.deployVerticle(ServiceVerticle.class, getWorkDeploymentOptions("Service")); var future2 = vertx.deployVerticle(ServiceVerticle.class, getWorkDeploymentOptions("Service"));
var future3 = vertx.deployVerticle(ReverseProxyVerticle.class, getWorkDeploymentOptions("proxy")); var future3 = vertx.deployVerticle(ReverseProxyVerticle.class, getWorkDeploymentOptions("proxy"));
JsonObject jsonObject = ((JsonObject) localMap.get(GLOBAL_CONFIG)).getJsonObject("proxy-server");
if (jsonObject != null) {
genPwd(jsonObject);
var future4 = vertx.deployVerticle(HttpProxyVerticle.class, getWorkDeploymentOptions("proxy"));
future4.onSuccess(LOGGER::info);
future4.onFailure(e -> LOGGER.error("Other handle error", e));
Future.all(future1, future2, future3, future4)
.onSuccess(this::deployWorkVerticalSuccess)
.onFailure(this::deployVerticalFailed);
} else {
Future.all(future1, future2, future3) Future.all(future1, future2, future3)
.onSuccess(this::deployWorkVerticalSuccess) .onSuccess(this::deployWorkVerticalSuccess)
.onFailure(this::deployVerticalFailed); .onFailure(this::deployVerticalFailed);
}
}).onFailure(e -> LOGGER.error("Other handle error", e)); }).onFailure(e -> LOGGER.error("Other handle error", e));
} }
private static void genPwd(JsonObject jsonObject) {
if (jsonObject.getBoolean("randUserPwd")) {
var username = UUID.randomUUID().toString().replace("-", "").substring(0, 16);
var password = UUID.randomUUID().toString().replace("-", "").substring(0, 16);
jsonObject.put("username", username);
jsonObject.put("password", password);
}
LOGGER.info("=============server info=================");
LOGGER.info("\nport: {}\nusername: {}\npassword: {}",
jsonObject.getString("port"),
jsonObject.getString("username"),
jsonObject.getString("password"));
LOGGER.info("==============server info================");
}
/** /**
* 部署失败 * 部署失败
* *
@@ -178,6 +208,42 @@ public final class Deploy {
var t1 = ((double) (System.currentTimeMillis() - startTime)) / 1000; var t1 = ((double) (System.currentTimeMillis() - startTime)) / 1000;
var t2 = ((double) System.currentTimeMillis() - ManagementFactory.getRuntimeMXBean().getStartTime()) / 1000; var t2 = ((double) System.currentTimeMillis() - ManagementFactory.getRuntimeMXBean().getStartTime()) / 1000;
LOGGER.info("web服务启动成功 -> 用时: {}s, jvm启动用时: {}s", t1, t2); LOGGER.info("web服务启动成功 -> 用时: {}s, jvm启动用时: {}s", t1, t2);
// 检查是否处于安装引导模式(数据库未配置)
Object installMode = VertxHolder.getVertxInstance().sharedData()
.getLocalMap(LOCAL).get("installMode");
if (Boolean.TRUE.equals(installMode)) {
LOGGER.info("系统处于安装引导模式,等待用户完成数据库配置后再启动后置初始化...");
return;
}
// 正常模式:部署 PostExecVerticle 执行 AppRun 实现
deployPostExec();
}
/**
* 部署 PostExecVerticle执行所有 AppRun 实现)
* 安装引导完成后也可手动调用此方法触发后置初始化
*/
public void deployPostExec() {
var vertx = VertxHolder.getVertxInstance();
var postExecFuture = vertx.deployVerticle(PostExecVerticle.class, getWorkDeploymentOptions("postExec", 2));
postExecFuture.onSuccess(id -> {
LOGGER.info("PostExecVerticle 部署成功AppRun 实现执行完成");
}).onFailure(e -> {
LOGGER.error("PostExecVerticle 部署失败", e);
});
}
/**
* 重新部署 ServiceVerticle重新注册因 DB 未就绪而失败的服务到 EventBus
* 安装引导完成、DB 初始化后调用
*/
public void redeployServices() {
var vertx = VertxHolder.getVertxInstance();
vertx.deployVerticle(ServiceVerticle.class, getWorkDeploymentOptions("Service"))
.onSuccess(id -> LOGGER.info("ServiceVerticle 重新部署成功DB 相关服务已注册"))
.onFailure(e -> LOGGER.error("ServiceVerticle 重新部署失败", e));
} }
/** /**

View File

@@ -9,6 +9,7 @@ import java.lang.annotation.*;
public @interface HandleSortFilter { public @interface HandleSortFilter {
/** /**
* 注册顺序,数字越大越先注册<br> * 注册顺序,数字越大越先注册<br>
* 前置拦截器会先执行后注册即数字小的, 后置拦截器会先执行先注册的即数字大的<br>
* 值<0时会过滤掉该处理器 * 值<0时会过滤掉该处理器
*/ */
int value() default 0; int value() default 0;

View File

@@ -0,0 +1,12 @@
package cn.qaiu.vx.core.base;
import io.vertx.core.json.JsonObject;
public interface AppRun {
/**
* 执行方法
* @param config 启动配置文件
*/
void execute(JsonObject config);
}

View File

@@ -38,6 +38,20 @@ public interface BaseHttpApi {
handleAfterInterceptor(ctx, jsonResult.toJsonObject()); handleAfterInterceptor(ctx, jsonResult.toJsonObject());
} }
default void doFireJsonObjectResponse(RoutingContext ctx, JsonObject jsonObject, int statusCode) {
if (!ctx.response().ended()) {
fireJsonObjectResponse(ctx, jsonObject, statusCode);
}
handleAfterInterceptor(ctx, jsonObject);
}
default <T> void doFireJsonResultResponse(RoutingContext ctx, JsonResult<T> jsonResult, int statusCode) {
if (!ctx.response().ended()) {
fireJsonResultResponse(ctx, jsonResult, statusCode);
}
handleAfterInterceptor(ctx, jsonResult.toJsonObject());
}
default Set<AfterInterceptor> getAfterInterceptor() { default Set<AfterInterceptor> getAfterInterceptor() {

View File

@@ -0,0 +1,23 @@
package cn.qaiu.vx.core.base;
import cn.qaiu.vx.core.annotaions.HandleSortFilter;
import io.vertx.core.json.JsonObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 默认的AppRun实现示例
* <br>Create date 2024-01-01 00:00:00
*
* @author <a href="https://qaiu.top">QAIU</a>
*/
@HandleSortFilter
public class DefaultAppRun implements AppRun {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultAppRun.class);
@Override
public void execute(JsonObject config) {
LOGGER.info("======> AppRun实现类开始执行配置数: {}", config.size());
}
}

View File

@@ -23,8 +23,6 @@ import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.*; import io.vertx.ext.web.handler.*;
import io.vertx.ext.web.handler.sockjs.SockJSHandler; import io.vertx.ext.web.handler.sockjs.SockJSHandler;
import io.vertx.ext.web.handler.sockjs.SockJSHandlerOptions; 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 javassist.CtClass;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
@@ -76,15 +74,15 @@ public class RouterHandlerFactory implements BaseHttpApi {
// 主路由 // 主路由
Router mainRouter = Router.router(VertxHolder.getVertxInstance()); Router mainRouter = Router.router(VertxHolder.getVertxInstance());
mainRouter.route().handler(ctx -> { mainRouter.route().handler(ctx -> {
String realPath = ctx.request().uri();; String realPath = ctx.request().uri();
if (realPath.startsWith(REROUTE_PATH_PREFIX)) { if (realPath.startsWith(REROUTE_PATH_PREFIX)) {
// vertx web proxy暂不支持rewrite, 所以这里进行手动替换, 请求地址中的请求path前缀替换为originPath // vertx web proxy暂不支持rewrite, 所以这里进行手动替换, 请求地址中的请求path前缀替换为originPath
String rePath = realPath.substring(REROUTE_PATH_PREFIX.length()); String rePath = realPath.replace(REROUTE_PATH_PREFIX, "");
ctx.reroute(rePath); ctx.reroute(rePath);
return; return;
} }
LOGGER.debug("The HTTP service request address information ===>path:{}, uri:{}, method:{}", LOGGER.debug("New request:{}, {}, {}",
ctx.request().path(), ctx.request().absoluteURI(), ctx.request().method()); ctx.request().path(), ctx.request().absoluteURI(), ctx.request().method());
ctx.response().headers().add(ACCESS_CONTROL_ALLOW_ORIGIN, "*"); ctx.response().headers().add(ACCESS_CONTROL_ALLOW_ORIGIN, "*");
ctx.response().headers().add(DATE, LocalDateTime.now().format(ISO_LOCAL_DATE_TIME)); ctx.response().headers().add(DATE, LocalDateTime.now().format(ISO_LOCAL_DATE_TIME));
@@ -100,16 +98,6 @@ public class RouterHandlerFactory implements BaseHttpApi {
// 配置文件上传路径 // 配置文件上传路径
mainRouter.route().handler(BodyHandler.create().setUploadsDirectory("uploads")); 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(); Set<Handler<RoutingContext>> interceptorSet = getInterceptorSet();
Route route0 = mainRouter.route("/*"); Route route0 = mainRouter.route("/*");
@@ -189,10 +177,10 @@ public class RouterHandlerFactory implements BaseHttpApi {
if (ctx.response().ended()) return; if (ctx.response().ended()) return;
// 超时处理器状态码503 // 超时处理器状态码503
if (ctx.statusCode() == 503 || ctx.failure() == null) { if (ctx.statusCode() == 503 || ctx.failure() == null) {
doFireJsonResultResponse(ctx, JsonResult.error("未知异常, 请联系管理员", 500)); doFireJsonResultResponse(ctx, JsonResult.error("未知异常, 请联系管理员"), 503);
} else { } else {
ctx.failure().printStackTrace(); ctx.failure().printStackTrace();
doFireJsonResultResponse(ctx, JsonResult.error(ctx.failure().getMessage(), 500)); doFireJsonResultResponse(ctx, JsonResult.error(ctx.failure().getMessage()), 500);
} }
}); });
} else if (method.isAnnotationPresent(SockRouteMapper.class)) { } else if (method.isAnnotationPresent(SockRouteMapper.class)) {
@@ -246,7 +234,7 @@ public class RouterHandlerFactory implements BaseHttpApi {
*/ */
private Set<Handler<RoutingContext>> getInterceptorSet() { private Set<Handler<RoutingContext>> getInterceptorSet() {
// 配置拦截 // 配置拦截
return getBeforeInterceptor().stream().map(BeforeInterceptor::doHandle).collect(Collectors.toSet()); return getBeforeInterceptor().stream().map(BeforeInterceptor::doHandle).collect(Collectors.toCollection(LinkedHashSet::new));
} }
/** /**
@@ -315,19 +303,19 @@ public class RouterHandlerFactory implements BaseHttpApi {
final MultiMap queryParams = ctx.queryParams(); final MultiMap queryParams = ctx.queryParams();
// 解析body-json参数 // 解析body-json参数
// 只处理POST/PUT/PATCH等有body的请求方法避免GET请求读取body导致"Request has already been read"错误 if (HttpHeaderValues.APPLICATION_JSON.toString().equals(ctx.parsedHeaders().contentType().value())) {
String httpMethod = ctx.request().method().name();
if (("POST".equals(httpMethod) || "PUT".equals(httpMethod) || "PATCH".equals(httpMethod))
&& ctx.parsedHeaders() != null && ctx.parsedHeaders().contentType() != null
&& HttpHeaderValues.APPLICATION_JSON.toString().equals(ctx.parsedHeaders().contentType().value())
&& ctx.body() != null && ctx.body().asJsonObject() != null) {
JsonObject body = ctx.body().asJsonObject(); JsonObject body = ctx.body().asJsonObject();
if (body != null) { if (body != null) {
methodParametersTemp.forEach((k, v) -> { methodParametersTemp.forEach((k, v) -> {
String typeName = v.getRight().getName();
// 直接绑定 JsonObject 类型参数
if (JsonObject.class.getName().equals(typeName)) {
parameterValueList.put(k, body);
}
// 只解析已配置包名前缀的实体类 // 只解析已配置包名前缀的实体类
if (CommonUtil.matchRegList(entityPackagesReg.getList(), v.getRight().getName())) { else if (CommonUtil.matchRegList(entityPackagesReg.getList(), typeName)) {
try { try {
Class<?> aClass = Class.forName(v.getRight().getName()); Class<?> aClass = Class.forName(typeName);
JsonObject data = CommonUtil.getSubJsonForEntity(body, aClass); JsonObject data = CommonUtil.getSubJsonForEntity(body, aClass);
if (!data.isEmpty()) { if (!data.isEmpty()) {
Object entity = data.mapTo(aClass); Object entity = data.mapTo(aClass);
@@ -336,17 +324,21 @@ public class RouterHandlerFactory implements BaseHttpApi {
} catch (ClassNotFoundException e) { } catch (ClassNotFoundException e) {
e.printStackTrace(); e.printStackTrace();
} }
}
});
} else {
// body 可能是 JsonArray
JsonArray bodyArray = ctx.body().asJsonArray();
if (bodyArray != null) {
methodParametersTemp.forEach((k, v) -> {
if (JsonArray.class.getName().equals(v.getRight().getName())) {
parameterValueList.put(k, bodyArray);
} }
}); });
} }
} else if (("POST".equals(httpMethod) || "PUT".equals(httpMethod) || "PATCH".equals(httpMethod))
&& ctx.body() != null && ctx.body().length() > 0) {
try {
queryParams.addAll(ParamUtil.paramsToMap(ctx.body().asString()));
} catch (Exception e) {
LOGGER.debug("Failed to parse body as params: {}", e.getMessage());
} }
} else if (ctx.body() != null) {
queryParams.addAll(ParamUtil.paramsToMap(ctx.body().asString()));
} }
// 解析其他参数 // 解析其他参数
@@ -365,12 +357,6 @@ public class RouterHandlerFactory implements BaseHttpApi {
parameterValueList.put(k, ctx.request()); parameterValueList.put(k, ctx.request());
} else if (HttpServerResponse.class.getName().equals(v.getRight().getName())) { } else if (HttpServerResponse.class.getName().equals(v.getRight().getName())) {
parameterValueList.put(k, ctx.response()); parameterValueList.put(k, ctx.response());
} else if (JsonObject.class.getName().equals(v.getRight().getName())) {
if (ctx.body() != null && ctx.body().asJsonObject() != null) {
parameterValueList.put(k, ctx.body().asJsonObject());
} else {
parameterValueList.put(k, new JsonObject());
}
} else if (parameterValueList.get(k) == null } else if (parameterValueList.get(k) == null
&& CommonUtil.matchRegList(entityPackagesReg.getList(), v.getRight().getName())) { && CommonUtil.matchRegList(entityPackagesReg.getList(), v.getRight().getName())) {
// 绑定实体类 // 绑定实体类
@@ -381,45 +367,48 @@ public class RouterHandlerFactory implements BaseHttpApi {
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
} else if (parameterValueList.get(k) == null
&& JsonObject.class.getName().equals(v.getRight().getName())) {
// 兜底: content-type 非 application/json 时尝试从 body 解析 JsonObject
if (ctx.body() != null) {
JsonObject jo = ctx.body().asJsonObject();
if (jo != null) parameterValueList.put(k, jo);
}
} else if (parameterValueList.get(k) == null
&& JsonArray.class.getName().equals(v.getRight().getName())) {
// 兜底: content-type 非 application/json 时尝试从 body 解析 JsonArray
if (ctx.body() != null) {
JsonArray ja = ctx.body().asJsonArray();
if (ja != null) parameterValueList.put(k, ja);
}
} }
}); });
// 调用handle 获取响应对象 // 调用handle 获取响应对象
Object[] parameterValueArray = parameterValueList.values().toArray(new Object[0]); Object[] parameterValueArray = parameterValueList.values().toArray(new Object[0]);
// 打印调试信息,确认参数注入的情况
if (LOGGER.isDebugEnabled() && method.getName().equals("donateAccount")) {
LOGGER.debug("donateAccount parameter list:");
int i = 0;
for (Map.Entry<String, Object> entry : parameterValueList.entrySet()) {
LOGGER.debug("Param [{}]: {} = {}", i++, entry.getKey(),
entry.getValue() != null ? entry.getValue().toString() : "null");
}
}
try { try {
// 反射调用 // 反射调用
Object data = ReflectionUtil.invokeWithArguments(method, instance, parameterValueArray); Object data = ReflectionUtil.invokeWithArguments(method, instance, parameterValueArray);
if (data != null) { if (data != null) {
if (data instanceof JsonResult) { if (data instanceof JsonResult jsonResult) {
doFireJsonResultResponse(ctx, (JsonResult<?>) data); doFireJsonResultResponse(ctx, (JsonResult<?>) data, jsonResult.getCode());
} }
if (data instanceof JsonObject) { if (data instanceof JsonObject) {
doFireJsonObjectResponse(ctx, ((JsonObject) data)); doFireJsonObjectResponse(ctx, ((JsonObject) data));
} else if (data instanceof Future) { // 处理异步响应 } else if (data instanceof Future) { // 处理异步响应
((Future<?>) data).onSuccess(res -> { ((Future<?>) data).onSuccess(res -> {
if (res instanceof JsonResult) { if (res instanceof JsonResult jsonResult) {
doFireJsonResultResponse(ctx, (JsonResult<?>) res); doFireJsonResultResponse(ctx, jsonResult, jsonResult.getCode());
} }
if (res instanceof JsonObject) { if (res instanceof JsonObject) {
doFireJsonObjectResponse(ctx, ((JsonObject) res)); doFireJsonObjectResponse(ctx, ((JsonObject) res));
} else if (res != null) { } else if (res != null) {
doFireJsonResultResponse(ctx, JsonResult.data(res)); doFireJsonResultResponse(ctx, JsonResult.data(res));
} else { } else {
handleAfterInterceptor(ctx, null); doFireJsonResultResponse(ctx, JsonResult.data(null));
} }
}).onFailure(e -> doFireJsonResultResponse(ctx, JsonResult.error(e.getMessage()))); }).onFailure(e -> doFireJsonResultResponse(ctx, JsonResult.error(e.getMessage()), 500));
} else { } else {
doFireJsonResultResponse(ctx, JsonResult.data(data)); doFireJsonResultResponse(ctx, JsonResult.data(data));
} }
@@ -434,7 +423,7 @@ public class RouterHandlerFactory implements BaseHttpApi {
err = e.getCause().getMessage(); err = e.getCause().getMessage();
} }
} }
doFireJsonResultResponse(ctx, JsonResult.error(err)); doFireJsonResultResponse(ctx, JsonResult.error(err), 500);
} }
} }

View File

@@ -3,10 +3,12 @@ package cn.qaiu.vx.core.interceptor;
import io.vertx.core.Handler; import io.vertx.core.Handler;
import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.RoutingContext;
import static cn.qaiu.vx.core.util.ResponseUtil.sendError;
/** /**
* 前置拦截器接口 * 前置拦截器接口
* <p>
* 注意Vert.x是异步非阻塞框架不能在Event Loop中使用synchronized等阻塞操作
* 所有操作都应该是非阻塞的使用Vert.x的上下文数据存储机制保证线程安全。
* </p>
* *
* @author <a href="https://qaiu.top">QAIU</a> * @author <a href="https://qaiu.top">QAIU</a>
*/ */
@@ -14,28 +16,25 @@ public interface BeforeInterceptor extends Handler<RoutingContext> {
String IS_NEXT = "RoutingContextIsNext"; String IS_NEXT = "RoutingContextIsNext";
default Handler<RoutingContext> doHandle() { default Handler<RoutingContext> doHandle() {
return ctx -> { return ctx -> {
// 加同步锁 // 【优化】移除synchronized锁Vert.x的RoutingContext本身就是线程安全的
synchronized (BeforeInterceptor.class) { // 每个请求都有独立的RoutingContext不需要额外加锁
ctx.put(IS_NEXT, false); ctx.put(IS_NEXT, false);
BeforeInterceptor.this.handle(ctx); handle(ctx); // 调用具体的处理逻辑
if (!(Boolean) ctx.get(IS_NEXT) && !ctx.response().ended()) { // 确保如果没有调用doNext()并且响应未结束,则返回错误
sendError(ctx, 403); // if (!(Boolean) ctx.get(IS_NEXT) && !ctx.response().ended()) {
} // sendError(ctx, 403);
} // }
}; };
} }
default void doNext(RoutingContext context) { default void doNext(RoutingContext context) {
// 设置上下文状态为可以继续执行 // 【优化】移除synchronized锁
// 添加同步锁保障多线程下执行时序 // RoutingContext的put和next操作是线程安全的不需要额外同步
synchronized (BeforeInterceptor.class) {
context.put(IS_NEXT, true); context.put(IS_NEXT, true);
context.next(); context.next(); // 继续执行下一个处理器
}
} }
void handle(RoutingContext context); void handle(RoutingContext context); // 实现具体的拦截处理逻辑
} }

View File

@@ -1,7 +1,7 @@
/** /**
* ModuleGen cn.qaiu.vx.core * ModuleGen cn.qaiu.vx.core
*/ */
@ModuleGen(name = "vertx-http-proxy", groupPackage = "cn.qaiu.vx.core", useFutures = true) @ModuleGen(name = "vertx-http-proxy", groupPackage = "cn.qaiu.vx.core")
package cn.qaiu.vx.core; package cn.qaiu.vx.core;
import io.vertx.codegen.annotations.ModuleGen; import io.vertx.codegen.annotations.ModuleGen;

View File

@@ -5,7 +5,7 @@ import io.vertx.serviceproxy.ServiceProxyBuilder;
/** /**
* @author Xu Haidong * @author Xu Haidong
* Create at 2018/8/15 * @date 2018/8/15
*/ */
public final class AsyncServiceUtil { public final class AsyncServiceUtil {

View File

@@ -13,6 +13,7 @@ import java.net.Socket;
import java.net.URL; import java.net.URL;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.List; import java.util.List;
import java.util.LinkedHashSet;
import java.util.Map; import java.util.Map;
import java.util.Properties; import java.util.Properties;
import java.util.Set; import java.util.Set;
@@ -117,7 +118,7 @@ public class CommonUtil {
return set.stream().filter(c1 -> { return set.stream().filter(c1 -> {
HandleSortFilter s1 = c1.getAnnotation(HandleSortFilter.class); HandleSortFilter s1 = c1.getAnnotation(HandleSortFilter.class);
if (s1 != null) { if (s1 != null) {
return s1.value() > 0; return s1.value() >= 0;
} else { } else {
return true; return true;
} }
@@ -138,7 +139,7 @@ public class CommonUtil {
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
}).collect(Collectors.toSet()); }).collect(Collectors.toCollection(LinkedHashSet::new));
} }
private static String appVersion; private static String appVersion;

View File

@@ -4,9 +4,13 @@ import io.vertx.config.ConfigRetriever;
import io.vertx.config.ConfigRetrieverOptions; import io.vertx.config.ConfigRetrieverOptions;
import io.vertx.config.ConfigStoreOptions; import io.vertx.config.ConfigStoreOptions;
import io.vertx.core.Future; import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.Vertx; import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject; import io.vertx.core.json.JsonObject;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
/** /**
* 异步读取配置工具类 * 异步读取配置工具类
* <br>Create date 2021/9/2 1:23 * <br>Create date 2021/9/2 1:23
@@ -24,7 +28,29 @@ public class ConfigUtil {
* @return JsonObject的Future * @return JsonObject的Future
*/ */
public static Future<JsonObject> readConfig(String format, String path, Vertx vertx) { public static Future<JsonObject> readConfig(String format, String path, Vertx vertx) {
// 读取yml配置 // 支持 classpath: 前缀从类路径读取,否则从文件系统读取
if (path != null && path.startsWith("classpath:")) {
String resource = path.substring("classpath:".length());
// 使用 executeBlocking(Callable) 直接返回 Future<JsonObject>
return vertx.executeBlocking(() -> {
InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(resource);
if (is == null) {
throw new RuntimeException("classpath resource not found: " + resource);
}
try (InputStream in = is) {
byte[] bytes = in.readAllBytes();
String content = new String(bytes, StandardCharsets.UTF_8);
if ("json".equalsIgnoreCase(format)) {
return new JsonObject(content);
} else {
throw new RuntimeException("unsupported classpath format: " + format);
}
}
});
}
Promise<JsonObject> promise = Promise.promise();
ConfigStoreOptions store = new ConfigStoreOptions() ConfigStoreOptions store = new ConfigStoreOptions()
.setType("file") .setType("file")
.setFormat(format) .setFormat(format)
@@ -33,10 +59,22 @@ public class ConfigUtil {
ConfigRetriever retriever = ConfigRetriever ConfigRetriever retriever = ConfigRetriever
.create(vertx, new ConfigRetrieverOptions().addStore(store)); .create(vertx, new ConfigRetrieverOptions().addStore(store));
return retriever.getConfig(); // 异步获取配置
// 成功直接完成 promise
retriever.getConfig()
.onSuccess(promise::complete)
.onFailure(err -> {
// 配置读取失败,直接返回失败 Future
promise.fail(new RuntimeException(
"读取配置文件失败: " + path, err));
retriever.close();
});
return promise.future();
} }
/** /**
* 异步读取Yaml配置文件 * 异步读取Yaml配置文件
* *

View File

@@ -0,0 +1,20 @@
package cn.qaiu.vx.core.util;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import java.util.concurrent.ExecutionException;
public class FutureUtils {
public static <T> T getResult(Future<T> future) {
try {
return future.toCompletionStage().toCompletableFuture().get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
public static <T> T getResult(Promise<T> promise) {
return promise.future().toCompletionStage().toCompletableFuture().join();
}
}

View File

@@ -16,7 +16,7 @@ import java.time.format.DateTimeFormatter;
/** /**
* @author <a href="https://qaiu.top">QAIU</a> * @author <a href="https://qaiu.top">QAIU</a>
* Create at 2023/10/14 9:07 * @date 2023/10/14 9:07
*/ */
public class JacksonConfig { public class JacksonConfig {

View File

@@ -36,6 +36,8 @@ import static cn.qaiu.vx.core.util.ConfigConstant.BASE_LOCATIONS;
*/ */
public final class ReflectionUtil { public final class ReflectionUtil {
// 缓存Reflections实例避免重复扫描每次扫描约35K+值耗时1-3秒占用大量内存
private static final Map<String, Reflections> REFLECTIONS_CACHE = new java.util.concurrent.ConcurrentHashMap<>();
/** /**
* 以默认配置的基础包路径获取反射器 * 以默认配置的基础包路径获取反射器
@@ -47,43 +49,41 @@ public final class ReflectionUtil {
} }
/** /**
* 获取反射器 * 获取反射器(带缓存)
* *
* @param packageAddress Package address String * @param packageAddress Package address String
* @return Reflections object * @return Reflections object
*/ */
public static Reflections getReflections(String packageAddress) { public static Reflections getReflections(String packageAddress) {
return REFLECTIONS_CACHE.computeIfAbsent(packageAddress, key -> {
List<String> packageAddressList; List<String> packageAddressList;
if (packageAddress.contains(",")) { if (key.contains(",")) {
packageAddressList = Arrays.asList(packageAddress.split(",")); packageAddressList = Arrays.asList(key.split(","));
} else if (packageAddress.contains(";")) { } else if (key.contains(";")) {
packageAddressList = Arrays.asList(packageAddress.split(";")); packageAddressList = Arrays.asList(key.split(";"));
} else { } else {
packageAddressList = Collections.singletonList(packageAddress); packageAddressList = Collections.singletonList(key);
} }
return createReflections(packageAddressList);
return getReflections(packageAddressList); });
} }
/** /**
* 获取反射器 * 获取反射器(带缓存)
* *
* @param packageAddresses Package address List * @param packageAddresses Package address List
* @return Reflections object * @return Reflections object
*/ */
public static Reflections getReflections(List<String> packageAddresses) { public static Reflections getReflections(List<String> packageAddresses) {
ConfigurationBuilder configurationBuilder = new ConfigurationBuilder(); String cacheKey = String.join(",", packageAddresses);
FilterBuilder filterBuilder = new FilterBuilder(); return REFLECTIONS_CACHE.computeIfAbsent(cacheKey, key -> createReflections(packageAddresses));
packageAddresses.forEach(str -> { }
Collection<URL> urls = ClasspathHelper.forPackage(str.trim());
configurationBuilder.addUrls(urls);
filterBuilder.includePackage(str.trim());
});
// 采坑记录 2021-05-08 private static Reflections createReflections(List<String> packageAddresses) {
// 发现注解api层 没有继承父类时 这里反射一直有问题(Scanner SubTypesScanner was not configured) ConfigurationBuilder configurationBuilder = new ConfigurationBuilder()
// 因此这里需要手动配置各种Scanner扫描器 -- https://blog.csdn.net/qq_29499107/article/details/106889781 .addClassLoaders(Thread.currentThread().getContextClassLoader())
configurationBuilder.setScanners( .forPackages(packageAddresses.toArray(new String[0]))
.setScanners(
Scanners.SubTypes.filterResultsBy(s -> true), //允许getAllTypes获取所有Object的子类, 不设置为false则 getAllTypes Scanners.SubTypes.filterResultsBy(s -> true), //允许getAllTypes获取所有Object的子类, 不设置为false则 getAllTypes
// 会报错.默认为true. // 会报错.默认为true.
new MethodParameterNamesScanner(), //设置方法参数名称 扫描器,否则调用getConstructorParamNames 会报错 new MethodParameterNamesScanner(), //设置方法参数名称 扫描器,否则调用getConstructorParamNames 会报错
@@ -91,8 +91,6 @@ public final class ReflectionUtil {
new MemberUsageScanner(), //设置 member 扫描器,否则 getMethodUsage 会报错 new MemberUsageScanner(), //设置 member 扫描器,否则 getMethodUsage 会报错
Scanners.TypesAnnotated //设置类注解 扫描器 ,否则 getTypesAnnotatedWith 会报错 Scanners.TypesAnnotated //设置类注解 扫描器 ,否则 getTypesAnnotatedWith 会报错
); );
configurationBuilder.filterInputsBy(filterBuilder);
return new Reflections(configurationBuilder); return new Reflections(configurationBuilder);
} }

View File

@@ -13,6 +13,7 @@ public class ResponseUtil {
public static void redirect(HttpServerResponse response, String url) { public static void redirect(HttpServerResponse response, String url) {
response.putHeader(CONTENT_TYPE, "text/html; charset=utf-8") response.putHeader(CONTENT_TYPE, "text/html; charset=utf-8")
.putHeader("Referrer-Policy", "no-referrer")
.putHeader(HttpHeaders.LOCATION, url).setStatusCode(302).end(); .putHeader(HttpHeaders.LOCATION, url).setStatusCode(302).end();
} }
@@ -22,14 +23,22 @@ public class ResponseUtil {
} }
public static void fireJsonObjectResponse(RoutingContext ctx, JsonObject jsonObject) { public static void fireJsonObjectResponse(RoutingContext ctx, JsonObject jsonObject) {
ctx.response().putHeader(CONTENT_TYPE, "application/json; charset=utf-8") fireJsonObjectResponse(ctx, jsonObject, 200);
.setStatusCode(200)
.end(jsonObject.encode());
} }
public static void fireJsonObjectResponse(HttpServerResponse ctx, JsonObject jsonObject) { public static void fireJsonObjectResponse(HttpServerResponse ctx, JsonObject jsonObject) {
fireJsonObjectResponse(ctx, jsonObject, 200);
}
public static void fireJsonObjectResponse(RoutingContext ctx, JsonObject jsonObject, int statusCode) {
ctx.response().putHeader(CONTENT_TYPE, "application/json; charset=utf-8")
.setStatusCode(statusCode)
.end(jsonObject.encode());
}
public static void fireJsonObjectResponse(HttpServerResponse ctx, JsonObject jsonObject, int statusCode) {
ctx.putHeader(CONTENT_TYPE, "application/json; charset=utf-8") ctx.putHeader(CONTENT_TYPE, "application/json; charset=utf-8")
.setStatusCode(200) .setStatusCode(statusCode)
.end(jsonObject.encode()); .end(jsonObject.encode());
} }
@@ -37,6 +46,10 @@ public class ResponseUtil {
fireJsonObjectResponse(ctx, jsonResult.toJsonObject()); fireJsonObjectResponse(ctx, jsonResult.toJsonObject());
} }
public static <T> void fireJsonResultResponse(RoutingContext ctx, JsonResult<T> jsonResult, int statusCode) {
fireJsonObjectResponse(ctx, jsonResult.toJsonObject(), statusCode);
}
public static <T> void fireJsonResultResponse(HttpServerResponse ctx, JsonResult<T> jsonResult) { public static <T> void fireJsonResultResponse(HttpServerResponse ctx, JsonResult<T> jsonResult) {
fireJsonObjectResponse(ctx, jsonResult.toJsonObject()); fireJsonObjectResponse(ctx, jsonResult.toJsonObject());
} }

View File

@@ -1,50 +1,77 @@
package cn.qaiu.vx.core.verticle; package cn.qaiu.vx.core.verticle;
import io.vertx.core.AbstractVerticle; import io.vertx.core.AbstractVerticle;
import io.vertx.core.Vertx;
import io.vertx.core.VertxOptions;
import io.vertx.core.dns.AddressResolverOptions;
import io.vertx.core.http.*; import io.vertx.core.http.*;
import io.vertx.core.json.JsonObject;
import io.vertx.core.net.NetClient; import io.vertx.core.net.NetClient;
import io.vertx.core.net.NetClientOptions; import io.vertx.core.net.NetClientOptions;
import io.vertx.core.net.NetSocket;
import io.vertx.core.net.ProxyOptions; import io.vertx.core.net.ProxyOptions;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URI;
import java.util.Base64; import java.util.Base64;
import static cn.qaiu.vx.core.util.ConfigConstant.GLOBAL_CONFIG;
import static cn.qaiu.vx.core.util.ConfigConstant.LOCAL;
/** /**
* *
*/ */
public class HttpProxyVerticle extends AbstractVerticle { public class HttpProxyVerticle extends AbstractVerticle {
private static final Logger LOGGER = LoggerFactory.getLogger(HttpProxyVerticle.class);
private HttpClient httpClient; private HttpClient httpClient;
private NetClient netClient; private NetClient netClient;
private JsonObject proxyPreConf;
private JsonObject proxyServerConf;
@Override @Override
public void start() { public void start() {
ProxyOptions proxyOptions = new ProxyOptions().setHost("127.0.0.1").setPort(7890); proxyServerConf = ((JsonObject)vertx.sharedData().getLocalMap(LOCAL).get(GLOBAL_CONFIG)).getJsonObject("proxy-server");
proxyPreConf = ((JsonObject)vertx.sharedData().getLocalMap(LOCAL).get(GLOBAL_CONFIG)).getJsonObject("proxy-pre");
Integer serverPort = proxyServerConf.getInteger("port");
ProxyOptions proxyOptions = null;
if (proxyPreConf != null && StringUtils.isNotBlank(proxyPreConf.getString("ip"))) {
proxyOptions = new ProxyOptions(proxyPreConf);
}
// 初始化 HTTP 客户端,用于向目标服务器发送 HTTP 请求 // 初始化 HTTP 客户端,用于向目标服务器发送 HTTP 请求
HttpClientOptions httpClientOptions = new HttpClientOptions(); HttpClientOptions httpClientOptions = new HttpClientOptions();
httpClient = vertx.createHttpClient(httpClientOptions.setProxyOptions(proxyOptions)); if (proxyOptions != null) {
httpClientOptions.setProxyOptions(proxyOptions);
}
httpClient = vertx.createHttpClient(httpClientOptions);
// 创建并启动 HTTP 代理服务器,监听指定端口 // 创建并启动 HTTP 代理服务器,监听指定端口
HttpServer server = vertx.createHttpServer(new HttpServerOptions().setClientAuth(ClientAuth.REQUIRED)); HttpServerOptions httpServerOptions = new HttpServerOptions();
if (proxyServerConf.containsKey("username") &&
StringUtils.isNotBlank(proxyServerConf.getString("username"))) {
httpServerOptions.setClientAuth(ClientAuth.REQUIRED);
}
HttpServer server = vertx.createHttpServer();
server.requestHandler(this::handleClientRequest); server.requestHandler(this::handleClientRequest);
// 初始化 NetClient用于在 CONNECT 请求中建立 TCP 连接隧道 // 初始化 NetClient用于在 CONNECT 请求中建立 TCP 连接隧道
netClient = vertx.createNetClient(new NetClientOptions() NetClientOptions netClientOptions = new NetClientOptions();
.setProxyOptions(proxyOptions)
if (proxyOptions != null) {
httpClientOptions.setProxyOptions(proxyOptions);
}
netClient = vertx.createNetClient(netClientOptions
.setConnectTimeout(15000) .setConnectTimeout(15000)
.setTrustAll(true)); .setTrustAll(true));
// 启动 HTTP 代理服务器 // 启动 HTTP 代理服务器
server.listen(7891, ar -> { server.listen(serverPort)
if (ar.succeeded()) { .onSuccess(res-> LOGGER.info("HTTP Proxy server started on port {}", serverPort))
System.out.println("HTTP Proxy server started on port 7891"); .onFailure(err-> LOGGER.error("Failed to start HTTP Proxy server: " + err.getMessage()));
} else {
System.err.println("Failed to start HTTP Proxy server: " + ar.cause());
}
});
} }
// 处理 HTTP CONNECT 请求,用于代理 HTTPS 流量 // 处理 HTTP CONNECT 请求,用于代理 HTTPS 流量
@@ -66,37 +93,37 @@ public class HttpProxyVerticle extends AbstractVerticle {
} }
clientRequest.pause(); clientRequest.pause();
// 通过 NetClient 连接目标服务器并创建隧道 // 通过 NetClient 连接目标服务器并创建隧道
netClient.connect(targetPort, targetHost, connectionAttempt -> { netClient.connect(targetPort, targetHost)
if (connectionAttempt.succeeded()) { .onSuccess(targetSocket -> {
NetSocket targetSocket = connectionAttempt.result(); // Upgrade client connection to NetSocket and implement bidirectional data flow
clientRequest.toNetSocket()
// 升级客户端连接到 NetSocket 并实现双向数据流 .onSuccess(clientSocket -> {
clientRequest.toNetSocket().onComplete(clientSocketAttempt -> { // Set up bidirectional data forwarding
if (clientSocketAttempt.succeeded()) {
NetSocket clientSocket = clientSocketAttempt.result();
// 设置双向数据流转发
clientSocket.handler(targetSocket::write); clientSocket.handler(targetSocket::write);
targetSocket.handler(clientSocket::write); targetSocket.handler(clientSocket::write);
// 关闭其中一方时关闭另一方 // Close the other socket when one side closes
clientSocket.closeHandler(v -> targetSocket.close()); clientSocket.closeHandler(v -> targetSocket.close());
targetSocket.closeHandler(v -> clientSocket.close()); targetSocket.closeHandler(v -> clientSocket.close());
} else { })
System.err.println("Failed to upgrade client connection to socket: " + clientSocketAttempt.cause().getMessage()); .onFailure(clientSocketAttempt -> {
System.err.println("Failed to upgrade client connection to socket: " + clientSocketAttempt.getMessage());
targetSocket.close(); targetSocket.close();
clientRequest.response().setStatusCode(500).end("Internal Server Error"); clientRequest.response().setStatusCode(500).end("Internal Server Error");
}
}); });
} else { })
System.err.println("Failed to connect to target: " + connectionAttempt.cause().getMessage()); .onFailure(connectionAttempt -> {
System.err.println("Failed to connect to target: " + connectionAttempt.getMessage());
clientRequest.response().setStatusCode(502).end("Bad Gateway: Unable to connect to target"); clientRequest.response().setStatusCode(502).end("Bad Gateway: Unable to connect to target");
}
}); });
} }
// 处理客户端的 HTTP 请求 // 处理客户端的 HTTP 请求
private void handleClientRequest(HttpServerRequest clientRequest) { private void handleClientRequest(HttpServerRequest clientRequest) {
// 打印来源ip和访问目标URI
LOGGER.debug("source: {}, target: {}", clientRequest.remoteAddress().toString(), clientRequest.uri());
if (proxyServerConf.containsKey("username") &&
StringUtils.isNotBlank(proxyServerConf.getString("username"))) {
String s = clientRequest.headers().get("Proxy-Authorization"); String s = clientRequest.headers().get("Proxy-Authorization");
if (s == null) { if (s == null) {
clientRequest.response().setStatusCode(403).end(); clientRequest.response().setStatusCode(403).end();
@@ -104,11 +131,16 @@ public class HttpProxyVerticle extends AbstractVerticle {
} }
String[] split = new String(Base64.getDecoder().decode(s.replace("Basic ", ""))).split(":"); String[] split = new String(Base64.getDecoder().decode(s.replace("Basic ", ""))).split(":");
if (split.length > 1) { if (split.length > 1) {
System.out.println(split[0]);
System.out.println(split[1]);
// TODO // TODO
String username = proxyServerConf.getString("username");
String password = proxyServerConf.getString("password");
if (!split[0].equals(username) || !split[1].equals(password)) {
LOGGER.info("-----auth failed------\nusername: {}\npassword: {}", username, password);
clientRequest.response().setStatusCode(403).end();
return;
}
}
} }
if (clientRequest.method() == HttpMethod.CONNECT) { if (clientRequest.method() == HttpMethod.CONNECT) {
// 处理 CONNECT 请求 // 处理 CONNECT 请求
@@ -129,7 +161,7 @@ public class HttpProxyVerticle extends AbstractVerticle {
} }
String targetHost = hostHeader.split(":")[0]; String targetHost = hostHeader.split(":")[0];
int targetPort = 80; // 默认为 HTTP 的端口 int targetPort = extractPortFromUrl(clientRequest.uri()); // 默认为 HTTP 的端口
clientRequest.pause(); // 暂停客户端请求的读取,避免数据丢失 clientRequest.pause(); // 暂停客户端请求的读取,避免数据丢失
httpClient.request(clientRequest.method(), targetPort, targetHost, clientRequest.uri()) httpClient.request(clientRequest.method(), targetPort, targetHost, clientRequest.uri())
@@ -140,16 +172,19 @@ public class HttpProxyVerticle extends AbstractVerticle {
clientRequest.headers().forEach(header -> request.putHeader(header.getKey(), header.getValue())); clientRequest.headers().forEach(header -> request.putHeader(header.getKey(), header.getValue()));
// 将客户端请求的 body 转发给目标服务器 // 将客户端请求的 body 转发给目标服务器
clientRequest.bodyHandler(body -> request.send(body, ar -> { clientRequest.bodyHandler(body ->
if (ar.succeeded()) { request.send(body)
var response = ar.result(); .onSuccess(response -> {
clientRequest.response().setStatusCode(response.statusCode()); clientRequest.response().setStatusCode(response.statusCode());
clientRequest.response().headers().setAll(response.headers()); clientRequest.response().headers().setAll(response.headers());
response.body().onSuccess(b-> clientRequest.response().end(b)); response.body()
} else { .onSuccess(b -> clientRequest.response().end(b))
clientRequest.response().setStatusCode(502).end("Bad Gateway: Unable to reach target"); .onFailure(err -> clientRequest.response()
} .setStatusCode(502).end("Bad Gateway: Unable to reach target"));
})); })
.onFailure(err -> clientRequest.response()
.setStatusCode(502).end("Bad Gateway: Unable to reach target"))
);
}) })
.onFailure(err -> { .onFailure(err -> {
err.printStackTrace(); err.printStackTrace();
@@ -157,28 +192,43 @@ public class HttpProxyVerticle extends AbstractVerticle {
}); });
} }
/**
* 从 URL 中提取端口号
*
* @param urlString URL 字符串
* @return 提取的端口号,如果没有指定端口,则返回默认端口
*/
public static int extractPortFromUrl(String urlString) {
try {
URI uri = new URI(urlString);
int port = uri.getPort();
// 如果 URL 没有指定端口,使用默认端口
if (port == -1) {
if ("https".equalsIgnoreCase(uri.getScheme())) {
port = 443; // HTTPS 默认端口
} else {
port = 80; // HTTP 默认端口
}
}
return port;
} catch (Exception e) {
e.printStackTrace();
// 出现异常时返回 -1表示提取失败
return -1;
}
}
@Override @Override
public void stop() { public void stop() {
// 停止 HTTP 客户端以释放资源 // 停止 HTTP 客户端以释放资源
if (httpClient != null) { if (httpClient != null) {
httpClient.close(); httpClient.close();
} }
if (netClient != null) {
netClient.close();
}
} }
/**
* TODO add Deploy
* @param args
*/
public static void main(String[] args) {
// 配置 DNS 解析器,使用多个 DNS 服务器来提升解析速度
Vertx vertx = Vertx.vertx(new VertxOptions()
.setAddressResolverOptions(new AddressResolverOptions()
.addServer("114.114.114.114")
.addServer("114.114.115.115")
.addServer("8.8.8.8")
.addServer("8.8.4.4")));
// 部署 Verticle 并启动动态 HTTP 代理服务器
vertx.deployVerticle(new HttpProxyVerticle());
}
} }

View File

@@ -0,0 +1,68 @@
package cn.qaiu.vx.core.verticle;
import cn.qaiu.vx.core.base.AppRun;
import cn.qaiu.vx.core.base.DefaultAppRun;
import cn.qaiu.vx.core.util.CommonUtil;
import cn.qaiu.vx.core.util.ReflectionUtil;
import cn.qaiu.vx.core.util.SharedDataUtil;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Promise;
import io.vertx.core.json.JsonObject;
import org.reflections.Reflections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 后置执行Verticle - 在core启动后立即执行AppRun实现
* <br>Create date 2024-01-01 00:00:00
*
* @author <a href="https://qaiu.top">QAIU</a>
*/
public class PostExecVerticle extends AbstractVerticle {
private static final Logger LOGGER = LoggerFactory.getLogger(PostExecVerticle.class);
private static final Set<AppRun> appRunImplementations;
private static final AtomicBoolean lock = new AtomicBoolean(false);
static {
Reflections reflections = ReflectionUtil.getReflections();
Set<Class<? extends AppRun>> subTypesOf = reflections.getSubTypesOf(AppRun.class);
subTypesOf.add(DefaultAppRun.class);
appRunImplementations = CommonUtil.sortClassSet(subTypesOf);
if (appRunImplementations.isEmpty()) {
LOGGER.warn("未找到 AppRun 接口的实现类");
} else {
LOGGER.info("找到 {} 个 AppRun 接口的实现类", appRunImplementations.size());
}
}
@Override
public void start(Promise<Void> startPromise) {
if (!lock.compareAndSet(false, true)) {
return;
}
LOGGER.info("PostExecVerticle 开始执行...");
if (appRunImplementations != null && !appRunImplementations.isEmpty()) {
appRunImplementations.forEach(appRun -> {
try {
LOGGER.info("执行 AppRun 实现: {}", appRun.getClass().getName());
JsonObject globalConfig = SharedDataUtil.getJsonConfig("globalConfig");
appRun.execute(globalConfig);
LOGGER.info("AppRun 实现 {} 执行完成", appRun.getClass().getName());
} catch (Exception e) {
LOGGER.error("执行 AppRun 实现 {} 时发生错误",appRun.getClass().getName(), e);
}
});
} else {
LOGGER.info("未找到 AppRun 接口的实现类");
}
LOGGER.info("PostExecVerticle 执行完成");
startPromise.complete();
}
}

View File

@@ -5,8 +5,10 @@ import io.vertx.core.AbstractVerticle;
import io.vertx.core.Future; import io.vertx.core.Future;
import io.vertx.core.Promise; import io.vertx.core.Promise;
import io.vertx.core.http.HttpClient; import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpClientOptions;
import io.vertx.core.http.HttpServer; import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerOptions; import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject; import io.vertx.core.json.JsonObject;
import io.vertx.core.net.PemKeyCertOptions; import io.vertx.core.net.PemKeyCertOptions;
@@ -15,6 +17,9 @@ import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.StaticHandler; import io.vertx.ext.web.handler.StaticHandler;
import io.vertx.ext.web.proxy.handler.ProxyHandler; import io.vertx.ext.web.proxy.handler.ProxyHandler;
import io.vertx.httpproxy.HttpProxy; import io.vertx.httpproxy.HttpProxy;
import io.vertx.httpproxy.ProxyContext;
import io.vertx.httpproxy.ProxyInterceptor;
import io.vertx.httpproxy.ProxyResponse;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -22,13 +27,16 @@ import org.slf4j.LoggerFactory;
import java.io.File; import java.io.File;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.nio.file.Path; import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/** /**
* <p>反向代理服务</p> * <p>反向代理服务</p>
* <p>可以根据配置文件自动生成代理服务</p> * <p>可以根据配置文件自动生成代理服务</p>
* <p>可以配置多个服务, 配置文件见示例</p> * <p>可以配置多个服务, 配置文件见示例</p>
* <p>【优化】支持高并发场景,连接池复用,避免线程阻塞</p>
* <br>Create date 2021/9/2 0:41 * <br>Create date 2021/9/2 0:41
* *
* @author <a href="https://qaiu.top">QAIU</a> * @author <a href="https://qaiu.top">QAIU</a>
@@ -47,14 +55,83 @@ public class ReverseProxyVerticle extends AbstractVerticle {
public static String REROUTE_PATH_PREFIX = "/__rrvpspp"; //re_route_vert_proxy_server_path_prefix 硬编码 public static String REROUTE_PATH_PREFIX = "/__rrvpspp"; //re_route_vert_proxy_server_path_prefix 硬编码
/**
* 【优化】HttpClient连接池按host:port缓存复用避免每个请求都创建新连接
*/
private final Map<String, HttpClient> httpClientPool = new ConcurrentHashMap<>();
/**
* 【优化】高并发场景下的HttpClient配置
*/
private static final int MAX_POOL_SIZE = 100; // 最大连接池大小
private static final int MAX_WAIT_QUEUE_SIZE = 500; // 最大等待队列大小
private static final int CONNECT_TIMEOUT = 30000; // 连接超时30秒
private static final int IDLE_TIMEOUT = 60; // 空闲超时60秒
private static final boolean KEEP_ALIVE = true; // 启用Keep-Alive
private static final boolean PIPELINING = true; // 启用HTTP管线化
@Override @Override
public void start(Promise<Void> startPromise) { public void start(Promise<Void> startPromise) {
CONFIG.onSuccess(this::handleProxyConfList); CONFIG.onSuccess(this::handleProxyConfList).onFailure(e -> {
LOGGER.info("web代理配置已禁用当前仅支持API调用");
});
// createFileListener // createFileListener
startPromise.complete(); startPromise.complete();
} }
/**
* 【优化】Verticle停止时清理HttpClient连接池
*/
@Override
public void stop(Promise<Void> stopPromise) {
LOGGER.info("Stopping ReverseProxyVerticle, closing {} HttpClient connections...", httpClientPool.size());
httpClientPool.values().forEach(client -> {
try {
client.close();
} catch (Exception e) {
LOGGER.warn("Error closing HttpClient: {}", e.getMessage());
}
});
httpClientPool.clear();
stopPromise.complete();
}
/**
* 【优化】获取或创建HttpClient实现连接池复用
* @param host 目标主机
* @param port 目标端口
* @return HttpClient实例
*/
private HttpClient getOrCreateHttpClient(String host, int port) {
String key = host + ":" + port;
return httpClientPool.computeIfAbsent(key, k -> {
LOGGER.info("Creating new HttpClient for {}", key);
HttpClientOptions options = new HttpClientOptions()
.setMaxPoolSize(MAX_POOL_SIZE) // 连接池大小
.setMaxWaitQueueSize(MAX_WAIT_QUEUE_SIZE) // 等待队列大小
.setConnectTimeout(CONNECT_TIMEOUT) // 连接超时
.setIdleTimeout(IDLE_TIMEOUT) // 空闲超时
.setKeepAlive(KEEP_ALIVE) // Keep-Alive
.setKeepAliveTimeout(120) // Keep-Alive超时120秒
.setPipelining(PIPELINING) // HTTP管线化
.setPipeliningLimit(10) // 管线化限制
.setDecompressionSupported(true) // 支持解压响应
.setTcpKeepAlive(true) // TCP Keep-Alive
.setTcpNoDelay(true) // 禁用Nagle算法降低延迟
.setTcpFastOpen(true) // 启用TCP Fast Open
.setTcpQuickAck(true) // 启用TCP Quick ACK
.setReuseAddress(true) // 允许地址重用
.setReusePort(true); // 允许端口重用
return vertx.createHttpClient(options);
});
}
/**
* 全局可信上游代理 IP 集合(如 nginx仅这些 IP 的 X-Forwarded-For 会被信任
*/
private Set<String> globalTrustedProxies = new HashSet<>();
/** /**
* 获取主配置文件 * 获取主配置文件
* *
@@ -62,6 +139,15 @@ public class ReverseProxyVerticle extends AbstractVerticle {
*/ */
private void handleProxyConfList(JsonObject config) { private void handleProxyConfList(JsonObject config) {
serverName = config.getString("server-name"); serverName = config.getString("server-name");
// 解析全局 trusted-proxies
JsonArray trustedArr = config.getJsonArray("trusted-proxies");
if (trustedArr != null) {
trustedArr.forEach(ip -> {
if (ip instanceof String) {
globalTrustedProxies.add(((String) ip).trim());
}
});
}
JsonArray proxyConfList = config.getJsonArray("proxy"); JsonArray proxyConfList = config.getJsonArray("proxy");
if (proxyConfList != null) { if (proxyConfList != null) {
proxyConfList.forEach(proxyConf -> { proxyConfList.forEach(proxyConf -> {
@@ -72,32 +158,89 @@ public class ReverseProxyVerticle extends AbstractVerticle {
} }
} }
/**
* 解析真实客户端 IP。
* 若直连来源在可信代理列表中,优先取 X-Real-IP其次取 X-Forwarded-For 第一个值;
* 否则直接使用直连对端地址。
*/
private String resolveClientIp(HttpServerRequest request) {
String peerIp = request.remoteAddress().host();
if (globalTrustedProxies.contains(peerIp)) {
String realIp = request.getHeader("X-Real-IP");
if (StringUtils.isNotBlank(realIp)) {
return realIp.trim();
}
String xff = request.getHeader("X-Forwarded-For");
if (StringUtils.isNotBlank(xff)) {
return xff.split(",")[0].trim();
}
}
return peerIp;
}
/**
* 解析 proxy-set-headers 中的 nginx 风格变量。
* 支持:$remote_addr、$proxy_add_x_forwarded_for、$scheme、$host
* 其他值作为字面量直接使用。
*/
private String resolveHeaderVariable(String tpl, HttpServerRequest req, String clientIp) {
return switch (tpl) {
case "$remote_addr" -> clientIp;
case "$proxy_add_x_forwarded_for" -> {
String existing = req.getHeader("X-Forwarded-For");
yield StringUtils.isNotBlank(existing) ? existing + ", " + clientIp : clientIp;
}
case "$scheme" -> req.isSSL() ? "https" : "http";
case "$host" -> req.getHeader("Host");
default -> tpl;
};
}
/** /**
* 处理单个反向代理配置 * 处理单个反向代理配置
* *
* @param proxyConf 代理配置 * @param proxyConf 代理配置
*/ */
private void handleProxyConf(JsonObject proxyConf) { private void handleProxyConf(JsonObject proxyConf) {
// page404 path: 兼容不同启动目录(根目录或子模块目录) // page404 path
String configured404 = proxyConf.getString("page404"); if (proxyConf.containsKey(
String resolved404 = resolveExistingPath(configured404, false);
if (resolved404 == null) { "page404")) {
resolved404 = resolveExistingPath(DEFAULT_PATH_404, false); System.getProperty("user.dir");
} String path = proxyConf.getString("page404");
proxyConf.put("page404", resolved404 == null ? DEFAULT_PATH_404 : resolved404); if (StringUtils.isEmpty(path)) {
proxyConf.put("page404", DEFAULT_PATH_404);
} else {
if (!path.startsWith("/")) {
path = "/" + path;
}
if (!new File(System.getProperty("user.dir") + path).exists()) {
proxyConf.put("page404", DEFAULT_PATH_404);
}
}
} else {
proxyConf.put("page404", DEFAULT_PATH_404);
}
final HttpClient httpClient = VertxHolder.getVertxInstance().createHttpClient();
Router proxyRouter = Router.router(vertx); Router proxyRouter = Router.router(vertx);
// Add Server name header // Add Server name header
proxyRouter.route().handler(ctx -> { proxyRouter.route().handler(ctx -> {
String realPath = ctx.request().uri();
if (realPath.startsWith(REROUTE_PATH_PREFIX)) {
// vertx web proxy暂不支持rewrite, 所以这里进行手动替换, 请求地址中的请求path前缀替换为originPath
String rePath = realPath.replace(REROUTE_PATH_PREFIX, "");
ctx.reroute(rePath);
return;
}
ctx.response().putHeader("Server", serverName); ctx.response().putHeader("Server", serverName);
ctx.next(); ctx.next();
}); });
// http api proxy // http api proxy
if (proxyConf.containsKey("location")) { if (proxyConf.containsKey("location")) {
handleLocation(proxyConf.getJsonArray("location"), httpClient, proxyRouter); handleLocation(proxyConf.getJsonArray("location"), proxyRouter);
} }
// static server // static server
@@ -106,7 +249,9 @@ public class ReverseProxyVerticle extends AbstractVerticle {
} }
// Send page404 page // Send page404 page
proxyRouter.errorHandler(404, ctx -> ctx.response().sendFile(proxyConf.getString("page404"))); proxyRouter.errorHandler(404, ctx -> {
ctx.response().sendFile(proxyConf.getString("page404"));
});
HttpServer server = getHttpsServer(proxyConf); HttpServer server = getHttpsServer(proxyConf);
server.requestHandler(proxyRouter); server.requestHandler(proxyRouter);
@@ -118,8 +263,16 @@ public class ReverseProxyVerticle extends AbstractVerticle {
private HttpServer getHttpsServer(JsonObject proxyConf) { private HttpServer getHttpsServer(JsonObject proxyConf) {
HttpServerOptions httpServerOptions = new HttpServerOptions() HttpServerOptions httpServerOptions = new HttpServerOptions()
.setCompressionSupported(true); // 【优化】高并发服务器配置
.setTcpKeepAlive(true) // TCP Keep-Alive
.setTcpNoDelay(true) // 禁用Nagle算法
.setCompressionSupported(true) // 启用压缩
.setAcceptBacklog(50000) // 增加积压队列到50000
.setIdleTimeout(120) // 空闲超时120秒
.setTcpFastOpen(true) // 启用TCP Fast Open
.setTcpQuickAck(true) // 启用TCP Quick ACK
.setReuseAddress(true) // 允许地址重用
.setReusePort(true); // 允许端口重用
if (proxyConf.containsKey("ssl")) { if (proxyConf.containsKey("ssl")) {
JsonObject sslConfig = proxyConf.getJsonObject("ssl"); JsonObject sslConfig = proxyConf.getJsonObject("ssl");
@@ -169,18 +322,10 @@ public class ReverseProxyVerticle extends AbstractVerticle {
StaticHandler staticHandler; StaticHandler staticHandler;
if (staticConf.containsKey("root")) { if (staticConf.containsKey("root")) {
String configuredRoot = staticConf.getString("root"); staticHandler = StaticHandler.create(staticConf.getString("root"));
String resolvedRoot = resolveStaticRoot(configuredRoot);
if (resolvedRoot != null) {
staticHandler = StaticHandler.create(resolvedRoot);
} else {
LOGGER.warn("static root not found, fallback to configured path: {}", configuredRoot);
staticHandler = StaticHandler.create(configuredRoot);
}
} else { } else {
staticHandler = StaticHandler.create(); staticHandler = StaticHandler.create();
} }
if (staticConf.containsKey("directory-listing")) { if (staticConf.containsKey("directory-listing")) {
staticHandler.setDirectoryListing(staticConf.getBoolean("directory-listing")); staticHandler.setDirectoryListing(staticConf.getBoolean("directory-listing"));
} else if (staticConf.containsKey("index")) { } else if (staticConf.containsKey("index")) {
@@ -193,10 +338,9 @@ public class ReverseProxyVerticle extends AbstractVerticle {
* 处理Location配置 代理请求Location(和nginx类似?) * 处理Location配置 代理请求Location(和nginx类似?)
* *
* @param locationsConf location配置 * @param locationsConf location配置
* @param httpClient 客户端
* @param proxyRouter 代理路由 * @param proxyRouter 代理路由
*/ */
private void handleLocation(JsonArray locationsConf, HttpClient httpClient, Router proxyRouter) { private void handleLocation(JsonArray locationsConf, Router proxyRouter) {
locationsConf.stream().map(e -> (JsonObject) e).forEach(location -> { locationsConf.stream().map(e -> (JsonObject) e).forEach(location -> {
// 代理规则 // 代理规则
@@ -212,9 +356,33 @@ public class ReverseProxyVerticle extends AbstractVerticle {
String originPath = url.getPath(); String originPath = url.getPath();
LOGGER.info("path {}, originPath {}, to {}:{}", path, originPath, host, port); LOGGER.info("path {}, originPath {}, to {}:{}", path, originPath, host, port);
// 注意这里不能origin多个代理地址, 一个实例只能代理一个origin // 【优化】使用连接池获取HttpClient避免每个location都创建新连接
final HttpClient httpClient = getOrCreateHttpClient(host, port);
final HttpProxy httpProxy = HttpProxy.reverseProxy(httpClient); final HttpProxy httpProxy = HttpProxy.reverseProxy(httpClient);
httpProxy.origin(port, host); httpProxy.origin(port, host);
// proxy-set-headers 支持nginx 风格变量替换)
if (location.containsKey("proxy-set-headers")) {
final JsonObject headerConf = location.getJsonObject("proxy-set-headers");
httpProxy.addInterceptor(new ProxyInterceptor() {
@Override
public Future<ProxyResponse> handleProxyRequest(ProxyContext ctx) {
HttpServerRequest incoming = ctx.request().proxiedRequest();
String clientIp = resolveClientIp(incoming);
headerConf.forEach(entry -> {
Object val = entry.getValue();
if (val != null) {
String resolved = resolveHeaderVariable(val.toString(), incoming, clientIp);
if (resolved != null) {
ctx.request().putHeader(entry.getKey(), resolved);
}
}
});
return ProxyInterceptor.super.handleProxyRequest(ctx);
}
});
}
if (StringUtils.isEmpty(path)) { if (StringUtils.isEmpty(path)) {
return; return;
} }
@@ -223,6 +391,7 @@ public class ReverseProxyVerticle extends AbstractVerticle {
if (StringUtils.isEmpty(originPath) || path.equals(originPath)) { if (StringUtils.isEmpty(originPath) || path.equals(originPath)) {
Route route = path.startsWith("~") ? proxyRouter.routeWithRegex(path.substring(1)) Route route = path.startsWith("~") ? proxyRouter.routeWithRegex(path.substring(1))
: proxyRouter.route(path); : proxyRouter.route(path);
// 【优化】为代理处理器添加超时
route.handler(ProxyHandler.create(httpProxy)); route.handler(ProxyHandler.create(httpProxy));
} else { } else {
// 配置 /api/, / => 请求 /api/test 代理后 /test // 配置 /api/, / => 请求 /api/test 代理后 /test
@@ -241,6 +410,46 @@ public class ReverseProxyVerticle extends AbstractVerticle {
ctx.next(); ctx.next();
} }
}); });
// 计算唯一后缀,避免多个 location 冲突
// String uniqueKey = (host + ":" + port + "|" + path).replaceAll("[^a-zA-Z0-9:_|/]", "");
// String uniqueSuffix = Integer.toHexString(uniqueKey.hashCode());
//
//// 规格化 originPath
// //String originPath = url.getPath(); // 原值
// if (StringUtils.isBlank(originPath)) originPath = "/";
//
//// 处理 index.html 的情况:用于首页兜底,其它子路径仍按目录穿透
// String indexFile;
// if (originPath.endsWith(".html")) {
// indexFile = originPath; // 例如 /index.html
// originPath = "/"; // 目录穿透基准改为根
// } else {
// indexFile = null;
// }
//
//// 唯一内部挂载前缀
// final String originMount = REROUTE_PATH_PREFIX + uniqueSuffix + originPath;
//
//// 1) 目标挂载:所有被重写的请求最终到这里走 ProxyHandler
// proxyRouter.route(originMount + "*").handler(ProxyHandler.create(httpProxy));
//
//// 2) 从外部前缀 -> 内部挂载 的重写
// final String path0 = path;
// proxyRouter.route(path0 + "*").handler(ctx -> {
// String uri = ctx.request().uri();
// if (!uri.startsWith(path0)) { ctx.next(); return; }
//
// // 首页兜底:访问 /n2 或 /n2/ 时,重写到 index.html如果配置了
// if (indexFile != null && (uri.equals(path0) || uri.equals(path0.substring(0, path0.length()-1)))) {
// String rePath = originMount.endsWith("/") ? (originMount + indexFile.substring(1)) : (originMount + indexFile);
// ctx.reroute(rePath);
// return;
// }
//
// // 一般穿透:/n2/xxx -> originMount + xxx
// String rePath = uri.replaceFirst("^" + path0, originMount);
// ctx.reroute(rePath);
// });
} }
} catch (MalformedURLException e) { } catch (MalformedURLException e) {
@@ -249,77 +458,4 @@ public class ReverseProxyVerticle extends AbstractVerticle {
}); });
} }
/**
* 解析配置路径: 优先绝对路径, 否则尝试 user.dir 和 user.dir/..。
*/
private String resolveExistingPath(String path, boolean directory) {
if (StringUtils.isBlank(path)) {
return null;
}
File directFile = new File(path);
if (existsByType(directFile, directory)) {
return directFile.getAbsolutePath();
}
String userDir = System.getProperty("user.dir");
File inUserDir = new File(userDir, path);
if (existsByType(inUserDir, directory)) {
return inUserDir.getAbsolutePath();
}
File inParentDir = new File(new File(userDir).getParentFile(), path);
if (existsByType(inParentDir, directory)) {
return inParentDir.getAbsolutePath();
}
return null;
}
/**
* StaticHandler 只接受相对 web root不接受以 / 开头的绝对路径。
*/
private String resolveStaticRoot(String path) {
if (StringUtils.isBlank(path)) {
return null;
}
File directFile = new File(path);
if (existsByType(directFile, true)) {
return path;
}
String userDir = System.getProperty("user.dir");
File inUserDir = new File(userDir, path);
if (existsByType(inUserDir, true)) {
return relativizePath(new File(userDir), inUserDir);
}
File userDirFile = new File(userDir);
File parentDir = userDirFile.getParentFile();
File inParentDir = parentDir == null ? null : new File(parentDir, path);
if (existsByType(inParentDir, true)) {
return relativizePath(userDirFile, inParentDir);
}
return null;
}
private String relativizePath(File baseDir, File target) {
try {
Path basePath = baseDir.toPath().toAbsolutePath().normalize();
Path targetPath = target.toPath().toAbsolutePath().normalize();
return basePath.relativize(targetPath).toString().replace(File.separatorChar, '/');
} catch (IllegalArgumentException ignored) {
return target.getPath().replace(File.separatorChar, '/');
}
}
private boolean existsByType(File file, boolean directory) {
if (file == null || !file.exists()) {
return false;
}
return directory ? file.isDirectory() : file.isFile();
}
} }

View File

@@ -48,10 +48,19 @@ public class RouterVerticle extends AbstractVerticle {
} else { } else {
options = new HttpServerOptions(); options = new HttpServerOptions();
} }
// 绑定到 0.0.0.0 以允许外部访问
options.setHost("0.0.0.0");
options.setPort(port); options.setPort(port);
// 【优化】高并发服务器配置
options.setTcpKeepAlive(true) // TCP Keep-Alive
.setTcpNoDelay(true) // 禁用Nagle算法降低延迟
.setCompressionSupported(true) // 启用压缩
.setAcceptBacklog(50000) // 增加积压队列到50000防止高并发时连接被拒绝
.setIdleTimeout(120) // 空闲超时120秒
.setTcpFastOpen(true) // 启用TCP Fast Open
.setTcpQuickAck(true) // 启用TCP Quick ACK
.setReuseAddress(true) // 允许地址重用
.setReusePort(true); // 允许端口重用
server = vertx.createHttpServer(options); server = vertx.createHttpServer(options);
server.requestHandler(router).webSocketHandler(s->{}).listen() server.requestHandler(router).webSocketHandler(s->{}).listen()

View File

@@ -29,20 +29,23 @@ public class ServiceVerticle extends AbstractVerticle {
Reflections reflections = ReflectionUtil.getReflections(); Reflections reflections = ReflectionUtil.getReflections();
handlers = reflections.getTypesAnnotatedWith(Service.class); handlers = reflections.getTypesAnnotatedWith(Service.class);
} }
@Override @Override
public void start(Promise<Void> startPromise) { public void start(Promise<Void> startPromise) {
ServiceBinder binder = new ServiceBinder(vertx); ServiceBinder binder = new ServiceBinder(vertx);
if (null != handlers && handlers.size() > 0) { if (null != handlers && handlers.size() > 0) {
// handlers转为拼接类列表xxx,yyy,zzz
StringBuilder serviceNames = new StringBuilder();
handlers.forEach(asyncService -> { handlers.forEach(asyncService -> {
try { try {
serviceNames.append(asyncService.getName()).append("|");
BaseAsyncService asInstance = (BaseAsyncService) ReflectionUtil.newWithNoParam(asyncService); BaseAsyncService asInstance = (BaseAsyncService) ReflectionUtil.newWithNoParam(asyncService);
binder.setAddress(asInstance.getAddress()).register(asInstance.getAsyncInterfaceClass(), asInstance); binder.setAddress(asInstance.getAddress()).register(asInstance.getAsyncInterfaceClass(), asInstance);
} catch (Exception e) { } catch (Exception e) {
LOGGER.error(e.getMessage()); LOGGER.error("Failed to register service: {}", asyncService.getName(), e);
} }
}); });
LOGGER.info("registered async services -> id: {}", ID.getAndIncrement());
LOGGER.info("registered async services -> id: {}, name: {}", ID.getAndIncrement(), serviceNames.toString());
} }
startPromise.complete(); startPromise.complete();
} }

View File

@@ -0,0 +1,89 @@
package cn.qaiu.vx.core.verticle.conf;
import io.vertx.codegen.annotations.DataObject;
import io.vertx.codegen.json.annotations.JsonGen;
import io.vertx.core.json.JsonObject;
import io.vertx.core.net.ProxyOptions;
import java.util.UUID;
@DataObject
//@JsonGen(publicConverter = false)
public class HttpProxyConf {
public static final String DEFAULT_USERNAME = UUID.randomUUID().toString();
public static final String DEFAULT_PASSWORD = UUID.randomUUID().toString();
public static final Integer DEFAULT_PORT = 6432;
public static final Integer DEFAULT_TIMEOUT = 15000;
Integer timeout;
String username;
String password;
Integer port;
ProxyOptions preProxyOptions;
public HttpProxyConf() {
this.username = DEFAULT_USERNAME;
this.password = DEFAULT_PASSWORD;
this.timeout = DEFAULT_PORT;
this.timeout = DEFAULT_TIMEOUT;
this.preProxyOptions = new ProxyOptions();
}
public HttpProxyConf(JsonObject json) {
this();
}
public Integer getTimeout() {
return timeout;
}
public HttpProxyConf setTimeout(Integer timeout) {
this.timeout = timeout;
return this;
}
public String getUsername() {
return username;
}
public HttpProxyConf setUsername(String username) {
this.username = username;
return this;
}
public String getPassword() {
return password;
}
public HttpProxyConf setPassword(String password) {
this.password = password;
return this;
}
public Integer getPort() {
return port;
}
public HttpProxyConf setPort(Integer port) {
this.port = port;
return this;
}
public ProxyOptions getPreProxyOptions() {
return preProxyOptions;
}
public HttpProxyConf setPreProxyOptions(ProxyOptions preProxyOptions) {
this.preProxyOptions = preProxyOptions;
return this;
}
}

View File

@@ -1,2 +1,2 @@
app.version=${project.version} app.version=${project.version}
build=${maven.build.timestamp} build=${build.timestamp}

View File

@@ -0,0 +1,134 @@
package cn.qaiu.vx.core.test;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import org.junit.Assert;
import org.junit.Test;
/**
* 单元测试:验证 RouterHandlerFactory 关于 JsonObject/JsonArray 参数绑定的核心分支逻辑是否正确
* (不启动整个 Vert.x 服务器,直接用 Vert.x JsonObject/JsonArray API 模拟验证关键逻辑)
*/
public class JsonBodyBindingLogicTest {
// === 模拟 handlerMethod 中的 JSON body 绑定逻辑 ===
/**
* 模拟content-type = application/jsonbody 是 JsonObject
* 期望JsonObject 类型参数被正确绑定
*/
@Test
public void testJsonObjectBinding() {
String bodyStr = "{\"name\":\"test\",\"value\":123}";
// 模拟 ctx.body().asJsonObject()
JsonObject body = parseAsJsonObject(bodyStr);
Assert.assertNotNull("body 应能解析为 JsonObject", body);
// 模拟绑定逻辑中的类型判断
String targetType = JsonObject.class.getName();
boolean matched = JsonObject.class.getName().equals(targetType);
Assert.assertTrue("JsonObject 类型应命中绑定分支", matched);
// 模拟结果
Object bound = body; // parameterValueList.put(k, body)
Assert.assertNotNull("JsonObject 参数应被绑定非null", bound);
Assert.assertEquals("name字段应为test", "test", ((JsonObject) bound).getString("name"));
Assert.assertEquals("value字段应为123", 123, (int) ((JsonObject) bound).getInteger("value"));
System.out.println("[PASS] testJsonObjectBinding: JsonObject 绑定成功 -> " + bound);
}
/**
* 模拟content-type = application/jsonbody 是 JsonArray
* 期望JsonArray 类型参数被正确绑定
*/
@Test
public void testJsonArrayBinding() {
String bodyStr = "[1,2,3]";
// body 解析为 JsonObject 应返回 null
JsonObject bodyAsObj = parseAsJsonObject(bodyStr);
Assert.assertNull("JsonArray body 解析为 JsonObject 应为 null", bodyAsObj);
// 进入 else 分支,解析为 JsonArray
JsonArray bodyArr = parseAsJsonArray(bodyStr);
Assert.assertNotNull("body 应能解析为 JsonArray", bodyArr);
String targetType = JsonArray.class.getName();
boolean matched = JsonArray.class.getName().equals(targetType);
Assert.assertTrue("JsonArray 类型应命中绑定分支", matched);
Object bound = bodyArr;
Assert.assertNotNull("JsonArray 参数应被绑定非null", bound);
Assert.assertEquals("数组大小应为3", 3, ((JsonArray) bound).size());
System.out.println("[PASS] testJsonArrayBinding: JsonArray 绑定成功, size=" + ((JsonArray) bound).size());
}
/**
* 验证旧代码的 bug条件 ctx.body().asJsonObject() != null 会把 JsonArray body 排除在外
* 新代码只判断 content-type在 body==null 时才进 else 分支处理 JsonArray
*/
@Test
public void testOldConditionBug() {
String jsonArrayBody = "[1,2,3]";
// 旧代码条件content-type==json && asJsonObject()!=null
// 对于 JsonArray bodyasJsonObject() 返回 null整个 if 跳过
JsonObject wrongParsed = parseAsJsonObject(jsonArrayBody);
boolean oldConditionPassed = wrongParsed != null; // 旧代码的第二个条件
Assert.assertFalse("旧代码 bug: JsonArray body 会导致 asJsonObject()==null整个分支跳过", oldConditionPassed);
// 新代码:先进 ifbody==null 再走 else 解析 JsonArray
boolean newConditionFirst = true; // content-type 匹配
JsonObject newBody = parseAsJsonObject(jsonArrayBody);
boolean newBodyIsNull = newBody == null; // null -> 进 else
Assert.assertTrue("新代码: body 解析为 null 时应走 else 分支解析 JsonArray", newBodyIsNull);
JsonArray newArr = parseAsJsonArray(jsonArrayBody);
Assert.assertNotNull("新代码: else 分支正确解析出 JsonArray", newArr);
System.out.println("[PASS] testOldConditionBug: 修复验证通过,新代码正确处理 JsonArray body");
}
/**
* 验证JsonObject 参数旧代码没有绑定分支(只处理实体类)
*/
@Test
public void testOldMissingJsonObjectBranch() {
String bodyStr = "{\"key\":\"value\"}";
JsonObject body = parseAsJsonObject(bodyStr);
// 旧代码只调用 matchRegList(entityPackagesReg, typeName)
// 对于 io.vertx.core.json.JsonObject该方法返回 false不会被绑定
String typeName = JsonObject.class.getName(); // "io.vertx.core.json.JsonObject"
// entityPackagesReg 一般是 "cn.qaiu.*" 这类,不会匹配 io.vertx
boolean oldWouldBind = typeName.startsWith("cn.qaiu"); // 模拟旧代码逻辑
Assert.assertFalse("旧代码 bug: JsonObject 参数不会被绑定", oldWouldBind);
// 新代码:增加了 JsonObject 类型判断
boolean newWouldBind = JsonObject.class.getName().equals(typeName);
Assert.assertTrue("新代码: JsonObject 参数应能被绑定", newWouldBind);
System.out.println("[PASS] testOldMissingJsonObjectBranch: 修复验证通过");
}
// ===== 辅助方法:模拟 Vert.x RequestBody 的 asJsonObject/asJsonArray 行为 =====
private JsonObject parseAsJsonObject(String str) {
try {
return new JsonObject(str);
} catch (Exception e) {
return null;
}
}
private JsonArray parseAsJsonArray(String str) {
try {
return new JsonArray(str);
} catch (Exception e) {
return null;
}
}
}

View File

@@ -0,0 +1,125 @@
package cn.qaiu.vx.core.test;
import cn.qaiu.vx.core.util.VertxHolder;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* 集成测试: 验证 RouterHandlerFactory 对 JsonObject/JsonArray 参数绑定逻辑是否正确
*
* 运行方式: mvn test-compile -pl core && java -cp "core/target/test-classes:core/target/classes:..." \
* cn.qaiu.vx.core.test.RouterHandlerBindingTest
*
* 或直接在 IDE 中运行 main 方法。
*/
public class RouterHandlerBindingTest {
static final int TEST_PORT = 18989;
public static void main(String[] args) throws Exception {
System.out.println("=== RouterHandler JsonObject/JsonArray 绑定测试 ===\n");
// 1. 先初始化 Vert.x 与 VertxHolder ——必须在加载 RouterHandlerFactory 之前
Vertx vertx = Vertx.vertx();
VertxHolder.init(vertx);
// 2. 向 SharedData 注入最小化配置
// baseLocations 指向测试包,使 Reflections 只扫描 TestJsonHandler
vertx.sharedData().getLocalMap("local").put("customConfig", new JsonObject()
.put("baseLocations", "cn.qaiu.vx.core.test")
.put("routeTimeOut", 30000)
.put("entityPackagesReg", new JsonArray()));
// ReverseProxyVerticle.<clinit> 需要 globalConfig.proxyConf非空字符串即可
vertx.sharedData().getLocalMap("local").put("globalConfig", new JsonObject()
.put("proxyConf", "proxy.yml"));
// 3. 创建 Router此时才触发 BaseHttpApi.reflections 静态字段初始化)
// 用反射延迟加载,确保上面的 SharedData 已就绪
cn.qaiu.vx.core.handlerfactory.RouterHandlerFactory factory =
new cn.qaiu.vx.core.handlerfactory.RouterHandlerFactory("api");
io.vertx.ext.web.Router router = factory.createRouter();
// 4. 启动 HTTP 服务器
CountDownLatch latch = new CountDownLatch(1);
vertx.createHttpServer()
.requestHandler(router)
.listen(TEST_PORT, res -> {
if (res.succeeded()) {
System.out.println("✔ 测试服务器启动成功 port=" + TEST_PORT);
} else {
System.err.println("✘ 服务器启动失败: " + res.cause().getMessage());
}
latch.countDown();
});
if (!latch.await(5, TimeUnit.SECONDS)) {
System.err.println("服务器启动超时");
vertx.close();
System.exit(1);
}
Thread.sleep(100); // 等 Vert.x 就绪
// 5. 执行测试
boolean allPassed = true;
allPassed &= testJsonObject();
allPassed &= testJsonArray();
// 6. 关闭
CountDownLatch closeLatch = new CountDownLatch(1);
vertx.close(v -> closeLatch.countDown());
closeLatch.await(3, TimeUnit.SECONDS);
System.out.println("\n" + (allPassed ? "✅ 全部测试通过!" : "❌ 存在测试失败!"));
System.exit(allPassed ? 0 : 1);
}
// ---------- 子测试 ----------
private static boolean testJsonObject() throws Exception {
String bodyStr = "{\"name\":\"test\",\"value\":123}";
String respBody = post("/api/test/json-object", bodyStr);
System.out.println("[JsonObject] 响应: " + respBody);
JsonObject result = new JsonObject(respBody);
JsonObject data = result.getJsonObject("data");
boolean bound = data != null && Boolean.TRUE.equals(data.getBoolean("bound"));
System.out.println("[JsonObject] " + (bound
? "PASS ✅ body 正确绑定为 JsonObject"
: "FAIL ❌ body 未绑定 (null)"));
return bound;
}
private static boolean testJsonArray() throws Exception {
String bodyStr = "[1,2,3]";
String respBody = post("/api/test/json-array", bodyStr);
System.out.println("[JsonArray] 响应: " + respBody);
JsonObject result = new JsonObject(respBody);
JsonObject data = result.getJsonObject("data");
boolean bound = data != null
&& Boolean.TRUE.equals(data.getBoolean("bound"))
&& Integer.valueOf(3).equals(data.getInteger("size"));
System.out.println("[JsonArray] " + (bound
? "PASS ✅ body 正确绑定为 JsonArray, size=3"
: "FAIL ❌ body 未绑定 或 size 不对"));
return bound;
}
private static String post(String path, String body) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:" + TEST_PORT + path))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
return client.send(req, HttpResponse.BodyHandlers.ofString()).body();
}
}

View File

@@ -0,0 +1,36 @@
package cn.qaiu.vx.core.test;
import cn.qaiu.vx.core.annotaions.RouteHandler;
import cn.qaiu.vx.core.annotaions.RouteMapping;
import cn.qaiu.vx.core.enums.MIMEType;
import cn.qaiu.vx.core.enums.RouteMethod;
import cn.qaiu.vx.core.model.JsonResult;
import io.vertx.core.Future;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
/**
* 用于测试 RouterHandlerFactory 对 JsonObject/JsonArray 参数绑定的测试 Handler
*/
@RouteHandler("test")
public class TestJsonHandler {
/** POST /api/test/json-object Body: {"name":"test","value":123} */
@RouteMapping(value = "/json-object", method = RouteMethod.POST, requestMIMEType = MIMEType.APPLICATION_JSON)
public Future<JsonResult> testJsonObject(JsonObject body) {
// 只返回是否绑定成功及已知字段值,不嵌套原始 body 避免 toJsonObject() 循环
boolean bound = body != null;
String nameVal = bound ? body.getString("name", "") : "";
return Future.succeededFuture(JsonResult.data(new io.vertx.core.json.JsonObject()
.put("bound", bound)
.put("name", nameVal)));
}
/** POST /api/test/json-array Body: [1,2,3] */
@RouteMapping(value = "/json-array", method = RouteMethod.POST, requestMIMEType = MIMEType.APPLICATION_JSON)
public Future<JsonResult> testJsonArray(JsonArray body) {
return Future.succeededFuture(JsonResult.data(new io.vertx.core.json.JsonObject()
.put("bound", body != null)
.put("size", body != null ? body.size() : -1)));
}
}

View File

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

View File

@@ -5,6 +5,15 @@ const axiosInstance = axios.create({
withCredentials: true // 重要允许跨域请求携带cookie withCredentials: true // 重要允许跨域请求携带cookie
}); });
// 请求拦截器将存储的Token添加到Authorization请求头
axiosInstance.interceptors.request.use(config => {
const token = localStorage.getItem('playground_token');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
});
/** /**
* 演练场API服务 * 演练场API服务
*/ */
@@ -30,7 +39,12 @@ export const playgroundApi = {
async login(password) { async login(password) {
try { try {
const response = await axiosInstance.post('/v2/playground/login', { password }); const response = await axiosInstance.post('/v2/playground/login', { password });
return response.data; const data = response.data;
// 登录成功时从响应中提取并保存Token
if ((data.code === 200 || data.success) && data.data?.token) {
localStorage.setItem('playground_token', data.data.token);
}
return data;
} catch (error) { } catch (error) {
throw new Error(error.response?.data?.error || error.message || '登录失败'); throw new Error(error.response?.data?.error || error.message || '登录失败');
} }

View File

@@ -1146,10 +1146,11 @@ function parseById(shareLinkInfo, http, logger) {
const isAuthed = res.data.authed || res.data.public; const isAuthed = res.data.authed || res.data.public;
authed.value = isAuthed; authed.value = isAuthed;
// 如果后端session已失效清除localStorage // 如果后端认证已失效清除localStorage中的认证信息
if (!isAuthed && savedAuth === 'true') { if (!isAuthed && savedAuth === 'true') {
localStorage.removeItem('playground_authed'); localStorage.removeItem('playground_authed');
localStorage.removeItem('playground_auth_time'); localStorage.removeItem('playground_auth_time');
localStorage.removeItem('playground_token');
} }
return isAuthed; return isAuthed;

View File

@@ -4,6 +4,10 @@ import io.vertx.core.json.JsonObject;
import lombok.Data; import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.security.SecureRandom;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/** /**
* JS演练场配置 * JS演练场配置
* *
@@ -13,11 +17,21 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
public class PlaygroundConfig { public class PlaygroundConfig {
/** Token有效期24小时 */
private static final long TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000L;
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
/** /**
* 单例实例 * 单例实例
*/ */
private static PlaygroundConfig instance; private static PlaygroundConfig instance;
/**
* 已颁发的认证Token及其创建时间
*/
private final Map<String, Long> validTokens = new ConcurrentHashMap<>();
/** /**
* 是否启用演练场 * 是否启用演练场
* 默认false不启用 * 默认false不启用
@@ -42,6 +56,39 @@ public class PlaygroundConfig {
private PlaygroundConfig() { private PlaygroundConfig() {
} }
/**
* 生成并存储一个新的认证Token同时清理过期Token
*/
public String generateToken() {
// 清理过期Token防止Map无限增长
long now = System.currentTimeMillis();
validTokens.entrySet().removeIf(e -> now - e.getValue() > TOKEN_EXPIRY_MS);
// 使用SecureRandom生成32字节的密码学安全Token
byte[] bytes = new byte[32];
SECURE_RANDOM.nextBytes(bytes);
StringBuilder sb = new StringBuilder(64);
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
String token = sb.toString();
validTokens.put(token, now);
return token;
}
/**
* 校验Token是否合法且未过期
*/
public boolean validateToken(String token) {
if (token == null || token.isEmpty()) return false;
Long createdAt = validTokens.get(token);
if (createdAt == null) return false;
if (System.currentTimeMillis() - createdAt > TOKEN_EXPIRY_MS) {
validTokens.remove(token);
return false;
}
return true;
}
/** /**
* 获取单例实例 * 获取单例实例
*/ */

View File

@@ -21,7 +21,6 @@ import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse; import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.json.JsonObject; import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.Session;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -47,7 +46,6 @@ public class PlaygroundApi {
private static final int MAX_PARSER_COUNT = 100; private static final int MAX_PARSER_COUNT = 100;
private static final int MAX_CODE_LENGTH = 128 * 1024; // 128KB 代码长度限制 private static final int MAX_CODE_LENGTH = 128 * 1024; // 128KB 代码长度限制
private static final String SESSION_AUTH_KEY = "playgroundAuthed";
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class); private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
/** /**
@@ -74,14 +72,14 @@ public class PlaygroundApi {
return true; return true;
} }
// 否则检查Session中的认证状态 // 检查 Authorization: Bearer <token> 请求头
Session session = ctx.session(); String authHeader = ctx.request().getHeader("Authorization");
if (session == null) { if (authHeader != null && authHeader.startsWith("Bearer ") && authHeader.length() > 7) {
return false; String token = authHeader.substring(7).trim();
return config.validateToken(token);
} }
Boolean authed = session.get(SESSION_AUTH_KEY); return false;
return authed != null && authed;
} }
/** /**
@@ -116,12 +114,8 @@ public class PlaygroundApi {
try { try {
PlaygroundConfig config = PlaygroundConfig.getInstance(); PlaygroundConfig config = PlaygroundConfig.getInstance();
// 如果是公开模式,直接成功 // 如果是公开模式,直接成功不需要token
if (config.isPublic()) { if (config.isPublic()) {
Session session = ctx.session();
if (session != null) {
session.put(SESSION_AUTH_KEY, true);
}
promise.complete(JsonResult.success("公开模式,无需密码").toJsonObject()); promise.complete(JsonResult.success("公开模式,无需密码").toJsonObject());
return promise.future(); return promise.future();
} }
@@ -137,11 +131,9 @@ public class PlaygroundApi {
// 验证密码 // 验证密码
if (config.getPassword().equals(password)) { if (config.getPassword().equals(password)) {
Session session = ctx.session(); String token = config.generateToken();
if (session != null) { JsonObject tokenData = new JsonObject().put("token", token);
session.put(SESSION_AUTH_KEY, true); promise.complete(JsonResult.data(tokenData).toJsonObject());
}
promise.complete(JsonResult.success("登录成功").toJsonObject());
} else { } else {
promise.complete(JsonResult.error("密码错误").toJsonObject()); promise.complete(JsonResult.error("密码错误").toJsonObject());
} }

View File

@@ -0,0 +1,54 @@
# 反向代理
server-name: Vert.x-proxy-server(v4.1.2)
proxy:
- listen: 16401
# 404的路径
page404: webroot/nfd-front/index.html
static:
path: /
add-headers:
x-token: ABC
root: webroot/nfd-front/
# index: index.html
# ~开头(没有空格)表示正则匹配否则为前缀匹配, 当origin带子路径时进行路由重写,
# 1.origin代理地址端口后有目录(包括 / ),转发后地址:代理地址+访问URL目录部分去除location匹配目录
# 2.origin代理地址端口后无任何转发后地址代理地址+访问URL目录部
location:
- path: ~^/(json/|v2/|d/|parser|ye/|lz/|cow/|ec/|fj/|fc/|le/|qq/|ws/|iz/|ce/).*
origin: 127.0.0.1:16400
# json/parser -> xxx/parser
# - path: /json/
# origin: 127.0.0.1:16400/
- path: /n1/
origin: 127.0.0.1:16400/v2/
# # SSL HTTPS配置
ssl:
enable: false
# 强制https 暂不支持
#ssl_force: true
# SSL 协议版本
ssl_protocols: TLSv1.2
# 证书
ssl_certificate: ssl/server.pem
# 私钥
ssl_certificate_key: ssl/privkey.key
# 加密套件 ssl_ciphers 暂不支持
# ssl_ciphers: AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
# - listen: 8086
# static:
# path: /t2/
# root: webroot/test/
# index: sockTest.html
# location:
# - path: /real/
# origin: 127.0.0.1:8088
# sock:
# - path: /real/
# origin: 127.0.0.1:8088