diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index 0a4fa8c..bea3e6a 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -262,9 +262,27 @@ jobs:
name: ${{ matrix.artifact-name }}
path: ${{ matrix.artifact-name }}.zip
- - name: 上传到 Release
+ # ================================================================
+ # 阶段三:创建 GitHub Release(只跑一次,避免矩阵任务重复拼接更新日志)
+ # ================================================================
+ publish-release:
+ name: 发布 GitHub Release
+ needs: native-package
+ if: github.event_name != 'pull_request' && github.ref_type == 'tag'
+ runs-on: ubuntu-latest
+ steps:
+ - name: 下载原生安装包
+ uses: actions/download-artifact@v4
+ with:
+ pattern: netdisk-fast-download-*
+ merge-multiple: true
+
+ - name: 创建 Release
uses: softprops/action-gh-release@v2
with:
- files: ${{ matrix.artifact-name }}.zip
+ files: |
+ netdisk-fast-download-linux-amd64.zip
+ netdisk-fast-download-windows-amd64.zip
tag_name: ${{ github.ref_name }}
generate_release_notes: true
+ fail_on_unmatched_files: true
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 9f0f2f6..4c6333b 100644
--- a/parser/src/main/java/cn/qaiu/parser/impl/LzTool.java
+++ b/parser/src/main/java/cn/qaiu/parser/impl/LzTool.java
@@ -1,5 +1,6 @@
package cn.qaiu.parser.impl;
+import cn.qaiu.WebClientVertxInit;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
@@ -8,20 +9,30 @@ import io.netty.handler.codec.http.cookie.DefaultCookie;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.Promise;
+import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject;
+import io.vertx.core.net.ProxyOptions;
+import io.vertx.core.net.ProxyType;
+import io.vertx.ext.web.client.HttpResponse;
import io.vertx.ext.web.client.WebClient;
+import io.vertx.ext.web.client.WebClientOptions;
import io.vertx.ext.web.client.WebClientSession;
+import org.apache.commons.lang3.StringUtils;
import org.openjdk.nashorn.api.scripting.ScriptObjectMirror;
import javax.script.ScriptException;
+import java.io.ByteArrayInputStream;
import java.net.MalformedURLException;
+import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import java.util.zip.GZIPInputStream;
/**
* 蓝奏云解析工具
@@ -30,12 +41,26 @@ import java.util.regex.Pattern;
*/
public class LzTool extends PanBase {
- WebClientSession webClientSession = WebClientSession.create(clientNoRedirects);
+ /** ESA 对 gzip 响应常见不带可识别的 Content-Encoding,需客户端自动解压。 */
+ private final WebClient lzClient;
+ private WebClientSession webClientSession;
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 P_WP_SIGN = Pattern.compile("wp_sign\\s*=\\s*'([^']+)'");
+ 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_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*'([^']+)'");
+ private static final Pattern P_KDNS = Pattern.compile("var\\s+kdns\\s*=\\s*(\\d+)");
+ private static final Pattern P_FILEMORE = Pattern.compile(
+ "url\\s*:\\s*'(/filemoreajax\\.php\\?file=\\d+)'[\\s\\S]*?data\\s*:\\s*\\{([^}]+)\\}");
+ private static final Pattern P_DATA_KV = Pattern.compile("'(\\w+)'\\s*:\\s*('(?:\\\\'|[^'])*'|\\d+|\\w+)");
+ private static final Pattern P_INLINE_SCRIPT =
+ Pattern.compile("(?is)");
private static final Pattern FILE_SIZE_PATTERN = Pattern.compile(">文件大小:(.*?)
|\"n_filesize\">大小:(.*?)");
private static final Pattern SHARE_USER_PATTERN = Pattern.compile(">分享用户:(.*?)|获取(.*?)的文件|\"user-name\">(.*?)");
private static final Pattern DESCRIPTION_PATTERN = Pattern.compile("(?s)文件描述:
(.*?)|class=\"n_box_des\">(.*?)");
@@ -43,13 +68,14 @@ public class LzTool extends PanBase {
private static final Pattern CREATE_TIME_PATTERN = Pattern.compile(">上传时间:(.*?)<");
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 IFRAME_SRC_PATTERN = Pattern.compile(
+ "src\\s*=\\s*[\"'](/fn\\?[^\"'\\s>]+)[\"']", Pattern.CASE_INSENSITIVE);
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("""
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: identity
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cache-Control: max-age=0
DNT: 1
@@ -68,20 +94,115 @@ public class LzTool extends PanBase {
public LzTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
+ this.lzClient = createLzClient(shareLinkInfo);
+ this.webClientSession = WebClientSession.create(lzClient);
+ }
+
+ /**
+ * ESA 对 gzip 响应常见不带可识别的 Content-Encoding,需客户端自动解压;
+ * 代理模式必须带上 shareLinkInfo 里的 proxy,不能绕过全局代理。
+ */
+ private static WebClient createLzClient(ShareLinkInfo shareLinkInfo) {
+ WebClientOptions opts = new WebClientOptions()
+ .setFollowRedirects(false)
+ .setDecompressionSupported(true)
+ .setUserAgentEnabled(false)
+ .setConnectTimeout(10000)
+ .setIdleTimeout(12);
+ if (shareLinkInfo != null && shareLinkInfo.getOtherParam().containsKey("proxy")) {
+ JsonObject proxy = (JsonObject) shareLinkInfo.getOtherParam().get("proxy");
+ if (proxy != null && proxy.getString("host") != null) {
+ ProxyOptions proxyOptions = new ProxyOptions()
+ .setType(ProxyType.valueOf(proxy.getString("type", "http").toUpperCase()))
+ .setHost(proxy.getString("host"))
+ .setPort(proxy.getInteger("port", 0));
+ if (StringUtils.isNotEmpty(proxy.getString("username"))) {
+ proxyOptions.setUsername(proxy.getString("username"));
+ }
+ if (StringUtils.isNotEmpty(proxy.getString("password"))) {
+ proxyOptions.setPassword(proxy.getString("password"));
+ }
+ opts.setProxyOptions(proxyOptions);
+ }
+ }
+ return WebClient.create(WebClientVertxInit.get(), opts);
+ }
+
+ /**
+ * Vert.x decompressionSupported 可能已经解压,但响应头仍带 gzip;
+ * 再走 PanBase.asText 会二次解压得到乱码,iframe / down_p 都匹配不到。
+ */
+ @Override
+ protected String asText(HttpResponse> res) {
+ try {
+ Object raw = res.body();
+ if (raw instanceof Buffer body && body.length() > 0) {
+ int i = 0;
+ int len = body.length();
+ while (i < len) {
+ byte c = body.getByte(i);
+ if (c != ' ' && c != '\n' && c != '\r' && c != '\t') {
+ if (c == '<' || c == '{' || c == '[') {
+ return body.toString();
+ }
+ if ((c & 0xff) == 0x1f && i + 1 < len && (body.getByte(i + 1) & 0xff) == 0x8b) {
+ return gunzipUtf8(body);
+ }
+ break;
+ }
+ i++;
+ }
+ }
+ } catch (Exception ignored) {
+ }
+ return super.asText(res);
+ }
+
+ private static String gunzipUtf8(Buffer body) {
+ try (GZIPInputStream in = new GZIPInputStream(new ByteArrayInputStream(body.getBytes()))) {
+ return new String(in.readAllBytes(), StandardCharsets.UTF_8);
+ } catch (Exception e) {
+ return body.toString();
+ }
+ }
+
+ @Override
+ protected JsonObject asJson(HttpResponse> res) {
+ JsonObject parsed = parseLzJson(asText(res));
+ if (parsed != null) {
+ return parsed;
+ }
+ return super.asJson(res);
+ }
+
+ /** ajax 常被标成 gzip/text/json,body 可能已解压或为空,不能直接 bodyAsJsonObject。 */
+ private static JsonObject parseLzJson(String text) {
+ if (text == null) {
+ return null;
+ }
+ String t = text.trim();
+ int start = t.indexOf('{');
+ if (start < 0) {
+ return null;
+ }
+ try {
+ return new JsonObject(t.substring(start));
+ } catch (Exception e) {
+ return null;
+ }
}
public Future parse() {
String sUrl = shareLinkInfo.getStandardUrl();
String pwd = shareLinkInfo.getSharePassword();
- WebClient client = clientNoRedirects;
- client.getAbs(sUrl)
+ webClientSession.getAbs(sUrl)
.putHeaders(headers0)
.send().onSuccess(res -> {
try {
String html = asText(res);
if (hasAcwArg1(html)) {
- webClientSession = WebClientSession.create(clientNoRedirects);
+ webClientSession = WebClientSession.create(lzClient);
if (!setCookie(html, sUrl)) {
fail("蓝奏云反爬 arg1 Cookie 解析失败,页面内容异常");
return;
@@ -117,14 +238,14 @@ public class LzTool extends PanBase {
fail("分享已失效或文件已取消分享");
return;
}
- // 检测是否为目录分享链接 (含 /s/、/b/ 路径段或 b 开头的路径段)
- if (sUrl.matches(".*/(s|b)/[^/]+.*") || sUrl.matches(".*/b[^/]+.*")) {
+ // 检测是否为目录分享链接 (含 /s/、/b/ 路径段或 b0 开头的路径段)
+ if (sUrl.matches(".*/(s|b)/[^/]+.*") || sUrl.matches(".*/b0[^/]+.*")) {
fail("该链接为蓝奏云目录分享,请使用目录解析接口");
return;
}
// 若仍是校验页 (parse()中cookie域名与实际URL不匹配时会出现), 重试一次
if (hasAcwArg1(html)) {
- webClientSession = WebClientSession.create(clientNoRedirects);
+ webClientSession = WebClientSession.create(lzClient);
if (!setCookie(html, sUrl)) {
fail("蓝奏云反爬 arg1 Cookie 解析失败,页面内容异常");
return;
@@ -168,63 +289,33 @@ public class LzTool extends PanBase {
Matcher matcher = IFRAME_SRC_PATTERN.matcher(html);
// 没有Iframe说明是加密分享, 匹配sign通过密码请求下载页面
if (!matcher.find()) {
+ boolean pwdPage = html.contains("down_p") || html.contains("id=\"pwd\"") || html.contains("id='pwd'");
+ if (pwdPage && (pwd == null || pwd.isBlank())) {
+ fail("需要访问密码");
+ return;
+ }
try {
- String jsText = getJsByPwd(pwd, html, "document.getElementById('rpt')");
- ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, "down_p");
- getDownURL(sUrl, scriptObjectMirror);
+ if (!postAjaxFromHtml(sUrl, html, pwd)) {
+ fail("未找到下载参数,可能密码错误或分享已失效 htmlLen=" + html.length()
+ + " hasFn=" + html.contains("/fn?")
+ + " hasIframe=" + html.contains("iframe"));
+ }
} catch (Exception e) {
- fail(e, "js引擎执行失败");
+ fail(e, "js引擎执行失败 htmlLen=" + html.length()
+ + " hasFn=" + html.contains("/fn?")
+ + " hasIframe=" + html.contains("iframe"));
}
} else {
// 没有密码
String iframePath = matcher.group(1);
- String absoluteURI = SHARE_URL_PREFIX + iframePath;
+ String absoluteURI = joinUrl(SHARE_URL_PREFIX, iframePath);
// 创建局部副本,避免修改实例字段导致累积
MultiMap headersCopy = MultiMap.caseInsensitiveMultiMap().addAll(headers0);
headersCopy.add("Referer", absoluteURI);
webClientSession.getAbs(absoluteURI).putHeaders(headersCopy).send().onSuccess(res2 -> {
try {
String html2 = asText(res2);
- if (isShareCancelledPage(html2)) {
- fail("分享已失效或文件已取消分享");
- return;
- }
- String jsText = getJsText(html2);
- if (jsText == null) {
- if (!setCookie(html2, absoluteURI)) {
- fail("蓝奏云反爬 arg1 Cookie 解析失败,页面内容异常");
- return;
- }
- webClientSession.getAbs(absoluteURI).send().onSuccess(res3 -> {
- try {
- String html3 = asText(res3);
- if (isShareCancelledPage(html3)) {
- fail("分享已失效或文件已取消分享");
- return;
- }
- String jsText3 = getJsText(html3);
- if (jsText3 != null) {
- try {
- ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText3, null);
- getDownURL(sUrl, scriptObjectMirror);
- } catch (ScriptException | NoSuchMethodException e) {
- fail(e, "引擎执行失败");
- }
- } else {
- fail(SHARE_URL_PREFIX + iframePath + " -> " + sUrl + ": 获取失败0, 可能分享已失效");
- }
- } catch (Exception e) {
- fail("蓝奏云 iframe 响应处理异常: {}", e.getMessage());
- }
- }).onFailure(handleFail(absoluteURI));
- } else {
- try {
- ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, null);
- getDownURL(sUrl, scriptObjectMirror);
- } catch (ScriptException | NoSuchMethodException e) {
- fail(e, "js引擎执行失败");
- }
- }
+ handleIframeHtml(html2, sUrl, absoluteURI, iframePath, headersCopy);
} catch (Exception e) {
fail("蓝奏云 iframe 响应处理异常: {}", e.getMessage());
}
@@ -232,6 +323,43 @@ public class LzTool extends PanBase {
}
}
+ private void handleIframeHtml(String html2, String sUrl, String absoluteURI, String iframePath, MultiMap headersCopy) {
+ if (isShareCancelledPage(html2)) {
+ fail("分享已失效或文件已取消分享");
+ return;
+ }
+ if (hasAcwArg1(html2)) {
+ if (!setCookie(html2, absoluteURI)) {
+ fail("蓝奏云反爬 arg1 Cookie 解析失败,页面内容异常");
+ return;
+ }
+ webClientSession.getAbs(absoluteURI).putHeaders(headersCopy).send().onSuccess(res3 -> {
+ try {
+ String html3 = asText(res3);
+ if (isShareCancelledPage(html3)) {
+ fail("分享已失效或文件已取消分享");
+ return;
+ }
+ submitIframeAjax(html3, sUrl, absoluteURI, iframePath);
+ } catch (Exception e) {
+ fail("蓝奏云 iframe 响应处理异常: {}", e.getMessage());
+ }
+ }).onFailure(handleFail(absoluteURI));
+ return;
+ }
+ submitIframeAjax(html2, sUrl, absoluteURI, iframePath);
+ }
+
+ private void submitIframeAjax(String iframeHtml, String sUrl, String absoluteURI, String iframePath) {
+ try {
+ if (!postAjaxFromHtml(absoluteURI, iframeHtml, null)) {
+ fail(SHARE_URL_PREFIX + iframePath + " -> " + sUrl + ": 获取失败0, 可能分享已失效");
+ }
+ } catch (ScriptException | NoSuchMethodException e) {
+ fail(e, "js引擎执行失败");
+ }
+ }
+
private boolean setCookie(String html, String url) {
String arg1 = extractAcwArg1(html);
if (arg1 == null) {
@@ -262,9 +390,17 @@ public class LzTool extends PanBase {
String jsText = getJsText(html);
if (jsText == null) {
- throw new RuntimeException("获取失败1, 可能分享已失效");
+ throw new RuntimeException("获取失败1, 可能分享已失效 htmlLen=" + (html == null ? 0 : html.length())
+ + " hasFn=" + (html != null && html.contains("/fn?"))
+ + " hasScript=" + (html != null && html.contains("";
int index = html.lastIndexOf(jsTagStart);
if (index == -1) {
return null;
}
int startPos = index + jsTagStart.length();
- int endPos = html.indexOf(jsTagEnd, startPos);
- if (endPos <= startPos) {
+ int endPos = html.indexOf("", startPos);
+ if (endPos < 0) {
return null;
}
- return html.substring(startPos, endPos).replaceAll("", "");
+ String fallback = html.substring(startPos, endPos).replaceAll("", "");
+ if (fallback.contains("var arg1=") || fallback.contains("arg1='")) {
+ return null;
+ }
+ if (fallback.contains("$.ajax") || fallback.contains("wp_sign") || fallback.contains("down_p")
+ || fallback.contains("ajaxdata") || fallback.contains("filemoreajax")) {
+ return fallback;
+ }
+ return null;
}
static String extractAcwArg1(String html) {
@@ -316,20 +474,210 @@ public class LzTool extends PanBase {
return html != null && html.contains("var arg1='");
}
+ private boolean postAjaxFromHtml(String referer, String html, String pwd)
+ throws ScriptException, NoSuchMethodException {
+ AjaxCall call = extractAjaxFromHtml(html, pwd);
+ if (call != null) {
+ getDownURL(referer, call);
+ return true;
+ }
+ String jsText;
+ try {
+ jsText = (pwd != null && !pwd.isBlank())
+ ? getJsByPwd(pwd, html, "document.getElementById('rpt')")
+ : getJsText(html);
+ } catch (RuntimeException e) {
+ return false;
+ }
+ if (jsText == null) {
+ return false;
+ }
+ String fun = (pwd != null && !pwd.isBlank() && jsText.contains("down_p")) ? "down_p" : null;
+ ScriptObjectMirror mirror = JsExecUtils.executeDynamicJs(jsText, fun, pwd);
+ if (mirror == null) {
+ return false;
+ }
+ getDownURL(referer, mirror);
+ return true;
+ }
+
+ /** 页面里提取出的 ajax 调用:相对路径 + 表单参数。 */
+ record AjaxCall(String path, Map form) {
+ MultiMap toForm() {
+ MultiMap m = MultiMap.caseInsensitiveMultiMap();
+ form.forEach(m::set);
+ return m;
+ }
+ }
+
+ private void getDownURL(String referer, AjaxCall call) {
+ Map obj = new LinkedHashMap<>();
+ obj.put("url", call.path());
+ obj.put("data", call.form());
+ getDownURL(referer, obj);
+ }
+
+ static AjaxCall extractAjaxFromHtml(String html, String pwd) {
+ if (html == null || html.isEmpty()) {
+ return null;
+ }
+ Matcher ajax = P_AJAX_PATH.matcher(html);
+ if (!ajax.find()) {
+ return null;
+ }
+ String ajaxPath = ajax.group(1);
+ Map data = new LinkedHashMap<>();
+ data.put("action", "downprocess");
+ Matcher wp = P_WP_SIGN.matcher(html);
+ Matcher ad = P_AJAXDATA.matcher(html);
+ Matcher isngis = P_ISNGIS.matcher(html);
+ String lastIsngis = null;
+ while (isngis.find()) {
+ if (!isngis.group(1).isEmpty()) {
+ lastIsngis = isngis.group(1);
+ }
+ }
+ String kd = "1";
+ Matcher kdns = P_KDNS.matcher(html);
+ if (kdns.find()) {
+ kd = kdns.group(1);
+ }
+ if (wp.find()) {
+ data.put("sign", wp.group(1));
+ if (ad.find()) {
+ data.put("websignkey", ad.group(1));
+ data.put("signs", ad.group(1));
+ }
+ data.put("websign", "");
+ Matcher ws = P_WEBSIGN.matcher(html);
+ if (ws.find()) {
+ data.put("websign", ws.group(1));
+ }
+ data.put("kd", kd);
+ data.put("ves", "1");
+ if (pwd != null && !pwd.isEmpty()) {
+ data.put("p", pwd);
+ }
+ } else if (lastIsngis != null) {
+ data.put("sign", lastIsngis);
+ data.put("kd", kd);
+ if (pwd != null && !pwd.isEmpty()) {
+ data.put("p", pwd);
+ }
+ } else {
+ List signs = new ArrayList<>();
+ Matcher sm = P_SIGN.matcher(html);
+ while (sm.find()) {
+ signs.add(sm.group(1));
+ }
+ if (signs.isEmpty()) {
+ return null;
+ }
+ data.put("sign", signs.size() > 1 ? signs.get(1) : signs.get(0));
+ if (pwd != null && !pwd.isEmpty()) {
+ data.put("p", pwd);
+ }
+ data.put("kd", kd);
+ Matcher ad2 = P_AJAXDATA.matcher(html);
+ if (ad2.find()) {
+ data.put("websignkey", ad2.group(1));
+ data.put("signs", ad2.group(1));
+ }
+ }
+ return new AjaxCall("/" + ajaxPath, data);
+ }
+
+ static AjaxCall extractFolderAjax(String html, String pwd) {
+ if (html == null || html.isEmpty()) {
+ return null;
+ }
+ Matcher block = P_FILEMORE.matcher(html);
+ if (!block.find()) {
+ return null;
+ }
+ Map data = new LinkedHashMap<>();
+ Matcher kv = P_DATA_KV.matcher(block.group(2));
+ while (kv.find()) {
+ String key = kv.group(1);
+ String raw = kv.group(2);
+ if ("pwd".equals(key)) {
+ data.put(key, pwd == null ? "" : pwd);
+ continue;
+ }
+ if ("pg".equals(key) || "pgs".equals(key)) {
+ data.put("pg", "1");
+ continue;
+ }
+ data.put(key, resolveJsValue(html, raw));
+ }
+ if (!data.containsKey("fid") || !data.containsKey("t") || !data.containsKey("k")) {
+ return null;
+ }
+ if (pwd != null && !pwd.isEmpty()) {
+ data.put("pwd", pwd);
+ }
+ return new AjaxCall(block.group(1), data);
+ }
+
+ private static String resolveJsValue(String html, String raw) {
+ if (raw == null) {
+ return "";
+ }
+ if (raw.length() >= 2 && raw.charAt(0) == '\'' && raw.charAt(raw.length() - 1) == '\'') {
+ return raw.substring(1, raw.length() - 1);
+ }
+ if (raw.matches("\\d+")) {
+ return raw;
+ }
+ Matcher m = Pattern.compile("var\\s+" + Pattern.quote(raw) + "\\s*=\\s*'([^']*)'").matcher(html);
+ if (m.find()) {
+ return m.group(1);
+ }
+ if ("pgs".equals(raw)) {
+ return "1";
+ }
+ return raw;
+ }
+
+ private static void parseFormString(MultiMap map, String form) {
+ if (form == null || form.isBlank()) {
+ return;
+ }
+ for (String part : form.split("&")) {
+ int eq = part.indexOf('=');
+ if (eq > 0) {
+ map.add(part.substring(0, eq), part.substring(eq + 1));
+ }
+ }
+ }
+
private void getDownURL(String key, Map obj) {
if (obj == null) {
fail("需要访问密码");
return;
}
- Map, ?> signMap = (Map, ?>)obj.get("data");
+ Object dataObj = obj.get("data");
+ if (dataObj == null) {
+ fail("需要访问密码");
+ return;
+ }
String url0 = String.valueOf(obj.get("url"));
MultiMap map = MultiMap.caseInsensitiveMultiMap();
- signMap.forEach((k, v) -> {
- map.add((String) k, v.toString());
- });
+ if (dataObj instanceof CharSequence) {
+ parseFormString(map, dataObj.toString());
+ } else if (dataObj instanceof Map, ?> signMap) {
+ signMap.forEach((k, v) -> {
+ if (k != null) {
+ map.add(k.toString(), v == null ? "" : v.toString());
+ }
+ });
+ } else {
+ fail("需要访问密码");
+ return;
+ }
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-Encoding: gzip, deflate, br
+ Accept-Encoding: identity
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
Connection: keep-alive
@@ -347,83 +695,176 @@ public class LzTool extends PanBase {
headers.set("referer", key);
// action=downprocess&signs=%3Fctdf&websignkey=I5gl&sign=BWMGOF1sBTRWXwI9BjZdYVA7BDhfNAIyUG9UawJtUGMIPlAhACkCa1UyUTAAYFxvUj5XY1E7UGFXaFVq&websign=&kd=1&ves=1
- String url = SHARE_URL_PREFIX + url0;
+ String url = joinUrl(SHARE_URL_PREFIX, url0);
webClientSession.postAbs(url).putHeaders(headers).sendForm(map).onSuccess(res2 -> {
try {
JsonObject urlJson = asJson(res2);
- String name = urlJson.getString("inf");
- if (urlJson.getInteger("zt") != 1) {
- fail(name);
+ Object infVal = urlJson.getValue("inf");
+ String name = infVal instanceof CharSequence ? infVal.toString() : null;
+ Integer zt = urlJson.getInteger("zt");
+ if (zt == null || zt != 1) {
+ fail(name != null ? name : String.valueOf(infVal));
return;
}
// 文件名
- if (urlJson.containsKey("inf") && urlJson.getMap().get("inf") instanceof CharSequence) {
- ((FileInfo)shareLinkInfo.getOtherParam().get("fileInfo")).setFileName(name);
+ if (name != null) {
+ Object fi = shareLinkInfo.getOtherParam().get("fileInfo");
+ if (fi instanceof FileInfo fileInfo) {
+ fileInfo.setFileName(name);
+ }
}
String downUrl = urlJson.getString("dom") + "/file/" + urlJson.getString("url");
- headers.remove("Referer");
- webClientSession.getAbs(downUrl).putHeaders(headers).send()
- .onSuccess(res3 -> {
- try {
- String location = res3.headers().get("Location");
- if (location == null) {
- String text = asText(res3);
- if (isShareCancelledPage(text)) {
- fail(downUrl + " -> 分享已失效或文件已取消分享");
- return;
- }
- // 使用cookie 再请求一次
- headers.add("Referer", downUrl);
- String arg1 = extractAcwArg1(text);
- if (arg1 == null) {
- fail(downUrl + " -> 蓝奏云反爬 arg1 Cookie 解析失败,可能分享已失效");
- return;
- }
- String acw_sc__v2 = AcwScV2Generator.acwScV2Simple(arg1);
- // 从 downUrl 中动态提取域名
- String downDomain = ".lanrar.com";
- try {
- java.net.URL du = new java.net.URL(downUrl);
- String h = du.getHost();
- int dot = h.indexOf('.');
- if (dot >= 0) downDomain = h.substring(dot);
- } catch (MalformedURLException ignored) {}
- // 创建一个 Cookie 并放入 CookieStore
- DefaultCookie nettyCookie = new DefaultCookie("acw_sc__v2", acw_sc__v2);
- nettyCookie.setDomain(downDomain);
- nettyCookie.setPath("/");
- nettyCookie.setSecure(false);
- nettyCookie.setHttpOnly(false);
- WebClientSession webClientSession2 = WebClientSession.create(clientNoRedirects);
- webClientSession2.cookieStore().put(nettyCookie);
- webClientSession2.getAbs(downUrl).putHeaders(headers).send()
- .onSuccess(res4 -> {
- try {
- String location0 = res4.headers().get("Location");
- if (location0 == null) {
- fail(downUrl + " -> 直链获取失败2, 可能分享已失效");
- } else {
- setDateAndComplete(location0);
- }
- } catch (Exception e) {
- fail("蓝奏云直链二次响应处理异常: {}", e.getMessage());
- }
- }).onFailure(handleFail(downUrl));
- return;
- }
- setDateAndComplete(location);
- } catch (Exception e) {
- fail("蓝奏云直链响应处理异常: {}", e.getMessage());
- }
- })
- .onFailure(handleFail(downUrl));
+ followFileUrl(downUrl, headers);
} catch (Exception e) {
- fail("解析异常");
+ fail(e, "解析异常");
}
}).onFailure(handleFail(url));
}
+ /**
+ * 下载域已不再走 arg1/acw_sc__v2 挑战页;带 down_ip=1 时通常直接 302。
+ * 未跳转则走页面 down_r → POST /ajax.php 二次验证。
+ */
+ private void followFileUrl(String downUrl, MultiMap headers) {
+ String origin = originOf(downUrl, "https://developer2.lanrar.com");
+ putSessionCookie("down_ip", "1", downUrl, ".lanrar.com");
+ headers.set("referer", origin);
+ webClientSession.getAbs(downUrl).putHeaders(headers).send()
+ .onSuccess(res3 -> {
+ try {
+ String location = res3.headers().get("Location");
+ if (location != null) {
+ setDateAndComplete(location);
+ return;
+ }
+ String text = asText(res3);
+ if (isShareCancelledPage(text)) {
+ fail(downUrl + " -> 分享已失效或文件已取消分享");
+ return;
+ }
+ if (text.contains("down_r") && text.contains("ajax.php")) {
+ verifyDownloadPage(origin, downUrl, text, headers);
+ return;
+ }
+ if (hasAcwArg1(text)) {
+ retryFileUrlWithAcw(downUrl, text, headers);
+ return;
+ }
+ fail(downUrl + " -> 直链获取失败2, 可能分享已失效");
+ } catch (Exception e) {
+ fail("蓝奏云直链响应处理异常: {}", e.getMessage());
+ }
+ })
+ .onFailure(handleFail(downUrl));
+ }
+
+ private void verifyDownloadPage(String origin, String downUrl, String html, MultiMap headers) {
+ Matcher fileM = Pattern.compile("'file'\\s*:\\s*'([^']+)'").matcher(html);
+ Matcher signM = Pattern.compile("'sign'\\s*:\\s*'([^']+)'").matcher(html);
+ if (!fileM.find() || !signM.find()) {
+ fail(downUrl + " -> 二次验证参数缺失");
+ return;
+ }
+ String file = fileM.group(1);
+ String sign = signM.group(1);
+ WebClientVertxInit.get().setTimer(2000, id -> postVerifyAjax(origin, downUrl, file, sign, headers, false));
+ }
+
+ private void postVerifyAjax(String origin, String downUrl, String file, String sign, MultiMap headers, boolean filePath) {
+ MultiMap map = MultiMap.caseInsensitiveMultiMap();
+ map.add("file", file);
+ map.add("el", "2");
+ map.add("sign", sign);
+ String ajaxUrl = origin + (filePath ? "/file/ajax.php" : "/ajax.php");
+ headers.set("referer", origin);
+ webClientSession.postAbs(ajaxUrl).putHeaders(headers).sendForm(map).onSuccess(res -> {
+ try {
+ JsonObject json = asJson(res);
+ Integer zt = json.getInteger("zt");
+ String u = json.getString("url");
+ if (zt != null && zt == 1 && u != null && u.startsWith("http") && !u.contains("SignError")) {
+ setDateAndComplete(u);
+ return;
+ }
+ if (!filePath) {
+ postVerifyAjax(origin, downUrl, file, sign, headers, true);
+ return;
+ }
+ fail(downUrl + " -> 二次验证失败: " + (u != null ? u : String.valueOf(json.getValue("inf"))));
+ } catch (Exception e) {
+ fail("二次验证解析异常: {}", e.getMessage());
+ }
+ }).onFailure(handleFail(ajaxUrl));
+ }
+
+ private void retryFileUrlWithAcw(String downUrl, String text, MultiMap headers) {
+ String arg1 = extractAcwArg1(text);
+ if (arg1 == null) {
+ fail(downUrl + " -> 直链获取失败2, 可能分享已失效");
+ return;
+ }
+ String acw_sc__v2 = AcwScV2Generator.acwScV2Simple(arg1);
+ putSessionCookie("acw_sc__v2", acw_sc__v2, downUrl, ".lanrar.com");
+ headers.set("referer", originOf(downUrl, "https://developer2.lanrar.com"));
+ webClientSession.getAbs(downUrl).putHeaders(headers).send()
+ .onSuccess(res4 -> {
+ try {
+ String location0 = res4.headers().get("Location");
+ if (location0 == null) {
+ fail(downUrl + " -> 直链获取失败2, 可能分享已失效");
+ } else {
+ setDateAndComplete(location0);
+ }
+ } catch (Exception e) {
+ fail("蓝奏云直链二次响应处理异常: {}", e.getMessage());
+ }
+ }).onFailure(handleFail(downUrl));
+ }
+
+ private void putSessionCookie(String name, String value, String url, String fallbackDomain) {
+ String domain = fallbackDomain;
+ try {
+ java.net.URL urlObj = new java.net.URL(url);
+ String host = urlObj.getHost();
+ int firstDot = host.indexOf('.');
+ if (firstDot >= 0) {
+ domain = host.substring(firstDot);
+ }
+ } catch (MalformedURLException ignored) {}
+ DefaultCookie nettyCookie = new DefaultCookie(name, value);
+ nettyCookie.setDomain(domain);
+ nettyCookie.setPath("/");
+ nettyCookie.setSecure(false);
+ nettyCookie.setHttpOnly(false);
+ webClientSession.cookieStore().put(nettyCookie);
+ }
+
+ private static String originOf(String url, String fallback) {
+ try {
+ java.net.URL u = new java.net.URL(url);
+ return u.getProtocol() + "://" + u.getHost();
+ } catch (MalformedURLException e) {
+ return fallback;
+ }
+ }
+
+ private static String joinUrl(String base, String path) {
+ if (path == null || path.isBlank()) {
+ return base;
+ }
+ if (path.startsWith("http://") || path.startsWith("https://")) {
+ return path;
+ }
+ if (base.endsWith("/") && path.startsWith("/")) {
+ return base.substring(0, base.length() - 1) + path;
+ }
+ if (!base.endsWith("/") && !path.startsWith("/")) {
+ return base + "/" + path;
+ }
+ return base + path;
+ }
+
private void setDateAndComplete(String location0) {
// 分享时间 提取url中的时间戳格式:lanzoui.com/abc/abc/yyyy/mm/dd/
Matcher matcher = URL_DATE_PATTERN.matcher(location0);
@@ -460,7 +901,7 @@ public class LzTool extends PanBase {
String html = asText(res);
// 检查是否需要 cookie 验证
if (hasAcwArg1(html)) {
- webClientSession = WebClientSession.create(clientNoRedirects);
+ webClientSession = WebClientSession.create(lzClient);
if (!setCookie(html, sUrl)) {
promise.tryFail(baseMsg() + "蓝奏云反爬 arg1 Cookie 解析失败,页面内容异常");
return;
@@ -492,21 +933,26 @@ public class LzTool extends PanBase {
promise.tryFail(baseMsg() + "分享已失效或文件已取消分享");
return;
}
- // 检测是否为文件分享链接 (不含 /s/、/b/ 路径段且不含 b 开头的路径段)
- if (!sUrl.matches(".*/(s|b)/[^/]+.*") && !sUrl.matches(".*/b[^/]+.*")) {
+ // 检测是否为文件分享链接 (不含 /s/、/b/ 路径段且不含 b0 开头的路径段)
+ if (!sUrl.matches(".*/(s|b)/[^/]+.*") && !sUrl.matches(".*/b0[^/]+.*")) {
promise.tryFail(baseMsg() + "该链接为蓝奏云文件分享,请使用文件解析接口");
return;
}
try {
- String jsText = getJsByPwd(pwd, html, "var urls =window.location.href");
- ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, "file");
- Map data = CastUtil.cast(scriptObjectMirror.get("data"));
- MultiMap map = MultiMap.caseInsensitiveMultiMap();
- data.forEach((k, v) -> map.set(k, v.toString()));
- log.debug("解析参数: {}", map);
+ AjaxCall call = extractFolderAjax(html, pwd);
+ if (call == null) {
+ String jsText = getJsByPwd(pwd, html, "var urls =window.location.href");
+ ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, "file", pwd);
+ Map data = CastUtil.cast(scriptObjectMirror.get("data"));
+ Map form = new LinkedHashMap<>();
+ data.forEach((k, v) -> form.put(k, String.valueOf(v)));
+ call = new AjaxCall("/filemoreajax.php?file=" + form.get("fid"), form);
+ }
+ log.debug("解析参数: {}", call.form());
MultiMap headers = getHeaders(sUrl);
+ MultiMap map = call.toForm();
- String url = SHARE_URL_PREFIX + "filemoreajax.php?file=" + data.get("fid");
+ String url = joinUrl(SHARE_URL_PREFIX, call.path());
webClientSession.postAbs(url).putHeaders(headers).sendForm(map).onSuccess(res2 -> {
try {
String resBody = asText(res2);
@@ -542,7 +988,10 @@ public class LzTool extends PanBase {
promise.tryFail(baseMsg() + "蓝奏云文件列表响应为空");
return;
}
- JsonObject fileListJson = new JsonObject(responseBody);
+ JsonObject fileListJson = parseLzJson(responseBody);
+ if (fileListJson == null) {
+ fileListJson = new JsonObject(responseBody);
+ }
if (fileListJson.getInteger("zt") != 1) {
promise.tryFail(baseMsg() + fileListJson.getString("info"));
return;
diff --git a/parser/src/main/java/cn/qaiu/util/JsContent.java b/parser/src/main/java/cn/qaiu/util/JsContent.java
index 42c84a4..9d6afba 100644
--- a/parser/src/main/java/cn/qaiu/util/JsContent.java
+++ b/parser/src/main/java/cn/qaiu/util/JsContent.java
@@ -101,59 +101,384 @@ public interface JsContent {
""";
String lz = """
/**
- * 蓝奏云解析器js签名获取工具
+ * 蓝奏云解析器 JS 沙箱:伪装 jQuery / document / window。
+ * 新版页面会用 document.cookie、location.reload、querySelector、
+ * $('#pwd').val()、.html()、.css() 等,这里做成可链式的最小实现。
+ * kdns.js 在浏览器里是 `var killdns = true`,需一并注入,否则 kd 会被改成 0。
*/
var signObj;
+ var __lzPwd = '';
+ var killdns = true;
+ function __lzSetPwd(p) {
+ __lzPwd = p == null ? '' : String(p);
+ }
+
+ function __lzEl(id) {
+ var nid = String(id == null ? '' : id).replace(/^[#.]/, '');
+ var el = {
+ id: nid,
+ _value: nid === 'pwd' ? __lzPwd : '',
+ checked: false,
+ disabled: false,
+ innerHTML: '',
+ innerText: '',
+ textContent: '',
+ className: '',
+ style: { display: '', visibility: '', width: '', height: '' },
+ classList: {
+ add: function () {},
+ remove: function () {},
+ contains: function () { return false; },
+ toggle: function () {}
+ },
+ setAttribute: function () {},
+ getAttribute: function () { return null; },
+ removeAttribute: function () {},
+ addEventListener: function (t, fn) {
+ if (typeof fn === 'function') {
+ try { fn(); } catch (e) {}
+ }
+ },
+ removeEventListener: function () {},
+ appendChild: function (n) { return n; },
+ removeChild: function (n) { return n; },
+ insertBefore: function (n) { return n; },
+ click: function () {},
+ focus: function () {},
+ blur: function () {},
+ submit: function () {},
+ select: function () {},
+ reset: function () {}
+ };
+ el.parentNode = el;
+ el.parentElement = el;
+ el.children = [];
+ el.childNodes = [];
+ el.firstChild = null;
+ el.lastChild = null;
+ try {
+ Object.defineProperty(el, 'value', {
+ get: function () { return nid === 'pwd' ? __lzPwd : el._value; },
+ set: function (v) {
+ el._value = v;
+ if (nid === 'pwd') {
+ __lzPwd = v == null ? '' : String(v);
+ }
+ }
+ });
+ } catch (e) {
+ el.value = el._value;
+ }
+ return el;
+ }
+
+ function __lzJq(sel) {
+ var id = '';
+ if (typeof sel === 'string') {
+ id = sel.replace(/^[#.]/, '');
+ } else if (sel && sel.id) {
+ id = String(sel.id);
+ }
+ var el = (sel && sel.style && sel.addEventListener) ? sel : __lzEl(id);
+ var api = {
+ 0: el,
+ length: 1,
+ selector: sel,
+ ready: function (fn) {
+ if (typeof fn === 'function') {
+ try { fn(jQuery); } catch (e) {}
+ }
+ return api;
+ },
+ on: function (t, fn) {
+ if (typeof fn === 'function') {
+ try { fn(); } catch (e) {}
+ }
+ return api;
+ },
+ off: function () { return api; },
+ bind: function (t, fn) { return api.on(t, fn); },
+ unbind: function () { return api; },
+ click: function (fn) {
+ if (typeof fn === 'function') {
+ try { fn(); } catch (e) {}
+ }
+ return api;
+ },
+ focus: function (fn) {
+ if (typeof fn === 'function') {
+ try { fn(); } catch (e) {}
+ }
+ return api;
+ },
+ blur: function () { return api; },
+ keyup: function (fn) {
+ if (typeof fn === 'function') {
+ try { fn(); } catch (e) {}
+ }
+ return api;
+ },
+ keydown: function (fn) { return api.keyup(fn); },
+ keypress: function (fn) { return api.keyup(fn); },
+ submit: function (fn) { return api.click(fn); },
+ change: function (fn) { return api.click(fn); },
+ hover: function () { return api; },
+ val: function (v) {
+ if (arguments.length === 0) {
+ if (typeof el.value !== 'undefined') {
+ return el.value;
+ }
+ return (id === 'pwd') ? __lzPwd : '';
+ }
+ el.value = v;
+ return api;
+ },
+ html: function (v) {
+ if (arguments.length === 0) {
+ return el.innerHTML;
+ }
+ el.innerHTML = v;
+ return api;
+ },
+ text: function (v) {
+ if (arguments.length === 0) {
+ return el.innerText;
+ }
+ el.innerText = v;
+ el.textContent = v;
+ return api;
+ },
+ attr: function (k, v) {
+ if (arguments.length < 2) {
+ return null;
+ }
+ return api;
+ },
+ prop: function (k, v) {
+ if (arguments.length < 2) {
+ return false;
+ }
+ return api;
+ },
+ css: function () { return api; },
+ addClass: function () { return api; },
+ removeClass: function () { return api; },
+ toggleClass: function () { return api; },
+ hasClass: function () { return false; },
+ show: function () {
+ el.style.display = '';
+ return api;
+ },
+ hide: function () {
+ el.style.display = 'none';
+ return api;
+ },
+ fadeIn: function () { return api; },
+ fadeOut: function () { return api; },
+ animate: function () { return api; },
+ find: function () { return api; },
+ parent: function () { return api; },
+ children: function () { return api; },
+ eq: function () { return api; },
+ first: function () { return api; },
+ last: function () { return api; },
+ each: function (fn) {
+ if (typeof fn === 'function') {
+ try { fn.call(el, 0, el); } catch (e) {}
+ }
+ return api;
+ },
+ append: function () { return api; },
+ prepend: function () { return api; },
+ remove: function () { return api; },
+ empty: function () { return api; },
+ ajax: function (obj) {
+ signObj = obj;
+ return api;
+ },
+ get: function () { return el; }
+ };
+ return api;
+ }
var $, jQuery;
-
- $ = jQuery = function () {
- return new jQuery.fn.init();
- }
+ $ = jQuery = function (sel) {
+ if (typeof sel === 'function') {
+ try { sel(jQuery); } catch (e) {}
+ return __lzJq(document);
+ }
+ return __lzJq(sel);
+ };
jQuery.fn = jQuery.prototype = {
- init: function () {
- return {
- focus: function (a) {
-
- },
- keyup: function(a) {
-
- },
- ajax: function (obj) {
- signObj = obj
- },
- val: function(a) {
-
- },
-
- }
- },
-
- }
-
+ init: function (sel) {
+ return __lzJq(sel);
+ }
+ };
jQuery.fn.init.prototype = jQuery.fn;
-
+ $.fn = jQuery.fn;
$.ajax = function (obj) {
- signObj = obj
- }
+ signObj = obj;
+ return {
+ done: function () { return this; },
+ fail: function () { return this; },
+ always: function () { return this; }
+ };
+ };
+ $.get = function () {};
+ $.post = function () {};
+ $.extend = function () {
+ var t = arguments[0] || {};
+ for (var i = 1; i < arguments.length; i++) {
+ var s = arguments[i];
+ if (s) {
+ for (var k in s) {
+ if (s.hasOwnProperty(k)) {
+ t[k] = s[k];
+ }
+ }
+ }
+ }
+ return t;
+ };
+ $.each = function (obj, fn) {
+ if (!obj || typeof fn !== 'function') {
+ return obj;
+ }
+ if (typeof obj.length === 'number') {
+ for (var i = 0; i < obj.length; i++) {
+ fn.call(obj[i], i, obj[i]);
+ }
+ } else {
+ for (var k in obj) {
+ if (obj.hasOwnProperty(k)) {
+ fn.call(obj[k], k, obj[k]);
+ }
+ }
+ }
+ return obj;
+ };
+ $.isFunction = function (f) { return typeof f === 'function'; };
+ $.isArray = function (a) {
+ return Object.prototype.toString.call(a) === '[object Array]';
+ };
+ $.trim = function (s) {
+ return s == null ? '' : String(s).replace(/^\\s+|\\s+$/g, '');
+ };
+
+ var __lzLocation = {
+ href: '',
+ search: '',
+ pathname: '/',
+ hash: '',
+ host: '',
+ hostname: '',
+ protocol: 'https:',
+ port: '',
+ origin: '',
+ assign: function () {},
+ replace: function () {},
+ reload: function () {}
+ };
var document = {
- getElementById: function (v) {
- return {
- value: 'v',
- style: {
- display: ''
- },
- addEventListener: function() {}
+ cookie: '',
+ title: '',
+ domain: '',
+ referrer: '',
+ readyState: 'complete',
+ hidden: false,
+ visibilityState: 'visible',
+ documentElement: null,
+ body: null,
+ head: null,
+ location: __lzLocation,
+ getElementById: function (id) { return __lzEl(id); },
+ getElementsByClassName: function () { return []; },
+ getElementsByTagName: function (t) {
+ return t === 'script' ? [] : [__lzEl(t)];
+ },
+ getElementsByName: function () { return []; },
+ querySelector: function (s) { return __lzEl(s); },
+ querySelectorAll: function (s) { return [__lzEl(s)]; },
+ createElement: function (t) { return __lzEl(t); },
+ createTextNode: function (t) { return { nodeValue: t, data: t }; },
+ createDocumentFragment: function () { return __lzEl('fragment'); },
+ addEventListener: function (t, fn) {
+ if (typeof fn === 'function') {
+ try { fn(); } catch (e) {}
}
},
+ removeEventListener: function () {},
+ write: function () {},
+ writeln: function () {},
+ open: function () {},
+ close: function () {}
+ };
+ document.documentElement = __lzEl('html');
+ document.body = __lzEl('body');
+ document.head = __lzEl('head');
+
+ var navigator = {
+ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
+ platform: 'Win32',
+ language: 'zh-CN',
+ cookieEnabled: true,
+ onLine: true
+ };
+
+ var location = __lzLocation;
+ var console = {
+ log: function () {},
+ warn: function () {},
+ error: function () {},
+ info: function () {},
+ debug: function () {}
+ };
+
+ function setTimeout(fn, delay) {
+ if (typeof fn === 'function' && (!delay || delay <= 0)) {
+ try { fn(); } catch (e) {}
+ }
+ return 0;
}
-
- var window = {location: {}}
+ function setInterval() { return 0; }
+ function clearTimeout() {}
+ function clearInterval() {}
+
+ var window = {
+ location: __lzLocation,
+ document: document,
+ navigator: navigator,
+ console: console,
+ innerWidth: 1920,
+ innerHeight: 1080,
+ setTimeout: setTimeout,
+ setInterval: setInterval,
+ clearTimeout: clearTimeout,
+ clearInterval: clearInterval,
+ addEventListener: function (t, fn) {
+ if (typeof fn === 'function') {
+ try { fn(); } catch (e) {}
+ }
+ },
+ removeEventListener: function () {},
+ atob: function (s) { return s; },
+ btoa: function (s) { return s; }
+ };
+ window.window = window;
+ window.top = window;
+ window.self = window;
+ window.parent = window;
+ window.jQuery = jQuery;
+ window.$ = $;
+
+ var top = window;
+ var self = window;
+ var parent = window;
""";
String kwSignString = """
diff --git a/parser/src/main/java/cn/qaiu/util/JsExecUtils.java b/parser/src/main/java/cn/qaiu/util/JsExecUtils.java
index 0abd79a..342b214 100644
--- a/parser/src/main/java/cn/qaiu/util/JsExecUtils.java
+++ b/parser/src/main/java/cn/qaiu/util/JsExecUtils.java
@@ -50,21 +50,47 @@ public class JsExecUtils {
*/
public static ScriptObjectMirror executeDynamicJs(String jsText, String funName) throws ScriptException,
NoSuchMethodException {
- ScriptEngine engine = ENGINE_MANAGER.getEngineByName("JavaScript"); // 得到脚本引擎
+ return executeDynamicJs(jsText, funName, null);
+ }
+
+ /**
+ * @param pwd 分享密码,写入伪 DOM(#pwd / getElementById('pwd')),供新版页面取值
+ */
+ public static ScriptObjectMirror executeDynamicJs(String jsText, String funName, String pwd) throws ScriptException,
+ NoSuchMethodException {
+ ScriptEngine engine = ENGINE_MANAGER.getEngineByName("JavaScript");
try {
- engine.eval(JsContent.lz + "\n" + jsText);
+ engine.eval(JsContent.lz);
Invocable inv = (Invocable) engine;
- //调用js中的函数
- if (StringUtils.isNotEmpty(funName)) {
- inv.invokeFunction(funName);
+ if (pwd != null) {
+ inv.invokeFunction("__lzSetPwd", pwd);
}
- return (ScriptObjectMirror) engine.get("signObj");
+ try {
+ engine.eval(jsText);
+ if (StringUtils.isNotEmpty(funName)) {
+ inv.invokeFunction(funName);
+ }
+ } catch (ScriptException | NoSuchMethodException | RuntimeException e) {
+ ScriptObjectMirror captured = asSignObj(engine.get("signObj"));
+ if (captured != null) {
+ return captured;
+ }
+ throw e;
+ }
+ return asSignObj(engine.get("signObj"));
} finally {
- // 清理引擎持有的引用,帮助 GC 回收
clearEngineBindings(engine);
}
}
+ private static ScriptObjectMirror asSignObj(Object sign) {
+ if (sign instanceof ScriptObjectMirror mirror
+ && (mirror.get("url") != null || mirror.get("data") != null)) {
+ return mirror;
+ }
+ return null;
+ }
+
/**
* 调用执行js文件(使用缓存的 ScriptEngineManager 创建新引擎实例)
diff --git a/parser/src/main/resources/js/lz.js b/parser/src/main/resources/js/lz.js
index 7df1e7b..fd16f53 100644
--- a/parser/src/main/resources/js/lz.js
+++ b/parser/src/main/resources/js/lz.js
@@ -1,46 +1,379 @@
/**
- * 蓝奏云解析器js签名获取工具
+ * 蓝奏云解析器 JS 沙箱:伪装 jQuery / document / window。
+ * 新版页面会用 document.cookie、location.reload、querySelector、
+ * $('#pwd').val()、.html()、.css() 等,这里做成可链式的最小实现。
+ * kdns.js 在浏览器里是 `var killdns = true`,需一并注入,否则 kd 会被改成 0。
*/
var signObj;
+var __lzPwd = '';
+var killdns = true;
+function __lzSetPwd(p) {
+ __lzPwd = p == null ? '' : String(p);
+}
+
+function __lzEl(id) {
+ var nid = String(id == null ? '' : id).replace(/^[#.]/, '');
+ var el = {
+ id: nid,
+ _value: nid === 'pwd' ? __lzPwd : '',
+ checked: false,
+ disabled: false,
+ innerHTML: '',
+ innerText: '',
+ textContent: '',
+ className: '',
+ style: { display: '', visibility: '', width: '', height: '' },
+ classList: {
+ add: function () {},
+ remove: function () {},
+ contains: function () { return false; },
+ toggle: function () {}
+ },
+ setAttribute: function () {},
+ getAttribute: function () { return null; },
+ removeAttribute: function () {},
+ addEventListener: function (t, fn) {
+ if (typeof fn === 'function') {
+ try { fn(); } catch (e) {}
+ }
+ },
+ removeEventListener: function () {},
+ appendChild: function (n) { return n; },
+ removeChild: function (n) { return n; },
+ insertBefore: function (n) { return n; },
+ click: function () {},
+ focus: function () {},
+ blur: function () {},
+ submit: function () {},
+ select: function () {},
+ reset: function () {}
+ };
+ el.parentNode = el;
+ el.parentElement = el;
+ el.children = [];
+ el.childNodes = [];
+ el.firstChild = null;
+ el.lastChild = null;
+ try {
+ Object.defineProperty(el, 'value', {
+ get: function () { return nid === 'pwd' ? __lzPwd : el._value; },
+ set: function (v) {
+ el._value = v;
+ if (nid === 'pwd') {
+ __lzPwd = v == null ? '' : String(v);
+ }
+ }
+ });
+ } catch (e) {
+ el.value = el._value;
+ }
+ return el;
+}
+
+function __lzJq(sel) {
+ var id = '';
+ if (typeof sel === 'string') {
+ id = sel.replace(/^[#.]/, '');
+ } else if (sel && sel.id) {
+ id = String(sel.id);
+ }
+ var el = (sel && sel.style && sel.addEventListener) ? sel : __lzEl(id);
+ var api = {
+ 0: el,
+ length: 1,
+ selector: sel,
+ ready: function (fn) {
+ if (typeof fn === 'function') {
+ try { fn(jQuery); } catch (e) {}
+ }
+ return api;
+ },
+ on: function (t, fn) {
+ if (typeof fn === 'function') {
+ try { fn(); } catch (e) {}
+ }
+ return api;
+ },
+ off: function () { return api; },
+ bind: function (t, fn) { return api.on(t, fn); },
+ unbind: function () { return api; },
+ click: function (fn) {
+ if (typeof fn === 'function') {
+ try { fn(); } catch (e) {}
+ }
+ return api;
+ },
+ focus: function (fn) {
+ if (typeof fn === 'function') {
+ try { fn(); } catch (e) {}
+ }
+ return api;
+ },
+ blur: function () { return api; },
+ keyup: function (fn) {
+ if (typeof fn === 'function') {
+ try { fn(); } catch (e) {}
+ }
+ return api;
+ },
+ keydown: function (fn) { return api.keyup(fn); },
+ keypress: function (fn) { return api.keyup(fn); },
+ submit: function (fn) { return api.click(fn); },
+ change: function (fn) { return api.click(fn); },
+ hover: function () { return api; },
+ val: function (v) {
+ if (arguments.length === 0) {
+ if (typeof el.value !== 'undefined') {
+ return el.value;
+ }
+ return (id === 'pwd') ? __lzPwd : '';
+ }
+ el.value = v;
+ return api;
+ },
+ html: function (v) {
+ if (arguments.length === 0) {
+ return el.innerHTML;
+ }
+ el.innerHTML = v;
+ return api;
+ },
+ text: function (v) {
+ if (arguments.length === 0) {
+ return el.innerText;
+ }
+ el.innerText = v;
+ el.textContent = v;
+ return api;
+ },
+ attr: function (k, v) {
+ if (arguments.length < 2) {
+ return null;
+ }
+ return api;
+ },
+ prop: function (k, v) {
+ if (arguments.length < 2) {
+ return false;
+ }
+ return api;
+ },
+ css: function () { return api; },
+ addClass: function () { return api; },
+ removeClass: function () { return api; },
+ toggleClass: function () { return api; },
+ hasClass: function () { return false; },
+ show: function () {
+ el.style.display = '';
+ return api;
+ },
+ hide: function () {
+ el.style.display = 'none';
+ return api;
+ },
+ fadeIn: function () { return api; },
+ fadeOut: function () { return api; },
+ animate: function () { return api; },
+ find: function () { return api; },
+ parent: function () { return api; },
+ children: function () { return api; },
+ eq: function () { return api; },
+ first: function () { return api; },
+ last: function () { return api; },
+ each: function (fn) {
+ if (typeof fn === 'function') {
+ try { fn.call(el, 0, el); } catch (e) {}
+ }
+ return api;
+ },
+ append: function () { return api; },
+ prepend: function () { return api; },
+ remove: function () { return api; },
+ empty: function () { return api; },
+ ajax: function (obj) {
+ signObj = obj;
+ return api;
+ },
+ get: function () { return el; }
+ };
+ return api;
+}
var $, jQuery;
-
-$ = jQuery = function () {
- return new jQuery.fn.init();
-}
+$ = jQuery = function (sel) {
+ if (typeof sel === 'function') {
+ try { sel(jQuery); } catch (e) {}
+ return __lzJq(document);
+ }
+ return __lzJq(sel);
+};
jQuery.fn = jQuery.prototype = {
- init: function () {
- return {
- focus: function (a) {
-
- },
- keyup: function(a) {
-
- },
- ajax: function (obj) {
- signObj = obj
- }
-
- }
- },
-
-}
-
+ init: function (sel) {
+ return __lzJq(sel);
+ }
+};
jQuery.fn.init.prototype = jQuery.fn;
+$.fn = jQuery.fn;
-
-// 伪装jquery.ajax函数获取关键数据
$.ajax = function (obj) {
- signObj = obj
-}
+ signObj = obj;
+ return {
+ done: function () { return this; },
+ fail: function () { return this; },
+ always: function () { return this; }
+ };
+};
+$.get = function () {};
+$.post = function () {};
+$.extend = function () {
+ var t = arguments[0] || {};
+ for (var i = 1; i < arguments.length; i++) {
+ var s = arguments[i];
+ if (s) {
+ for (var k in s) {
+ if (s.hasOwnProperty(k)) {
+ t[k] = s[k];
+ }
+ }
+ }
+ }
+ return t;
+};
+$.each = function (obj, fn) {
+ if (!obj || typeof fn !== 'function') {
+ return obj;
+ }
+ if (typeof obj.length === 'number') {
+ for (var i = 0; i < obj.length; i++) {
+ fn.call(obj[i], i, obj[i]);
+ }
+ } else {
+ for (var k in obj) {
+ if (obj.hasOwnProperty(k)) {
+ fn.call(obj[k], k, obj[k]);
+ }
+ }
+ }
+ return obj;
+};
+$.isFunction = function (f) { return typeof f === 'function'; };
+$.isArray = function (a) {
+ return Object.prototype.toString.call(a) === '[object Array]';
+};
+$.trim = function (s) {
+ return s == null ? '' : String(s).replace(/^\s+|\s+$/g, '');
+};
+
+var __lzLocation = {
+ href: '',
+ search: '',
+ pathname: '/',
+ hash: '',
+ host: '',
+ hostname: '',
+ protocol: 'https:',
+ port: '',
+ origin: '',
+ assign: function () {},
+ replace: function () {},
+ reload: function () {}
+};
var document = {
- getElementById: function (v) {
- return {
- value: 'v'
+ cookie: '',
+ title: '',
+ domain: '',
+ referrer: '',
+ readyState: 'complete',
+ hidden: false,
+ visibilityState: 'visible',
+ documentElement: null,
+ body: null,
+ head: null,
+ location: __lzLocation,
+ getElementById: function (id) { return __lzEl(id); },
+ getElementsByClassName: function () { return []; },
+ getElementsByTagName: function (t) {
+ return t === 'script' ? [] : [__lzEl(t)];
+ },
+ getElementsByName: function () { return []; },
+ querySelector: function (s) { return __lzEl(s); },
+ querySelectorAll: function (s) { return [__lzEl(s)]; },
+ createElement: function (t) { return __lzEl(t); },
+ createTextNode: function (t) { return { nodeValue: t, data: t }; },
+ createDocumentFragment: function () { return __lzEl('fragment'); },
+ addEventListener: function (t, fn) {
+ if (typeof fn === 'function') {
+ try { fn(); } catch (e) {}
}
},
+ removeEventListener: function () {},
+ write: function () {},
+ writeln: function () {},
+ open: function () {},
+ close: function () {}
+};
+document.documentElement = __lzEl('html');
+document.body = __lzEl('body');
+document.head = __lzEl('head');
+
+var navigator = {
+ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
+ platform: 'Win32',
+ language: 'zh-CN',
+ cookieEnabled: true,
+ onLine: true
+};
+
+var location = __lzLocation;
+var console = {
+ log: function () {},
+ warn: function () {},
+ error: function () {},
+ info: function () {},
+ debug: function () {}
+};
+
+function setTimeout(fn, delay) {
+ if (typeof fn === 'function' && (!delay || delay <= 0)) {
+ try { fn(); } catch (e) {}
+ }
+ return 0;
}
+function setInterval() { return 0; }
+function clearTimeout() {}
+function clearInterval() {}
+
+var window = {
+ location: __lzLocation,
+ document: document,
+ navigator: navigator,
+ console: console,
+ innerWidth: 1920,
+ innerHeight: 1080,
+ setTimeout: setTimeout,
+ setInterval: setInterval,
+ clearTimeout: clearTimeout,
+ clearInterval: clearInterval,
+ addEventListener: function (t, fn) {
+ if (typeof fn === 'function') {
+ try { fn(); } catch (e) {}
+ }
+ },
+ removeEventListener: function () {},
+ atob: function (s) { return s; },
+ btoa: function (s) { return s; }
+};
+window.window = window;
+window.top = window;
+window.self = window;
+window.parent = window;
+window.jQuery = jQuery;
+window.$ = $;
+
+var top = window;
+var self = window;
+var parent = window;
diff --git a/parser/src/test/java/cn/qaiu/parser/impl/LzToolAjaxExtractTest.java b/parser/src/test/java/cn/qaiu/parser/impl/LzToolAjaxExtractTest.java
new file mode 100644
index 0000000..104f77e
--- /dev/null
+++ b/parser/src/test/java/cn/qaiu/parser/impl/LzToolAjaxExtractTest.java
@@ -0,0 +1,52 @@
+package cn.qaiu.parser.impl;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class LzToolAjaxExtractTest {
+
+ @Test
+ public void testExtractWpSignAjaxfile() {
+ String html = """
+
+ """;
+ LzTool.AjaxCall call = LzTool.extractAjaxFromHtml(html, null);
+ assertNotNull(call);
+ assertEquals("/ajaxfile.php?file=150233466", call.path());
+ assertEquals("BWMGOF5v", call.form().get("sign"));
+ assertEquals("MrBR", call.form().get("websignkey"));
+ assertEquals("downprocess", call.form().get("action"));
+ assertEquals("1", call.form().get("kd"));
+ }
+
+ @Test
+ public void testExtractPasswordSign() {
+ String html = """
+
+
+ """;
+ LzTool.AjaxCall call = LzTool.extractAjaxFromHtml(html, "e4k4");
+ assertNotNull(call);
+ assertEquals("/ajaxm.php?file=1", call.path());
+ assertEquals("ABC123", call.form().get("sign"));
+ assertEquals("e4k4", call.form().get("p"));
+ }
+}
diff --git a/parser/src/test/java/cn/qaiu/util/JsExecUtilsLzTest.java b/parser/src/test/java/cn/qaiu/util/JsExecUtilsLzTest.java
new file mode 100644
index 0000000..96a52c0
--- /dev/null
+++ b/parser/src/test/java/cn/qaiu/util/JsExecUtilsLzTest.java
@@ -0,0 +1,90 @@
+package cn.qaiu.util;
+
+import org.junit.Test;
+import org.openjdk.nashorn.api.scripting.ScriptObjectMirror;
+
+import java.util.Map;
+
+import static org.junit.Assert.*;
+
+/**
+ * 新版蓝奏页面会调用更多 document / jQuery API,沙箱必须能跑完并抓住 $.ajax。
+ */
+public class JsExecUtilsLzTest {
+
+ @Test
+ public void testNewIframeAjaxWithDomApis() throws Exception {
+ String js = """
+ var lanosso = '';
+ var down_1 = '';
+ var wsk_sign = 'c20230908';
+ var wp_sign = 'SIGN_ABC';
+ var ajaxdata = 'MrBR';
+ var kdns = 1;
+ if (typeof(killdns)=='undefined'){
+ var kdns = 0;
+ }
+ document.cookie = 'x=1';
+ document.location.reload();
+ document.querySelector('#tourl');
+ document.createElement('div');
+ $.ajax({
+ type : 'post',
+ url : '/ajaxfile.php?file=150233466',
+ data : { 'action':'downprocess','websignkey':ajaxdata,'signs':ajaxdata,'sign':wp_sign,'websign':'','kd':kdns,'ves':1 },
+ dataType : 'json',
+ success:function(msg){
+ $("#tourl").html("ok");
+ $("#outime").css("display","block");
+ }
+ });
+ """;
+ ScriptObjectMirror sign = JsExecUtils.executeDynamicJs(js, null);
+ assertNotNull(sign);
+ assertEquals("/ajaxfile.php?file=150233466", String.valueOf(sign.get("url")));
+ @SuppressWarnings("unchecked")
+ Map data = (Map) sign.get("data");
+ assertEquals("downprocess", String.valueOf(data.get("action")));
+ assertEquals("SIGN_ABC", String.valueOf(data.get("sign")));
+ assertEquals("MrBR", String.valueOf(data.get("websignkey")));
+ assertEquals("1", String.valueOf(data.get("kd")));
+ }
+
+ @Test
+ public void testPwdViaJqueryValAndDocument() throws Exception {
+ String js = """
+ function down_p(){
+ var pwd = $('#pwd').val();
+ var pwd2 = document.getElementById('pwd').value;
+ var pwd3 = document.querySelector('#pwd').value;
+ $(".passwdinput").focus();
+ $.ajax({
+ type : 'post',
+ url : '/ajaxm.php',
+ data : { 'action':'downprocess','sign':'S1','p':pwd,'p2':pwd2,'p3':pwd3 }
+ });
+ }
+ """;
+ ScriptObjectMirror sign = JsExecUtils.executeDynamicJs(js, "down_p", "e4k4");
+ assertNotNull(sign);
+ @SuppressWarnings("unchecked")
+ Map data = (Map) sign.get("data");
+ assertEquals("e4k4", String.valueOf(data.get("p")));
+ assertEquals("e4k4", String.valueOf(data.get("p2")));
+ assertEquals("e4k4", String.valueOf(data.get("p3")));
+ }
+
+ @Test
+ public void testReadyAndSuccessCallbackDoNotDropAjax() throws Exception {
+ String js = """
+ $(function(){
+ document.getElementById('rpt').style.display = 'none';
+ $.ajax({ url: '/ajaxm.php?file=1', data: { a: 1 } });
+ $("#tourl").html("x");
+ });
+ """;
+ ScriptObjectMirror sign = JsExecUtils.executeDynamicJs(js, null);
+ assertNotNull(sign);
+ assertTrue(String.valueOf(sign.get("url")).contains("ajaxm.php"));
+ }
+}
diff --git a/pom.xml b/pom.xml
index a8834dd..c2f9d53 100644
--- a/pom.xml
+++ b/pom.xml
@@ -17,7 +17,7 @@
- 0.4.2
+ 0.4.3
17
17
17