fix(parser): harden built-in pan parsers

This commit is contained in:
yukaidi
2026-06-10 21:20:40 +08:00
parent b6b7f0d8b7
commit 0103841fb5
18 changed files with 1199 additions and 416 deletions

View File

@@ -247,14 +247,14 @@ public enum PanDomainTemplate {
"https://cowtransfer.com/s/{shareKey}", "https://cowtransfer.com/s/{shareKey}",
CowTool.class), CowTool.class),
CT("城通网盘", CT("城通网盘",
compile("https://(?:[a-zA-Z\\d-]+\\.)?(ctfile|545c|u062|ghpym|474b)\\.com/f(ile)?/" + compile("https?://(?:[a-zA-Z\\d-]+\\.)?(ctfile|545c|u062|ghpym|474b)\\.com/f(ile)?/" +
"(?<KEY>[0-9a-zA-Z_-]+)(\\?p=(?<PWD>\\w+))?"), "(?<KEY>[0-9a-zA-Z_-]+)/?(?:\\?(?:(?:[^#&]*&)*p=(?<PWD>\\w+)(?:&[^#]*)?|[^#]*))?"),
"https://ctfile.com/file/{shareKey}", "https://ctfile.com/file/{shareKey}",
CtTool.class), CtTool.class),
// https://url94.ctfile.com/d/64115194-164803691-48508c?p=7609&d=164803691&fk=decb36 // https://url94.ctfile.com/d/64115194-164803691-48508c?p=7609&d=164803691&fk=decb36
CTD("城通网盘-目录", CTD("城通网盘-目录",
compile("https://(?:[a-zA-Z\\d-]+\\.)?(ctfile|545c|u062|ghpym|474b)\\.com/d/" + compile("https?://(?:[a-zA-Z\\d-]+\\.)?(ctfile|545c|u062|ghpym|474b)\\.com/d/" +
"(?<KEY>[0-9a-zA-Z_-]+)(\\?p=(?<PWD>\\w+))?"), "(?<KEY>[0-9a-zA-Z_-]+)/?(?:\\?(?:(?:[^#&]*&)*p=(?<PWD>\\w+)(?:&[^#]*)?|[^#]*))?"),
"https://ctfile.com/d/{shareKey}", "https://ctfile.com/d/{shareKey}",
CtTool.class), CtTool.class),
// https://www.vyuyun.com/s/QMa6ie?password=I4KG7H // https://www.vyuyun.com/s/QMa6ie?password=I4KG7H

View File

@@ -2,6 +2,7 @@ package cn.qaiu.parser.impl;
import cn.qaiu.entity.FileInfo; import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo; import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.IPanTool;
import cn.qaiu.parser.PanBase; import cn.qaiu.parser.PanBase;
import io.vertx.core.Future; import io.vertx.core.Future;
import io.vertx.core.buffer.Buffer; import io.vertx.core.buffer.Buffer;
@@ -160,6 +161,7 @@ public class CeTool extends PanBase {
} catch (Exception e) { } catch (Exception e) {
log.debug("v3 share API解析失败: {}", e.getMessage()); log.debug("v3 share API解析失败: {}", e.getMessage());
} }
tryV4ShareApi(baseUrl, key, pwd);
}).onFailure(t -> { }).onFailure(t -> {
log.debug("v3 share API请求失败: {}", t.getMessage()); log.debug("v3 share API请求失败: {}", t.getMessage());
// 请求失败,尝试 v4 或下一个解析器 // 请求失败,尝试 v4 或下一个解析器
@@ -206,7 +208,8 @@ public class CeTool extends PanBase {
*/ */
private void delegateToCe4Tool() { private void delegateToCe4Tool() {
log.debug("检测到Cloudreve 4.x转发到Ce4Tool处理"); log.debug("检测到Cloudreve 4.x转发到Ce4Tool处理");
new Ce4Tool(shareLinkInfo).parse().onComplete(promise); Ce4Tool ce4Tool = new Ce4Tool(shareLinkInfo);
IPanTool.closeAfter(ce4Tool, ce4Tool::parse).onComplete(promise);
} }

View File

@@ -3,6 +3,7 @@ package cn.qaiu.parser.impl;
import cn.qaiu.entity.FileInfo; import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo; import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase; import cn.qaiu.parser.PanBase;
import cn.qaiu.util.CommonUtils;
import cn.qaiu.util.FileSizeConverter; import cn.qaiu.util.FileSizeConverter;
import io.vertx.core.Future; import io.vertx.core.Future;
import io.vertx.core.Promise; import io.vertx.core.Promise;
@@ -12,6 +13,10 @@ import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.client.HttpRequest; import io.vertx.ext.web.client.HttpRequest;
import io.vertx.uritemplate.UriTemplate; import io.vertx.uritemplate.UriTemplate;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -24,19 +29,22 @@ import java.util.regex.Pattern;
*/ */
public class CtTool extends PanBase { public class CtTool extends PanBase {
private static final String API_URL_PREFIX = "https://webapi.ctfile.com"; private static final String API_URL_PREFIX = "https://webapi.ctfile.com";
private static final String SHARE_FILE_URL_PREFIX = "https://ctfile.com/file/";
private static final String AJAX_ACCEPT = "application/json, text/javascript, */*; q=0.01";
private static final String BROWSER_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
private static final int FILE_LIST_PAGE_SIZE = 200;
private static final int MAX_FILE_LIST_PAGES = 50;
// https://webapi.ctfile.com/getfile.php?path=f&f=64115194-17569800420720-06c697& // https://webapi.ctfile.com/getfile.php?path=f&f=64115194-17569800420720-06c697&
// passcode=7609&r=0.6611183001986635&ref=&url=https%3A%2F%2Furl94.ctfile.com%2Ff%2F64115194-17569800420720-06c697%3Fp%3D7609 // passcode=7609&r=0.6611183001986635&ref=&url=https%3A%2F%2Furl94.ctfile.com%2Ff%2F64115194-17569800420720-06c697%3Fp%3D7609
private static final String API1 = API_URL_PREFIX + "/getfile.php?path={path}" + private static final String API1 = API_URL_PREFIX + "/getfile.php?path={path}" +
"&f={shareKey}&passcode={pwd}&r={rand}&ref=&url={url}"; "&f={shareKey}&passcode={pwd}&r={rand}&ref=&url={url}";
// https://webapi.ctfile.com/get_file_url.php?uid=64115194&fid=17569800420720&folder_id=0& // https://webapi.ctfile.com/get_down_url.php?uid=64115194&fid=17569800420720&
// share_id=&file_chk=af5c8757a49cbc69a557eb3da59b246c&start_time=1780471868&wait_seconds=0& // file_chk=af5c8757a49cbc69a557eb3da59b246c&start_time=1780471868&wait_seconds=0&rd=0.36...
// mb=0&app=0&acheck=1&verifycode=1780471868.2951fe63abedf36ec02f34ed5711ce70&rd=0.36350981353622636 private static final String API2 = API_URL_PREFIX + "/get_down_url.php?" +
private static final String API2 = API_URL_PREFIX + "/get_file_url.php?" + "uid={uid}&fid={fid}&file_chk={file_chk}" +
"uid={uid}&fid={fid}&folder_id=0&share_id=&file_chk={file_chk}" + "&start_time={start_time}&wait_seconds={wait_seconds}&rd={rand}";
"&start_time={start_time}&wait_seconds={wait_seconds}&mb=0&app=0&acheck=1" +
"&verifycode={verifycode}&rd={rand}";
// https://webapi.ctfile.com/getdir.php?path=d&d=64115194-164803691-48508c& // https://webapi.ctfile.com/getdir.php?path=d&d=64115194-164803691-48508c&
// folder_id=164803691&fk=decb36&passcode=7609&r=0.23...&ref=&url=https://url94.ctfile.com/d/... // folder_id=164803691&fk=decb36&passcode=7609&r=0.23...&ref=&url=https://url94.ctfile.com/d/...
@@ -44,15 +52,22 @@ public class CtTool extends PanBase {
"&d={shareKey}&folder_id={folder_id}&fk={fk}&passcode={pwd}&r={rand}&ref=&url={url}"; "&d={shareKey}&folder_id={folder_id}&fk={fk}&passcode={pwd}&r={rand}&ref=&url={url}";
// DataTables参数用于获取目录文件列表 // DataTables参数用于获取目录文件列表
private static final String FILE_LIST_PARAMS = "&sEcho=1&iColumns=4&sColumns=%2C%2C%2C" + private static final String FILE_LIST_PARAMS_TEMPLATE = "&sEcho=1&iColumns=4&sColumns=%2C%2C%2C" +
"&iDisplayStart=0&iDisplayLength=500&mDataProp_0=0&mDataProp_1=1&mDataProp_2=2&mDataProp_3=3" + "&iDisplayStart={start}&iDisplayLength={length}" +
"&mDataProp_0=0&sSearch_0=&bRegex_0=false&bSearchable_0=true&bSortable_0=false" +
"&mDataProp_1=1&sSearch_1=&bRegex_1=false&bSearchable_1=true&bSortable_1=true" +
"&mDataProp_2=2&sSearch_2=&bRegex_2=false&bSearchable_2=true&bSortable_2=true" +
"&mDataProp_3=3&sSearch_3=&bRegex_3=false&bSearchable_3=true&bSortable_3=true" +
"&sSearch=&bRegex=false" +
"&iSortCol_0=3&sSortDir_0=desc&iSortingCols=1"; "&iSortCol_0=3&sSortDir_0=desc&iSortingCols=1";
// 文件列表HTML解析正则 // 文件列表HTML解析正则
private static final Pattern FILE_ID_PATTERN = Pattern.compile("value=\"f(\\d+)\""); private static final Pattern FILE_ID_PATTERN = Pattern.compile("value=[\"']f(\\d+)[\"']");
private static final Pattern FILE_HREF_PATTERN = Pattern.compile("href=\"#/f/([^\"]+)\""); private static final Pattern FOLDER_ID_PATTERN = Pattern.compile("value=[\"']d(\\d+)[\"']");
private static final Pattern FILE_NAME_PATTERN = Pattern.compile(">([^<]+)</a>"); private static final Pattern FILE_HREF_PATTERN = Pattern.compile("href=[\"']#/f/([^\"']+)[\"']");
private static final Pattern FILE_ICON_PATTERN = Pattern.compile("alt=\"([^\"]+)\""); private static final Pattern FILE_NAME_PATTERN = Pattern.compile("<a\\b[^>]*>([^<]+)</a>", Pattern.CASE_INSENSITIVE);
private static final Pattern FILE_ICON_PATTERN = Pattern.compile("alt=[\"']([^\"']+)[\"']");
private static final Pattern SUBDIR_PATTERN = Pattern.compile("load_subdir\\s*\\((\\d+)\\s*,\\s*['\"]([^'\"]+)['\"]\\)");
/** /**
* 子类重写此构造方法不需要添加额外逻辑 * 子类重写此构造方法不需要添加额外逻辑
@@ -73,39 +88,54 @@ public class CtTool extends PanBase {
@Override @Override
public Future<String> parse() { public Future<String> parse() {
final String shareKey = shareLinkInfo.getShareKey(); final String shareKey = shareLinkInfo.getShareKey();
if (shareKey.indexOf('-') == -1) { if (shareKey == null || shareKey.indexOf('-') == -1) {
fail("shareKey格式不正确找不到'-': {}", shareKey); fail("shareKey格式不正确找不到'-': {}", shareKey);
return promise.future(); return promise.future();
} }
String[] split = shareKey.split("-"); String[] split = shareKey.split("-");
String uid = split[0], fid = split[1]; if (split.length < 2 || split[0].isBlank() || split[1].isBlank()) {
// 获取url path fail("shareKey格式不正确: {}", shareKey);
int i1 = shareLinkInfo.getShareUrl().indexOf("com/"); return promise.future();
int i2 = shareLinkInfo.getShareUrl().lastIndexOf("/"); }
String path = shareLinkInfo.getShareUrl().substring(i1 + 4, i2); String fallbackUid = split[0], fallbackFid = split[1];
String path = extractPath(shareLinkInfo.getShareUrl());
HttpRequest<Buffer> bufferHttpRequest1 = clientSession.getAbs(UriTemplate.of(API1)) HttpRequest<Buffer> bufferHttpRequest1 = withCtAjaxHeaders(clientSession.getAbs(UriTemplate.of(API1))
.setTemplateParam("path", path) .setTemplateParam("path", path)
.setTemplateParam("shareKey", shareKey) .setTemplateParam("shareKey", shareKey)
.setTemplateParam("pwd", shareLinkInfo.getSharePassword()) .setTemplateParam("pwd", shareLinkInfo.getSharePassword())
.setTemplateParam("rand", String.valueOf(Math.random())) .setTemplateParam("rand", String.valueOf(Math.random()))
.setTemplateParam("url", shareLinkInfo.getShareUrl()); .setTemplateParam("url", shareLinkInfo.getShareUrl()), shareLinkInfo.getShareUrl());
bufferHttpRequest1 bufferHttpRequest1
.send().onSuccess(res -> { .send().onSuccess(res -> {
try {
var resJson = asJson(res); var resJson = asJson(res);
if (resJson.containsKey("file")) { if (resJson == null || resJson.isEmpty()) {
var fileJson = resJson.getJsonObject("file"); fail("解析失败, 上游返回空响应或非JSON响应");
if (fileJson.containsKey("file_chk")) { return;
var file_chk = fileJson.getString("file_chk"); }
String startTime = fileJson.getValue("start_time").toString(); Object fileValue = resJson.getValue("file");
String waitSeconds = fileJson.getValue("wait_seconds").toString(); if (!(fileValue instanceof JsonObject)) {
String verifycode = fileJson.getString("verifycode"); fail("解析失败, 文件信息为空或格式错误, 可能分享已失效: {}", resJson);
return;
}
var fileJson = (JsonObject) fileValue;
String uid = resolveDownloadUid(fileJson, fallbackUid);
String fid = resolveDownloadFid(fileJson, fallbackFid);
String fileChk = fileJson.getString("file_chk");
String startTime = valueToString(fileJson.getValue("start_time"));
String waitSeconds = valueToString(fileJson.getValue("wait_seconds"));
if (uid.isBlank() || fid.isBlank() || fileChk == null || fileChk.isBlank()
|| startTime.isBlank() || waitSeconds.isBlank()) {
fail("解析失败, 下载参数不完整, 可能分享已失效或者分享密码不对: {}", fileJson);
return;
}
// 提取文件信息并存储 // 提取文件信息并存储
FileInfo fileInfo = new FileInfo() FileInfo fileInfo = new FileInfo()
.setFileName(fileJson.getString("file_name")) .setFileName(fileJson.getString("file_name"))
.setFileId(String.valueOf(fileJson.getLong("file_id", 0L))) .setFileId(fid)
.setSizeStr(fileJson.getString("file_size")) .setSizeStr(fileJson.getString("file_size"))
.setCreateTime(fileJson.getString("file_time")) .setCreateTime(fileJson.getString("file_time"))
.setCreateBy(fileJson.getString("username")) .setCreateBy(fileJson.getString("username"))
@@ -113,39 +143,48 @@ public class CtTool extends PanBase {
.setPanType(shareLinkInfo.getType()); .setPanType(shareLinkInfo.getType());
shareLinkInfo.getOtherParam().put("fileInfo", fileInfo); shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
HttpRequest<Buffer> bufferHttpRequest2 = clientSession.getAbs(UriTemplate.of(API2)) HttpRequest<Buffer> bufferHttpRequest2 = withCtAjaxHeaders(clientSession.getAbs(UriTemplate.of(API2))
.setTemplateParam("uid", uid) .setTemplateParam("uid", uid)
.setTemplateParam("fid", fid) .setTemplateParam("fid", fid)
.setTemplateParam("file_chk", file_chk) .setTemplateParam("file_chk", fileChk)
.setTemplateParam("start_time", startTime) .setTemplateParam("start_time", startTime)
.setTemplateParam("wait_seconds", waitSeconds) .setTemplateParam("wait_seconds", waitSeconds)
.setTemplateParam("verifycode", verifycode) .setTemplateParam("rand", String.valueOf(Math.random())), shareLinkInfo.getShareUrl());
.setTemplateParam("rand", String.valueOf(Math.random()));
bufferHttpRequest2 bufferHttpRequest2
.send().onSuccess(res2 -> { .send().onSuccess(res2 -> handleDownloadUrlResponse(res2))
JsonObject resJson2 = asJson(res2); .onFailure(t -> fail("下载链接请求失败: {}", t.getMessage()));
if (resJson2.containsKey("downurl")) { } catch (Exception e) {
String downloadUrl = resJson2.getString("downurl"); fail("解析失败: {}", e.getMessage());
}
}).onFailure(t -> fail("文件信息请求失败: {}", t.getMessage()));
return promise.future();
}
private void handleDownloadUrlResponse(io.vertx.ext.web.client.HttpResponse<Buffer> res) {
try {
JsonObject resJson = asJson(res);
if (resJson == null || resJson.isEmpty()) {
fail("解析失败, 下载接口返回空响应或非JSON响应");
return;
}
String downloadUrl = resJson.getString("downurl");
if (downloadUrl == null || downloadUrl.isBlank()) {
fail("解析失败, 可能分享已失效: json: {} 字段 {} 不存在", resJson, "downurl");
return;
}
// 存储下载元数据,包括必要的请求头 // 存储下载元数据,包括必要的请求头
Map<String, String> headers = new HashMap<>(); Map<String, String> headers = new HashMap<>();
headers.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); headers.put("User-Agent", BROWSER_UA);
if (shareLinkInfo.getShareUrl() != null && !shareLinkInfo.getShareUrl().isBlank()) {
headers.put("Referer", shareLinkInfo.getShareUrl()); headers.put("Referer", shareLinkInfo.getShareUrl());
}
// 使用新的 completeWithMeta 方法 // 使用新的 completeWithMeta 方法
completeWithMeta(downloadUrl, headers); completeWithMeta(downloadUrl, headers);
} else { } catch (Exception e) {
fail("解析失败, 可能分享已失效: json: {} 字段 {} 不存在", resJson2, "downurl"); fail("解析失败, 下载接口响应处理异常: {}", e.getMessage());
} }
}).onFailure(handleFail(bufferHttpRequest1.queryParams().toString()));
} else {
fail("解析失败, file_chk找不到, 可能分享已失效或者分享密码不对: {}", fileJson);
}
} else {
fail("解析失败, 文件信息为空, 可能分享已失效");
}
}).onFailure(handleFail(bufferHttpRequest1.queryParams().toString()));
return promise.future();
} }
@Override @Override
@@ -157,122 +196,478 @@ public class CtTool extends PanBase {
final String pwd = shareLinkInfo.getSharePassword(); final String pwd = shareLinkInfo.getSharePassword();
// shareKey格式: uid-folder_id-hash (例如 64115194-164803691-48508c) // shareKey格式: uid-folder_id-hash (例如 64115194-164803691-48508c)
if (shareKey == null) {
listPromise.fail(baseMsg() + " shareKey为空");
return listPromise.future();
}
String[] split = shareKey.split("-"); String[] split = shareKey.split("-");
if (split.length < 2) { if (split.length < 2) {
listPromise.fail(baseMsg() + " shareKey格式不正确: " + shareKey); listPromise.fail(baseMsg() + " shareKey格式不正确: " + shareKey);
return listPromise.future(); return listPromise.future();
} }
String folderId = split[1]; String path = extractPath(shareUrl);
Object dirId = shareLinkInfo.getOtherParam() == null ? null : shareLinkInfo.getOtherParam().get("dirId");
DirectoryContext directoryContext = resolveDirectoryContext(shareUrl, dirId);
// 从分享URL中提取fk参数 HttpRequest<Buffer> getDirRequest = withCtAjaxHeaders(clientSession.getAbs(UriTemplate.of(API_GETDIR))
String fk = extractQueryParam(shareUrl, "fk");
// 从URL中提取path (例如从 "https://url94.ctfile.com/d/xxx?p=..." 中提取 "d")
int comIdx = shareUrl.indexOf("com/");
int qIdx = shareUrl.indexOf('?');
String pathAndKey = qIdx > 0 ? shareUrl.substring(comIdx + 4, qIdx) : shareUrl.substring(comIdx + 4);
int slashIdx = pathAndKey.indexOf('/');
String path = slashIdx > 0 ? pathAndKey.substring(0, slashIdx) : pathAndKey;
clientSession.getAbs(UriTemplate.of(API_GETDIR))
.setTemplateParam("path", path) .setTemplateParam("path", path)
.setTemplateParam("shareKey", shareKey) .setTemplateParam("shareKey", shareKey)
.setTemplateParam("folder_id", folderId) .setTemplateParam("folder_id", directoryContext.folderId)
.setTemplateParam("fk", fk != null ? fk : "") .setTemplateParam("fk", directoryContext.folderKey)
.setTemplateParam("pwd", pwd != null ? pwd : "") .setTemplateParam("pwd", pwd != null ? pwd : "")
.setTemplateParam("rand", String.valueOf(Math.random())) .setTemplateParam("rand", String.valueOf(Math.random()))
.setTemplateParam("url", shareUrl) .setTemplateParam("url", shareUrl), shareUrl);
.send().onSuccess(res -> {
getDirRequest.send().onSuccess(res -> {
try {
var resJson = asJson(res); var resJson = asJson(res);
if (resJson == null || resJson.isEmpty()) {
failListPromise(listPromise, baseMsg() + " 目录解析失败: 上游返回空响应或非JSON响应");
return;
}
if (!resJson.containsKey("file")) { if (!resJson.containsKey("file")) {
listPromise.fail(baseMsg() + " 目录解析失败: " + resJson.encode()); failListPromise(listPromise, baseMsg() + " 目录解析失败: " + resJson.encode());
return; return;
} }
var dirInfo = resJson.getJsonObject("file"); Object dirInfoValue = resJson.getValue("file");
String fileListRelUrl = dirInfo.getString("url"); if (!(dirInfoValue instanceof JsonObject)) {
if (fileListRelUrl == null) { failListPromise(listPromise, baseMsg() + " 目录解析失败: file字段格式错误: " + resJson.encode());
listPromise.fail(baseMsg() + " 文件列表URL为空"); return;
}
JsonObject dirInfo = (JsonObject) dirInfoValue;
Object fileListUrlValue = dirInfo.getValue("url");
String fileListRelUrl = fileListUrlValue instanceof String ? ((String) fileListUrlValue).trim() : "";
if (fileListRelUrl.isBlank()) {
failListPromise(listPromise, baseMsg() + " " + buildDirectoryFailureMessage(resJson, dirInfo));
return; return;
} }
String fileListUrl = API_URL_PREFIX + fileListRelUrl + FILE_LIST_PARAMS; fetchFileListPage(toCtApiUrl(fileListRelUrl), 0, 0, new ArrayList<>(), listPromise,
clientSession.getAbs(fileListUrl) shareLinkInfo.getType(), getDomainName(), shareUrl, pwd);
.send().onSuccess(res2 -> {
var listJson = asJson(res2);
JsonArray aaData = listJson.getJsonArray("aaData");
if (aaData == null) {
listPromise.fail(baseMsg() + " 文件列表为空");
return;
}
List<FileInfo> fileList = new ArrayList<>();
String panType = shareLinkInfo.getType();
for (int i = 0; i < aaData.size(); i++) {
var row = aaData.getJsonArray(i);
try {
String checkboxHtml = row.getString(0);
String nameCellHtml = row.getString(1);
String sizeStr = row.getString(2).trim();
// 从checkbox HTML中提取文件ID
String fileId = null;
Matcher idMatcher = FILE_ID_PATTERN.matcher(checkboxHtml);
if (idMatcher.find()) fileId = idMatcher.group(1);
// 从文件名单元格HTML中提取临时分享key
String fileShareKey = null;
Matcher hrefMatcher = FILE_HREF_PATTERN.matcher(nameCellHtml);
if (hrefMatcher.find()) fileShareKey = hrefMatcher.group(1);
// 提取文件名
String fileName = null;
Matcher nameMatcher = FILE_NAME_PATTERN.matcher(nameCellHtml);
if (nameMatcher.find()) fileName = nameMatcher.group(1).trim();
// 提取文件图标/类型
String fileIcon = null;
Matcher iconMatcher = FILE_ICON_PATTERN.matcher(nameCellHtml);
if (iconMatcher.find()) fileIcon = iconMatcher.group(1);
if (fileName == null || fileShareKey == null) continue;
long sizeBytes = 0;
try {
sizeBytes = FileSizeConverter.convertToBytes(sizeStr);
} catch (Exception ignored) {}
FileInfo fileInfo = new FileInfo()
.setFileName(fileName)
.setFileId(fileId)
.setSizeStr(sizeStr)
.setSize(sizeBytes)
.setFileType(fileIcon)
.setFileIcon(fileIcon)
.setPanType(panType)
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s",
getDomainName(), panType, fileShareKey));
fileList.add(fileInfo);
} catch (Exception e) { } catch (Exception e) {
log.warn("解析文件行失败: {}", e.getMessage()); failListPromise(listPromise, baseMsg() + " 目录解析失败: " + e.getMessage());
} }
} }).onFailure(t -> failListPromise(listPromise, t));
listPromise.complete(fileList);
}).onFailure(listPromise::fail);
}).onFailure(listPromise::fail);
return listPromise.future(); return listPromise.future();
} }
private String extractQueryParam(String url, String paramName) { private void fetchFileListPage(String fileListBaseUrl, int start, int pageIndex, List<FileInfo> fileList,
if (url == null) return null; Promise<List<FileInfo>> listPromise, String panType, String domainName,
String shareUrl, String pwd) {
try {
if (pageIndex >= MAX_FILE_LIST_PAGES) {
failListPromise(listPromise, baseMsg() + " 文件列表解析失败: 分页超过最大限制 " + MAX_FILE_LIST_PAGES
+ " (start=" + start + ", length=" + FILE_LIST_PAGE_SIZE + ")");
return;
}
String fileListUrl = appendQueryParams(fileListBaseUrl,
buildFileListParams(start, FILE_LIST_PAGE_SIZE) + "&_=" + System.currentTimeMillis());
withCtAjaxHeaders(clientSession.getAbs(fileListUrl), shareUrl)
.send()
.onSuccess(res -> handleFileListPageResponse(fileListBaseUrl, start, pageIndex, fileList,
listPromise, panType, domainName, shareUrl, pwd, res))
.onFailure(t -> failListPromise(listPromise, t));
} catch (Exception e) {
failListPromise(listPromise, baseMsg() + " 文件列表解析失败: " + e.getMessage()
+ " (start=" + start + ", length=" + FILE_LIST_PAGE_SIZE + ")");
}
}
private void handleFileListPageResponse(String fileListBaseUrl, int start, int pageIndex, List<FileInfo> fileList,
Promise<List<FileInfo>> listPromise, String panType, String domainName,
String shareUrl, String pwd, io.vertx.ext.web.client.HttpResponse<Buffer> res) {
try {
var listJson = asJson(res);
if (listJson == null || listJson.isEmpty()) {
failListPromise(listPromise, baseMsg() + " 文件列表解析失败: 上游返回空响应或非JSON响应"
+ " (start=" + start + ", length=" + FILE_LIST_PAGE_SIZE + ")");
return;
}
Object aaDataValue = listJson.getValue("aaData");
if (!(aaDataValue instanceof JsonArray)) {
failListPromise(listPromise, baseMsg() + " 文件列表解析失败: aaData为空: " + listJson.encode());
return;
}
JsonArray aaData = (JsonArray) aaDataValue;
for (int i = 0; i < aaData.size(); i++) {
try {
Object rowValue = aaData.getValue(i);
if (!(rowValue instanceof JsonArray)) {
log.warn("城通文件列表行格式错误: {}", rowValue);
continue;
}
FileInfo fileInfo = parseFileListRow((JsonArray) rowValue, panType,
domainName, shareUrl, pwd);
if (fileInfo != null) {
fileList.add(fileInfo);
}
} catch (Exception e) {
log.warn("解析文件行失败: {}", e.getMessage());
}
}
int nextStart = start + aaData.size();
int total = parseFileListTotal(listJson);
if (isUnexpectedEmptyFileListPage(start, aaData.size(), total)) {
failListPromise(listPromise, baseMsg() + " 文件列表解析失败: 上游返回空分页"
+ " (start=" + start + ", total=" + total + ")");
return;
}
if (shouldFetchNextFileListPage(start, aaData.size(), total)) {
fetchFileListPage(fileListBaseUrl, nextStart, pageIndex + 1, fileList,
listPromise, panType, domainName, shareUrl, pwd);
} else {
completeListPromise(listPromise, fileList);
}
} catch (Exception e) {
failListPromise(listPromise, baseMsg() + " 文件列表解析失败: " + e.getMessage()
+ " (start=" + start + ", length=" + FILE_LIST_PAGE_SIZE + ")");
}
}
@Override
public Future<String> parseById() {
Object paramValue = shareLinkInfo.getOtherParam().get("paramJson");
if (!(paramValue instanceof JsonObject)) {
Promise<String> parsePromise = Promise.promise();
parsePromise.fail(baseMsg() + " 缺少下载参数paramJson");
return parsePromise.future();
}
JsonObject paramJson = (JsonObject) paramValue;
if (!applyFileParam(shareLinkInfo, paramJson)) {
Promise<String> parsePromise = Promise.promise();
parsePromise.fail(baseMsg() + " 下载参数id为空");
return parsePromise.future();
}
return parse();
}
static boolean applyFileParam(ShareLinkInfo shareLinkInfo, JsonObject paramJson) {
String fileShareKey = paramJson.getString("id");
if (fileShareKey == null || fileShareKey.isBlank()) {
return false;
}
shareLinkInfo.setSharePassword(paramJson.getString("pwd", ""));
shareLinkInfo.setShareKey(fileShareKey);
shareLinkInfo.setShareUrl(SHARE_FILE_URL_PREFIX + fileShareKey);
shareLinkInfo.setStandardUrl(SHARE_FILE_URL_PREFIX + fileShareKey);
return true;
}
static String resolveDownloadUid(JsonObject fileJson, String fallbackUid) {
return firstNonBlank(valueToString(fileJson.getValue("userid")), fallbackUid);
}
static String resolveDownloadFid(JsonObject fileJson, String fallbackFid) {
return firstNonBlank(valueToString(fileJson.getValue("file_id")), fallbackFid);
}
private HttpRequest<Buffer> withCtAjaxHeaders(HttpRequest<Buffer> request, String shareUrl) {
request.putHeader("User-Agent", BROWSER_UA)
.putHeader("Accept", AJAX_ACCEPT)
.putHeader("X-Requested-With", "XMLHttpRequest");
if (shareUrl != null && !shareUrl.isBlank()) {
request.putHeader("Referer", shareUrl);
}
String origin = extractOrigin(shareUrl);
if (!origin.isBlank()) {
request.putHeader("Origin", origin);
}
return request;
}
static FileInfo parseFileListRow(JsonArray row, String panType, String domainName, String shareUrl, String pwd) {
if (row == null || row.size() < 2) {
return null;
}
String checkboxHtml = rowString(row, 0);
String nameCellHtml = rowString(row, 1);
String sizeStr = rowString(row, 2).trim();
String dateStr = rowString(row, 3).trim();
if (nameCellHtml.isBlank()) {
return null;
}
String fileName = matchFirst(FILE_NAME_PATTERN, nameCellHtml);
String fileIcon = matchFirst(FILE_ICON_PATTERN, nameCellHtml);
if (fileName == null || fileName.isBlank()) {
return null;
}
Matcher subdirMatcher = SUBDIR_PATTERN.matcher(nameCellHtml);
boolean hasSubdirCall = subdirMatcher.find();
if (hasSubdirCall || "folder".equalsIgnoreCase(fileIcon)) {
String folderId = hasSubdirCall ? subdirMatcher.group(1) : null;
String folderKey = hasSubdirCall ? subdirMatcher.group(2) : "";
if (folderId == null) {
folderId = matchFirst(FOLDER_ID_PATTERN, checkboxHtml);
}
if (folderId == null || folderId.isBlank()) {
return null;
}
String dirId = folderId + ":" + folderKey;
FileInfo fileInfo = new FileInfo()
.setFileName(fileName.trim())
.setFileId(folderId)
.setSize(0L)
.setSizeStr(sizeStr.isBlank() ? "0B" : sizeStr)
.setFileType("folder")
.setFileIcon(fileIcon)
.setPanType(panType)
.setParserUrl(buildFolderParserUrl(domainName, shareUrl, dirId, pwd));
if (!dateStr.isBlank()) {
fileInfo.setCreateTime(dateStr).setUpdateTime(dateStr);
}
return fileInfo;
}
String fileShareKey = matchFirst(FILE_HREF_PATTERN, nameCellHtml);
if (fileShareKey == null || fileShareKey.isBlank()) {
return null;
}
String fileId = matchFirst(FILE_ID_PATTERN, checkboxHtml);
JsonObject paramJson = new JsonObject()
.put("id", fileShareKey)
.put("fileName", fileName.trim())
.put("pwd", pwd == null ? "" : pwd);
String param = CommonUtils.urlBase64Encode(paramJson.encode());
long sizeBytes = 0;
try {
sizeBytes = sizeStr.isBlank() ? 0 : FileSizeConverter.convertToBytes(sizeStr);
} catch (Exception ignored) {
}
FileInfo fileInfo = new FileInfo()
.setFileName(fileName.trim())
.setFileId(fileId)
.setSizeStr(sizeStr)
.setSize(sizeBytes)
.setFileType(fileIcon != null ? fileIcon : "file")
.setFileIcon(fileIcon)
.setPanType(panType)
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s",
domainName, panType, param));
if (!dateStr.isBlank()) {
fileInfo.setCreateTime(dateStr).setUpdateTime(dateStr);
}
return fileInfo;
}
private static String buildFolderParserUrl(String domainName, String shareUrl, String dirId, String pwd) {
String url = String.format("%s/v2/getFileList?url=%s&dirId=%s",
domainName, urlEncode(shareUrl), urlEncode(dirId));
if (pwd != null && !pwd.isBlank()) {
url += "&pwd=" + urlEncode(pwd);
}
return url;
}
static String extractQueryParam(String url, String paramName) {
if (url == null || paramName == null) return null;
int qIdx = url.indexOf('?'); int qIdx = url.indexOf('?');
if (qIdx < 0) return null; if (qIdx < 0) return null;
String query = url.substring(qIdx + 1); String query = url.substring(qIdx + 1);
int fragmentIdx = query.indexOf('#');
if (fragmentIdx >= 0) {
query = query.substring(0, fragmentIdx);
}
for (String param : query.split("&")) { for (String param : query.split("&")) {
int eqIdx = param.indexOf('='); int eqIdx = param.indexOf('=');
if (eqIdx > 0 && param.substring(0, eqIdx).equals(paramName)) { if (eqIdx > 0 && urlDecode(param.substring(0, eqIdx)).equals(paramName)) {
return param.substring(eqIdx + 1); return urlDecode(param.substring(eqIdx + 1));
} }
} }
return null; return null;
} }
static String extractPath(String shareUrl) {
if (shareUrl == null) {
return "";
}
int comIdx = shareUrl.indexOf("com/");
if (comIdx < 0) {
return "";
}
int pathStart = comIdx + 4;
int pathEnd = shareUrl.indexOf('/', pathStart);
if (pathEnd < 0) {
pathEnd = shareUrl.indexOf('?', pathStart);
}
if (pathEnd < 0) {
pathEnd = shareUrl.length();
}
return shareUrl.substring(pathStart, pathEnd);
}
static String extractFolderKey(String shareUrl) {
return trimToEmpty(extractQueryParam(shareUrl, "fk"));
}
static DirectoryContext resolveDirectoryContext(String shareUrl, Object dirIdObj) {
String dirId = dirIdObj == null ? "" : urlDecode(String.valueOf(dirIdObj).trim());
if (!dirId.isBlank()) {
String[] split = dirId.split(":", 2);
return new DirectoryContext(trimToDefault(split[0], "undefined"),
split.length > 1 ? trimToEmpty(split[1]) : "");
}
String queryFolderId = firstNonBlank(extractQueryParam(shareUrl, "folder_id"), extractQueryParam(shareUrl, "d"));
String queryFk = extractFolderKey(shareUrl);
if (!queryFolderId.isBlank() || !queryFk.isBlank()) {
return new DirectoryContext(trimToDefault(queryFolderId, "undefined"), queryFk);
}
return new DirectoryContext("undefined", "");
}
static String buildDirectoryFailureMessage(JsonObject resJson, JsonObject dirInfo) {
String code = valueToString(resJson.getValue("code"));
String message = valueToString(dirInfo.getValue("message"));
if (message != null && !message.isBlank()) {
return "目录解析失败: " + message + " (code=" + code + ")";
}
if ("423".equals(code)) {
return "目录解析失败: 需要访问密码或该分享受限 (code=423)";
}
return "目录解析失败: 文件列表URL为空, 上游响应: " + resJson.encode();
}
static String buildFileListParams(int start, int length) {
return FILE_LIST_PARAMS_TEMPLATE
.replace("{start}", String.valueOf(Math.max(0, start)))
.replace("{length}", String.valueOf(Math.max(1, length)));
}
static int parseFileListTotal(JsonObject listJson) {
int displayTotal = parseInteger(listJson.getValue("iTotalDisplayRecords"), -1);
return displayTotal >= 0 ? displayTotal : parseInteger(listJson.getValue("iTotalRecords"), -1);
}
static boolean shouldFetchNextFileListPage(int start, int rowCount, int total) {
if (rowCount <= 0) {
return false;
}
int fetchedThrough = start + rowCount;
return total < 0 ? rowCount >= FILE_LIST_PAGE_SIZE : fetchedThrough < total;
}
static boolean isUnexpectedEmptyFileListPage(int start, int rowCount, int total) {
return total >= 0 && start < total && rowCount <= 0;
}
private static int parseInteger(Object value, int defaultValue) {
if (value instanceof Number) {
return ((Number) value).intValue();
}
if (value == null) {
return defaultValue;
}
try {
return Integer.parseInt(value.toString());
} catch (NumberFormatException e) {
return defaultValue;
}
}
private static void failListPromise(Promise<List<FileInfo>> listPromise, String message) {
if (!listPromise.future().isComplete()) {
listPromise.fail(message);
}
}
private static void failListPromise(Promise<List<FileInfo>> listPromise, Throwable throwable) {
if (!listPromise.future().isComplete()) {
listPromise.fail(throwable);
}
}
private static void completeListPromise(Promise<List<FileInfo>> listPromise, List<FileInfo> fileList) {
if (!listPromise.future().isComplete()) {
listPromise.complete(fileList);
}
}
private static String toCtApiUrl(String url) {
if (url.startsWith("http://") || url.startsWith("https://")) {
return url;
}
return API_URL_PREFIX + url;
}
private static String appendQueryParams(String url, String params) {
String normalizedParams = params != null && params.startsWith("&") ? params.substring(1) : params;
return url + (url.contains("?") ? "&" : "?") + normalizedParams;
}
private static String rowString(JsonArray row, int index) {
if (row == null || index >= row.size()) {
return "";
}
return valueToString(row.getValue(index));
}
private static String valueToString(Object value) {
return value == null ? "" : value.toString();
}
private static String matchFirst(Pattern pattern, String text) {
if (text == null) {
return null;
}
Matcher matcher = pattern.matcher(text);
return matcher.find() ? matcher.group(1) : null;
}
private static String firstNonBlank(String first, String second) {
return !trimToEmpty(first).isBlank() ? trimToEmpty(first) : trimToEmpty(second);
}
private static String trimToDefault(String value, String defaultValue) {
String result = trimToEmpty(value);
return result.isBlank() ? defaultValue : result;
}
private static String trimToEmpty(String value) {
return value == null ? "" : value.trim();
}
private static String urlEncode(String value) {
return URLEncoder.encode(value == null ? "" : value, StandardCharsets.UTF_8);
}
private static String urlDecode(String value) {
if (value == null) {
return "";
}
try {
return URLDecoder.decode(value, StandardCharsets.UTF_8);
} catch (Exception e) {
return value;
}
}
private static String extractOrigin(String shareUrl) {
try {
URI uri = URI.create(shareUrl);
if (uri.getScheme() == null || uri.getHost() == null) {
return "";
}
String origin = uri.getScheme() + "://" + uri.getHost();
return uri.getPort() > 0 ? origin + ":" + uri.getPort() : origin;
} catch (Exception e) {
return "";
}
}
static final class DirectoryContext {
final String folderId;
final String folderKey;
DirectoryContext(String folderId, String folderKey) {
this.folderId = folderId;
this.folderKey = folderKey;
}
}
} }

View File

@@ -25,6 +25,10 @@ public class FcTool extends PanBase {
private static final String DOWN_REQUEST_URL = "https://v2.fangcloud.cn/apps/files/download?file_id={fid}" + private static final String DOWN_REQUEST_URL = "https://v2.fangcloud.cn/apps/files/download?file_id={fid}" +
"&scenario=share&unique_name={uname}"; "&scenario=share&unique_name={uname}";
// 静态编译的正则表达式,避免每次调用都重新编译
private static final Pattern REQUEST_TOKEN_PATTERN = Pattern.compile("name=\"requesttoken\"\\s+value=\"([a-zA-Z0-9_+=]+)\"");
private static final Pattern TYPED_ID_PATTERN = Pattern.compile("id=\"typed_id\"\\s+value=\"file_(\\d+)\"");
public FcTool(ShareLinkInfo shareLinkInfo) { public FcTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo); super(shareLinkInfo);
} }
@@ -41,8 +45,7 @@ public class FcTool extends PanBase {
if (StringUtils.isNotEmpty(pwd)) { if (StringUtils.isNotEmpty(pwd)) {
// 获取requesttoken // 获取requesttoken
String html = res.bodyAsString(); String html = res.bodyAsString();
Pattern compile = Pattern.compile("name=\"requesttoken\"\\s+value=\"([a-zA-Z0-9_+=]+)\""); Matcher matcher = REQUEST_TOKEN_PATTERN.matcher(html);
Matcher matcher = compile.matcher(html);
if (!matcher.find()) { if (!matcher.find()) {
fail(SHARE_URL_PREFIX + " 未匹配到加密分享的密码输入页面的requesttoken"); fail(SHARE_URL_PREFIX + " 未匹配到加密分享的密码输入页面的requesttoken");
return; return;
@@ -71,8 +74,7 @@ public class FcTool extends PanBase {
WebClientSession sClient) { WebClientSession sClient) {
// 从HTML中找到文件id // 从HTML中找到文件id
String html = res.bodyAsString(); String html = res.bodyAsString();
Pattern compile = Pattern.compile("id=\"typed_id\"\\s+value=\"file_(\\d+)\""); Matcher matcher = TYPED_ID_PATTERN.matcher(html);
Matcher matcher = compile.matcher(html);
if (!matcher.find()) { if (!matcher.find()) {
fail(SHARE_URL_PREFIX + " 未匹配到文件id(typed_id)"); fail(SHARE_URL_PREFIX + " 未匹配到文件id(typed_id)");
return; return;

View File

@@ -169,8 +169,13 @@ public class FjTool extends PanBase {
// 文件Id // 文件Id
JsonObject fileInfo = resJson.getJsonArray("list").getJsonObject(0); JsonObject fileInfo = resJson.getJsonArray("list").getJsonObject(0);
JsonArray fileListArray = fileInfo.getJsonArray("fileList");
if (fileListArray == null || fileListArray.isEmpty()) {
fail(FIRST_REQUEST_URL + " 文件列表为空: " + fileInfo);
return;
}
// 如果是目录返回目录ID // 如果是目录返回目录ID
JsonObject fileList = fileInfo.getJsonArray("fileList").getJsonObject(0); JsonObject fileList = fileListArray.getJsonObject(0);
if (fileList.getInteger("fileType") == 2) { if (fileList.getInteger("fileType") == 2) {
promise.complete(fileList.getInteger("folderId").toString()); promise.complete(fileList.getInteger("folderId").toString());
return; return;

View File

@@ -31,10 +31,12 @@ public class GenShortUrl extends PanBase {
private static final String WRAPPER_URL = "https://www.so.com/link?m=ewgUSYiFWXIoTybC3fJH8YoJy8y10iRquo6cazgINwWjTn3HvVJ92TrCJu0PmMUR0RMDfOAucP3wa4G8j64SrhNH9Z0Cr0PEyn9ASuvpkUGmAjjUEGJkO5%2BIDGWVrEkPHsL7UsoKO6%2BlT%2BD6r&ccc="; private static final String WRAPPER_URL = "https://www.so.com/link?m=ewgUSYiFWXIoTybC3fJH8YoJy8y10iRquo6cazgINwWjTn3HvVJ92TrCJu0PmMUR0RMDfOAucP3wa4G8j64SrhNH9Z0Cr0PEyn9ASuvpkUGmAjjUEGJkO5%2BIDGWVrEkPHsL7UsoKO6%2BlT%2BD6r&ccc=";
private static final String MID = "5095144728824883"; // 微博的mid private static final String MID = "5095144728824883"; // 微博的mid
private static final Pattern SHORT_URL_PATTERN = Pattern.compile("(https?)://t.cn/\\w+");
private static final Pattern COMMENT_ID_PATTERN = Pattern.compile("comment_id=\"(\\d+)\"");
private static final MultiMap HEADER = HeadersMultiMap.headers() private static final MultiMap HEADER = HeadersMultiMap.headers()
.add("Content-Type", "application/x-www-form-urlencoded") .add("Content-Type", "application/x-www-form-urlencoded")
.add("Referer", "https://www.weibo.com") .add("Referer", "https://www.weibo.com")
.add("Content-Type", "application/x-www-form-urlencoded")
.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36"); .add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36");
Cookie cookie = new DefaultCookie("SUB", "_2A25KJE5vDeRhGeRJ6lsR9SjJzDuIHXVpWM-nrDV8PUJbkNAbLVPlkW1NUmJm3GjYtRHBsHdMUKafkdTL_YheMEmu"); Cookie cookie = new DefaultCookie("SUB", "_2A25KJE5vDeRhGeRJ6lsR9SjJzDuIHXVpWM-nrDV8PUJbkNAbLVPlkW1NUmJm3GjYtRHBsHdMUKafkdTL_YheMEmu");
@@ -64,11 +66,12 @@ public class GenShortUrl extends PanBase {
String shortUrl = extractShortUrl(comment); String shortUrl = extractShortUrl(comment);
if (shortUrl != null) { if (shortUrl != null) {
log.info("生成的短链:{}", shortUrl); log.info("生成的短链:{}", shortUrl);
// 先完成 promise返回短链
promise.complete(shortUrl);
// 异步清理评论best-effort不影响结果
String commentId = extractCommentId(comment); String commentId = extractCommentId(comment);
if (commentId != null) { if (commentId != null) {
deleteComment(commentId); deleteComment(commentId);
} else {
promise.fail("未能提取评论ID");
} }
} else { } else {
promise.fail("未能生成短链"); promise.fail("未能生成短链");
@@ -103,8 +106,7 @@ public class GenShortUrl extends PanBase {
} }
private String extractShortUrl(String comment) { private String extractShortUrl(String comment) {
Pattern pattern = Pattern.compile("(https?)://t.cn/\\w+"); Matcher matcher = SHORT_URL_PATTERN.matcher(comment);
Matcher matcher = pattern.matcher(comment);
if (matcher.find()) { if (matcher.find()) {
return matcher.group(0); return matcher.group(0);
} }
@@ -112,8 +114,7 @@ public class GenShortUrl extends PanBase {
} }
private String extractCommentId(String comment) { private String extractCommentId(String comment) {
Pattern pattern = Pattern.compile("comment_id=\"(\\d+)\""); Matcher matcher = COMMENT_ID_PATTERN.matcher(comment);
Matcher matcher = pattern.matcher(comment);
if (matcher.find()) { if (matcher.find()) {
return matcher.group(1); return matcher.group(1);
} }

View File

@@ -52,4 +52,14 @@ public class IzSelectorTool implements IPanTool {
public Future<String> parseById() { public Future<String> parseById() {
return selectedTool.parseById(); return selectedTool.parseById();
} }
@Override
public ShareLinkInfo getShareLinkInfo() {
return selectedTool.getShareLinkInfo();
}
@Override
public void close() {
IPanTool.closeQuietly(selectedTool);
}
} }

View File

@@ -445,11 +445,70 @@ public class IzTool extends PanBase {
private void down(HttpResponse<Buffer> res2) { private void down(HttpResponse<Buffer> res2) {
MultiMap headers = res2.headers(); MultiMap headers = res2.headers();
if (!headers.contains("Location") || StringUtils.isBlank(headers.get("Location"))) { String location = headers.get("Location");
fail("找不到下载链接可能服务器已被禁止或者配置的认证信息有误"); if (StringUtils.isBlank(location)) {
fail("{}", buildMissingLocationMessage(res2));
return; return;
} }
promise.complete(headers.get("Location")); promise.complete(location);
}
private String buildMissingLocationMessage(HttpResponse<Buffer> response) {
StringBuilder message = new StringBuilder("未获取到下载重定向地址");
message.append(", HTTP ").append(response.statusCode());
String body = null;
try {
body = asText(response);
} catch (Exception e) {
body = "<响应体读取失败: " + e.getMessage() + ">";
}
if (StringUtils.isNotBlank(body)) {
try {
JsonObject json = new JsonObject(body);
String upstreamMsg = json.getString("msg");
Object code = json.getValue("code");
if (StringUtils.isNotBlank(upstreamMsg)) {
message.append(", 上游返回: ").append(upstreamMsg);
if (code != null) {
message.append(" (code=").append(code).append(")");
}
} else {
message.append(", 响应体: ").append(previewBody(body));
}
} catch (Exception ignored) {
message.append(", 响应体: ").append(previewBody(body));
}
} else {
message.append(", 响应体为空");
}
Object fileName = shareLinkInfo.getOtherParam().get("fileName");
Object fileSize = shareLinkInfo.getOtherParam().get("fileSizeFormat");
if (fileName != null) {
message.append(", 文件: ").append(fileName);
}
if (fileSize != null) {
message.append(", 大小: ").append(fileSize);
}
if (!hasConfiguredAuth()) {
message.append(", 当前为免登录解析,上游可能要求登录、会员或人工处理");
}
return message.toString();
}
private boolean hasConfiguredAuth() {
Object authObj = shareLinkInfo.getOtherParam().get("auths");
if (!(authObj instanceof MultiMap auths)) {
return false;
}
return StringUtils.isNotBlank(auths.get("username")) && StringUtils.isNotBlank(auths.get("password"));
}
private String previewBody(String body) {
int maxLength = 500;
return body.length() <= maxLength ? body : body.substring(0, maxLength) + "...";
} }
// 目录解析 // 目录解析

View File

@@ -414,11 +414,70 @@ public class IzToolWithAuth extends PanBase {
private void down(HttpResponse<Buffer> res2) { private void down(HttpResponse<Buffer> res2) {
MultiMap headers = res2.headers(); MultiMap headers = res2.headers();
if (!headers.contains("Location") || StringUtils.isBlank(headers.get("Location"))) { String location = headers.get("Location");
fail("找不到下载链接可能服务器已被禁止或者配置的认证信息有误"); if (StringUtils.isBlank(location)) {
fail("{}", buildMissingLocationMessage(res2));
return; return;
} }
promise.complete(headers.get("Location")); promise.complete(location);
}
private String buildMissingLocationMessage(HttpResponse<Buffer> response) {
StringBuilder message = new StringBuilder("未获取到下载重定向地址");
message.append(", HTTP ").append(response.statusCode());
String body = null;
try {
body = asText(response);
} catch (Exception e) {
body = "<响应体读取失败: " + e.getMessage() + ">";
}
if (StringUtils.isNotBlank(body)) {
try {
JsonObject json = new JsonObject(body);
String upstreamMsg = json.getString("msg");
Object code = json.getValue("code");
if (StringUtils.isNotBlank(upstreamMsg)) {
message.append(", 上游返回: ").append(upstreamMsg);
if (code != null) {
message.append(" (code=").append(code).append(")");
}
} else {
message.append(", 响应体: ").append(previewBody(body));
}
} catch (Exception ignored) {
message.append(", 响应体: ").append(previewBody(body));
}
} else {
message.append(", 响应体为空");
}
Object fileName = shareLinkInfo.getOtherParam().get("fileName");
Object fileSize = shareLinkInfo.getOtherParam().get("fileSizeFormat");
if (fileName != null) {
message.append(", 文件: ").append(fileName);
}
if (fileSize != null) {
message.append(", 大小: ").append(fileSize);
}
if (!hasConfiguredAuth()) {
message.append(", 当前为免登录解析,上游可能要求登录、会员或人工处理");
}
return message.toString();
}
private boolean hasConfiguredAuth() {
Object authObj = shareLinkInfo.getOtherParam().get("auths");
if (!(authObj instanceof MultiMap auths)) {
return false;
}
return StringUtils.isNotBlank(auths.get("username")) && StringUtils.isNotBlank(auths.get("password"));
}
private String previewBody(String body) {
int maxLength = 500;
return body.length() <= maxLength ? body : body.substring(0, maxLength) + "...";
} }
// 目录解析 // 目录解析

View File

@@ -15,6 +15,8 @@ import org.openjdk.nashorn.api.scripting.ScriptObjectMirror;
import javax.script.ScriptException; import javax.script.ScriptException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -31,6 +33,20 @@ public class LzTool extends PanBase {
WebClientSession webClientSession = WebClientSession.create(clientNoRedirects); WebClientSession webClientSession = WebClientSession.create(clientNoRedirects);
public static final String SHARE_URL_PREFIX = "https://w1.lanzn.com/"; public static final String SHARE_URL_PREFIX = "https://w1.lanzn.com/";
// 静态编译的正则表达式,避免每次调用都重新编译
private static final Pattern FILE_NAME_PATTERN = Pattern.compile("padding: 56px 0px 20px 0px;\">(.*?)<|filenajax\">(.*?)<");
private static final Pattern FILE_SIZE_PATTERN = Pattern.compile(">文件大小:</span>(.*?)<br>|\"n_filesize\">大小:(.*?)</div>");
private static final Pattern SHARE_USER_PATTERN = Pattern.compile(">分享用户:</span><font>(.*?)</font>|获取<span>(.*?)</span>的文件|\"user-name\">(.*?)</");
private static final Pattern DESCRIPTION_PATTERN = Pattern.compile("(?s)文件描述:</span><br>(.*?)</td>|class=\"n_box_des\">(.*?)</div>");
private static final Pattern FILE_ID_PATTERN = Pattern.compile("\\?f=(.*?)&|fid = (.*?);");
private static final Pattern CREATE_TIME_PATTERN = Pattern.compile(">上传时间:</span>(.*?)<");
private static final Pattern URL_DATE_PATTERN = Pattern.compile("(\\d{4}/\\d{1,2}/\\d{1,2})");
private static final Pattern ARG1_PATTERN = Pattern.compile("var arg1='([^']+)'");
private static final Pattern IFRAME_SRC_PATTERN = Pattern.compile("src=\"(/fn\\?[a-zA-Z\\d_+/=]{16,})\"");
private static final Pattern RELATIVE_TIME_PATTERN = Pattern.compile("^(\\d+|几)\\s*(分钟|小时)前$");
private static final Pattern DATE_PATTERN = Pattern.compile("^(\\d{4})\\s*[-/年]\\s*(\\d{1,2})\\s*[-/月]\\s*(\\d{1,2})\\s*日?$");
private static final Pattern MONTH_DAY_PATTERN = Pattern.compile("^(\\d{1,2})\\s*月\\s*(\\d{1,2})\\s*日?$");
MultiMap headers0 = HeaderUtils.parseHeaders(""" MultiMap headers0 = HeaderUtils.parseHeaders("""
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate Accept-Encoding: gzip, deflate
@@ -62,42 +78,72 @@ public class LzTool extends PanBase {
client.getAbs(sUrl) client.getAbs(sUrl)
.putHeaders(headers0) .putHeaders(headers0)
.send().onSuccess(res -> { .send().onSuccess(res -> {
try {
String html = asText(res); String html = asText(res);
if (html.contains("var arg1='")) { if (hasAcwArg1(html)) {
webClientSession = WebClientSession.create(clientNoRedirects); webClientSession = WebClientSession.create(clientNoRedirects);
setCookie(html, sUrl); if (!setCookie(html, sUrl)) {
fail("蓝奏云反爬 arg1 Cookie 解析失败,页面内容异常");
return;
}
webClientSession.getAbs(sUrl) webClientSession.getAbs(sUrl)
.putHeaders(headers0) .putHeaders(headers0)
.send().onSuccess(res2 -> { .send().onSuccess(res2 -> {
try {
String html2 = asText(res2); String html2 = asText(res2);
doParser(html2, pwd, sUrl); doParser(html2, pwd, sUrl);
}); } catch (Exception e) {
fail("蓝奏云页面响应处理异常: {}", e.getMessage());
}
}).onFailure(handleFail(sUrl));
} else { } else {
doParser(html, pwd, sUrl); doParser(html, pwd, sUrl);
} }
} catch (Exception e) {
fail("蓝奏云页面响应处理异常: {}", e.getMessage());
}
}).onFailure(handleFail(sUrl)); }).onFailure(handleFail(sUrl));
return promise.future(); return promise.future();
} }
private void doParser(String html, String pwd, String sUrl) { private void doParser(String html, String pwd, String sUrl) {
if (html == null || html.isBlank()) {
fail("蓝奏云页面响应为空");
return;
}
if (isShareCancelledPage(html)) {
fail("分享已失效或文件已取消分享");
return;
}
// 检测是否为目录分享链接 (含 /s/、/b/ 路径段或 b 开头的路径段) // 检测是否为目录分享链接 (含 /s/、/b/ 路径段或 b 开头的路径段)
if (sUrl.matches(".*/(s|b)/[^/]+.*") || sUrl.matches(".*/b[^/]+.*")) { if (sUrl.matches(".*/(s|b)/[^/]+.*") || sUrl.matches(".*/b[^/]+.*")) {
fail("该链接为蓝奏云目录分享,请使用目录解析接口"); fail("该链接为蓝奏云目录分享,请使用目录解析接口");
return; return;
} }
// 若仍是校验页 (parse()中cookie域名与实际URL不匹配时会出现), 重试一次 // 若仍是校验页 (parse()中cookie域名与实际URL不匹配时会出现), 重试一次
if (html.contains("var arg1='")) { if (hasAcwArg1(html)) {
webClientSession = WebClientSession.create(clientNoRedirects); webClientSession = WebClientSession.create(clientNoRedirects);
setCookie(html, sUrl); if (!setCookie(html, sUrl)) {
fail("蓝奏云反爬 arg1 Cookie 解析失败,页面内容异常");
return;
}
webClientSession.getAbs(sUrl).putHeaders(headers0).send().onSuccess(res -> { webClientSession.getAbs(sUrl).putHeaders(headers0).send().onSuccess(res -> {
try {
String html2 = asText(res); String html2 = asText(res);
if (html2.contains("var arg1='")) { if (isShareCancelledPage(html2)) {
fail("分享已失效或文件已取消分享");
return;
}
if (hasAcwArg1(html2)) {
fail("蓝奏云反爬校验失败,请稍后重试"); fail("蓝奏云反爬校验失败,请稍后重试");
return; return;
} }
doParserInternal(html2, pwd, sUrl); doParserInternal(html2, pwd, sUrl);
} catch (Exception e) {
fail("蓝奏云页面响应处理异常: {}", e.getMessage());
}
}).onFailure(handleFail(sUrl)); }).onFailure(handleFail(sUrl));
return; return;
} }
@@ -105,14 +151,21 @@ public class LzTool extends PanBase {
} }
private void doParserInternal(String html, String pwd, String sUrl) { private void doParserInternal(String html, String pwd, String sUrl) {
if (html == null || html.isBlank()) {
fail("蓝奏云页面响应为空");
return;
}
if (isShareCancelledPage(html)) {
fail("分享已失效或文件已取消分享");
return;
}
try { try {
setFileInfo(html, shareLinkInfo); setFileInfo(html, shareLinkInfo);
} catch (Exception e) { } catch (Exception e) {
log.error("文件信息解析异常", e); log.error("文件信息解析异常", e);
} }
// 匹配iframe // 匹配iframe
Pattern compile = Pattern.compile("src=\"(/fn\\?[a-zA-Z\\d_+/=]{16,})\""); Matcher matcher = IFRAME_SRC_PATTERN.matcher(html);
Matcher matcher = compile.matcher(html);
// 没有Iframe说明是加密分享, 匹配sign通过密码请求下载页面 // 没有Iframe说明是加密分享, 匹配sign通过密码请求下载页面
if (!matcher.find()) { if (!matcher.find()) {
try { try {
@@ -126,14 +179,29 @@ public class LzTool extends PanBase {
// 没有密码 // 没有密码
String iframePath = matcher.group(1); String iframePath = matcher.group(1);
String absoluteURI = SHARE_URL_PREFIX + iframePath; String absoluteURI = SHARE_URL_PREFIX + iframePath;
webClientSession.getAbs(absoluteURI).putHeaders(headers0).send().onSuccess(res2 -> { // 创建局部副本,避免修改实例字段导致累积
MultiMap headersCopy = MultiMap.caseInsensitiveMultiMap().addAll(headers0);
headersCopy.add("Referer", absoluteURI);
webClientSession.getAbs(absoluteURI).putHeaders(headersCopy).send().onSuccess(res2 -> {
try {
String html2 = asText(res2); String html2 = asText(res2);
if (isShareCancelledPage(html2)) {
fail("分享已失效或文件已取消分享");
return;
}
String jsText = getJsText(html2); String jsText = getJsText(html2);
if (jsText == null) { if (jsText == null) {
headers0.add("Referer", absoluteURI); if (!setCookie(html2, absoluteURI)) {
setCookie(html2, absoluteURI); fail("蓝奏云反爬 arg1 Cookie 解析失败,页面内容异常");
return;
}
webClientSession.getAbs(absoluteURI).send().onSuccess(res3 -> { webClientSession.getAbs(absoluteURI).send().onSuccess(res3 -> {
try {
String html3 = asText(res3); String html3 = asText(res3);
if (isShareCancelledPage(html3)) {
fail("分享已失效或文件已取消分享");
return;
}
String jsText3 = getJsText(html3); String jsText3 = getJsText(html3);
if (jsText3 != null) { if (jsText3 != null) {
try { try {
@@ -145,7 +213,10 @@ public class LzTool extends PanBase {
} else { } else {
fail(SHARE_URL_PREFIX + iframePath + " -> " + sUrl + ": 获取失败0, 可能分享已失效"); fail(SHARE_URL_PREFIX + iframePath + " -> " + sUrl + ": 获取失败0, 可能分享已失效");
} }
}); } catch (Exception e) {
fail("蓝奏云 iframe 响应处理异常: {}", e.getMessage());
}
}).onFailure(handleFail(absoluteURI));
} else { } else {
try { try {
ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, null); ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, null);
@@ -154,18 +225,18 @@ public class LzTool extends PanBase {
fail(e, "js引擎执行失败"); fail(e, "js引擎执行失败");
} }
} }
} catch (Exception e) {
fail("蓝奏云 iframe 响应处理异常: {}", e.getMessage());
}
}).onFailure(handleFail(SHARE_URL_PREFIX)); }).onFailure(handleFail(SHARE_URL_PREFIX));
} }
} }
private void setCookie(String html, String url) { private boolean setCookie(String html, String url) {
int beginIndex = html.indexOf("arg1='") + 6; String arg1 = extractAcwArg1(html);
int endIndex = html.indexOf("';", beginIndex); if (arg1 == null) {
if (beginIndex < 6 || endIndex == -1 || endIndex <= beginIndex) { return false;
fail("蓝奏云反爬 arg1 Cookie 解析失败,页面内容异常");
return;
} }
String arg1 = html.substring(beginIndex, endIndex);
String acw_sc__v2 = AcwScV2Generator.acwScV2Simple(arg1); String acw_sc__v2 = AcwScV2Generator.acwScV2Simple(arg1);
// 从 URL 中动态提取域名(如 lanzoum.com, lanzoux.com 等) // 从 URL 中动态提取域名(如 lanzoum.com, lanzoux.com 等)
String domain = ".lanzn.com"; // 默认兜底 String domain = ".lanzn.com"; // 默认兜底
@@ -184,6 +255,7 @@ public class LzTool extends PanBase {
nettyCookie.setSecure(false); nettyCookie.setSecure(false);
nettyCookie.setHttpOnly(false); nettyCookie.setHttpOnly(false);
webClientSession.cookieStore().put(nettyCookie); webClientSession.cookieStore().put(nettyCookie);
return true;
} }
private String getJsByPwd(String pwd, String html, String subText) { private String getJsByPwd(String pwd, String html, String subText) {
@@ -201,6 +273,9 @@ public class LzTool extends PanBase {
} }
private String getJsText(String html) { private String getJsText(String html) {
if (html == null) {
return null;
}
String jsTagStart = "<script type=\"text/javascript\">"; String jsTagStart = "<script type=\"text/javascript\">";
String jsTagEnd = "</script>"; String jsTagEnd = "</script>";
int index = html.lastIndexOf(jsTagStart); int index = html.lastIndexOf(jsTagStart);
@@ -209,9 +284,38 @@ public class LzTool extends PanBase {
} }
int startPos = index + jsTagStart.length(); int startPos = index + jsTagStart.length();
int endPos = html.indexOf(jsTagEnd, startPos); int endPos = html.indexOf(jsTagEnd, startPos);
if (endPos <= startPos) {
return null;
}
return html.substring(startPos, endPos).replaceAll("<!--.*-->", ""); return html.substring(startPos, endPos).replaceAll("<!--.*-->", "");
} }
static String extractAcwArg1(String html) {
if (html == null) {
return null;
}
int beginIndex = html.indexOf("arg1='");
if (beginIndex < 0) {
return null;
}
beginIndex += 6;
int endIndex = html.indexOf("';", beginIndex);
if (endIndex <= beginIndex) {
return null;
}
return html.substring(beginIndex, endIndex);
}
static boolean isShareCancelledPage(String html) {
return html != null
&& ((html.contains("来晚啦") && html.contains("取消分享"))
|| (html.contains("class=\"off\"") && html.contains("取消分享")));
}
private static boolean hasAcwArg1(String html) {
return html != null && html.contains("var arg1='");
}
private void getDownURL(String key, Map<String, ?> obj) { private void getDownURL(String key, Map<String, ?> obj) {
if (obj == null) { if (obj == null) {
fail("需要访问密码"); fail("需要访问密码");
@@ -225,7 +329,7 @@ public class LzTool extends PanBase {
}); });
MultiMap headers = HeaderUtils.parseHeaders(""" MultiMap headers = HeaderUtils.parseHeaders("""
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br, zstd Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6 Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cache-Control: no-cache Cache-Control: no-cache
Connection: keep-alive Connection: keep-alive
@@ -261,13 +365,21 @@ public class LzTool extends PanBase {
headers.remove("Referer"); headers.remove("Referer");
webClientSession.getAbs(downUrl).putHeaders(headers).send() webClientSession.getAbs(downUrl).putHeaders(headers).send()
.onSuccess(res3 -> { .onSuccess(res3 -> {
try {
String location = res3.headers().get("Location"); String location = res3.headers().get("Location");
if (location == null) { if (location == null) {
String text = asText(res3); String text = asText(res3);
if (isShareCancelledPage(text)) {
fail(downUrl + " -> 分享已失效或文件已取消分享");
return;
}
// 使用cookie 再请求一次 // 使用cookie 再请求一次
headers.add("Referer", downUrl); headers.add("Referer", downUrl);
int beginIndex = text.indexOf("arg1='") + 6; String arg1 = extractAcwArg1(text);
String arg1 = text.substring(beginIndex, text.indexOf("';", beginIndex)); if (arg1 == null) {
fail(downUrl + " -> 蓝奏云反爬 arg1 Cookie 解析失败,可能分享已失效");
return;
}
String acw_sc__v2 = AcwScV2Generator.acwScV2Simple(arg1); String acw_sc__v2 = AcwScV2Generator.acwScV2Simple(arg1);
// 从 downUrl 中动态提取域名 // 从 downUrl 中动态提取域名
String downDomain = ".lanrar.com"; String downDomain = ".lanrar.com";
@@ -287,16 +399,23 @@ public class LzTool extends PanBase {
webClientSession2.cookieStore().put(nettyCookie); webClientSession2.cookieStore().put(nettyCookie);
webClientSession2.getAbs(downUrl).putHeaders(headers).send() webClientSession2.getAbs(downUrl).putHeaders(headers).send()
.onSuccess(res4 -> { .onSuccess(res4 -> {
try {
String location0 = res4.headers().get("Location"); String location0 = res4.headers().get("Location");
if (location0 == null) { if (location0 == null) {
fail(downUrl + " -> 直链获取失败2, 可能分享已失效"); fail(downUrl + " -> 直链获取失败2, 可能分享已失效");
} else { } else {
setDateAndComplete(location0); setDateAndComplete(location0);
} }
} catch (Exception e) {
fail("蓝奏云直链二次响应处理异常: {}", e.getMessage());
}
}).onFailure(handleFail(downUrl)); }).onFailure(handleFail(downUrl));
return; return;
} }
setDateAndComplete(location); setDateAndComplete(location);
} catch (Exception e) {
fail("蓝奏云直链响应处理异常: {}", e.getMessage());
}
}) })
.onFailure(handleFail(downUrl)); .onFailure(handleFail(downUrl));
} catch (Exception e) { } catch (Exception e) {
@@ -307,10 +426,9 @@ public class LzTool extends PanBase {
private void setDateAndComplete(String location0) { private void setDateAndComplete(String location0) {
// 分享时间 提取url中的时间戳格式lanzoui.com/abc/abc/yyyy/mm/dd/ // 分享时间 提取url中的时间戳格式lanzoui.com/abc/abc/yyyy/mm/dd/
String regex = "(\\d{4}/\\d{1,2}/\\d{1,2})"; Matcher matcher = URL_DATE_PATTERN.matcher(location0);
Matcher matcher = Pattern.compile(regex).matcher(location0);
if (matcher.find()) { if (matcher.find()) {
String dateStr = matcher.group().replace("/", "-"); String dateStr = parseLanzouFileTime(matcher.group());
((FileInfo)shareLinkInfo.getOtherParam().get("fileInfo")).setCreateTime(dateStr); ((FileInfo)shareLinkInfo.getOtherParam().get("fileInfo")).setCreateTime(dateStr);
} }
promise.complete(location0); promise.complete(location0);
@@ -338,26 +456,45 @@ public class LzTool extends PanBase {
String pwd = shareLinkInfo.getSharePassword(); String pwd = shareLinkInfo.getSharePassword();
webClientSession.getAbs(sUrl).send().onSuccess(res -> { webClientSession.getAbs(sUrl).send().onSuccess(res -> {
try {
String html = asText(res); String html = asText(res);
// 检查是否需要 cookie 验证 // 检查是否需要 cookie 验证
if (html.contains("var arg1='")) { if (hasAcwArg1(html)) {
webClientSession = WebClientSession.create(clientNoRedirects); webClientSession = WebClientSession.create(clientNoRedirects);
setCookie(html, sUrl); if (!setCookie(html, sUrl)) {
promise.tryFail(baseMsg() + "蓝奏云反爬 arg1 Cookie 解析失败,页面内容异常");
return;
}
// 重新请求 // 重新请求
webClientSession.getAbs(sUrl).send().onSuccess(res2 -> { webClientSession.getAbs(sUrl).send().onSuccess(res2 -> {
try {
handleFileListParse(asText(res2), pwd, sUrl, promise); handleFileListParse(asText(res2), pwd, sUrl, promise);
}).onFailure(err -> promise.fail(err)); } catch (Exception e) {
promise.tryFail(e);
}
}).onFailure(promise::tryFail);
return; return;
} }
handleFileListParse(html, pwd, sUrl, promise); handleFileListParse(html, pwd, sUrl, promise);
}).onFailure(err -> promise.fail(err)); } catch (Exception e) {
promise.tryFail(e);
}
}).onFailure(promise::tryFail);
return promise.future(); return promise.future();
} }
private void handleFileListParse(String html, String pwd, String sUrl, Promise<List<FileInfo>> promise) { private void handleFileListParse(String html, String pwd, String sUrl, Promise<List<FileInfo>> promise) {
if (html == null || html.isBlank()) {
promise.tryFail(baseMsg() + "蓝奏云页面响应为空");
return;
}
if (isShareCancelledPage(html)) {
promise.tryFail(baseMsg() + "分享已失效或文件已取消分享");
return;
}
// 检测是否为文件分享链接 (不含 /s/、/b/ 路径段且不含 b 开头的路径段) // 检测是否为文件分享链接 (不含 /s/、/b/ 路径段且不含 b 开头的路径段)
if (!sUrl.matches(".*/(s|b)/[^/]+.*") && !sUrl.matches(".*/b[^/]+.*")) { if (!sUrl.matches(".*/(s|b)/[^/]+.*") && !sUrl.matches(".*/b[^/]+.*")) {
promise.fail(baseMsg() + "该链接为蓝奏云文件分享,请使用文件解析接口"); promise.tryFail(baseMsg() + "该链接为蓝奏云文件分享,请使用文件解析接口");
return; return;
} }
try { try {
@@ -371,28 +508,43 @@ public class LzTool extends PanBase {
String url = SHARE_URL_PREFIX + "filemoreajax.php?file=" + data.get("fid"); String url = SHARE_URL_PREFIX + "filemoreajax.php?file=" + data.get("fid");
webClientSession.postAbs(url).putHeaders(headers).sendForm(map).onSuccess(res2 -> { webClientSession.postAbs(url).putHeaders(headers).sendForm(map).onSuccess(res2 -> {
try {
String resBody = asText(res2); String resBody = asText(res2);
// 再次检查是否需要 cookie 验证 // 再次检查是否需要 cookie 验证
if (resBody.contains("var arg1='")) { if (hasAcwArg1(resBody)) {
setCookie(resBody, url); if (!setCookie(resBody, url)) {
promise.tryFail(baseMsg() + "蓝奏云反爬 arg1 Cookie 解析失败,页面内容异常");
return;
}
// 重新请求 // 重新请求
webClientSession.postAbs(url).putHeaders(headers).sendForm(map).onSuccess(res3 -> { webClientSession.postAbs(url).putHeaders(headers).sendForm(map).onSuccess(res3 -> {
try {
handleFileListResponse(asText(res3), promise); handleFileListResponse(asText(res3), promise);
}).onFailure(err -> promise.fail(err)); } catch (Exception e) {
promise.tryFail(e);
}
}).onFailure(promise::tryFail);
return; return;
} }
handleFileListResponse(resBody, promise); handleFileListResponse(resBody, promise);
}).onFailure(err -> promise.fail(err)); } catch (Exception e) {
promise.tryFail(e);
}
}).onFailure(promise::tryFail);
} catch (ScriptException | NoSuchMethodException | RuntimeException e) { } catch (ScriptException | NoSuchMethodException | RuntimeException e) {
promise.fail(e); promise.tryFail(e);
} }
} }
private void handleFileListResponse(String responseBody, Promise<List<FileInfo>> promise) { private void handleFileListResponse(String responseBody, Promise<List<FileInfo>> promise) {
try { try {
if (responseBody == null || responseBody.isBlank()) {
promise.tryFail(baseMsg() + "蓝奏云文件列表响应为空");
return;
}
JsonObject fileListJson = new JsonObject(responseBody); JsonObject fileListJson = new JsonObject(responseBody);
if (fileListJson.getInteger("zt") != 1) { if (fileListJson.getInteger("zt") != 1) {
promise.fail(baseMsg() + fileListJson.getString("info")); promise.tryFail(baseMsg() + fileListJson.getString("info"));
return; return;
} }
List<FileInfo> list = new ArrayList<>(); List<FileInfo> list = new ArrayList<>();
@@ -423,7 +575,7 @@ public class LzTool extends PanBase {
String param = CommonUtils.urlBase64Encode(paramJson.encode()); String param = CommonUtils.urlBase64Encode(paramJson.encode());
fileInfo.setFileName(fileName) fileInfo.setFileName(fileName)
.setFileId(id) .setFileId(id)
.setCreateTime(fileJson.getString("time")) .setCreateTime(parseLanzouFileTime(fileJson.getString("time")))
.setFileType(fileJson.getString("icon")) .setFileType(fileJson.getString("icon"))
.setSizeStr(fileJson.getString("size")) .setSizeStr(fileJson.getString("size"))
.setSize(sizeNum) .setSize(sizeNum)
@@ -436,10 +588,46 @@ public class LzTool extends PanBase {
}); });
promise.complete(list); promise.complete(list);
} catch (Exception e) { } catch (Exception e) {
promise.fail(e); promise.tryFail(e);
} }
} }
private static String parseLanzouFileTime(String timeText) {
if (timeText == null || timeText.isBlank()) {
return timeText;
}
String normalized = timeText.trim().replaceAll("\\s+", " ");
Matcher matcher = RELATIVE_TIME_PATTERN.matcher(normalized);
if (matcher.matches()) {
int amount = "".equals(matcher.group(1)) ? 1 : Integer.parseInt(matcher.group(1));
String unit = matcher.group(2);
LocalDateTime time = LocalDateTime.now();
if ("小时".equals(unit)) {
time = time.minusHours(amount);
} else {
time = time.minusMinutes(amount);
}
return time.toLocalDate().toString();
}
matcher = DATE_PATTERN.matcher(normalized);
if (matcher.matches()) {
return LocalDate.of(
Integer.parseInt(matcher.group(1)),
Integer.parseInt(matcher.group(2)),
Integer.parseInt(matcher.group(3))
).toString();
}
matcher = MONTH_DAY_PATTERN.matcher(normalized);
if (matcher.matches()) {
return LocalDate.of(
LocalDate.now().getYear(),
Integer.parseInt(matcher.group(1)),
Integer.parseInt(matcher.group(2))
).toString();
}
return normalized;
}
@Override @Override
public Future<String> parseById() { public Future<String> parseById() {
JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson"); JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson");
@@ -455,13 +643,13 @@ public class LzTool extends PanBase {
shareLinkInfo.getOtherParam().put("fileInfo", fileInfo); shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
try { try {
// 提取文件名 // 提取文件名
String fileName = CommonUtils.extract(html, Pattern.compile("padding: 56px 0px 20px 0px;\">(.*?)<|filenajax\">(.*?)<")); String fileName = CommonUtils.extract(html, FILE_NAME_PATTERN);
String sizeStr = CommonUtils.extract(html, Pattern.compile(">文件大小:</span>(.*?)<br>|\"n_filesize\">大小:(.*?)</div>")); String sizeStr = CommonUtils.extract(html, FILE_SIZE_PATTERN);
String createBy = CommonUtils.extract(html, Pattern.compile(">分享用户:</span><font>(.*?)</font>|获取<span>(.*?)</span>的文件|\"user-name\">(.*?)</")); String createBy = CommonUtils.extract(html, SHARE_USER_PATTERN);
String description = CommonUtils.extract(html, Pattern.compile("(?s)文件描述:</span><br>(.*?)</td>|class=\"n_box_des\">(.*?)</div>")); String description = CommonUtils.extract(html, DESCRIPTION_PATTERN);
// String icon = CommonUtils.extract(html, Pattern.compile("class=\"n_file_icon\" src=\"(.*?)\"")); // String icon = CommonUtils.extract(html, Pattern.compile("class=\"n_file_icon\" src=\"(.*?)\""));
String fileId = CommonUtils.extract(html, Pattern.compile("\\?f=(.*?)&|fid = (.*?);")); String fileId = CommonUtils.extract(html, FILE_ID_PATTERN);
String createTime = CommonUtils.extract(html, Pattern.compile(">上传时间:</span>(.*?)<")); String createTime = CommonUtils.extract(html, CREATE_TIME_PATTERN);
try { try {
fileInfo.setFileName(fileName) fileInfo.setFileName(fileName)
.setCreateBy(createBy) .setCreateBy(createBy)
@@ -469,7 +657,7 @@ public class LzTool extends PanBase {
.setDescription(description) .setDescription(description)
.setFileType("file") .setFileType("file")
.setFileId(fileId) .setFileId(fileId)
.setCreateTime(createTime); .setCreateTime(parseLanzouFileTime(createTime));
if (sizeStr != null && !sizeStr.isBlank()) { if (sizeStr != null && !sizeStr.isBlank()) {
long bytes = FileSizeConverter.convertToBytes(sizeStr); long bytes = FileSizeConverter.convertToBytes(sizeStr);
fileInfo.setSize(bytes).setSizeStr(FileSizeConverter.convertToReadableSize(bytes)); fileInfo.setSize(bytes).setSizeStr(FileSizeConverter.convertToReadableSize(bytes));

View File

@@ -21,6 +21,8 @@ public class MkgsTool extends PanBase {
public static final String API_URL = "https://m.kugou.com/app/i/getSongInfo.php?cmd=playInfo&hash={hash}"; public static final String API_URL = "https://m.kugou.com/app/i/getSongInfo.php?cmd=playInfo&hash={hash}";
private static final Pattern HASH_PATTERN = Pattern.compile("\"hash\"\\s*:\\s*\"([A-F0-9]+)\"");
private static final MultiMap headers = MultiMap.caseInsensitiveMultiMap(); private static final MultiMap headers = MultiMap.caseInsensitiveMultiMap();
static { static {
// 设置 User-Agent // 设置 User-Agent
@@ -78,10 +80,7 @@ public class MkgsTool extends PanBase {
protected void downUrl(String locationURL) { protected void downUrl(String locationURL) {
client.getAbs(locationURL).putHeaders(headers).send().onSuccess(res2->{ client.getAbs(locationURL).putHeaders(headers).send().onSuccess(res2->{
String body = res2.bodyAsString(); String body = res2.bodyAsString();
// 正则表达式匹配 hash 字段 Matcher matcher = HASH_PATTERN.matcher(body);
String regex = "\"hash\"\s*:\s*\"([A-F0-9]+)\"";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(body);
// 查找并输出 hash 字段的值 // 查找并输出 hash 字段的值
if (matcher.find()) { if (matcher.find()) {

View File

@@ -19,6 +19,8 @@ public class MkwTool extends PanBase {
public static final String API_URL = "https://www.kuwo.cn/api/v1/www/music/playUrl?mid={mid}&type=music&httpsStatus=1&reqId=&plat=web_www&from="; public static final String API_URL = "https://www.kuwo.cn/api/v1/www/music/playUrl?mid={mid}&type=music&httpsStatus=1&reqId=&plat=web_www&from=";
private static final Pattern COOKIE_PATTERN = Pattern.compile("([A-Za-z0-9_]+)=([A-Za-z0-9]+)");
public MkwTool(ShareLinkInfo shareLinkInfo) { public MkwTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo); super(shareLinkInfo);
@@ -29,12 +31,17 @@ public class MkwTool extends PanBase {
clientSession.getAbs(shareUrl).send().onSuccess(result -> { clientSession.getAbs(shareUrl).send().onSuccess(result -> {
String cookie = result.headers().get("set-cookie"); String cookie = result.headers().get("set-cookie");
if (cookie != null && !cookie.isEmpty()) { if (cookie == null || cookie.isEmpty()) {
fail("未获取到 cookie无法继续解析");
return;
}
Matcher matcher = COOKIE_PATTERN.matcher(cookie);
if (!matcher.find()) {
fail("cookie 格式不匹配");
return;
}
String regex = "([A-Za-z0-9_]+)=([A-Za-z0-9]+)";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(cookie);
if (matcher.find()) {
log.debug("cookie key: {}", matcher.group(1)); log.debug("cookie key: {}", matcher.group(1));
log.debug("cookie value: {}", matcher.group(2)); log.debug("cookie value: {}", matcher.group(2));
@@ -57,11 +64,8 @@ public class MkwTool extends PanBase {
log.error("解析失败", e); log.error("解析失败", e);
fail("解析失败"); fail("解析失败");
} }
}); }).onFailure(handleFail("获取下载链接失败"));
} }).onFailure(handleFail("请求分享页面失败"));
}
});
return promise.future(); return promise.future();
} }

View File

@@ -16,6 +16,9 @@ import java.util.regex.Pattern;
* 下载链接需要Referer: https://link.yunpan.com/ * 下载链接需要Referer: https://link.yunpan.com/
*/ */
public class P360Tool extends PanBase { public class P360Tool extends PanBase {
private static final Pattern NID_PATTERN = Pattern.compile("\"nid\": \"([^\"]+)\"");
public P360Tool(ShareLinkInfo shareLinkInfo) { public P360Tool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo); super(shareLinkInfo);
} }
@@ -43,9 +46,7 @@ public class P360Tool extends PanBase {
clientSession.getAbs(url) clientSession.getAbs(url)
.send() .send()
.onSuccess(res -> { .onSuccess(res -> {
// find "nid": "17402043311959599" Matcher matcher = NID_PATTERN.matcher(res.bodyAsString());
Pattern compile = Pattern.compile("\"nid\": \"([^\"]+)\"");
Matcher matcher = compile.matcher(res.bodyAsString());
AtomicReference<String> nid = new AtomicReference<>(); AtomicReference<String> nid = new AtomicReference<>();
if (matcher.find()) { if (matcher.find()) {
nid.set(matcher.group(1)); nid.set(matcher.group(1));
@@ -69,7 +70,7 @@ public class P360Tool extends PanBase {
clientSession.getAbs(url) clientSession.getAbs(url)
.send() .send()
.onSuccess(res3 -> { .onSuccess(res3 -> {
Matcher matcher1 = compile.matcher(res3.bodyAsString()); Matcher matcher1 = NID_PATTERN.matcher(res3.bodyAsString());
if (matcher1.find()) { if (matcher1.find()) {
nid.set(matcher1.group(1)); nid.set(matcher1.group(1));
} else { } else {

View File

@@ -14,6 +14,25 @@ import java.util.regex.Pattern;
*/ */
public class PcxTool extends PanBase { public class PcxTool extends PanBase {
private static final Pattern TITLE_PATTERN =
Pattern.compile("<title>([^<]+)</title>");
private static final Pattern FILENAME_INPUT_PATTERN =
Pattern.compile("<input id=\"filename\" type=\"hidden\" value=\"([^\"]+)\"");
private static final Pattern FILESIZE_PATTERN =
Pattern.compile("['\"]filesize['\"]\\s*:\\s*['\"]([^'\"]+)['\"]");
private static final Pattern SUFFIX_PATTERN =
Pattern.compile("['\"]suffix['\"]\\s*:\\s*['\"]([^'\"]+)['\"]");
private static final Pattern OBJECT_ID_PATTERN =
Pattern.compile("['\"]objectId['\"]\\s*:\\s*['\"]([^'\"]+)['\"]");
private static final Pattern CREATOR_PATTERN =
Pattern.compile("['\"]creator['\"]\\s*:\\s*['\"]([^'\"]+)['\"]");
private static final Pattern UPLOAD_DATE_PATTERN =
Pattern.compile("['\"]uploadDate['\"]\\s*:\\s*(\\d+)");
private static final Pattern THUMBNAIL_PATTERN =
Pattern.compile("['\"]thumbnail['\"]\\s*:\\s*['\"]([^'\"]+)['\"]");
private static final Pattern DOWNLOAD_PATTERN =
Pattern.compile("['\"]download['\"]\\s*:\\s*['\"]([^'\"]+)['\"]");
public PcxTool(ShareLinkInfo shareLinkInfo) { public PcxTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo); super(shareLinkInfo);
} }
@@ -44,9 +63,7 @@ public class PcxTool extends PanBase {
* 从HTML中提取download链接 * 从HTML中提取download链接
*/ */
private String extractDownloadUrl(String html) { private String extractDownloadUrl(String html) {
// 匹配 'download': 'https://xxx' 或 "download": "https://xxx" Matcher matcher = DOWNLOAD_PATTERN.matcher(html);
Pattern pattern = Pattern.compile("['\"]download['\"]\\s*:\\s*['\"]([^'\"]+)['\"]");
Matcher matcher = pattern.matcher(html);
if (matcher.find()) { if (matcher.find()) {
return matcher.group(1); return matcher.group(1);
} }
@@ -61,13 +78,13 @@ public class PcxTool extends PanBase {
FileInfo fileInfo = new FileInfo(); FileInfo fileInfo = new FileInfo();
// 提取文件名:从<title>标签或文件名input // 提取文件名:从<title>标签或文件名input
String fileName = extractByRegex(html, "<title>([^<]+)</title>"); String fileName = extractByRegex(html, TITLE_PATTERN);
if (fileName == null) { if (fileName == null) {
fileName = extractByRegex(html, "<input id=\"filename\" type=\"hidden\" value=\"([^\"]+)\""); fileName = extractByRegex(html, FILENAME_INPUT_PATTERN);
} }
// 提取文件大小:'filesize': 'xxx' 或 "filesize": "xxx" // 提取文件大小:'filesize': 'xxx' 或 "filesize": "xxx"
String fileSizeStr = extractByRegex(html, "['\"]filesize['\"]\\s*:\\s*['\"]([^'\"]+)['\"]"); String fileSizeStr = extractByRegex(html, FILESIZE_PATTERN);
Long fileSize = null; Long fileSize = null;
if (fileSizeStr != null) { if (fileSizeStr != null) {
try { try {
@@ -76,19 +93,19 @@ public class PcxTool extends PanBase {
} }
// 提取文件类型/后缀:'suffix': 'xxx' 或 "suffix": "xxx" // 提取文件类型/后缀:'suffix': 'xxx' 或 "suffix": "xxx"
String suffix = extractByRegex(html, "['\"]suffix['\"]\\s*:\\s*['\"]([^'\"]+)['\"]"); String suffix = extractByRegex(html, SUFFIX_PATTERN);
// 提取objectId文件ID'objectId': 'xxx' 或 "objectId": "xxx" // 提取objectId文件ID'objectId': 'xxx' 或 "objectId": "xxx"
String objectId = extractByRegex(html, "['\"]objectId['\"]\\s*:\\s*['\"]([^'\"]+)['\"]"); String objectId = extractByRegex(html, OBJECT_ID_PATTERN);
// 提取创建者:'creator': 'xxx' 或 "creator": "xxx" // 提取创建者:'creator': 'xxx' 或 "creator": "xxx"
String creator = extractByRegex(html, "['\"]creator['\"]\\s*:\\s*['\"]([^'\"]+)['\"]"); String creator = extractByRegex(html, CREATOR_PATTERN);
// 提取上传时间:'uploadDate': timestamp // 提取上传时间:'uploadDate': timestamp
String uploadDate = extractByRegex(html, "['\"]uploadDate['\"]\\s*:\\s*(\\d+)"); String uploadDate = extractByRegex(html, UPLOAD_DATE_PATTERN);
// 提取缩略图:'thumbnail': 'xxx' 或 "thumbnail": "xxx" // 提取缩略图:'thumbnail': 'xxx' 或 "thumbnail": "xxx"
String thumbnail = extractByRegex(html, "['\"]thumbnail['\"]\\s*:\\s*['\"]([^'\"]+)['\"]"); String thumbnail = extractByRegex(html, THUMBNAIL_PATTERN);
// 设置文件信息 // 设置文件信息
if (fileName != null) { if (fileName != null) {
@@ -141,8 +158,7 @@ public class PcxTool extends PanBase {
/** /**
* 使用正则表达式提取内容 * 使用正则表达式提取内容
*/ */
private String extractByRegex(String text, String regex) { private String extractByRegex(String text, Pattern pattern) {
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text); Matcher matcher = pattern.matcher(text);
if (matcher.find()) { if (matcher.find()) {
return matcher.group(1); return matcher.group(1);

View File

@@ -28,6 +28,7 @@ public class PdbTool extends PanBase implements IPanTool {
private static final String API_URL = private static final String API_URL =
"https://www.dropbox.com/sharing/fetch_user_content_link"; "https://www.dropbox.com/sharing/fetch_user_content_link";
static final String COOKIE_KEY = "__Host-js_csrf="; static final String COOKIE_KEY = "__Host-js_csrf=";
private static final Pattern CSRF_TOKEN_PATTERN = Pattern.compile(COOKIE_KEY + "([\\w-]+);");
public PdbTool(ShareLinkInfo shareLinkInfo) { public PdbTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo); super(shareLinkInfo);
@@ -47,7 +48,7 @@ public class PdbTool extends PanBase implements IPanTool {
fail("cookie未找到"); fail("cookie未找到");
return; return;
} }
Matcher matcher = Pattern.compile(COOKIE_KEY + "([\\w-]+);").matcher(collect.get(0)); Matcher matcher = CSRF_TOKEN_PATTERN.matcher(collect.get(0));
String _t; String _t;
if (matcher.find()) { if (matcher.find()) {
_t = matcher.group(1); _t = matcher.group(1);

View File

@@ -23,6 +23,16 @@ import java.util.regex.Pattern;
*/ */
public class PodTool extends PanBase { public class PodTool extends PanBase {
private static final int MAX_RESPONSE_BODY_BYTES = 8 * 1024 * 1024;
private static final java.time.Duration REQUEST_TIMEOUT = java.time.Duration.ofSeconds(30);
// 静态共享的 JDK HttpClient 实例,避免每次调用创建新实例
private static final HttpClient SHARED_HTTP_CLIENT = HttpClient.newBuilder()
.connectTimeout(java.time.Duration.ofSeconds(10))
.build();
private static volatile WorkerExecutor SHARED_WORKER_EXECUTOR;
private static volatile boolean workerExecutorShutdown = false;
/* /*
* https://1drv.ms/w/s!Alg0feQmCv2rnRFd60DQOmMa-Oh_?e=buaRtp --302-> * https://1drv.ms/w/s!Alg0feQmCv2rnRFd60DQOmMa-Oh_?e=buaRtp --302->
* https://api.onedrive.com/v1.0/drives/abfd0a26e47d3458/items/ABFD0A26E47D3458!3729?authkey=!AF3rQNA6Yxr46H8 * https://api.onedrive.com/v1.0/drives/abfd0a26e47d3458/items/ABFD0A26E47D3458!3729?authkey=!AF3rQNA6Yxr46H8
@@ -45,6 +55,13 @@ public class PodTool extends PanBase {
private static final Pattern redirectUrlRegex = private static final Pattern redirectUrlRegex =
Pattern.compile("resid=(?<cid1>[^!]+)!(?<cid2>[^&]+).+&redeem=(?<redeem>.+).*"); Pattern.compile("resid=(?<cid1>[^!]+)!(?<cid2>[^&]+).+&redeem=(?<redeem>.+).*");
private static final Pattern DOWNLOAD_URL_IN_RESPONSE_PATTERN =
Pattern.compile("\"downloadUrl\":\"(?<url>https?://[^\\s\"]+)");
private static final Pattern ACTION_URL_PATTERN =
Pattern.compile("'action'.+(?<url>https://.+)'\\)");
private static final Pattern TOKEN_PATTERN =
Pattern.compile("inputElem\\.value\\s*=\\s*'([^']+)'");
public PodTool(ShareLinkInfo shareLinkInfo) { public PodTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo); super(shareLinkInfo);
} }
@@ -97,7 +114,7 @@ public class PodTool extends PanBase {
sendHttpRequest(url, token).onSuccess(body -> { sendHttpRequest(url, token).onSuccess(body -> {
Matcher matcher1 = Matcher matcher1 =
Pattern.compile("\"downloadUrl\":\"(?<url>https?://[^\s\"]+)").matcher(body); DOWNLOAD_URL_IN_RESPONSE_PATTERN.matcher(body);
if (matcher1.find()) { if (matcher1.find()) {
// 响应体是 JSON 文本URL 中的 '&' 被转义为 \u0026需要反转义 // 响应体是 JSON 文本URL 中的 '&' 被转义为 \u0026需要反转义
complete(unescapeJsonUnicode(matcher1.group("url"))); complete(unescapeJsonUnicode(matcher1.group("url")));
@@ -121,11 +138,7 @@ public class PodTool extends PanBase {
} }
private String matcherUrl(String html) { private String matcherUrl(String html) {
Matcher urlMatcher = ACTION_URL_PATTERN.matcher(html);
// 正则表达式来匹配 URL
String urlRegex = "'action'.+(?<url>https://.+)'\\)";
Pattern urlPattern = Pattern.compile(urlRegex);
Matcher urlMatcher = urlPattern.matcher(html);
if (urlMatcher.find()) { if (urlMatcher.find()) {
String url = urlMatcher.group("url"); String url = urlMatcher.group("url");
@@ -165,10 +178,7 @@ public class PodTool extends PanBase {
private String matcherToken(String html) { private String matcherToken(String html) {
// 正则表达式来匹配 inputElem.value 中的 Token Matcher tokenMatcher = TOKEN_PATTERN.matcher(html);
String tokenRegex = "inputElem\\.value\\s*=\\s*'([^']+)'";
Pattern tokenPattern = Pattern.compile(tokenRegex);
Matcher tokenMatcher = tokenPattern.matcher(html);
if (tokenMatcher.find()) { if (tokenMatcher.find()) {
String token = tokenMatcher.group(1); String token = tokenMatcher.group(1);
@@ -180,11 +190,8 @@ public class PodTool extends PanBase {
public Future<String> sendHttpRequest2(String token, String redeem) { public Future<String> sendHttpRequest2(String token, String redeem) {
Promise<String> promise = Promise.promise(); Promise<String> promise = Promise.promise();
// 构造 HttpClient
HttpClient client = HttpClient.newHttpClient();
// 构造请求的 URI 和头部信息 // 构造请求的 URI 和头部信息
// https://onedrive.live.com/redir?cid=abfd0a26e47d3458&resid=ABFD0A26E47D3458!4465&ithint=file%2cxlsx&e=Ao2uSU&migratedtospo=true&redeem=aHR0cHM6Ly8xZHJ2Lm1zL3gvYy9hYmZkMGEyNmU0N2QzNDU4L0VWZzBmZVFtQ3YwZ2dLdHhFUUFBQUFBQlRQRWVDMTZfZk1EYk5FTjhEdTRta1E_ZT1BbzJ1U1U
String url = ("https://my.microsoftpersonalcontent.com/_api/v2.0/shares/u!%s/driveItem?$select=content" + String url = ("https://my.microsoftpersonalcontent.com/_api/v2.0/shares/u!%s/driveItem?$select=content" +
".downloadUrl").formatted(redeem); ".downloadUrl").formatted(redeem);
String authorizationHeader = "Badger " + token; String authorizationHeader = "Badger " + token;
@@ -192,15 +199,20 @@ public class PodTool extends PanBase {
// 构建请求 // 构建请求
HttpRequest request = HttpRequest.newBuilder() HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url)) .uri(URI.create(url))
.timeout(REQUEST_TIMEOUT)
.header("Authorization", authorizationHeader) .header("Authorization", authorizationHeader)
.build(); .build();
// 发送请求并处理响应 // 发送请求并处理响应(使用共享的 HttpClient
client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) SHARED_HTTP_CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofByteArray())
.thenApply(response -> { .thenApply(response -> {
log.debug("Response Status Code: {}", response.statusCode()); log.debug("Response Status Code: {}", response.statusCode());
log.debug("Response Body: {}", response.body()); promise.complete(toLimitedString(response.body()));
promise.complete(response.body()); return null;
})
.exceptionally(e -> {
log.error("sendHttpRequest2 请求失败: {}", e.getMessage());
promise.fail(e);
return null; return null;
}); });
@@ -208,18 +220,13 @@ public class PodTool extends PanBase {
} }
public Future<String> sendHttpRequest(String url, String token) { public Future<String> sendHttpRequest(String url, String token) {
// 创建一个 WorkerExecutor 用于异步执行阻塞的 HTTP 请求
WorkerExecutor executor = WebClientVertxInit.get().createSharedWorkerExecutor("http-client-worker");
Promise<String> promise = Promise.promise(); Promise<String> promise = Promise.promise();
executor.executeBlocking(() -> { getWorkerExecutor().executeBlocking(() -> {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = null;
try { try {
// 构造请求 // 构造请求
request = HttpRequest.newBuilder() HttpRequest request = HttpRequest.newBuilder()
.uri(new URI(url)) .uri(new URI(url))
.timeout(REQUEST_TIMEOUT)
.header("accept", "text/html,application/xhtml+xml,application/xml;q=0.9," + .header("accept", "text/html,application/xhtml+xml,application/xml;q=0.9," +
"image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;" + "image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;" +
"v=b3;q=0.7") "v=b3;q=0.7")
@@ -244,17 +251,49 @@ public class PodTool extends PanBase {
.POST(HttpRequest.BodyPublishers.ofString("badger_token=" + token)) .POST(HttpRequest.BodyPublishers.ofString("badger_token=" + token))
.build(); .build();
// 发起请求并获取响应 // 发起请求并获取响应(使用共享的 HttpClient
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<byte[]> response = SHARED_HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofByteArray());
// 返回响应体 // 返回响应体
promise.complete(response.body()); promise.complete(toLimitedString(response.body()));
return null; return null;
} catch (URISyntaxException | IOException | InterruptedException e) { } catch (URISyntaxException | IOException | InterruptedException e) {
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
throw new RuntimeException(e); throw new RuntimeException(e);
} }
}); }).onFailure(promise::fail);
return promise.future(); return promise.future();
} }
private static String toLimitedString(byte[] body) {
if (body.length > MAX_RESPONSE_BODY_BYTES) {
throw new IllegalArgumentException("OneDrive响应体过大: " + body.length + " bytes");
}
return new String(body, java.nio.charset.StandardCharsets.UTF_8);
}
private static WorkerExecutor getWorkerExecutor() {
synchronized (PodTool.class) {
if (workerExecutorShutdown) {
throw new IllegalStateException("OneDrive WorkerExecutor 已关闭");
}
if (SHARED_WORKER_EXECUTOR == null) {
SHARED_WORKER_EXECUTOR = WebClientVertxInit.get().createSharedWorkerExecutor("http-client-worker", 8);
}
return SHARED_WORKER_EXECUTOR;
}
}
public static void shutdownWorkerExecutor() {
synchronized (PodTool.class) {
workerExecutorShutdown = true;
if (SHARED_WORKER_EXECUTOR != null) {
SHARED_WORKER_EXECUTOR.close();
SHARED_WORKER_EXECUTOR = null;
}
}
}
} }

View File

@@ -1,6 +1,5 @@
package cn.qaiu.parser.impl; package cn.qaiu.parser.impl;
import cn.qaiu.WebClientVertxInit;
import cn.qaiu.entity.FileInfo; import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo; import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase; import cn.qaiu.parser.PanBase;
@@ -11,7 +10,6 @@ import io.vertx.core.MultiMap;
import io.vertx.core.Promise; import io.vertx.core.Promise;
import io.vertx.core.json.JsonObject; import io.vertx.core.json.JsonObject;
import io.vertx.core.json.pointer.JsonPointer; import io.vertx.core.json.pointer.JsonPointer;
import io.vertx.ext.web.client.WebClient;
import io.vertx.uritemplate.UriTemplate; import io.vertx.uritemplate.UriTemplate;
import java.util.List; import java.util.List;
@@ -53,9 +51,8 @@ public class PvyyTool extends PanBase {
@Override @Override
public Future<String> parse() { public Future<String> parse() {
// 请求downcode // 请求downcode - 使用父类的共享 WebClient 而非创建新实例
WebClient.create(WebClientVertxInit.get()) client.getAbs(api + shareLinkInfo.getShareKey())
.getAbs(api + shareLinkInfo.getShareKey())
.send() .send()
.onSuccess(res -> { .onSuccess(res -> {
if (res.statusCode() == 200) { if (res.statusCode() == 200) {

View File

@@ -63,6 +63,11 @@ public class QQscTool extends PanBase {
x-oidb: {"uint32_command":"0x93d4", "uint32_service_type":"1"} x-oidb: {"uint32_command":"0x93d4", "uint32_service_type":"1"}
"""); """);
private static final Pattern FILESET_ID_PATTERN = Pattern.compile(
"fileset_id[^a-f0-9]*([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})");
private static final Pattern TITLE_PATTERN = Pattern.compile("<title>(.*?)</title>");
public QQscTool(ShareLinkInfo shareLinkInfo) { public QQscTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo); super(shareLinkInfo);
} }
@@ -247,7 +252,9 @@ public class QQscTool extends PanBase {
.put("sort_order", 0))))) .put("sort_order", 0)))))
.put("support_folder_status", true); .put("support_folder_status", true);
MultiMap headers = GET_FILE_LIST_HEADERS.set("Referer", shareLinkInfo.getShareUrl()); // 创建局部副本,避免修改静态 MultiMap 导致并发污染
MultiMap headers = MultiMap.caseInsensitiveMultiMap().addAll(GET_FILE_LIST_HEADERS)
.set("Referer", shareLinkInfo.getShareUrl());
client.postAbs(GET_FILE_LIST_API) client.postAbs(GET_FILE_LIST_API)
.putHeaders(headers) .putHeaders(headers)
@@ -283,9 +290,7 @@ public class QQscTool extends PanBase {
String extractFilesetId(String html) { String extractFilesetId(String html) {
// Nuxt __NUXT_DATA__ 中 fileset_id 出现在缓存 key 的嵌套 JSON 中 // Nuxt __NUXT_DATA__ 中 fileset_id 出现在缓存 key 的嵌套 JSON 中
// 直接匹配 fileset_id 后面最近的 UUID跳过转义引号、冒号等非hex字符 // 直接匹配 fileset_id 后面最近的 UUID跳过转义引号、冒号等非hex字符
Pattern pattern = Pattern.compile( Matcher matcher = FILESET_ID_PATTERN.matcher(html);
"fileset_id[^a-f0-9]*([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})");
Matcher matcher = pattern.matcher(html);
if (matcher.find()) { if (matcher.find()) {
return matcher.group(1); return matcher.group(1);
} }
@@ -326,8 +331,7 @@ public class QQscTool extends PanBase {
} }
public static String extractFileNameFromTitle(String content) { public static String extractFileNameFromTitle(String content) {
Pattern pattern = Pattern.compile("<title>(.*?)</title>"); Matcher matcher = TITLE_PATTERN.matcher(content);
Matcher matcher = pattern.matcher(content);
if (matcher.find()) { if (matcher.find()) {
String fullTitle = matcher.group(1); String fullTitle = matcher.group(1);
int sepIndex = fullTitle.indexOf(""); int sepIndex = fullTitle.indexOf("");