js演练场

This commit is contained in:
q
2025-11-29 03:41:51 +08:00
parent df646b8c43
commit b4b1d7f923
25 changed files with 6379 additions and 112 deletions

View File

@@ -19,9 +19,17 @@ import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetAddress;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Pattern;
/**
* JavaScript HTTP客户端封装
@@ -37,6 +45,20 @@ public class JsHttpClient {
private final WebClient client;
private final WebClientSession clientSession;
private MultiMap headers;
private int timeoutSeconds = 30; // 默认超时时间30秒
// SSRF防护内网IP正则表达式
private static final Pattern PRIVATE_IP_PATTERN = Pattern.compile(
"^(127\\..*|10\\..*|172\\.(1[6-9]|2[0-9]|3[01])\\..*|192\\.168\\..*|169\\.254\\..*|::1|[fF][cCdD].*)"
);
// SSRF防护危险域名黑名单
private static final String[] DANGEROUS_HOSTS = {
"localhost",
"169.254.169.254", // AWS/阿里云等云服务元数据API
"metadata.google.internal", // GCP元数据
"100.100.100.200" // 阿里云元数据
};
public JsHttpClient() {
this.client = WebClient.create(WebClientVertxInit.get(), new WebClientOptions());;
@@ -86,12 +108,81 @@ public class JsHttpClient {
this.headers.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6");
}
/**
* 验证URL安全性SSRF防护- 仅拦截明显的内网攻击
* @param url 待验证的URL
* @throws SecurityException 如果URL不安全
*/
private void validateUrlSecurity(String url) {
try {
URI uri = new URI(url);
String host = uri.getHost();
if (host == null) {
log.debug("URL没有host信息: {}", url);
return; // 允许继续,可能是相对路径
}
String lowerHost = host.toLowerCase();
// 1. 检查明确的危险域名云服务元数据API等
for (String dangerous : DANGEROUS_HOSTS) {
if (lowerHost.equals(dangerous)) {
log.warn("🔒 安全拦截: 尝试访问云服务元数据API - {}", host);
throw new SecurityException("🔒 安全拦截: 禁止访问云服务元数据API");
}
}
// 2. 如果host是IP地址格式检查是否为内网IP
if (isIpAddress(lowerHost)) {
if (PRIVATE_IP_PATTERN.matcher(lowerHost).find()) {
log.warn("🔒 安全拦截: 尝试访问内网IP - {}", host);
throw new SecurityException("🔒 安全拦截: 禁止访问内网IP地址");
}
}
// 3. 对于域名尝试解析IP但不因解析失败而拦截
if (!isIpAddress(lowerHost)) {
try {
InetAddress addr = InetAddress.getByName(host);
String ip = addr.getHostAddress();
// 只拦截解析到内网IP的域名
if (PRIVATE_IP_PATTERN.matcher(ip).find()) {
log.warn("🔒 安全拦截: 域名解析到内网IP - {} -> {}", host, ip);
throw new SecurityException("🔒 安全拦截: 该域名指向内网地址");
}
} catch (UnknownHostException e) {
// DNS解析失败允许继续可能是外网域名暂时无法解析
log.debug("DNS解析失败允许继续: {}", host);
}
}
log.debug("URL安全检查通过: {}", url);
} catch (SecurityException e) {
throw e;
} catch (Exception e) {
// 其他异常不拦截,只记录日志
log.debug("URL验证异常允许继续: {}", url, e);
}
}
/**
* 判断字符串是否为IP地址格式
*/
private boolean isIpAddress(String host) {
// 简单判断是否为IPv4地址格式
return host.matches("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$") || host.contains(":");
}
/**
* 发起GET请求
* @param url 请求URL
* @return HTTP响应
*/
public JsHttpResponse get(String url) {
validateUrlSecurity(url);
return executeRequest(() -> {
HttpRequest<Buffer> request = client.getAbs(url);
if (!headers.isEmpty()) {
@@ -107,6 +198,7 @@ public class JsHttpClient {
* @return HTTP响应
*/
public JsHttpResponse getWithRedirect(String url) {
validateUrlSecurity(url);
return executeRequest(() -> {
HttpRequest<Buffer> request = client.getAbs(url);
if (!headers.isEmpty()) {
@@ -124,6 +216,7 @@ public class JsHttpClient {
* @return HTTP响应
*/
public JsHttpResponse getNoRedirect(String url) {
validateUrlSecurity(url);
return executeRequest(() -> {
HttpRequest<Buffer> request = client.getAbs(url);
if (!headers.isEmpty()) {
@@ -142,6 +235,7 @@ public class JsHttpClient {
* @return HTTP响应
*/
public JsHttpResponse post(String url, Object data) {
validateUrlSecurity(url);
return executeRequest(() -> {
HttpRequest<Buffer> request = client.postAbs(url);
if (!headers.isEmpty()) {
@@ -166,6 +260,84 @@ public class JsHttpClient {
});
}
/**
* 发起PUT请求
* @param url 请求URL
* @param data 请求数据
* @return HTTP响应
*/
public JsHttpResponse put(String url, Object data) {
validateUrlSecurity(url);
return executeRequest(() -> {
HttpRequest<Buffer> request = client.putAbs(url);
if (!headers.isEmpty()) {
request.putHeaders(headers);
}
if (data != null) {
if (data instanceof String) {
request.sendBuffer(Buffer.buffer((String) data));
} else if (data instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, String> mapData = (Map<String, String>) data;
request.sendForm(MultiMap.caseInsensitiveMultiMap().addAll(mapData));
} else {
request.sendJson(data);
}
} else {
request.send();
}
return request.send();
});
}
/**
* 发起DELETE请求
* @param url 请求URL
* @return HTTP响应
*/
public JsHttpResponse delete(String url) {
return executeRequest(() -> {
HttpRequest<Buffer> request = client.deleteAbs(url);
if (!headers.isEmpty()) {
request.putHeaders(headers);
}
return request.send();
});
}
/**
* 发起PATCH请求
* @param url 请求URL
* @param data 请求数据
* @return HTTP响应
*/
public JsHttpResponse patch(String url, Object data) {
return executeRequest(() -> {
HttpRequest<Buffer> request = client.patchAbs(url);
if (!headers.isEmpty()) {
request.putHeaders(headers);
}
if (data != null) {
if (data instanceof String) {
request.sendBuffer(Buffer.buffer((String) data));
} else if (data instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, String> mapData = (Map<String, String>) data;
request.sendForm(MultiMap.caseInsensitiveMultiMap().addAll(mapData));
} else {
request.sendJson(data);
}
} else {
request.send();
}
return request.send();
});
}
/**
* 设置请求头
* @param name 头名称
@@ -179,6 +351,105 @@ public class JsHttpClient {
return this;
}
/**
* 批量设置请求头
* @param headersMap 请求头Map
* @return 当前客户端实例(支持链式调用)
*/
public JsHttpClient putHeaders(Map<String, String> headersMap) {
if (headersMap != null) {
for (Map.Entry<String, String> entry : headersMap.entrySet()) {
if (entry.getKey() != null && entry.getValue() != null) {
headers.set(entry.getKey(), entry.getValue());
}
}
}
return this;
}
/**
* 删除指定请求头
* @param name 头名称
* @return 当前客户端实例(支持链式调用)
*/
public JsHttpClient removeHeader(String name) {
if (name != null) {
headers.remove(name);
}
return this;
}
/**
* 清空所有请求头(保留默认头)
* @return 当前客户端实例(支持链式调用)
*/
public JsHttpClient clearHeaders() {
headers.clear();
// 重新设置默认头
headers.set("Accept-Encoding", "gzip, deflate, br, zstd");
headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0");
headers.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6");
return this;
}
/**
* 获取所有请求头
* @return 请求头Map
*/
public Map<String, String> getHeaders() {
Map<String, String> result = new HashMap<>();
for (String name : headers.names()) {
result.put(name, headers.get(name));
}
return result;
}
/**
* 设置请求超时时间
* @param seconds 超时时间(秒)
* @return 当前客户端实例(支持链式调用)
*/
public JsHttpClient setTimeout(int seconds) {
if (seconds > 0) {
this.timeoutSeconds = seconds;
}
return this;
}
/**
* URL编码
* @param str 要编码的字符串
* @return 编码后的字符串
*/
public static String urlEncode(String str) {
if (str == null) {
return null;
}
try {
return URLEncoder.encode(str, StandardCharsets.UTF_8.name());
} catch (Exception e) {
log.error("URL编码失败", e);
return str;
}
}
/**
* URL解码
* @param str 要解码的字符串
* @return 解码后的字符串
*/
public static String urlDecode(String str) {
if (str == null) {
return null;
}
try {
return URLDecoder.decode(str, StandardCharsets.UTF_8.name());
} catch (Exception e) {
log.error("URL解码失败", e);
return str;
}
}
/**
* 发送表单数据(简单键值对)
* @param data 表单数据
@@ -201,7 +472,7 @@ public class JsHttpClient {
}
/**
* 发送multipart表单数据支持文件上传
* 发送multipart表单数据支持文本字段
* @param url 请求URL
* @param data 表单数据,支持:
* - Map<String, String>: 文本字段
@@ -271,16 +542,27 @@ public class JsHttpClient {
}
}).onFailure(Throwable::printStackTrace);
// 等待响应完成(最多30秒
// 等待响应完成(使用配置的超时时间
HttpResponse<Buffer> response = promise.future().toCompletionStage()
.toCompletableFuture()
.get(30, TimeUnit.SECONDS);
.get(timeoutSeconds, TimeUnit.SECONDS);
return new JsHttpResponse(response);
} catch (TimeoutException e) {
String errorMsg = "HTTP请求超时" + timeoutSeconds + "秒)";
log.error(errorMsg, e);
throw new RuntimeException(errorMsg, e);
} catch (Exception e) {
log.error("HTTP请求执行失败", e);
throw new RuntimeException("HTTP请求执行失败: " + e.getMessage(), e);
String errorMsg = e.getMessage();
if (errorMsg == null || errorMsg.trim().isEmpty()) {
errorMsg = e.getClass().getSimpleName();
if (e.getCause() != null && e.getCause().getMessage() != null) {
errorMsg += ": " + e.getCause().getMessage();
}
}
log.error("HTTP请求执行失败: " + errorMsg, e);
throw new RuntimeException("HTTP请求执行失败: " + errorMsg, e);
}
}
@@ -376,5 +658,29 @@ public class JsHttpClient {
public HttpResponse<Buffer> getOriginalResponse() {
return response;
}
/**
* 获取响应体字节数组
* @return 响应体字节数组
*/
public byte[] bodyBytes() {
Buffer buffer = response.body();
if (buffer == null) {
return new byte[0];
}
return buffer.getBytes();
}
/**
* 获取响应体大小
* @return 响应体大小(字节)
*/
public long bodySize() {
Buffer buffer = response.body();
if (buffer == null) {
return 0;
}
return buffer.length();
}
}
}

View File

@@ -8,12 +8,12 @@ import cn.qaiu.parser.custom.CustomParserConfig;
import io.vertx.core.Future;
import io.vertx.core.WorkerExecutor;
import io.vertx.core.json.JsonObject;
import org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory;
import org.openjdk.nashorn.api.scripting.ScriptObjectMirror;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import java.util.ArrayList;
import java.util.List;
@@ -63,12 +63,15 @@ public class JsParserExecutor implements IPanTool {
}
/**
* 初始化JavaScript引擎
* 初始化JavaScript引擎(带安全限制)
*/
private ScriptEngine initEngine() {
try {
ScriptEngineManager engineManager = new ScriptEngineManager();
ScriptEngine engine = engineManager.getEngineByName("JavaScript");
// 使用安全的ClassFilter创建Nashorn引擎
NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
// 正确的方法签名: getScriptEngine(String[] args, ClassLoader appLoader, ClassFilter classFilter)
ScriptEngine engine = factory.getScriptEngine(new String[0], null, new SecurityClassFilter());
if (engine == null) {
throw new RuntimeException("无法创建JavaScript引擎请确保Nashorn可用");
@@ -79,10 +82,19 @@ public class JsParserExecutor implements IPanTool {
engine.put("logger", jsLogger);
engine.put("shareLinkInfo", shareLinkInfoWrapper);
// 禁用Java对象访问
engine.eval("var Java = undefined;");
engine.eval("var JavaImporter = undefined;");
engine.eval("var Packages = undefined;");
engine.eval("var javax = undefined;");
engine.eval("var org = undefined;");
engine.eval("var com = undefined;");
log.debug("🔒 安全的JavaScript引擎初始化成功解析器类型: {}", config.getType());
// 执行JavaScript代码
engine.eval(config.getJsCode());
log.debug("JavaScript引擎初始化成功解析器类型: {}", config.getType());
return engine;
} catch (Exception e) {

View File

@@ -0,0 +1,118 @@
package cn.qaiu.parser.customjs;
import org.openjdk.nashorn.api.scripting.ClassFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* JavaScript执行器安全类过滤器
* 用于限制JavaScript代码可以访问的Java类防止恶意代码执行危险操作
*
* @author <a href="https://qaiu.top">QAIU</a>
*/
public class SecurityClassFilter implements ClassFilter {
private static final Logger log = LoggerFactory.getLogger(SecurityClassFilter.class);
// 危险类黑名单
private static final String[] DANGEROUS_CLASSES = {
// 系统命令执行
"java.lang.Runtime",
"java.lang.ProcessBuilder",
"java.lang.Process",
// 文件系统访问
"java.io.File",
"java.io.FileInputStream",
"java.io.FileOutputStream",
"java.io.FileReader",
"java.io.FileWriter",
"java.io.RandomAccessFile",
"java.nio.file.Files",
"java.nio.file.Paths",
"java.nio.file.Path",
"java.nio.channels.FileChannel",
// 系统访问
"java.lang.System",
"java.lang.SecurityManager",
// 反射相关
"java.lang.Class",
"java.lang.reflect.Method",
"java.lang.reflect.Field",
"java.lang.reflect.Constructor",
"java.lang.reflect.AccessibleObject",
"java.lang.ClassLoader",
// 网络访问
"java.net.Socket",
"java.net.ServerSocket",
"java.net.DatagramSocket",
"java.net.URL",
"java.net.URLConnection",
"java.net.HttpURLConnection",
"java.net.InetAddress",
// 线程和并发
"java.lang.Thread",
"java.lang.ThreadGroup",
"java.util.concurrent.Executor",
"java.util.concurrent.ExecutorService",
// 数据库访问
"java.sql.Connection",
"java.sql.Statement",
"java.sql.PreparedStatement",
"java.sql.DriverManager",
// 脚本引擎(防止嵌套执行)
"javax.script.ScriptEngine",
"javax.script.ScriptEngineManager",
// JVM控制
"java.lang.invoke.MethodHandle",
"sun.misc.Unsafe",
// Nashorn内部类
"jdk.nashorn.internal",
"jdk.internal",
};
@Override
public boolean exposeToScripts(String className) {
// 检查是否在黑名单中
for (String dangerous : DANGEROUS_CLASSES) {
if (className.equals(dangerous) || className.startsWith(dangerous + ".")) {
log.warn("🔒 安全拦截: JavaScript尝试访问危险类 - {}", className);
return false;
}
}
// 额外的包级别限制
String[] dangerousPackages = {
"java.lang.reflect.",
"java.io.",
"java.nio.",
"java.net.",
"java.sql.",
"javax.script.",
"sun.",
"jdk.internal.",
"jdk.nashorn.internal."
};
for (String pkg : dangerousPackages) {
if (className.startsWith(pkg)) {
log.warn("🔒 安全拦截: JavaScript尝试访问危险包 - {}", className);
return false;
}
}
// 默认也拒绝(白名单策略更安全,但这里为了兼容性使用黑名单)
// 如果要更严格,可以改为 return false
log.debug("允许访问类: {}", className);
return true;
}
}

View File

@@ -0,0 +1,790 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import cn.qaiu.util.CommonUtils;
import cn.qaiu.util.FileSizeConverter;
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.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.client.HttpRequest;
import io.vertx.ext.web.client.WebClient;
import io.vertx.uritemplate.UriTemplate;
import org.apache.commons.lang3.StringUtils;
import java.net.MalformedURLException;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.zip.CRC32;
import static cn.qaiu.util.RandomStringGenerator.gen36String;
/**
* 123盘解析器 v2 - 使用Android平台API
* 支持账号密码或token配置
*
* @author <a href="https://qaiu.top">QAIU</a>
*/
public class Ye2Tool extends PanBase {
public static final String SHARE_URL_PREFIX = "https://www.123pan.com/s/";
public static final String FIRST_REQUEST_URL = SHARE_URL_PREFIX + "{key}.html";
private static final String GET_SHARE_INFO_URL = "https://www.123pan.com/b/api/share/get?limit=100&next=1&orderBy=share_id&orderDirection=desc&shareKey={shareKey}&SharePwd={pwd}&ParentFileId={ParentFileId}&Page=1";
private static final String DOWNLOAD_API_URL = "https://www.123pan.com/b/api/file/download_info";
private static final String BATCH_DOWNLOAD_API_URL = "https://www.123pan.com/b/api/file/batch_download_share_info";
private static final String LOGIN_URL = "https://login.123pan.com/api/user/sign_in";
// 字符映射表
private static final String CHAR_MAP = "adefghlmyijnopkqrstubcvwsz";
private final MultiMap header = MultiMap.caseInsensitiveMultiMap();
// Token管理
private static String ssoToken;
private static long tokenExpireTime = 0L; // 毫秒时间戳
public Ye2Tool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
header.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6");
header.set("App-Version", "55");
header.set("Cache-Control", "no-cache");
header.set("Connection", "keep-alive");
header.set("LoginUuid", gen36String());
header.set("Pragma", "no-cache");
header.set("Referer", shareLinkInfo.getStandardUrl());
header.set("Sec-Fetch-Dest", "empty");
header.set("Sec-Fetch-Mode", "cors");
header.set("Sec-Fetch-Site", "same-origin");
header.set("User-Agent", "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Mobile Safari/537.36");
header.set("platform", "android");
header.set("Content-Type", "application/json");
}
/**
* 判断 token 是否过期
*/
private boolean isTokenExpired() {
return System.currentTimeMillis() > tokenExpireTime - 60_000; // 提前1分钟刷新
}
/**
* 计算CRC32并转换为16进制字符串
*/
private String crc32(String data) {
CRC32 crc32 = new CRC32();
crc32.update(data.getBytes());
long value = crc32.getValue();
return String.format("%08x", value);
}
/**
* 16进制转10进制
*/
private long hexToInt(String hexStr) {
return Long.parseLong(hexStr, 16);
}
/**
* 123盘的URL加密算法
* 参考Python代码中的encode123函数
*
* @param url 请求路径
* @param way 平台标识(如"android"
* @param version 版本号(如"55"
* @param timestamp 时间戳(毫秒)
* @return 加密后的URL参数格式?{y}={time_long}-{a}-{final_crc}
*/
private String encode123(String url, String way, String version, String timestamp) {
Random random = new Random();
// 生成随机数 a = int(10000000 * random.randint(1, 10000000) / 10000)
int randomInt = random.nextInt(10000000) + 1;
long a = (10000000L * randomInt) / 10000;
// 将时间戳转换为时间格式
long timeLong = Long.parseLong(timestamp) / 1000;
java.time.LocalDateTime dateTime = java.time.Instant.ofEpochSecond(timeLong)
.atZone(java.time.ZoneId.systemDefault())
.toLocalDateTime();
String timeStr = dateTime.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm"));
// 根据时间字符串生成g
StringBuilder g = new StringBuilder();
for (char c : timeStr.toCharArray()) {
int digit = Character.getNumericValue(c);
if (digit == 0) {
g.append(CHAR_MAP.charAt(0));
} else {
// 数字1对应索引0数字2对应索引1以此类推
g.append(CHAR_MAP.charAt(digit - 1));
}
}
// 计算y值CRC32的十进制
String y = String.valueOf(hexToInt(crc32(g.toString())));
// 计算最终的CRC32
String finalCrcInput = String.format("%d|%d|%s|%s|%s|%s", timeLong, a, url, way, version, y);
String finalCrc = String.valueOf(hexToInt(crc32(finalCrcInput)));
// 返回加密后的URL参数
return String.format("?%s=%d-%d-%s", y, timeLong, a, finalCrc);
}
public Future<String> parse() {
Future<String> tokenFuture;
// 检查是否直接提供了token
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
if (auths != null && auths.contains("token")) {
String providedToken = auths.get("token");
if (StringUtils.isNotEmpty(providedToken)) {
ssoToken = providedToken;
tokenFuture = Future.succeededFuture(providedToken);
} else {
// 如果没有提供token尝试登录
if (ssoToken == null || isTokenExpired()) {
tokenFuture = loginAndGetToken();
} else {
tokenFuture = Future.succeededFuture(ssoToken);
}
}
} else {
// 如果没有提供token尝试登录
if (ssoToken == null || isTokenExpired()) {
tokenFuture = loginAndGetToken();
} else {
tokenFuture = Future.succeededFuture(ssoToken);
}
}
// 1. 登录获取 sso-token 或使用提供的token
tokenFuture.onSuccess(token -> {
if (!token.equals("nologin")) {
// 2. 设置 header
ssoToken = token;
header.set("Authorization", "Bearer " + token);
}
final String dataKey = shareLinkInfo.getShareKey().replace(".html", "");
final String pwd = shareLinkInfo.getSharePassword();
// 3. 获取分享信息
client.getAbs(UriTemplate.of(GET_SHARE_INFO_URL))
.setTemplateParam("shareKey", dataKey)
.setTemplateParam("pwd", StringUtils.isEmpty(pwd) ? "" : pwd)
.setTemplateParam("ParentFileId", "0")
.putHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.putHeader("Referer", "https://www.123pan.com/")
.putHeader("Origin", "https://www.123pan.com")
.send()
.onSuccess(res -> {
JsonObject shareInfoJson = asJson(res);
if (shareInfoJson.getInteger("code") != 0) {
fail("获取分享信息失败: " + shareInfoJson.getString("message"));
return;
}
if (!shareInfoJson.containsKey("data") || !shareInfoJson.getJsonObject("data").containsKey("InfoList")) {
fail("返回数据格式错误");
return;
}
JsonObject data = shareInfoJson.getJsonObject("data");
if (data.getJsonArray("InfoList").size() == 0) {
fail("分享中没有文件");
return;
}
// 获取第一个文件信息
JsonObject fileInfo = data.getJsonArray("InfoList").getJsonObject(0);
// 检查是否需要登录
if (token.equals("nologin")) {
fail("该分享需要登录才能下载请提供账号密码或token");
return;
}
// 判断是否为文件夹: Type: 1为文件夹, 0为文件
if (fileInfo.getInteger("Type", 0) == 1) {
// 4. 获取文件夹打包下载链接
getZipDownUrl(client, fileInfo);
} else {
// 4. 获取文件下载链接
getDownUrl(client, fileInfo);
}
})
.onFailure(this.handleFail(GET_SHARE_INFO_URL));
}).onFailure(err -> {
fail("登录获取token失败: {}", err.getMessage());
});
return promise.future();
}
/**
* 登录并获取token
*/
private Future<String> loginAndGetToken() {
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
if (auths == null) {
return Future.succeededFuture("nologin");
}
String username = auths.get("username");
String password = auths.get("password");
if (username == null || password == null) {
return Future.succeededFuture("nologin");
}
Promise<String> promise = Promise.promise();
String loginUuid = gen36String();
JsonObject loginBody = new JsonObject()
.put("passport", username)
.put("password", password)
.put("remember", true);
client.postAbs(LOGIN_URL)
.putHeader("Content-Type", "application/json")
.putHeader("LoginUuid", loginUuid)
.putHeader("App-Version", "55")
.putHeader("platform", "web")
.sendJsonObject(loginBody)
.onSuccess(res -> {
JsonObject json = res.bodyAsJsonObject();
if (json == null) {
promise.fail("登录响应格式异常: " + res.bodyAsString());
return;
}
if (!json.containsKey("code")) {
promise.fail("登录响应格式异常: " + res.bodyAsString());
return;
}
if (json.getInteger("code") != 200) {
promise.fail("登录失败: " + json.getString("message"));
return;
}
JsonObject data = json.getJsonObject("data");
if (data == null || !data.containsKey("token")) {
promise.fail("未获取到token");
return;
}
ssoToken = data.getString("token");
String expireStr = data.getString("expire");
// 解析过期时间
if (StringUtils.isNotEmpty(expireStr)) {
tokenExpireTime = OffsetDateTime.parse(expireStr)
.toInstant().toEpochMilli();
} else {
// 如果没有过期时间默认1小时后过期
tokenExpireTime = System.currentTimeMillis() + 3600_000;
}
log.info("登录成功token: {}", ssoToken);
promise.complete(ssoToken);
})
.onFailure(promise::fail);
return promise.future();
}
/**
* 获取下载链接使用Android平台API
*/
private void getDownUrl(WebClient client, JsonObject fileInfo) {
setFileInfo(fileInfo);
// 构建请求数据
JsonObject jsonObject = new JsonObject();
jsonObject.put("driveId", 0);
jsonObject.put("etag", fileInfo.getString("Etag"));
jsonObject.put("fileId", fileInfo.getInteger("FileId"));
jsonObject.put("fileName", fileInfo.getString("FileName"));
jsonObject.put("s3keyFlag", fileInfo.getString("S3KeyFlag"));
jsonObject.put("size", fileInfo.getLong("Size"));
jsonObject.put("type", 0);
// 使用encode123加密URL参数
String timestamp = String.valueOf(System.currentTimeMillis());
String encryptedParams = encode123("/b/api/file/download_info", "android", "55", timestamp);
String apiUrl = DOWNLOAD_API_URL + encryptedParams;
log.info("Ye2 API URL: {}", apiUrl);
HttpRequest<Buffer> bufferHttpRequest = client.postAbs(apiUrl);
bufferHttpRequest.putHeader("platform", "android");
bufferHttpRequest.putHeader("App-Version", "55");
bufferHttpRequest.putHeader("Authorization", "Bearer " + ssoToken);
bufferHttpRequest.putHeader("User-Agent", "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36");
bufferHttpRequest.putHeader("Content-Type", "application/json");
bufferHttpRequest
.sendJsonObject(jsonObject)
.onSuccess(res2 -> {
JsonObject downURLJson = asJson(res2);
try {
if (downURLJson.getInteger("code") != 0) {
fail("Ye2: downURLJson返回值异常->" + downURLJson);
return;
}
} catch (Exception ignored) {
fail("Ye2: downURLJson格式异常->" + downURLJson);
return;
}
String downURL = downURLJson.getJsonObject("data").getString("DownloadUrl");
if (StringUtils.isEmpty(downURL)) {
downURL = downURLJson.getJsonObject("data").getString("DownloadURL");
}
if (StringUtils.isEmpty(downURL)) {
fail("Ye2: 未获取到下载链接");
return;
}
try {
Map<String, String> urlParams = CommonUtils.getURLParams(downURL);
String params = urlParams.get("params");
if (StringUtils.isEmpty(params)) {
// 如果没有params参数直接使用downURL
complete(downURL);
return;
}
byte[] decodeByte = Base64.getDecoder().decode(params);
String downUrl2 = new String(decodeByte);
clientNoRedirects.getAbs(downUrl2).putHeaders(header).send().onSuccess(res3 -> {
if (res3.statusCode() == 302 || res3.statusCode() == 301) {
String redirectUrl = res3.getHeader("Location");
if (StringUtils.isBlank(redirectUrl)) {
fail("重定向链接为空");
return;
}
complete(redirectUrl);
return;
}
JsonObject res3Json = asJson(res3);
try {
if (res3Json.getInteger("code") != 0) {
fail("Ye2: downUrl2返回值异常->" + res3Json);
return;
}
} catch (Exception ignored) {
fail("Ye2: downUrl2格式异常->" + downURLJson);
return;
}
String redirectUrl = res3Json.getJsonObject("data").getString("redirect_url");
if (StringUtils.isNotEmpty(redirectUrl)) {
complete(redirectUrl);
} else {
complete(downUrl2);
}
}).onFailure(err -> fail("获取直链失败: " + err.getMessage()));
} catch (MalformedURLException e) {
// 如果解析失败直接使用downURL
complete(downURL);
} catch (Exception e) {
fail("urlParams解析异常: " + e.getMessage());
}
}).onFailure(err -> fail("下载接口失败: " + err.getMessage()));
}
/**
* 获取文件夹打包下载链接使用Android平台API
*/
private void getZipDownUrl(WebClient client, JsonObject fileInfo) {
// 构建请求数据
JsonObject jsonObject = new JsonObject();
jsonObject.put("shareKey", shareLinkInfo.getShareKey().replace(".html", ""));
jsonObject.put("fileIdList", new JsonArray().add(JsonObject.of("fileId", fileInfo.getInteger("FileId"))));
// 使用encode123加密URL参数
String timestamp = String.valueOf(System.currentTimeMillis());
String encryptedParams = encode123("/b/api/file/batch_download_share_info", "android", "55", timestamp);
String apiUrl = BATCH_DOWNLOAD_API_URL + encryptedParams;
log.info("Ye2 Batch Download API URL: {}", apiUrl);
HttpRequest<Buffer> bufferHttpRequest = client.postAbs(apiUrl);
bufferHttpRequest.putHeader("platform", "android");
bufferHttpRequest.putHeader("App-Version", "55");
bufferHttpRequest.putHeader("Authorization", "Bearer " + ssoToken);
bufferHttpRequest.putHeader("User-Agent", "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36");
bufferHttpRequest.putHeader("Content-Type", "application/json");
bufferHttpRequest
.sendJsonObject(jsonObject)
.onSuccess(res2 -> {
JsonObject downURLJson = asJson(res2);
try {
if (downURLJson.getInteger("code") != 0) {
fail("Ye2: 文件夹打包下载接口返回值异常->" + downURLJson);
return;
}
} catch (Exception ignored) {
fail("Ye2: 文件夹打包下载接口格式异常->" + downURLJson);
return;
}
String downURL = downURLJson.getJsonObject("data").getString("DownloadUrl");
if (StringUtils.isEmpty(downURL)) {
downURL = downURLJson.getJsonObject("data").getString("DownloadURL");
}
if (StringUtils.isEmpty(downURL)) {
fail("Ye2: 未获取到文件夹打包下载链接");
return;
}
try {
Map<String, String> urlParams = CommonUtils.getURLParams(downURL);
String params = urlParams.get("params");
if (StringUtils.isEmpty(params)) {
// 如果没有params参数直接使用downURL
complete(downURL);
return;
}
byte[] decodeByte = Base64.getDecoder().decode(params);
String downUrl2 = new String(decodeByte);
clientNoRedirects.getAbs(downUrl2).putHeaders(header).send().onSuccess(res3 -> {
if (res3.statusCode() == 302 || res3.statusCode() == 301) {
String redirectUrl = res3.getHeader("Location");
if (StringUtils.isBlank(redirectUrl)) {
fail("重定向链接为空");
return;
}
complete(redirectUrl);
return;
}
JsonObject res3Json = asJson(res3);
try {
if (res3Json.getInteger("code") != 0) {
fail("Ye2: 文件夹打包下载重定向返回值异常->" + res3Json);
return;
}
} catch (Exception ignored) {
fail("Ye2: 文件夹打包下载重定向格式异常->" + downURLJson);
return;
}
String redirectUrl = res3Json.getJsonObject("data").getString("redirect_url");
if (StringUtils.isNotEmpty(redirectUrl)) {
complete(redirectUrl);
} else {
complete(downUrl2);
}
}).onFailure(err -> fail("获取文件夹打包下载直链失败: " + err.getMessage()));
} catch (MalformedURLException e) {
// 如果解析失败直接使用downURL
complete(downURL);
} catch (Exception e) {
fail("文件夹打包下载urlParams解析异常: " + e.getMessage());
}
}).onFailure(err -> fail("文件夹打包下载接口失败: " + err.getMessage()));
}
/**
* 设置文件信息
*/
void setFileInfo(JsonObject reqBodyJson) {
FileInfo fileInfo = new FileInfo();
fileInfo.setFileId(reqBodyJson.getInteger("FileId").toString());
fileInfo.setFileName(reqBodyJson.getString("FileName"));
fileInfo.setSize(reqBodyJson.getLong("Size"));
fileInfo.setHash(reqBodyJson.getString("Etag"));
String createAt = reqBodyJson.getString("CreateAt");
if (StringUtils.isNotEmpty(createAt)) {
fileInfo.setCreateTime(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.format(OffsetDateTime.parse(createAt).toLocalDateTime()));
}
String updateAt = reqBodyJson.getString("UpdateAt");
if (StringUtils.isNotEmpty(updateAt)) {
fileInfo.setUpdateTime(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.format(OffsetDateTime.parse(updateAt).toLocalDateTime()));
}
shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
}
/**
* 解析文件夹中的文件列表
*/
@Override
public Future<List<FileInfo>> parseFileList() {
Promise<List<FileInfo>> promise = Promise.promise();
String shareKey = shareLinkInfo.getShareKey().replace(".html", "");
String pwd = shareLinkInfo.getSharePassword();
String parentFileId = "0"; // 根目录的文件ID
// 如果参数里的目录ID不为空则直接解析目录
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
if (StringUtils.isNotBlank(dirId)) {
parentFileId = dirId;
}
// 确保已登录
Future<String> tokenFuture;
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
if (auths != null && auths.contains("token")) {
String providedToken = auths.get("token");
if (StringUtils.isNotEmpty(providedToken)) {
ssoToken = providedToken;
tokenFuture = Future.succeededFuture(providedToken);
} else {
if (ssoToken == null || isTokenExpired()) {
tokenFuture = loginAndGetToken();
} else {
tokenFuture = Future.succeededFuture(ssoToken);
}
}
} else {
if (ssoToken == null || isTokenExpired()) {
tokenFuture = loginAndGetToken();
} else {
tokenFuture = Future.succeededFuture(ssoToken);
}
}
String finalParentFileId = parentFileId;
tokenFuture.onSuccess(token -> {
if (token.equals("nologin")) {
promise.fail("该分享需要登录才能访问请提供账号密码或token");
return;
}
// 构造文件列表接口的URL
client.getAbs(UriTemplate.of(GET_SHARE_INFO_URL))
.setTemplateParam("shareKey", shareKey)
.setTemplateParam("pwd", StringUtils.isEmpty(pwd) ? "" : pwd)
.setTemplateParam("ParentFileId", finalParentFileId)
.putHeader("Authorization", "Bearer " + token)
.putHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.putHeader("Referer", "https://www.123pan.com/")
.putHeader("Origin", "https://www.123pan.com")
.send().onSuccess(res -> {
JsonObject response = asJson(res);
if (response.getInteger("code") != 0) {
promise.fail("API错误: " + response.getString("message"));
return;
}
if (!response.containsKey("data") || !response.getJsonObject("data").containsKey("InfoList")) {
promise.fail("返回数据格式错误");
return;
}
JsonArray infoList = response.getJsonObject("data").getJsonArray("InfoList");
List<FileInfo> result = new ArrayList<>();
// 遍历返回的文件和目录信息
for (int i = 0; i < infoList.size(); i++) {
JsonObject item = infoList.getJsonObject(i);
FileInfo fileInfo = new FileInfo();
// 构建下载参数
JsonObject postData = JsonObject.of()
.put("driveId", 0)
.put("etag", item.getString("Etag"))
.put("fileId", item.getInteger("FileId"))
.put("fileName", item.getString("FileName"))
.put("s3keyFlag", item.getString("S3KeyFlag"))
.put("size", item.getLong("Size"))
.put("type", 0);
String param = CommonUtils.urlBase64Encode(postData.encode());
if (item.getInteger("Type") == 0) { // 文件
fileInfo.setFileName(item.getString("FileName"))
.setFileId(item.getInteger("FileId").toString())
.setFileType("file")
.setSize(item.getLong("Size"))
.setHash(item.getString("Etag"))
.setSizeStr(FileSizeConverter.convertToReadableSize(item.getLong("Size")));
String createAt = item.getString("CreateAt");
if (StringUtils.isNotEmpty(createAt)) {
fileInfo.setCreateTime(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.format(OffsetDateTime.parse(createAt).toLocalDateTime()));
}
String updateAt = item.getString("UpdateAt");
if (StringUtils.isNotEmpty(updateAt)) {
fileInfo.setUpdateTime(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.format(OffsetDateTime.parse(updateAt).toLocalDateTime()));
}
fileInfo.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param))
.setPreviewUrl(String.format("%s/v2/viewUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param));
result.add(fileInfo);
} else if (item.getInteger("Type") == 1) { // 目录
fileInfo.setFileName(item.getString("FileName"))
.setFileId(item.getInteger("FileId").toString())
.setFileType("folder")
.setSize(0L);
String createAt = item.getString("CreateAt");
if (StringUtils.isNotEmpty(createAt)) {
fileInfo.setCreateTime(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.format(OffsetDateTime.parse(createAt).toLocalDateTime()));
}
String updateAt = item.getString("UpdateAt");
if (StringUtils.isNotEmpty(updateAt)) {
fileInfo.setUpdateTime(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.format(OffsetDateTime.parse(updateAt).toLocalDateTime()));
}
fileInfo.setParserUrl(
String.format("%s/v2/getFileList?url=%s&dirId=%s&pwd=%s",
getDomainName(),
shareLinkInfo.getShareUrl(),
item.getInteger("FileId"),
pwd)
);
result.add(fileInfo);
}
}
promise.complete(result);
}).onFailure(promise::fail);
}).onFailure(err -> promise.fail("登录获取token失败: " + err.getMessage()));
return promise.future();
}
/**
* 通过ID解析特定文件
*/
@Override
public Future<String> parseById() {
JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson");
// 确保已登录
Future<String> tokenFuture;
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
if (auths != null && auths.contains("token")) {
String providedToken = auths.get("token");
if (StringUtils.isNotEmpty(providedToken)) {
ssoToken = providedToken;
tokenFuture = Future.succeededFuture(providedToken);
} else {
if (ssoToken == null || isTokenExpired()) {
tokenFuture = loginAndGetToken();
} else {
tokenFuture = Future.succeededFuture(ssoToken);
}
}
} else {
if (ssoToken == null || isTokenExpired()) {
tokenFuture = loginAndGetToken();
} else {
tokenFuture = Future.succeededFuture(ssoToken);
}
}
tokenFuture.onSuccess(token -> {
if (token.equals("nologin")) {
fail("该分享需要登录才能下载请提供账号密码或token");
return;
}
// 使用encode123加密URL参数
String timestamp = String.valueOf(System.currentTimeMillis());
String encryptedParams = encode123("/b/api/file/download_info", "android", "55", timestamp);
String apiUrl = DOWNLOAD_API_URL + encryptedParams;
log.info("Ye2 parseById API URL: {}", apiUrl);
HttpRequest<Buffer> bufferHttpRequest = client.postAbs(apiUrl);
bufferHttpRequest.putHeader("platform", "android");
bufferHttpRequest.putHeader("App-Version", "55");
bufferHttpRequest.putHeader("Authorization", "Bearer " + token);
bufferHttpRequest.putHeader("User-Agent", "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36");
bufferHttpRequest.putHeader("Content-Type", "application/json");
bufferHttpRequest
.sendJsonObject(paramJson)
.onSuccess(res2 -> {
JsonObject downURLJson = asJson(res2);
try {
if (downURLJson.getInteger("code") != 0) {
fail("Ye2: downURLJson返回值异常->" + downURLJson);
return;
}
} catch (Exception ignored) {
fail("Ye2: downURLJson格式异常->" + downURLJson);
return;
}
String downURL = downURLJson.getJsonObject("data").getString("DownloadUrl");
if (StringUtils.isEmpty(downURL)) {
downURL = downURLJson.getJsonObject("data").getString("DownloadURL");
}
if (StringUtils.isEmpty(downURL)) {
fail("Ye2: 未获取到下载链接");
return;
}
try {
Map<String, String> urlParams = CommonUtils.getURLParams(downURL);
String params = urlParams.get("params");
if (StringUtils.isEmpty(params)) {
// 如果没有params参数直接使用downURL
complete(downURL);
return;
}
byte[] decodeByte = Base64.getDecoder().decode(params);
String downUrl2 = new String(decodeByte);
clientNoRedirects.getAbs(downUrl2).putHeaders(header).send().onSuccess(res3 -> {
if (res3.statusCode() == 302 || res3.statusCode() == 301) {
String redirectUrl = res3.getHeader("Location");
if (StringUtils.isBlank(redirectUrl)) {
fail("重定向链接为空");
return;
}
complete(redirectUrl);
return;
}
JsonObject res3Json = asJson(res3);
try {
if (res3Json.getInteger("code") != 0) {
fail("Ye2: downUrl2返回值异常->" + res3Json);
return;
}
} catch (Exception ignored) {
fail("Ye2: downUrl2格式异常->" + downURLJson);
return;
}
String redirectUrl = res3Json.getJsonObject("data").getString("redirect_url");
if (StringUtils.isNotEmpty(redirectUrl)) {
complete(redirectUrl);
} else {
complete(downUrl2);
}
}).onFailure(err -> fail("获取直链失败: " + err.getMessage()));
} catch (MalformedURLException e) {
// 如果解析失败直接使用downURL
complete(downURL);
} catch (Exception e) {
fail("urlParams解析异常: " + e.getMessage());
}
}).onFailure(err -> fail("下载接口失败: " + err.getMessage()));
}).onFailure(err -> fail("登录获取token失败: " + err.getMessage()));
return promise.future();
}
}