Compare commits

..

5 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] f3d95fa7c0 feat: only generate smart link after parse action; set directory share as smart link after parseDirectory 2026-08-01 02:43:23 +00:00
copilot-swe-agent[bot] 50e75168e5 fix: asJson logs details but only surfaces clean error to frontend 2026-08-01 02:26:06 +00:00
copilot-swe-agent[bot] 4bc880a41b fix: use tryParseJson in CeTool version detection to avoid premature promise failure on non-JSON responses 2026-08-01 02:12:01 +00:00
qaiu 7299fd8762 Merge pull request #207 from qaiu/cursor/ghsa-997r-ssrf-verify-2b8b
fix(security): GHSA-997r SSRF verification + residual hardening
2026-07-26 21:10:39 +08:00
Cursor Agent efbadde4ed fix(security): harden GHSA-997r Cloudreve SSRF residual paths
Disable redirect following on CE/Ce4 attacker-controlled requests and stop
echoing upstream response bodies in client-facing JSON errors. Add
assertPublicHost regression coverage for the advisory PoC hosts.

Co-authored-by: qaiu <qaiu@vip.qq.com>
2026-07-26 12:05:16 +00:00
13 changed files with 317 additions and 1551 deletions
+2 -20
View File
@@ -262,27 +262,9 @@ jobs:
name: ${{ matrix.artifact-name }} name: ${{ matrix.artifact-name }}
path: ${{ matrix.artifact-name }}.zip 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 uses: softprops/action-gh-release@v2
with: with:
files: | files: ${{ matrix.artifact-name }}.zip
netdisk-fast-download-linux-amd64.zip
netdisk-fast-download-windows-amd64.zip
tag_name: ${{ github.ref_name }} tag_name: ${{ github.ref_name }}
generate_release_notes: true generate_release_notes: true
fail_on_unmatched_files: true
@@ -377,19 +377,39 @@ public abstract class PanBase implements IPanTool, Closeable {
} }
} catch (Exception e) { } catch (Exception e) {
// 上游响应体可能来自内网探测目标或非JSON内容,仅写日志,避免经 HTTP 500 回传给调用方
if ("gzip".equalsIgnoreCase(contentEncoding)) { if ("gzip".equalsIgnoreCase(contentEncoding)) {
// gzip解压失败,记录错误 log.error("上游响应gzip解压或JSON解析失败: {}", e.getMessage());
log.error("响应gzip解压或JSON解析失败: {}", e.getMessage());
fail("响应gzip解压或JSON解析失败: {}", e.getMessage());
} else { } else {
String bodyPreview = responseBodyPreview(res); log.error("上游响应格式异常(非JSON): {}", responseBodyPreview(res));
log.error("解析失败: json格式异常: {}", bodyPreview);
fail("解析失败: json格式异常: {}", bodyPreview);
} }
fail("上游响应格式异常");
return JsonObject.of(); return JsonObject.of();
} }
} }
/**
* 尝试将响应体解析为 JsonObject,失败时静默返回 null(不调用 fail())。
* 适用于版本探测等场景:响应可能是 HTML,此时不应立即终止解析流程。
*
* @param res HttpResponse
* @return JsonObject,若响应体不是合法 JSON 则返回 null
*/
protected JsonObject tryParseJson(HttpResponse<?> res) {
String contentEncoding = res.getHeader("Content-Encoding");
try {
if ("gzip".equalsIgnoreCase(contentEncoding)) {
String decompressed = decompressGzip((Buffer) res.body());
return new JsonObject(decompressed);
} else {
return res.bodyAsJsonObject();
}
} catch (Exception e) {
log.debug("响应体不是合法JSON,跳过: {}", e.getMessage());
return null;
}
}
/** /**
* body To text的封装, 会自动处理异常, 会自动解压gzip * body To text的封装, 会自动处理异常, 会自动解压gzip
* @param res HttpResponse * @param res HttpResponse
@@ -111,7 +111,8 @@ public class Ce4Tool extends PanBase {
private void requestShareDetail(String baseUrl, String key, String pwd, String path) { private void requestShareDetail(String baseUrl, String key, String pwd, String path) {
String shareApiUrl = baseUrl + SHARE_API_PATH + key; String shareApiUrl = baseUrl + SHARE_API_PATH + key;
HttpRequest<Buffer> httpRequest = clientSession.getAbs(shareApiUrl); // 禁止跟随重定向:防止公网 host 302 到内网/元数据绕过 assertPublicHost
HttpRequest<Buffer> httpRequest = clientNoRedirects.getAbs(shareApiUrl);
if (pwd != null && !pwd.isEmpty()) { if (pwd != null && !pwd.isEmpty()) {
httpRequest.addQueryParam("password", pwd); httpRequest.addQueryParam("password", pwd);
} }
@@ -232,7 +233,7 @@ public class Ce4Tool extends PanBase {
.put("uris", new JsonArray().add(filePath)) .put("uris", new JsonArray().add(filePath))
.put("download", true); .put("download", true);
clientSession.postAbs(fileUrlApi) clientNoRedirects.postAbs(fileUrlApi)
.putHeader("Content-Type", "application/json") .putHeader("Content-Type", "application/json")
.sendJsonObject(requestBody) .sendJsonObject(requestBody)
.onSuccess(res -> { .onSuccess(res -> {
@@ -78,20 +78,18 @@ public class CeTool extends PanBase {
private void tryV4Ping(String baseUrl, String key, String pwd) { private void tryV4Ping(String baseUrl, String key, String pwd) {
String pingUrlV4 = baseUrl + PING_API_V4_PATH; String pingUrlV4 = baseUrl + PING_API_V4_PATH;
clientSession.getAbs(pingUrlV4).send().onSuccess(res -> { // 禁止跟随重定向:assertPublicHost 只校验初始 host,自动 30x 会绕过 SSRF 防护
clientNoRedirects.getAbs(pingUrlV4).send().onSuccess(res -> {
if (res.statusCode() == 200) { if (res.statusCode() == 200) {
try { // 使用 tryParseJson 而非 asJson,避免非JSON响应(如HTML)提前终止整个解析流程
JsonObject json = asJson(res); JsonObject json = tryParseJson(res);
// v4 ping 成功且返回有效JSON,使用 Ce4Tool // v4 ping 成功且返回有效JSON,使用 Ce4Tool
if (json != null && !json.isEmpty()) { if (json != null && !json.isEmpty()) {
log.debug("检测到Cloudreve 4.x (通过v4 ping)"); log.debug("检测到Cloudreve 4.x (通过v4 ping)");
delegateToCe4Tool(); delegateToCe4Tool();
return; return;
}
} catch (Exception e) {
// JSON解析失败,继续尝试 v3
log.debug("v4 ping返回非JSON响应,尝试v3");
} }
log.debug("v4 ping返回非JSON响应,尝试v3");
} }
// v4 ping失败或返回非JSON,尝试 v3 // v4 ping失败或返回非JSON,尝试 v3
tryV3Ping(baseUrl, key, pwd); tryV3Ping(baseUrl, key, pwd);
@@ -108,20 +106,17 @@ public class CeTool extends PanBase {
private void tryV3Ping(String baseUrl, String key, String pwd) { private void tryV3Ping(String baseUrl, String key, String pwd) {
String pingUrlV3 = baseUrl + PING_API_V3_PATH; String pingUrlV3 = baseUrl + PING_API_V3_PATH;
clientSession.getAbs(pingUrlV3).send().onSuccess(res -> { clientNoRedirects.getAbs(pingUrlV3).send().onSuccess(res -> {
if (res.statusCode() == 200) { if (res.statusCode() == 200) {
try { // 使用 tryParseJson 而非 asJson,避免非JSON响应(如HTML)提前终止整个解析流程
JsonObject json = asJson(res); JsonObject json = tryParseJson(res);
// v3 ping 成功且返回有效JSON,进一步验证是否为 v3 // v3 ping 成功且返回有效JSON,进一步验证是否为 v3
if (json != null && !json.isEmpty()) { if (json != null && !json.isEmpty()) {
// 尝试调用 v3 share API 来确认 // 尝试调用 v3 share API 来确认
verifyV3AndParse(baseUrl, key, pwd); verifyV3AndParse(baseUrl, key, pwd);
return; return;
}
} catch (Exception e) {
// JSON解析失败,不是Cloudreve盘
log.debug("v3 ping返回非JSON响应,不是Cloudreve盘");
} }
log.debug("v3 ping返回非JSON响应,不是Cloudreve盘");
} }
// v3 ping失败,不是Cloudreve盘 // v3 ping失败,不是Cloudreve盘
log.debug("v3 ping失败,尝试下一个解析器"); log.debug("v3 ping失败,尝试下一个解析器");
@@ -139,7 +134,7 @@ public class CeTool extends PanBase {
*/ */
private void verifyV3AndParse(String baseUrl, String key, String pwd) { private void verifyV3AndParse(String baseUrl, String key, String pwd) {
String shareApiUrl = baseUrl + SHARE_API_PATH + key; String shareApiUrl = baseUrl + SHARE_API_PATH + key;
HttpRequest<Buffer> httpRequest = clientSession.getAbs(shareApiUrl); HttpRequest<Buffer> httpRequest = clientNoRedirects.getAbs(shareApiUrl);
if (pwd != null && !pwd.isEmpty()) { if (pwd != null && !pwd.isEmpty()) {
httpRequest.addQueryParam("password", pwd); httpRequest.addQueryParam("password", pwd);
} }
@@ -175,7 +170,7 @@ public class CeTool extends PanBase {
*/ */
private void tryV4ShareApi(String baseUrl, String key, String pwd) { private void tryV4ShareApi(String baseUrl, String key, String pwd) {
String shareApiUrl = baseUrl + "/api/v4/share/info/" + key; String shareApiUrl = baseUrl + "/api/v4/share/info/" + key;
HttpRequest<Buffer> httpRequest = clientSession.getAbs(shareApiUrl); HttpRequest<Buffer> httpRequest = clientNoRedirects.getAbs(shareApiUrl);
if (pwd != null && !pwd.isEmpty()) { if (pwd != null && !pwd.isEmpty()) {
httpRequest.addQueryParam("password", pwd); httpRequest.addQueryParam("password", pwd);
} }
@@ -291,7 +286,8 @@ public class CeTool extends PanBase {
} }
private void getDownURL(String shareApiUrl) { private void getDownURL(String shareApiUrl) {
clientSession.putAbs(shareApiUrl) // PUT 默认不跟随重定向,但仍统一使用 no-redirect 客户端避免配置漂移
clientNoRedirects.putAbs(shareApiUrl)
.putHeader("Referer", shareLinkInfo.getShareUrl()) .putHeader("Referer", shareLinkInfo.getShareUrl())
.send().onSuccess(res -> { .send().onSuccess(res -> {
JsonObject jsonObject = asJson(res); JsonObject jsonObject = asJson(res);
@@ -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;
@@ -9,30 +8,20 @@ import io.netty.handler.codec.http.cookie.DefaultCookie;
import io.vertx.core.Future; import io.vertx.core.Future;
import io.vertx.core.MultiMap; import io.vertx.core.MultiMap;
import io.vertx.core.Promise; import io.vertx.core.Promise;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject; 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.WebClient;
import io.vertx.ext.web.client.WebClientOptions;
import io.vertx.ext.web.client.WebClientSession; import io.vertx.ext.web.client.WebClientSession;
import org.apache.commons.lang3.StringUtils;
import org.openjdk.nashorn.api.scripting.ScriptObjectMirror; import org.openjdk.nashorn.api.scripting.ScriptObjectMirror;
import javax.script.ScriptException; import javax.script.ScriptException;
import java.io.ByteArrayInputStream;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
/** /**
* 蓝奏云解析工具 * 蓝奏云解析工具
@@ -41,26 +30,12 @@ import java.util.zip.GZIPInputStream;
*/ */
public class LzTool extends PanBase { public class LzTool extends PanBase {
/** ESA 对 gzip 响应常见不带可识别的 Content-Encoding,需客户端自动解压。 */ WebClientSession webClientSession = WebClientSession.create(clientNoRedirects);
private final WebClient lzClient;
private WebClientSession webClientSession;
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_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)<script(?![^>]*\\bsrc\\s*=)[^>]*>(.*?)</script>");
private static final Pattern FILE_SIZE_PATTERN = Pattern.compile(">文件大小:</span>(.*?)<br>|\"n_filesize\">大小:(.*?)</div>"); 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 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 DESCRIPTION_PATTERN = Pattern.compile("(?s)文件描述:</span><br>(.*?)</td>|class=\"n_box_des\">(.*?)</div>");
@@ -68,14 +43,13 @@ public class LzTool extends PanBase {
private static final Pattern CREATE_TIME_PATTERN = Pattern.compile(">上传时间:</span>(.*?)<"); 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 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 ARG1_PATTERN = Pattern.compile("var arg1='([^']+)'");
private static final Pattern IFRAME_SRC_PATTERN = Pattern.compile( private static final Pattern IFRAME_SRC_PATTERN = Pattern.compile("src=\"(/fn\\?[a-zA-Z\\d_+/=]{16,})\"");
"src\\s*=\\s*[\"'](/fn\\?[^\"'\\s>]+)[\"']", Pattern.CASE_INSENSITIVE);
private static final Pattern RELATIVE_TIME_PATTERN = Pattern.compile("^(\\d+|几)\\s*(分钟|小时)前$"); 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 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*日?$"); 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: identity Accept-Encoding: gzip, deflate
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: max-age=0 Cache-Control: max-age=0
DNT: 1 DNT: 1
@@ -94,115 +68,20 @@ public class LzTool extends PanBase {
public LzTool(ShareLinkInfo shareLinkInfo) { public LzTool(ShareLinkInfo shareLinkInfo) {
super(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<String> parse() { public Future<String> parse() {
String sUrl = shareLinkInfo.getStandardUrl(); String sUrl = shareLinkInfo.getStandardUrl();
String pwd = shareLinkInfo.getSharePassword(); String pwd = shareLinkInfo.getSharePassword();
webClientSession.getAbs(sUrl) WebClient client = clientNoRedirects;
client.getAbs(sUrl)
.putHeaders(headers0) .putHeaders(headers0)
.send().onSuccess(res -> { .send().onSuccess(res -> {
try { try {
String html = asText(res); String html = asText(res);
if (hasAcwArg1(html)) { if (hasAcwArg1(html)) {
webClientSession = WebClientSession.create(lzClient); webClientSession = WebClientSession.create(clientNoRedirects);
if (!setCookie(html, sUrl)) { if (!setCookie(html, sUrl)) {
fail("蓝奏云反爬 arg1 Cookie 解析失败,页面内容异常"); fail("蓝奏云反爬 arg1 Cookie 解析失败,页面内容异常");
return; return;
@@ -238,14 +117,14 @@ public class LzTool extends PanBase {
fail("分享已失效或文件已取消分享"); fail("分享已失效或文件已取消分享");
return; return;
} }
// 检测是否为目录分享链接 (含 /s/、/b/ 路径段或 b0 开头的路径段) // 检测是否为目录分享链接 (含 /s/、/b/ 路径段或 b 开头的路径段)
if (sUrl.matches(".*/(s|b)/[^/]+.*") || sUrl.matches(".*/b0[^/]+.*")) { if (sUrl.matches(".*/(s|b)/[^/]+.*") || sUrl.matches(".*/b[^/]+.*")) {
fail("该链接为蓝奏云目录分享,请使用目录解析接口"); fail("该链接为蓝奏云目录分享,请使用目录解析接口");
return; return;
} }
// 若仍是校验页 (parse()中cookie域名与实际URL不匹配时会出现), 重试一次 // 若仍是校验页 (parse()中cookie域名与实际URL不匹配时会出现), 重试一次
if (hasAcwArg1(html)) { if (hasAcwArg1(html)) {
webClientSession = WebClientSession.create(lzClient); webClientSession = WebClientSession.create(clientNoRedirects);
if (!setCookie(html, sUrl)) { if (!setCookie(html, sUrl)) {
fail("蓝奏云反爬 arg1 Cookie 解析失败,页面内容异常"); fail("蓝奏云反爬 arg1 Cookie 解析失败,页面内容异常");
return; return;
@@ -289,33 +168,63 @@ public class LzTool extends PanBase {
Matcher matcher = IFRAME_SRC_PATTERN.matcher(html); Matcher matcher = IFRAME_SRC_PATTERN.matcher(html);
// 没有Iframe说明是加密分享, 匹配sign通过密码请求下载页面 // 没有Iframe说明是加密分享, 匹配sign通过密码请求下载页面
if (!matcher.find()) { 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 { try {
if (!postAjaxFromHtml(sUrl, html, pwd)) { String jsText = getJsByPwd(pwd, html, "document.getElementById('rpt')");
fail("未找到下载参数,可能密码错误或分享已失效 htmlLen=" + html.length() ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, "down_p");
+ " hasFn=" + html.contains("/fn?") getDownURL(sUrl, scriptObjectMirror);
+ " hasIframe=" + html.contains("iframe"));
}
} catch (Exception e) { } catch (Exception e) {
fail(e, "js引擎执行失败 htmlLen=" + html.length() fail(e, "js引擎执行失败");
+ " hasFn=" + html.contains("/fn?")
+ " hasIframe=" + html.contains("iframe"));
} }
} else { } else {
// 没有密码 // 没有密码
String iframePath = matcher.group(1); String iframePath = matcher.group(1);
String absoluteURI = joinUrl(SHARE_URL_PREFIX, iframePath); String absoluteURI = SHARE_URL_PREFIX + iframePath;
// 创建局部副本,避免修改实例字段导致累积 // 创建局部副本,避免修改实例字段导致累积
MultiMap headersCopy = MultiMap.caseInsensitiveMultiMap().addAll(headers0); MultiMap headersCopy = MultiMap.caseInsensitiveMultiMap().addAll(headers0);
headersCopy.add("Referer", absoluteURI); headersCopy.add("Referer", absoluteURI);
webClientSession.getAbs(absoluteURI).putHeaders(headersCopy).send().onSuccess(res2 -> { webClientSession.getAbs(absoluteURI).putHeaders(headersCopy).send().onSuccess(res2 -> {
try { try {
String html2 = asText(res2); String html2 = asText(res2);
handleIframeHtml(html2, sUrl, absoluteURI, iframePath, headersCopy); 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引擎执行失败");
}
}
} catch (Exception e) { } catch (Exception e) {
fail("蓝奏云 iframe 响应处理异常: {}", e.getMessage()); fail("蓝奏云 iframe 响应处理异常: {}", e.getMessage());
} }
@@ -323,43 +232,6 @@ 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) { private boolean setCookie(String html, String url) {
String arg1 = extractAcwArg1(html); String arg1 = extractAcwArg1(html);
if (arg1 == null) { if (arg1 == null) {
@@ -390,17 +262,9 @@ public class LzTool extends PanBase {
String jsText = getJsText(html); String jsText = getJsText(html);
if (jsText == null) { if (jsText == null) {
throw new RuntimeException("获取失败1, 可能分享已失效 htmlLen=" + (html == null ? 0 : html.length()) throw new RuntimeException("获取失败1, 可能分享已失效");
+ " hasFn=" + (html != null && html.contains("/fn?"))
+ " hasScript=" + (html != null && html.contains("<script")));
}
if (pwd != null) {
String quoted = "\"" + pwd.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
jsText = jsText.replace("document.getElementById('pwd').value", quoted);
jsText = jsText.replace("document.getElementById(\"pwd\").value", quoted);
jsText = jsText.replace("document.querySelector('#pwd').value", quoted);
jsText = jsText.replace("document.querySelector(\"#pwd\").value", quoted);
} }
jsText = jsText.replace("document.getElementById('pwd').value", "\"" + pwd + "\"");
int i = jsText.indexOf(subText); int i = jsText.indexOf(subText);
if (i > 0) { if (i > 0) {
jsText = jsText.substring(0, i); jsText = jsText.substring(0, i);
@@ -409,43 +273,21 @@ public class LzTool extends PanBase {
} }
private String getJsText(String html) { private String getJsText(String html) {
if (html == null || html.isEmpty()) { if (html == null) {
return null; return null;
} }
Matcher m = P_INLINE_SCRIPT.matcher(html);
String lastAjax = null;
while (m.find()) {
String body = m.group(1).replaceAll("<!--.*?-->", "").trim();
if (body.isEmpty() || body.contains("var arg1=") || body.contains("arg1='")) {
continue;
}
if (body.contains("$.ajax") || body.contains("down_p") || body.contains("wp_sign")
|| body.contains("ajaxdata") || body.contains("filemoreajax")) {
lastAjax = body;
}
}
if (lastAjax != null) {
return lastAjax;
}
String jsTagStart = "<script type=\"text/javascript\">"; String jsTagStart = "<script type=\"text/javascript\">";
String jsTagEnd = "</script>";
int index = html.lastIndexOf(jsTagStart); int index = html.lastIndexOf(jsTagStart);
if (index == -1) { if (index == -1) {
return null; return null;
} }
int startPos = index + jsTagStart.length(); int startPos = index + jsTagStart.length();
int endPos = html.indexOf("</script>", startPos); int endPos = html.indexOf(jsTagEnd, startPos);
if (endPos < 0) { if (endPos <= startPos) {
return null; return null;
} }
String fallback = html.substring(startPos, endPos).replaceAll("<!--.*-->", ""); return 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) { static String extractAcwArg1(String html) {
@@ -474,210 +316,20 @@ public class LzTool extends PanBase {
return html != null && html.contains("var arg1='"); 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<String, String> form) {
MultiMap toForm() {
MultiMap m = MultiMap.caseInsensitiveMultiMap();
form.forEach(m::set);
return m;
}
}
private void getDownURL(String referer, AjaxCall call) {
Map<String, Object> 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<String, String> 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<String> 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<String, String> 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<String, ?> obj) { private void getDownURL(String key, Map<String, ?> obj) {
if (obj == null) { if (obj == null) {
fail("需要访问密码"); fail("需要访问密码");
return; return;
} }
Object dataObj = obj.get("data"); Map<?, ?> signMap = (Map<?, ?>)obj.get("data");
if (dataObj == null) {
fail("需要访问密码");
return;
}
String url0 = String.valueOf(obj.get("url")); String url0 = String.valueOf(obj.get("url"));
MultiMap map = MultiMap.caseInsensitiveMultiMap(); MultiMap map = MultiMap.caseInsensitiveMultiMap();
if (dataObj instanceof CharSequence) { signMap.forEach((k, v) -> {
parseFormString(map, dataObj.toString()); map.add((String) k, v.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(""" 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: identity 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
@@ -695,176 +347,83 @@ public class LzTool extends PanBase {
headers.set("referer", key); headers.set("referer", key);
// action=downprocess&signs=%3Fctdf&websignkey=I5gl&sign=BWMGOF1sBTRWXwI9BjZdYVA7BDhfNAIyUG9UawJtUGMIPlAhACkCa1UyUTAAYFxvUj5XY1E7UGFXaFVq&websign=&kd=1&ves=1 // action=downprocess&signs=%3Fctdf&websignkey=I5gl&sign=BWMGOF1sBTRWXwI9BjZdYVA7BDhfNAIyUG9UawJtUGMIPlAhACkCa1UyUTAAYFxvUj5XY1E7UGFXaFVq&websign=&kd=1&ves=1
String url = joinUrl(SHARE_URL_PREFIX, url0); String url = SHARE_URL_PREFIX + url0;
webClientSession.postAbs(url).putHeaders(headers).sendForm(map).onSuccess(res2 -> { webClientSession.postAbs(url).putHeaders(headers).sendForm(map).onSuccess(res2 -> {
try { try {
JsonObject urlJson = asJson(res2); JsonObject urlJson = asJson(res2);
Object infVal = urlJson.getValue("inf"); String name = urlJson.getString("inf");
String name = infVal instanceof CharSequence ? infVal.toString() : null; if (urlJson.getInteger("zt") != 1) {
Integer zt = urlJson.getInteger("zt"); fail(name);
if (zt == null || zt != 1) {
fail(name != null ? name : String.valueOf(infVal));
return; return;
} }
// 文件名 // 文件名
if (name != null) { if (urlJson.containsKey("inf") && urlJson.getMap().get("inf") instanceof CharSequence) {
Object fi = shareLinkInfo.getOtherParam().get("fileInfo"); ((FileInfo)shareLinkInfo.getOtherParam().get("fileInfo")).setFileName(name);
if (fi instanceof FileInfo fileInfo) {
fileInfo.setFileName(name);
}
} }
String downUrl = urlJson.getString("dom") + "/file/" + urlJson.getString("url"); String downUrl = urlJson.getString("dom") + "/file/" + urlJson.getString("url");
followFileUrl(downUrl, headers); 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));
} catch (Exception e) { } catch (Exception e) {
fail(e, "解析异常"); fail("解析异常");
} }
}).onFailure(handleFail(url)); }).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) { private void setDateAndComplete(String location0) {
// 分享时间 提取url中的时间戳格式:lanzoui.com/abc/abc/yyyy/mm/dd/ // 分享时间 提取url中的时间戳格式:lanzoui.com/abc/abc/yyyy/mm/dd/
Matcher matcher = URL_DATE_PATTERN.matcher(location0); Matcher matcher = URL_DATE_PATTERN.matcher(location0);
@@ -901,7 +460,7 @@ public class LzTool extends PanBase {
String html = asText(res); String html = asText(res);
// 检查是否需要 cookie 验证 // 检查是否需要 cookie 验证
if (hasAcwArg1(html)) { if (hasAcwArg1(html)) {
webClientSession = WebClientSession.create(lzClient); webClientSession = WebClientSession.create(clientNoRedirects);
if (!setCookie(html, sUrl)) { if (!setCookie(html, sUrl)) {
promise.tryFail(baseMsg() + "蓝奏云反爬 arg1 Cookie 解析失败,页面内容异常"); promise.tryFail(baseMsg() + "蓝奏云反爬 arg1 Cookie 解析失败,页面内容异常");
return; return;
@@ -933,26 +492,21 @@ public class LzTool extends PanBase {
promise.tryFail(baseMsg() + "分享已失效或文件已取消分享"); promise.tryFail(baseMsg() + "分享已失效或文件已取消分享");
return; return;
} }
// 检测是否为文件分享链接 (不含 /s/、/b/ 路径段且不含 b0 开头的路径段) // 检测是否为文件分享链接 (不含 /s/、/b/ 路径段且不含 b 开头的路径段)
if (!sUrl.matches(".*/(s|b)/[^/]+.*") && !sUrl.matches(".*/b0[^/]+.*")) { if (!sUrl.matches(".*/(s|b)/[^/]+.*") && !sUrl.matches(".*/b[^/]+.*")) {
promise.tryFail(baseMsg() + "该链接为蓝奏云文件分享,请使用文件解析接口"); promise.tryFail(baseMsg() + "该链接为蓝奏云文件分享,请使用文件解析接口");
return; return;
} }
try { try {
AjaxCall call = extractFolderAjax(html, pwd); String jsText = getJsByPwd(pwd, html, "var urls =window.location.href");
if (call == null) { ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, "file");
String jsText = getJsByPwd(pwd, html, "var urls =window.location.href"); Map<String, Object> data = CastUtil.cast(scriptObjectMirror.get("data"));
ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, "file", pwd); MultiMap map = MultiMap.caseInsensitiveMultiMap();
Map<String, Object> data = CastUtil.cast(scriptObjectMirror.get("data")); data.forEach((k, v) -> map.set(k, v.toString()));
Map<String, String> form = new LinkedHashMap<>(); log.debug("解析参数: {}", map);
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 headers = getHeaders(sUrl);
MultiMap map = call.toForm();
String url = joinUrl(SHARE_URL_PREFIX, call.path()); 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 { try {
String resBody = asText(res2); String resBody = asText(res2);
@@ -988,10 +542,7 @@ public class LzTool extends PanBase {
promise.tryFail(baseMsg() + "蓝奏云文件列表响应为空"); promise.tryFail(baseMsg() + "蓝奏云文件列表响应为空");
return; return;
} }
JsonObject fileListJson = parseLzJson(responseBody); JsonObject fileListJson = new JsonObject(responseBody);
if (fileListJson == null) {
fileListJson = new JsonObject(responseBody);
}
if (fileListJson.getInteger("zt") != 1) { if (fileListJson.getInteger("zt") != 1) {
promise.tryFail(baseMsg() + fileListJson.getString("info")); promise.tryFail(baseMsg() + fileListJson.getString("info"));
return; return;
+37 -362
View File
@@ -101,384 +101,59 @@ public interface JsContent {
"""; """;
String lz = """ String lz = """
/** /**
* 蓝奏云解析器 JS 沙箱:伪装 jQuery / document / window。 * 蓝奏云解析器js签名获取工具
* 新版页面会用 document.cookie、location.reload、querySelector、
* $('#pwd').val()、.html()、.css() 等,这里做成可链式的最小实现。
* kdns.js 在浏览器里是 `var killdns = true`,需一并注入,否则 kd 会被改成 0。
*/ */
var signObj; 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; var $, jQuery;
$ = jQuery = function (sel) {
if (typeof sel === 'function') { $ = jQuery = function () {
try { sel(jQuery); } catch (e) {} return new jQuery.fn.init();
return __lzJq(document); }
}
return __lzJq(sel);
};
jQuery.fn = jQuery.prototype = { jQuery.fn = jQuery.prototype = {
init: function (sel) { init: function () {
return __lzJq(sel); return {
} focus: function (a) {
};
},
keyup: function(a) {
},
ajax: function (obj) {
signObj = obj
},
val: function(a) {
},
}
},
}
jQuery.fn.init.prototype = jQuery.fn; jQuery.fn.init.prototype = jQuery.fn;
$.fn = jQuery.fn;
$.ajax = function (obj) { $.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 = { var document = {
cookie: '', getElementById: function (v) {
title: '', return {
domain: '', value: 'v',
referrer: '', style: {
readyState: 'complete', display: ''
hidden: false, },
visibilityState: 'visible', addEventListener: function() {}
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() {} var window = {location: {}}
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 = """ String kwSignString = """
@@ -50,47 +50,21 @@ public class JsExecUtils {
*/ */
public static ScriptObjectMirror executeDynamicJs(String jsText, String funName) throws ScriptException, public static ScriptObjectMirror executeDynamicJs(String jsText, String funName) throws ScriptException,
NoSuchMethodException { NoSuchMethodException {
return executeDynamicJs(jsText, funName, null); ScriptEngine engine = ENGINE_MANAGER.getEngineByName("JavaScript"); // 得到脚本引擎
}
/**
* @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 { try {
engine.eval(JsContent.lz); engine.eval(JsContent.lz + "\n" + jsText);
Invocable inv = (Invocable) engine; Invocable inv = (Invocable) engine;
if (pwd != null) { //调用js中的函数
inv.invokeFunction("__lzSetPwd", pwd); if (StringUtils.isNotEmpty(funName)) {
inv.invokeFunction(funName);
} }
try { return (ScriptObjectMirror) engine.get("signObj");
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 { } finally {
// 清理引擎持有的引用,帮助 GC 回收
clearEngineBindings(engine); 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 创建新引擎实例) * 调用执行js文件(使用缓存的 ScriptEngineManager 创建新引擎实例)
+29 -362
View File
@@ -1,379 +1,46 @@
/** /**
* 蓝奏云解析器 JS 沙箱:伪装 jQuery / document / window。 * 蓝奏云解析器js签名获取工具
* 新版页面会用 document.cookie、location.reload、querySelector、
* $('#pwd').val()、.html()、.css() 等,这里做成可链式的最小实现。
* kdns.js 在浏览器里是 `var killdns = true`,需一并注入,否则 kd 会被改成 0。
*/ */
var signObj; 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; var $, jQuery;
$ = jQuery = function (sel) {
if (typeof sel === 'function') { $ = jQuery = function () {
try { sel(jQuery); } catch (e) {} return new jQuery.fn.init();
return __lzJq(document); }
}
return __lzJq(sel);
};
jQuery.fn = jQuery.prototype = { jQuery.fn = jQuery.prototype = {
init: function (sel) { init: function () {
return __lzJq(sel); return {
} focus: function (a) {
};
},
keyup: function(a) {
},
ajax: function (obj) {
signObj = obj
}
}
},
}
jQuery.fn.init.prototype = jQuery.fn; jQuery.fn.init.prototype = jQuery.fn;
$.fn = jQuery.fn;
// 伪装jquery.ajax函数获取关键数据
$.ajax = function (obj) { $.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 = { var document = {
cookie: '', getElementById: function (v) {
title: '', return {
domain: '', value: 'v'
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;
@@ -0,0 +1,44 @@
package cn.qaiu.parser;
import org.junit.Test;
import java.io.IOException;
import java.net.URL;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/**
* GHSA-997r-7xx2-p9x6 regression: Cloudreve generic parser must reject
* hosts that resolve to loopback / private / link-local / metadata ranges
* before any outbound request.
*/
public class AssertPublicHostTest {
@Test
public void rejectsLoopbackAndPrivateHosts() throws Exception {
String[] blocked = {
"http://127.0.0.1.nip.io/s/poc",
"http://localhost/s/poc",
"http://10.0.0.1/s/poc",
"http://192.168.1.1/s/poc",
"http://172.16.0.1/s/poc",
"http://169.254.169.254/s/poc",
"http://[::1]/s/poc"
};
for (String raw : blocked) {
try {
PanBase.assertPublicHost(new URL(raw));
fail("expected block for " + raw);
} catch (IOException expected) {
assertTrue(expected.getMessage().contains("不允许访问")
|| expected.getMessage().contains("无法解析"));
}
}
}
@Test
public void allowsPublicHost() throws Exception {
PanBase.assertPublicHost(new URL("https://example.com/s/demo"));
}
}
@@ -1,52 +0,0 @@
package cn.qaiu.parser.impl;
import org.junit.Test;
import static org.junit.Assert.*;
public class LzToolAjaxExtractTest {
@Test
public void testExtractWpSignAjaxfile() {
String html = """
<script>
var wp_sign = 'BWMGOF5v';
var ajaxdata = 'MrBR';
var kdns =1;
$.ajax({
type : 'post',
url : '/ajaxfile.php?file=150233466',
data : { 'action':'downprocess','websignkey':ajaxdata,'signs':ajaxdata,'sign':wp_sign,'websign':'','kd':kdns,'ves':1 }
});
</script>
""";
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 = """
<input id="pwd">
<script>
function down_p(){
$.ajax({
type : 'post',
url : '/ajaxm.php?file=1',
data : { 'action':'downprocess','sign':'ABC123','p':'x' }
});
}
</script>
""";
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"));
}
}
@@ -1,90 +0,0 @@
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<String, Object> data = (Map<String, Object>) 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<String, Object> data = (Map<String, Object>) 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"));
}
}
+1 -1
View File
@@ -17,7 +17,7 @@
</modules> </modules>
<properties> <properties>
<revision>0.4.3</revision> <revision>0.4.2</revision>
<java.version>17</java.version> <java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source> <maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target> <maven.compiler.target>17</maven.compiler.target>
+5 -7
View File
@@ -1156,7 +1156,6 @@ export default {
this.password = shortInfo.pwd this.password = shortInfo.pwd
} }
this.$message.success(`已识别短格式并自动转换,网盘类型: ${shortInfo.name}`) this.$message.success(`已识别短格式并自动转换,网盘类型: ${shortInfo.name}`)
this.updateDirectLink()
return return
} }
@@ -1169,7 +1168,6 @@ export default {
this.password = pwd this.password = pwd
} }
this.$message.success(`已从文本中识别到 ${linkInfo.name} 分享链接`) this.$message.success(`已从文本中识别到 ${linkInfo.name} 分享链接`)
this.updateDirectLink()
} }
}, },
@@ -1184,6 +1182,7 @@ export default {
clearResults() { clearResults() {
this.parseResult = {} this.parseResult = {}
this.downloadUrl = null this.downloadUrl = null
this.directLink = ''
this.markdownText = '' this.markdownText = ''
this.showQRCode = false this.showQRCode = false
this.statisticsData = {} this.statisticsData = {}
@@ -1327,8 +1326,10 @@ export default {
const directoryResult = await this.callAPI('/v2/getFileList', params) const directoryResult = await this.callAPI('/v2/getFileList', params)
this.directoryData = directoryResult.data || [] this.directoryData = directoryResult.data || []
this.showDirectoryTree = true this.showDirectoryTree = true
// 自动赋值分享链接 // 目录解析成功后,将目录落地页作为智能直链
this.showListLink = `${this.baseUrl}/showList?url=${encodeURIComponent(this.link)}` const listUrl = `${this.baseUrl}/showList?url=${encodeURIComponent(this.link)}`
this.showListLink = listUrl
this.directLink = listUrl
this.$message.success(`目录解析成功!共找到 ${this.directoryData.length} 个文件/文件夹`) this.$message.success(`目录解析成功!共找到 ${this.directoryData.length} 个文件/文件夹`)
} catch (error) { } catch (error) {
@@ -1427,7 +1428,6 @@ export default {
if (shortInfo.link !== this.link || shortInfo.pwd !== this.password) { if (shortInfo.link !== this.link || shortInfo.pwd !== this.password) {
this.password = shortInfo.pwd this.password = shortInfo.pwd
this.link = shortInfo.link this.link = shortInfo.link
this.updateDirectLink()
if (!this.hasClipboardSuccessTip) { if (!this.hasClipboardSuccessTip) {
this.$message.success(`自动识别分享成功, 网盘类型: ${shortInfo.name}; 分享URL ${this.link}; 分享密码: ${this.password || '空'}`) this.$message.success(`自动识别分享成功, 网盘类型: ${shortInfo.name}; 分享URL ${this.link}; 分享密码: ${this.password || '空'}`)
this.hasClipboardSuccessTip = true this.hasClipboardSuccessTip = true
@@ -1446,8 +1446,6 @@ export default {
if (linkInfo.link !== this.link || pwd !== this.password) { if (linkInfo.link !== this.link || pwd !== this.password) {
this.password = pwd this.password = pwd
this.link = linkInfo.link this.link = linkInfo.link
// 更新智能直链(包含认证参数)
this.updateDirectLink()
// 聚焦期间只提示一次 // 聚焦期间只提示一次
if (!this.hasClipboardSuccessTip) { if (!this.hasClipboardSuccessTip) {
this.$message.success(`自动识别分享成功, 网盘类型: ${linkInfo.name}; 分享URL ${this.link}; 分享密码: ${this.password || '空'}`) this.$message.success(`自动识别分享成功, 网盘类型: ${linkInfo.name}; 分享URL ${this.link}; 分享密码: ${this.password || '空'}`)