fix(parser): harden runtime resource handling

This commit is contained in:
yukaidi
2026-06-10 21:19:56 +08:00
parent 46c6827eda
commit b6b7f0d8b7
14 changed files with 1498 additions and 455 deletions
@@ -17,16 +17,35 @@ import java.util.zip.InflaterInputStream;
public class HttpResponseHelper {
static Logger LOGGER = LoggerFactory.getLogger(HttpResponseHelper.class);
private static final int MAX_RESPONSE_BODY_BYTES = 8 * 1024 * 1024;
private static final int MAX_DECOMPRESSED_CHARS = 16 * 1024 * 1024;
// -------------------- 公共方法 --------------------
public static String asText(HttpResponse<?> res) {
String encoding = res.getHeader(HttpHeaders.CONTENT_ENCODING.toString());
try {
Buffer body = toBuffer(res);
return asText(body, encoding);
} catch (IllegalArgumentException | UnsupportedOperationException e) {
throw e;
} catch (Exception e) {
LOGGER.error("asText: {}", e.getMessage(), e);
return null;
}
}
public static String asText(Buffer body, String encoding) {
try {
if (body == null) {
return "";
}
ensureBodyLimit(body);
if (encoding == null || "identity".equalsIgnoreCase(encoding)) {
return body.toString(StandardCharsets.UTF_8);
}
return decompress(body, encoding);
} catch (IllegalArgumentException | UnsupportedOperationException e) {
throw e;
} catch (Exception e) {
LOGGER.error("asText: {}", e.getMessage(), e);
return null;
@@ -36,6 +55,29 @@ public class HttpResponseHelper {
public static JsonObject asJson(HttpResponse<?> res) {
try {
String text = asText(res);
return parseJsonText(text);
} catch (IllegalArgumentException | UnsupportedOperationException e) {
throw e;
} catch (Exception e) {
LOGGER.error("asJson: {}", e.getMessage(), e);
return JsonObject.of();
}
}
public static JsonObject asJson(Buffer body, String encoding) {
try {
String text = asText(body, encoding);
return parseJsonText(text);
} catch (IllegalArgumentException | UnsupportedOperationException e) {
throw e;
} catch (Exception e) {
LOGGER.error("asJson: {}", e.getMessage(), e);
return JsonObject.of();
}
}
private static JsonObject parseJsonText(String text) {
try {
if (text != null) {
return new JsonObject(text);
} else {
@@ -53,13 +95,26 @@ public class HttpResponseHelper {
return res.body() instanceof Buffer ? (Buffer) res.body() : Buffer.buffer(res.bodyAsString());
}
private static void ensureBodyLimit(Buffer body) {
if (body != null && body.length() > MAX_RESPONSE_BODY_BYTES) {
throw new IllegalArgumentException("响应体过大: " + body.length() + " bytes");
}
}
private static void writeLimited(StringWriter writer, char[] buffer, int len) throws IOException {
if (writer.getBuffer().length() + len > MAX_DECOMPRESSED_CHARS) {
throw new IOException("解压后响应体过大");
}
writer.write(buffer, 0, len);
}
// -------------------- 通用解压分发 --------------------
private static String decompress(Buffer compressed, String encoding) throws IOException {
return switch (encoding.toLowerCase()) {
case "gzip" -> decompressGzip(compressed);
case "deflate" -> decompressDeflate(compressed);
case "br" -> decompressBrotli(compressed);
case "zstd" -> compressed.toString(StandardCharsets.UTF_8); // 暂时返回原始内容
case "zstd" -> throw new UnsupportedOperationException("不支持的 Content-Encoding: zstd");
default -> throw new UnsupportedOperationException("不支持的 Content-Encoding: " + encoding);
};
}
@@ -74,7 +129,7 @@ public class HttpResponseHelper {
char[] buffer = new char[4096];
int n;
while ((n = isr.read(buffer)) != -1) {
writer.write(buffer, 0, n);
writeLimited(writer, buffer, n);
}
return writer.toString();
}
@@ -99,7 +154,7 @@ public class HttpResponseHelper {
char[] buffer = new char[4096];
int n;
while ((n = isr.read(buffer)) != -1) {
writer.write(buffer, 0, n);
writeLimited(writer, buffer, n);
}
return writer.toString();
}
@@ -115,7 +170,7 @@ public class HttpResponseHelper {
char[] buffer = new char[4096];
int n;
while ((n = isr.read(buffer)) != -1) {
writer.write(buffer, 0, n);
writeLimited(writer, buffer, n);
}
return writer.toString();
}
@@ -1,7 +1,7 @@
package cn.qaiu.util;
import cn.qaiu.WebClientVertxInit;
import io.vertx.core.MultiMap;
import io.vertx.core.Vertx;
import io.vertx.core.http.impl.headers.HeadersMultiMap;
import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.client.WebClientSession;
@@ -19,6 +19,10 @@ import java.util.List;
public class IpExtractor {
private static final Logger log = LoggerFactory.getLogger(IpExtractor.class);
// 使用共享的 Vertx 实例,避免每次调用创建新实例导致资源泄漏
private static final WebClient SHARED_CLIENT = WebClient.create(WebClientVertxInit.get());
private static final WebClientSession SHARED_SESSION = WebClientSession.create(SHARED_CLIENT);
public static void main(String[] args) throws InterruptedException {
@@ -43,11 +47,9 @@ public class IpExtractor {
headers.add("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36");
headers.add("Content-Type", "application/x-www-form-urlencoded");
WebClient client = WebClient.create(Vertx.vertx());
WebClientSession webClientSession = WebClientSession.create(client);
webClientSession.getAbs("https://ip.ihuan.me").putHeaders(headers).send().onSuccess(res->{
SHARED_SESSION.getAbs("https://ip.ihuan.me").putHeaders(headers).send().onSuccess(res->{
log.debug("response: {}", res.toString());
webClientSession.getAbs("https://ip.ihuan.me").putHeaders(headers).send().onSuccess(res2->{
SHARED_SESSION.getAbs("https://ip.ihuan.me").putHeaders(headers).send().onSuccess(res2->{
log.debug("response2: {}", res2.toString());
});
@@ -46,34 +46,62 @@ public class JsExecUtils {
/**
* 调用执行蓝奏云js文件(每次动态JS代码,无法复用引擎)
* 注意:使用后清理引擎引用,帮助 GC 回收 Nashorn 引擎内部资源
*/
public static ScriptObjectMirror executeDynamicJs(String jsText, String funName) throws ScriptException,
NoSuchMethodException {
ScriptEngine engine = ENGINE_MANAGER.getEngineByName("JavaScript"); // 得到脚本引擎
engine.eval(JsContent.lz + "\n" + jsText);
Invocable inv = (Invocable) engine;
//调用js中的函数
if (StringUtils.isNotEmpty(funName)) {
inv.invokeFunction(funName);
try {
engine.eval(JsContent.lz + "\n" + jsText);
Invocable inv = (Invocable) engine;
//调用js中的函数
if (StringUtils.isNotEmpty(funName)) {
inv.invokeFunction(funName);
}
return (ScriptObjectMirror) engine.get("signObj");
} finally {
// 清理引擎持有的引用,帮助 GC 回收
clearEngineBindings(engine);
}
return (ScriptObjectMirror) engine.get("signObj");
}
/**
* 调用执行js文件(使用缓存的 ScriptEngineManager 创建新引擎实例)
* 注意:使用后清理引擎引用,帮助 GC 回收 Nashorn 引擎内部资源
*/
public static Object executeOtherJs(String jsText, String funName, Object ... args) throws ScriptException,
NoSuchMethodException {
ScriptEngine engine = ENGINE_MANAGER.getEngineByName("JavaScript"); // 得到脚本引擎
engine.eval(jsText);
Invocable inv = (Invocable) engine;
//调用js中的函数
if (StringUtils.isNotEmpty(funName)) {
return inv.invokeFunction(funName, args);
try {
engine.eval(jsText);
Invocable inv = (Invocable) engine;
//调用js中的函数
if (StringUtils.isNotEmpty(funName)) {
return inv.invokeFunction(funName, args);
}
throw new ScriptException("funName is null");
} finally {
// 清理引擎持有的引用,帮助 GC 回收
clearEngineBindings(engine);
}
}
/**
* 清理 ScriptEngine 的 bindings,帮助 GC 回收 Nashorn 引擎资源
*/
private static void clearEngineBindings(ScriptEngine engine) {
try {
if (engine != null) {
// 清理全局 bindings
var bindings = engine.getBindings(javax.script.ScriptContext.ENGINE_SCOPE);
if (bindings != null) {
bindings.clear();
}
}
} catch (Exception ignored) {
// 清理失败不影响主流程
}
throw new ScriptException("funName is null");
}
public static String getKwSign(String s, String pwd) {
@@ -1,8 +1,8 @@
package cn.qaiu.util;
import cn.qaiu.WebClientVertxInit;
import io.vertx.core.AsyncResult;
import io.vertx.core.MultiMap;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.impl.headers.HeadersMultiMap;
import io.vertx.ext.web.client.HttpResponse;
@@ -47,15 +47,13 @@ public class ReqIpUtil {
}
Vertx vertx = Vertx.vertx();
WebClient webClient = WebClient.create(vertx);
// 发送GET请求
WebClientSession webClientSession = WebClientSession.create(webClient);
// 使用共享的 Vertx 实例和 WebClient,避免每次创建新实例导致资源泄漏
private static final WebClient WEB_CLIENT = WebClient.create(WebClientVertxInit.get());
private static final WebClientSession WEB_CLIENT_SESSION = WebClientSession.create(WEB_CLIENT);
public void exec() {
webClientSession.getAbs(BASE_URL)
WEB_CLIENT_SESSION.getAbs(BASE_URL)
.putHeaders(headers) // 将请求头Map添加到请求中
.send(this::next);
}
@@ -67,7 +65,7 @@ public class ReqIpUtil {
HttpResponse<Buffer> res = response.result();
log.debug("Received response with status code {}", res.statusCode());
log.debug("Body: {}", res.body());
webClientSession.getAbs(BASE_URL_TEMPLATE).setTemplateParam("path", PATH1)
WEB_CLIENT_SESSION.getAbs(BASE_URL_TEMPLATE).setTemplateParam("path", PATH1)
.putHeaders(headers) // 将请求头Map添加到请求中
.send(response2 -> {
log.debug("response2: {}", response2.result().bodyAsString());