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..a28b16b 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,37 @@ 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"; + /** + * 目录 filemoreajax.php 换域顺序。个性域名上会立刻返回「已超时」。 + * wwww 只放最后,避免单一主机超时直接失败。 + * 文件 ajax 走 {@link #FILE_AJAX_FALLBACK_ORIGINS},不要把 apifile 混进目录列表。 + */ + static final List AJAX_FALLBACK_ORIGINS = List.of( + "https://w1.lanzn.com", + "https://www.lanzoux.com", + "https://wwww.lanzoux.com" + ); + /** + * 文件页 iframe / 密码页里的 ajaxfile.php 经常是绝对地址 + * {@code https://apifile.lanzouw.com/ajaxfile.php?file=…}。 + * 相对路径打到页面源站或 w1/www 会空响应。apifile 放在文件 ajax 兜底的最前, + * 页面源站空响应后立刻换过去,不在四 w 上耗光超时。 + */ + static final String APIFILE_ORIGIN = "https://apifile.lanzouw.com"; + static final List FILE_AJAX_FALLBACK_ORIGINS = List.of( + APIFILE_ORIGIN, + "https://w1.lanzn.com", + "https://www.lanzoux.com", + "https://wwww.lanzoux.com" + ); /** 分享页 / ajax 请求超时 */ private static final long REQUEST_TIMEOUT = 8000; /** 下载域跳转与二次验证超时,链路更长 */ @@ -54,6 +83,10 @@ public class LzTool extends PanBase { private static final Pattern P_AJAXDATA = Pattern.compile("ajaxdata\\s*=\\s*'([^']+)'"); private static final Pattern P_WEBSIGN = Pattern.compile("'websign'\\s*:\\s*'([^']*)'"); private static final Pattern P_WEBPAGE = Pattern.compile("[?&]webpage=([^&\"'\\s#]+)"); + /** 页面脚本里的绝对 ajax,例如 https://apifile.lanzouw.com/ajaxfile.php?file=123 */ + private static final Pattern P_AJAX_ABSOLUTE = Pattern.compile( + "['\"]((?:https?:)?//[^'\"\\s<>]+/ajax(?:m|file)\\.php\\?file=\\d+)['\"]", + Pattern.CASE_INSENSITIVE); private static final Pattern P_AJAX_PATH = Pattern.compile("(?:['\"/]|^)(ajax(?:m|file)\\.php\\?file=\\d+)"); private static final Pattern P_SIGN = Pattern.compile("'sign'\\s*:\\s*'([^']+)'"); private static final Pattern P_ISNGIS = Pattern.compile("var\\s+isngis\\s*=\\s*'([^']+)'"); @@ -259,18 +292,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 +316,167 @@ 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; + } + + /** + * 目录 filemore 的 POST 主机顺序:分享页源站优先(普通节点通常可用), + * 然后是稳定下载域。wwww.lanzoux.com 固定排在最后。 + */ + static List ajaxOrigins(String pageUrl) { + List origins = new ArrayList<>(); + addAjaxOrigin(origins, normalizeOrigin(originOf(pageUrl, ""))); + for (String candidate : AJAX_FALLBACK_ORIGINS) { + addAjaxOrigin(origins, candidate); + } + return origins; + } + + /** + * 文件 ajax 主机顺序。页面里若写出绝对 ajax URL,该主机最先尝试; + * 否则先打页面源站。空响应后再试 {@link #APIFILE_ORIGIN},四 w 仍在最后。 + * + * @param ajaxTarget {@link #fileAjaxTarget} 的返回值,绝对 URL 或相对路径;可为 null + */ + static List fileAjaxOrigins(String pageUrl, String ajaxTarget) { + List origins = new ArrayList<>(); + addAjaxOrigin(origins, originFromAjaxTarget(ajaxTarget)); + addAjaxOrigin(origins, normalizeOrigin(originOf(pageUrl, ""))); + for (String candidate : FILE_AJAX_FALLBACK_ORIGINS) { + addAjaxOrigin(origins, candidate); + } + return origins; + } + + /** + * 从分享页或 iframe HTML 取出文件 ajax 地址。 + * 绝对 URL(含协议相对 {@code //host/...})原样返回;否则返回以 {@code /} 开头的相对路径。 + */ + static String fileAjaxTarget(String html) { + if (html == null || html.isEmpty()) { + return null; + } + Matcher abs = P_AJAX_ABSOLUTE.matcher(html); + if (abs.find()) { + String url = abs.group(1); + if (url.startsWith("//")) { + url = "https:" + url; + } + return url; + } + Matcher ajax = P_AJAX_PATH.matcher(html); + if (!ajax.find()) { + return null; + } + return "/" + ajax.group(1); + } + + private static void addAjaxOrigin(List origins, String origin) { + if (origin != null && origin.contains("://") && !origins.contains(origin)) { + origins.add(origin); + } + } + + private static String originFromAjaxTarget(String ajaxTarget) { + if (ajaxTarget == null) { + return ""; + } + if (ajaxTarget.startsWith("http://") || ajaxTarget.startsWith("https://")) { + return normalizeOrigin(originOf(ajaxTarget, "")); + } + return ""; + } + + 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。 */ @@ -411,8 +600,12 @@ public class LzTool extends PanBase { return true; } - /** 页面里提取出的 ajax 调用:相对路径 + 表单参数。 */ - private record AjaxCall(String path, Map form) { + /** 页面里提取出的 ajax 调用。absoluteUrl 非空时优先打该主机。 */ + private record AjaxCall(String path, Map form, String absoluteUrl) { + private AjaxCall(String path, Map form) { + this(path, form, null); + } + MultiMap toForm() { MultiMap m = MultiMap.caseInsensitiveMultiMap(); form.forEach(m::set); @@ -424,11 +617,12 @@ public class LzTool extends PanBase { if (html == null || html.isEmpty()) { return null; } - Matcher ajax = P_AJAX_PATH.matcher(html); - if (!ajax.find()) { + String target = fileAjaxTarget(html); + if (target == null) { return null; } - String ajaxPath = ajax.group(1); + String absolute = target.startsWith("http://") || target.startsWith("https://") ? target : null; + String ajaxPath = absolute == null ? target : pathOfAbsolute(absolute); Map data = new LinkedHashMap<>(); data.put("action", "downprocess"); Matcher wp = P_WP_SIGN.matcher(html); @@ -482,7 +676,23 @@ public class LzTool extends PanBase { data.put("signs", ad2.group(1)); } } - return new AjaxCall("/" + ajaxPath, data); + return new AjaxCall(ajaxPath, data, absolute); + } + + private static String pathOfAbsolute(String url) { + try { + URL u = new URL(url); + String path = u.getPath() == null ? "" : u.getPath(); + if (u.getQuery() != null && !u.getQuery().isEmpty()) { + path = path + "?" + u.getQuery(); + } + if (!path.startsWith("/")) { + path = "/" + path; + } + return path; + } catch (Exception e) { + return url; + } } private static AjaxCall extractFolderAjax(String html, String pwd) { @@ -576,12 +786,58 @@ 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()); + // 文件 ajax 优先用页面里的绝对地址(常见 apifile.lanzouw.com)。 + // 页面源站空响应再换 FILE_AJAX_FALLBACK_ORIGINS,wwww 仍在最后。 + // iframe / 分享页的 Referer 保持原主机,不把页面 GET 改写到 wwww。 + String page = referer != null && !referer.isBlank() ? referer : resolveShareUrl(); + headers.set("referer", page); + String target = call.absoluteUrl() != null ? call.absoluteUrl() : call.path(); + postAjaxAt(fileAjaxOrigins(page, target), 0, 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 +1169,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 +1324,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..67aaef7 --- /dev/null +++ b/parser/src/test/java/cn/qaiu/parser/impl/LzToolShareUrlTest.java @@ -0,0 +1,178 @@ +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")); + } + + @Test + public void absoluteAjaxUrlIsKeptAndTriedFirst() { + String html = """ + url : 'https://apifile.lanzouw.com/ajaxfile.php?file=320665771', + data : { 'action':'downprocess','sign':isngis,'kd':kdns,'p':pwd, }, + """; + String target = LzTool.fileAjaxTarget(html); + assertEquals("https://apifile.lanzouw.com/ajaxfile.php?file=320665771", target); + + List origins = LzTool.fileAjaxOrigins( + "https://wwaqg.lanzouu.com/iqouM49vy9ib", target); + assertEquals("https://apifile.lanzouw.com", origins.get(0)); + assertTrue(origins.contains("https://wwaqg.lanzouu.com")); + assertEquals("https://wwww.lanzoux.com", origins.get(origins.size() - 1)); + assertTrue(origins.indexOf("https://apifile.lanzouw.com") + < origins.indexOf("https://wwaqg.lanzouu.com")); + assertTrue(origins.indexOf("https://wwww.lanzoux.com") + > origins.indexOf("https://w1.lanzn.com")); + assertEquals(1, origins.stream().filter("https://apifile.lanzouw.com"::equals).count()); + } + + @Test + public void protocolRelativeAjaxBecomesHttps() { + String html = "url : '//apifile.lanzouw.com/ajaxfile.php?file=9',"; + assertEquals("https://apifile.lanzouw.com/ajaxfile.php?file=9", LzTool.fileAjaxTarget(html)); + } + + @Test + public void absoluteAjaxWinsOverRelativePathInSamePage() { + String html = """ + //data : { url : '/ajaxfile.php?file=1' } + url : "https://apifile.lanzouw.com/ajaxm.php?file=42", + """; + assertEquals("https://apifile.lanzouw.com/ajaxm.php?file=42", LzTool.fileAjaxTarget(html)); + } + + @Test + public void relativeFileAjaxFallsThroughToApifileBeforeWwww() { + String html = "url : '/ajaxfile.php?file=150233466',"; + assertEquals("/ajaxfile.php?file=150233466", LzTool.fileAjaxTarget(html)); + + List origins = LzTool.fileAjaxOrigins("https://a.lanzouw.com/xxx", LzTool.fileAjaxTarget(html)); + assertEquals("https://a.lanzouw.com", origins.get(0)); + assertEquals("https://apifile.lanzouw.com", origins.get(1)); + assertEquals("https://wwww.lanzoux.com", origins.get(origins.size() - 1)); + assertTrue(origins.indexOf("https://apifile.lanzouw.com") + < origins.indexOf("https://wwww.lanzoux.com")); + assertFalse(LzTool.ajaxOrigins("https://a.lanzouw.com/xxx").contains("https://apifile.lanzouw.com")); + } + + @Test + public void missingAjaxTargetStillPutsApifileBeforeWwww() { + List origins = LzTool.fileAjaxOrigins("https://www.lanzoux.com/ihLkw1gezutg", null); + assertEquals("https://www.lanzoux.com", origins.get(0)); + assertEquals("https://apifile.lanzouw.com", origins.get(1)); + assertEquals("https://wwww.lanzoux.com", origins.get(origins.size() - 1)); + assertFalse(origins.get(0).contains("wwww")); + } +}