From 4d985cc96fe978c1af762b40ccd34557f3debd7c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 25 Sep 2026 06:46:11 +0000 Subject: [PATCH] =?UTF-8?q?fix(parser):=20=E8=93=9D=E5=A5=8F=E4=B8=AA?= =?UTF-8?q?=E6=80=A7=E5=9F=9F=E5=90=8D=E4=B8=8D=E5=86=8D=E5=BC=BA=E5=88=B6?= =?UTF-8?q?=E6=94=B9=E5=86=99=E5=88=B0=20wwww.lanzoux.com?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit a.lanzouw.com 的页面请求被改到四个 w 的主机后,部分出口会连接超时。 分享页 GET 保留原主机;ajax/filemore 再按 w1、www、wwww 顺序回退。 Co-authored-by: qaiu --- .../main/java/cn/qaiu/parser/impl/LzTool.java | 214 ++++++++++++++++-- .../qaiu/parser/impl/LzToolShareUrlTest.java | 119 ++++++++++ 2 files changed, 308 insertions(+), 25 deletions(-) create mode 100644 parser/src/test/java/cn/qaiu/parser/impl/LzToolShareUrlTest.java diff --git a/parser/src/main/java/cn/qaiu/parser/impl/LzTool.java b/parser/src/main/java/cn/qaiu/parser/impl/LzTool.java index d011746..78bbc1f 100644 --- a/parser/src/main/java/cn/qaiu/parser/impl/LzTool.java +++ b/parser/src/main/java/cn/qaiu/parser/impl/LzTool.java @@ -7,6 +7,7 @@ import cn.qaiu.parser.PanBase; import cn.qaiu.util.*; import io.netty.handler.codec.http.cookie.DefaultCookie; import io.vertx.core.Future; +import io.vertx.core.Handler; import io.vertx.core.MultiMap; import io.vertx.core.Promise; import io.vertx.core.buffer.Buffer; @@ -41,9 +42,23 @@ public class LzTool extends PanBase { private final WebClientSession webClientSession; - /** 实测服务器出口 IP 上 wwww.lanzoux.com 可用,且个性域名目录内文件 ID 也能打开。 */ - public static final String SHARE_URL_PREFIX = "https://wwww.lanzoux.com/"; - private static final String SHARE_ORIGIN = "https://wwww.lanzoux.com"; + /** + * 没有原始分享页主机时才使用(目录内文件 ID、shareKey 占位)。 + * 与 PanDomainTemplate.LZ 标准模板一致:{@code https://w1.lanzn.com/{shareKey}}。 + * 已有分享页不要改写到 wwww.lanzoux.com,部分出口连四个 w 的域名会超时。 + */ + public static final String SHARE_URL_PREFIX = "https://w1.lanzn.com/"; + /** 页面 URL 解析失败时的源站兜底,与 {@link #SHARE_URL_PREFIX} 同主机。 */ + private static final String SHARE_ORIGIN = "https://w1.lanzn.com"; + /** + * 个性域名上 ajaxfile.php / filemoreajax.php 会立刻返回「已超时」。 + * POST 失败后按这个顺序换稳定下载域,wwww 只放最后,避免单一主机超时直接失败。 + */ + static final List AJAX_FALLBACK_ORIGINS = List.of( + "https://w1.lanzn.com", + "https://www.lanzoux.com", + "https://wwww.lanzoux.com" + ); /** 分享页 / ajax 请求超时 */ private static final long REQUEST_TIMEOUT = 8000; /** 下载域跳转与二次验证超时,链路更长 */ @@ -259,18 +274,17 @@ public class LzTool extends PanBase { return path.isEmpty() || "-".equals(path); } - /** 统一改写到 {@link #SHARE_URL_PREFIX},只保留分享路径。 */ - private String resolveShareUrl() { - String u = shareLinkInfo.getShareUrl(); + /** + * 分享页 / 目录页 GET 保留原始主机和路径。 + * 只有分享路径缺失(空链接、shareKey 占位)时才落到 {@link #SHARE_URL_PREFIX}。 + */ + static String resolveSharePageUrl(String shareUrl, String standardUrl, String shareKey) { + String u = shareUrl; if (isPlaceholderShare(u)) { - u = shareLinkInfo.getStandardUrl(); + u = standardUrl; } if (isPlaceholderShare(u)) { - String key = shareLinkInfo.getShareKey(); - if (key == null || key.isBlank() || "-".equals(key)) { - return SHARE_URL_PREFIX; - } - return SHARE_URL_PREFIX + key; + return canonicalShareUrl(shareKey, null); } int q = u.indexOf('?'); String query = null; @@ -284,10 +298,117 @@ public class LzTool extends PanBase { } int pathStart = u.indexOf('/', schemeEnd + 3); if (pathStart < 0) { - String key = shareLinkInfo.getShareKey(); - return appendWebpage(SHARE_URL_PREFIX + (key == null ? "" : key), query); + String base = u.endsWith("/") ? u : u + "/"; + if (shareKey == null || shareKey.isBlank() || "-".equals(shareKey)) { + return appendWebpage(base, query); + } + return appendWebpage(base + shareKey, query); } - return appendWebpage(SHARE_ORIGIN + u.substring(pathStart), query); + return appendWebpage(u, query); + } + + private String resolveShareUrl() { + return resolveSharePageUrl( + shareLinkInfo.getShareUrl(), + shareLinkInfo.getStandardUrl(), + shareLinkInfo.getShareKey()); + } + + /** 无原始主机时用标准下载域拼分享路径,不再写死 wwww.lanzoux.com。 */ + private static String canonicalShareUrl(String shareKey, String query) { + if (shareKey == null || shareKey.isBlank() || "-".equals(shareKey)) { + return appendWebpage(SHARE_URL_PREFIX, query); + } + return appendWebpage(SHARE_URL_PREFIX + shareKey, query); + } + + /** + * 目录内文件页主机:沿用已有分享主机,否则用 {@link #SHARE_URL_PREFIX}。 + */ + static String sharePageHostBase(String shareUrl, String standardUrl) { + String u = !isPlaceholderShare(shareUrl) ? shareUrl : standardUrl; + if (!isPlaceholderShare(u)) { + String origin = normalizeOrigin(originOf(u, SHARE_ORIGIN)); + return origin + "/"; + } + return SHARE_URL_PREFIX; + } + + /** + * ajax / filemore 的 POST 主机顺序:分享页源站优先(普通节点通常可用), + * 然后是稳定下载域。wwww.lanzoux.com 固定排在最后。 + */ + static List ajaxOrigins(String pageUrl) { + List origins = new ArrayList<>(); + String pageOrigin = normalizeOrigin(originOf(pageUrl, "")); + if (pageOrigin.contains("://")) { + origins.add(pageOrigin); + } + for (String candidate : AJAX_FALLBACK_ORIGINS) { + if (!origins.contains(candidate)) { + origins.add(candidate); + } + } + return origins; + } + + private static String normalizeOrigin(String origin) { + if (origin == null) { + return ""; + } + String o = origin.trim(); + while (o.endsWith("/")) { + o = o.substring(0, o.length() - 1); + } + return o; + } + + /** + * 个性域名常见 {@code inf=已超时},或响应根本不是 JSON。这类结果说明当前主机不可用,应换下一个域名。 + * 密码错误、分享失效等业务错误不换域名。 + */ + static boolean ajaxResponseShouldFallback(String text) { + if (text == null || text.isBlank()) { + return true; + } + JsonObject json = parseLzJson(text); + if (json == null) { + return true; + } + Integer zt = jsonInt(json, "zt"); + if (zt != null && zt == 1) { + return false; + } + String info = ajaxInfoText(json); + return info != null && info.contains("超时"); + } + + private static Integer jsonInt(JsonObject json, String key) { + Object v = json.getValue(key); + if (v instanceof Number n) { + return n.intValue(); + } + if (v instanceof CharSequence) { + try { + return Integer.parseInt(v.toString().trim()); + } catch (NumberFormatException ignored) { + return null; + } + } + return null; + } + + private static String ajaxInfoText(JsonObject json) { + for (String key : new String[]{"inf", "info"}) { + Object v = json.getValue(key); + if (v instanceof CharSequence) { + String s = v.toString().trim(); + if (!s.isEmpty() && !"null".equalsIgnoreCase(s)) { + return s; + } + } + } + return null; } /** 目录文件 ID 可能带 webpage=,必须保留;pwd 走独立字段,不拼进 URL。 */ @@ -576,12 +697,56 @@ public class LzTool extends PanBase { private void getDownURL(String referer, AjaxCall call) { MultiMap headers = HeaderUtils.parseHeaders(DOWN_AJAX_HEADERS); - // 个性域名 ajaxfile.php 会立刻 inf=已超时;POST 必须打 wwww。iframe 请求 Referer 用 iframe 地址。 - headers.set("referer", referer != null && !referer.isBlank() ? referer : resolveShareUrl()); - String url = joinUrl(SHARE_ORIGIN + "/", call.path()); + // 个性域名 ajaxfile.php 会立刻 inf=已超时。先打页面源站,失败再换稳定下载域。 + // iframe 的 Referer 仍用 iframe 地址,不把页面 GET 改写到 wwww。 + String page = referer != null && !referer.isBlank() ? referer : resolveShareUrl(); + headers.set("referer", page); + postAjaxWithFallback(page, call, headers, this::handleAjaxDownResponse, null); + } + + /** + * 依次向 {@link #ajaxOrigins} 发 POST。连接失败或「已超时」换下一个主机; + * wwww.lanzoux.com 只在最后尝试。业务错误(密码、失效)立即返回。 + */ + private void postAjaxWithFallback(String pageUrl, AjaxCall call, MultiMap headers, + Handler onBody, Handler onFailure) { + postAjaxAt(ajaxOrigins(pageUrl), 0, call, headers, onBody, onFailure); + } + + private void postAjaxAt(List origins, int index, AjaxCall call, MultiMap headers, + Handler onBody, Handler onFailure) { + if (index >= origins.size()) { + String msg = "蓝奏 ajax 全部域名均失败"; + if (onFailure != null) { + onFailure.handle(new RuntimeException(msg)); + } else { + fail(msg); + } + return; + } + String url = joinUrl(origins.get(index) + "/", call.path()); + boolean hasNext = index + 1 < origins.size(); postFormWithArg1Retry(url, headers, call.toForm()) - .onSuccess(this::handleAjaxDownResponse) - .onFailure(handleFail(url)); + .onSuccess(text -> { + if (hasNext && ajaxResponseShouldFallback(text)) { + log.warn("蓝奏 ajax {} 不可用,改试下一域名", url); + postAjaxAt(origins, index + 1, call, headers, onBody, onFailure); + return; + } + onBody.handle(text); + }) + .onFailure(err -> { + if (hasNext) { + log.warn("蓝奏 ajax {} 请求失败: {},改试下一域名", url, err.getMessage()); + postAjaxAt(origins, index + 1, call, headers, onBody, onFailure); + return; + } + if (onFailure != null) { + onFailure.handle(err); + } else { + handleFail(url).handle(err); + } + }); } private void handleAjaxDownResponse(String text) { @@ -913,10 +1078,9 @@ public class LzTool extends PanBase { } log.debug("解析参数: {}", call.form()); - String url = joinUrl(originOf(sUrl, SHARE_ORIGIN) + "/", call.path()); - postFormWithArg1Retry(url, folderListHeaders(sUrl), call.toForm()) - .onSuccess(body -> handleFileListResponse(body, listPromise)) - .onFailure(listPromise::fail); + postAjaxWithFallback(sUrl, call, folderListHeaders(sUrl), + body -> handleFileListResponse(body, listPromise), + listPromise::fail); } catch (ScriptException | NoSuchMethodException | RuntimeException e) { listPromise.fail(e); } @@ -1069,7 +1233,7 @@ public class LzTool extends PanBase { if (webpage == null || webpage.isBlank()) { webpage = extractWebpage(shareLinkInfo.getShareUrl(), null); } - String fileUrl = SHARE_URL_PREFIX + id; + String fileUrl = sharePageHostBase(shareLinkInfo.getShareUrl(), shareLinkInfo.getStandardUrl()) + id; if (webpage != null && !webpage.isBlank()) { fileUrl = fileUrl + "?webpage=" + webpage; } diff --git a/parser/src/test/java/cn/qaiu/parser/impl/LzToolShareUrlTest.java b/parser/src/test/java/cn/qaiu/parser/impl/LzToolShareUrlTest.java new file mode 100644 index 0000000..b3dea7d --- /dev/null +++ b/parser/src/test/java/cn/qaiu/parser/impl/LzToolShareUrlTest.java @@ -0,0 +1,119 @@ +package cn.qaiu.parser.impl; + +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * 个性域名 a.lanzouw.com 不能被改写到 wwww.lanzoux.com(四个 w)。 + * 部分出口连不上四 w,页面请求会超时;ajax 只把四 w 当作最后兜底。 + */ +public class LzToolShareUrlTest { + + @Test + public void personalDomainPageStaysOffWwww() { + String resolved = LzTool.resolveSharePageUrl( + "https://a.lanzouw.com/xxx", + "https://w1.lanzn.com/xxx", + "xxx"); + assertEquals("https://a.lanzouw.com/xxx", resolved); + assertFalse(resolved.contains("wwww.lanzoux.com")); + } + + @Test + public void folderPageKeepsOriginalHost() { + String resolved = LzTool.resolveSharePageUrl( + "https://a.lanzouw.com/b710887", + "https://w1.lanzn.com/b710887", + "b710887"); + assertEquals("https://a.lanzouw.com/b710887", resolved); + } + + @Test + public void nodeShareIsNotRewrittenToWwww() { + String resolved = LzTool.resolveSharePageUrl( + "https://wwn.lanzouy.com/ihLkw1gezutg", + "https://w1.lanzn.com/ihLkw1gezutg", + "ihLkw1gezutg"); + assertEquals("https://wwn.lanzouy.com/ihLkw1gezutg", resolved); + } + + @Test + public void webpageQueryStaysOnOriginalHost() { + String resolved = LzTool.resolveSharePageUrl( + "https://a.lanzouw.com/iabc?webpage=wp1&pwd=secret", + "https://w1.lanzn.com/iabc", + "iabc"); + assertEquals("https://a.lanzouw.com/iabc?webpage=wp1", resolved); + } + + @Test + public void placeholderUsesStandardHostNotWwww() { + assertEquals("https://w1.lanzn.com/", LzTool.SHARE_URL_PREFIX); + assertFalse(LzTool.SHARE_URL_PREFIX.contains("wwww")); + String resolved = LzTool.resolveSharePageUrl( + "https://w1.lanzn.com/-", + "https://w1.lanzn.com/-", + "iFileId"); + assertEquals("https://w1.lanzn.com/iFileId", resolved); + assertFalse(resolved.contains("wwww.lanzoux.com")); + } + + @Test + public void missingShareUrlKeepsRealStandardHost() { + String resolved = LzTool.resolveSharePageUrl( + null, + "https://www.lanzoux.com/iabc", + "iabc"); + assertEquals("https://www.lanzoux.com/iabc", resolved); + } + + @Test + public void ajaxFallbackOrderPutsWwwwLast() { + List fallback = LzTool.AJAX_FALLBACK_ORIGINS; + assertEquals("https://w1.lanzn.com", fallback.get(0)); + assertEquals("https://www.lanzoux.com", fallback.get(1)); + assertEquals("https://wwww.lanzoux.com", fallback.get(fallback.size() - 1)); + + List origins = LzTool.ajaxOrigins("https://a.lanzouw.com/xxx"); + assertEquals("https://a.lanzouw.com", origins.get(0)); + assertEquals("https://w1.lanzn.com", origins.get(1)); + assertEquals("https://www.lanzoux.com", origins.get(2)); + assertEquals("https://wwww.lanzoux.com", origins.get(origins.size() - 1)); + assertTrue(origins.indexOf("https://wwww.lanzoux.com") + > origins.indexOf("https://w1.lanzn.com")); + } + + @Test + public void ajaxOriginsDoNotDuplicateKnownHost() { + List origins = LzTool.ajaxOrigins("https://www.lanzoux.com/iabc"); + assertEquals(3, origins.size()); + assertEquals("https://www.lanzoux.com", origins.get(0)); + assertEquals("https://w1.lanzn.com", origins.get(1)); + assertEquals("https://wwww.lanzoux.com", origins.get(2)); + } + + @Test + public void timeoutAjaxFallsBackButBusinessErrorDoesNot() { + assertTrue(LzTool.ajaxResponseShouldFallback("{\"zt\":0,\"inf\":\"已超时\"}")); + assertTrue(LzTool.ajaxResponseShouldFallback("{\"info\":\"请求超时\"}")); + assertTrue(LzTool.ajaxResponseShouldFallback("")); + assertTrue(LzTool.ajaxResponseShouldFallback("blocked")); + assertFalse(LzTool.ajaxResponseShouldFallback("{\"zt\":0,\"inf\":\"密码错误\"}")); + assertFalse(LzTool.ajaxResponseShouldFallback( + "{\"zt\":1,\"inf\":\"name.zip\",\"dom\":\"https://x\",\"url\":\"abc\"}")); + } + + @Test + public void filePagePrefersExistingHostOverWwww() { + assertEquals("https://a.lanzouw.com/", + LzTool.sharePageHostBase("https://a.lanzouw.com/b123", "https://w1.lanzn.com/b123")); + assertEquals("https://w1.lanzn.com/", + LzTool.sharePageHostBase("https://w1.lanzn.com/-", "https://w1.lanzn.com/-")); + assertFalse(LzTool.sharePageHostBase("https://a.lanzouw.com/b123", null).contains("wwww")); + } +}