Compare commits

...

4 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] f3d95fa7c0 feat: only generate smart link after parse action; set directory share as smart link after parseDirectory 2026-08-01 02:43:23 +00:00
copilot-swe-agent[bot] 50e75168e5 fix: asJson logs details but only surfaces clean error to frontend 2026-08-01 02:26:06 +00:00
copilot-swe-agent[bot] 4bc880a41b fix: use tryParseJson in CeTool version detection to avoid premature promise failure on non-JSON responses 2026-08-01 02:12:01 +00:00
qaiu 7299fd8762 Merge pull request #207 from qaiu/cursor/ghsa-997r-ssrf-verify-2b8b
fix(security): GHSA-997r SSRF verification + residual hardening
2026-07-26 21:10:39 +08:00
3 changed files with 47 additions and 36 deletions
@@ -377,20 +377,39 @@ public abstract class PanBase implements IPanTool, Closeable {
}
} catch (Exception e) {
// 上游响应体可能来自内网探测目标或非JSON内容,仅写日志,避免经 HTTP 500 回传给调用方
if ("gzip".equalsIgnoreCase(contentEncoding)) {
// gzip解压失败,记录错误
log.error("响应gzip解压或JSON解析失败: {}", e.getMessage());
fail("响应gzip解压或JSON解析失败: {}", e.getMessage());
log.error("上游响应gzip解压或JSON解析失败: {}", e.getMessage());
} else {
// 上游响应体可能来自内网探测目标,仅写日志,避免经 HTTP 500 回传给调用方
String bodyPreview = responseBodyPreview(res);
log.error("解析失败: json格式异常: {}", bodyPreview);
fail("解析失败: json格式异常");
log.error("上游响应格式异常(非JSON): {}", responseBodyPreview(res));
}
fail("上游响应格式异常");
return JsonObject.of();
}
}
/**
* 尝试将响应体解析为 JsonObject,失败时静默返回 null(不调用 fail())。
* 适用于版本探测等场景:响应可能是 HTML,此时不应立即终止解析流程。
*
* @param res HttpResponse
* @return JsonObject,若响应体不是合法 JSON 则返回 null
*/
protected JsonObject tryParseJson(HttpResponse<?> res) {
String contentEncoding = res.getHeader("Content-Encoding");
try {
if ("gzip".equalsIgnoreCase(contentEncoding)) {
String decompressed = decompressGzip((Buffer) res.body());
return new JsonObject(decompressed);
} else {
return res.bodyAsJsonObject();
}
} catch (Exception e) {
log.debug("响应体不是合法JSON,跳过: {}", e.getMessage());
return null;
}
}
/**
* body To text的封装, 会自动处理异常, 会自动解压gzip
* @param res HttpResponse
@@ -81,18 +81,15 @@ public class CeTool extends PanBase {
// 禁止跟随重定向:assertPublicHost 只校验初始 host,自动 30x 会绕过 SSRF 防护
clientNoRedirects.getAbs(pingUrlV4).send().onSuccess(res -> {
if (res.statusCode() == 200) {
try {
JsonObject json = asJson(res);
// v4 ping 成功且返回有效JSON,使用 Ce4Tool
if (json != null && !json.isEmpty()) {
log.debug("检测到Cloudreve 4.x (通过v4 ping)");
delegateToCe4Tool();
return;
}
} catch (Exception e) {
// JSON解析失败,继续尝试 v3
log.debug("v4 ping返回非JSON响应,尝试v3");
// 使用 tryParseJson 而非 asJson,避免非JSON响应(如HTML)提前终止整个解析流程
JsonObject json = tryParseJson(res);
// v4 ping 成功且返回有效JSON,使用 Ce4Tool
if (json != null && !json.isEmpty()) {
log.debug("检测到Cloudreve 4.x (通过v4 ping)");
delegateToCe4Tool();
return;
}
log.debug("v4 ping返回非JSON响应,尝试v3");
}
// v4 ping失败或返回非JSON,尝试 v3
tryV3Ping(baseUrl, key, pwd);
@@ -111,18 +108,15 @@ public class CeTool extends PanBase {
clientNoRedirects.getAbs(pingUrlV3).send().onSuccess(res -> {
if (res.statusCode() == 200) {
try {
JsonObject json = asJson(res);
// v3 ping 成功且返回有效JSON,进一步验证是否为 v3
if (json != null && !json.isEmpty()) {
// 尝试调用 v3 share API 来确认
verifyV3AndParse(baseUrl, key, pwd);
return;
}
} catch (Exception e) {
// JSON解析失败,不是Cloudreve盘
log.debug("v3 ping返回非JSON响应,不是Cloudreve盘");
// 使用 tryParseJson 而非 asJson,避免非JSON响应(如HTML)提前终止整个解析流程
JsonObject json = tryParseJson(res);
// v3 ping 成功且返回有效JSON,进一步验证是否为 v3
if (json != null && !json.isEmpty()) {
// 尝试调用 v3 share API 来确认
verifyV3AndParse(baseUrl, key, pwd);
return;
}
log.debug("v3 ping返回非JSON响应,不是Cloudreve盘");
}
// v3 ping失败,不是Cloudreve盘
log.debug("v3 ping失败,尝试下一个解析器");
+5 -7
View File
@@ -1156,7 +1156,6 @@ export default {
this.password = shortInfo.pwd
}
this.$message.success(`已识别短格式并自动转换,网盘类型: ${shortInfo.name}`)
this.updateDirectLink()
return
}
@@ -1169,7 +1168,6 @@ export default {
this.password = pwd
}
this.$message.success(`已从文本中识别到 ${linkInfo.name} 分享链接`)
this.updateDirectLink()
}
},
@@ -1184,6 +1182,7 @@ export default {
clearResults() {
this.parseResult = {}
this.downloadUrl = null
this.directLink = ''
this.markdownText = ''
this.showQRCode = false
this.statisticsData = {}
@@ -1327,8 +1326,10 @@ export default {
const directoryResult = await this.callAPI('/v2/getFileList', params)
this.directoryData = directoryResult.data || []
this.showDirectoryTree = true
// 自动赋值分享链接
this.showListLink = `${this.baseUrl}/showList?url=${encodeURIComponent(this.link)}`
// 目录解析成功后,将目录落地页作为智能直链
const listUrl = `${this.baseUrl}/showList?url=${encodeURIComponent(this.link)}`
this.showListLink = listUrl
this.directLink = listUrl
this.$message.success(`目录解析成功!共找到 ${this.directoryData.length} 个文件/文件夹`)
} catch (error) {
@@ -1427,7 +1428,6 @@ export default {
if (shortInfo.link !== this.link || shortInfo.pwd !== this.password) {
this.password = shortInfo.pwd
this.link = shortInfo.link
this.updateDirectLink()
if (!this.hasClipboardSuccessTip) {
this.$message.success(`自动识别分享成功, 网盘类型: ${shortInfo.name}; 分享URL ${this.link}; 分享密码: ${this.password || '空'}`)
this.hasClipboardSuccessTip = true
@@ -1446,8 +1446,6 @@ export default {
if (linkInfo.link !== this.link || pwd !== this.password) {
this.password = pwd
this.link = linkInfo.link
// 更新智能直链(包含认证参数)
this.updateDirectLink()
// 聚焦期间只提示一次
if (!this.hasClipboardSuccessTip) {
this.$message.success(`自动识别分享成功, 网盘类型: ${linkInfo.name}; 分享URL ${this.link}; 分享密码: ${this.password || '空'}`)