mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2026-08-27 20:12:02 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d3ef521773 | |||
| 2969c92d3d | |||
| 2fde421169 | |||
| 3494271150 | |||
| 675d930f86 | |||
| 5e20ed5050 | |||
| 91f4fcaa76 | |||
| 0754fe20f1 | |||
| 034fd20d3b | |||
| 24bf98bca9 | |||
| 275f6b9e6d | |||
| 5db9e422f4 |
@@ -175,11 +175,11 @@ jobs:
|
|||||||
|
|
||||||
if [ -z "$RAW_MODULES" ] || [[ "$RAW_MODULES" == *"Missing"* ]] || [[ "$RAW_MODULES" == *"Error"* ]]; then
|
if [ -z "$RAW_MODULES" ] || [[ "$RAW_MODULES" == *"Missing"* ]] || [[ "$RAW_MODULES" == *"Error"* ]]; then
|
||||||
# ⚠️ 回退列表:若项目新增了需要 java.* / jdk.* 模块的依赖,需同步更新此处
|
# ⚠️ 回退列表:若项目新增了需要 java.* / jdk.* 模块的依赖,需同步更新此处
|
||||||
RAW_MODULES="java.base,java.logging,java.sql,java.naming,java.management,java.xml,jdk.unsupported,java.net.http,java.instrument,java.security.jgss,java.security.sasl,java.desktop,jdk.crypto.ec"
|
RAW_MODULES="java.base,java.logging,java.sql,java.naming,java.management,java.xml,java.scripting,jdk.unsupported,java.net.http,java.instrument,java.security.jgss,java.security.sasl,java.desktop,jdk.crypto.ec"
|
||||||
echo "jdeps 分析失败,使用回退模块列表"
|
echo "jdeps 分析失败,使用回退模块列表"
|
||||||
else
|
else
|
||||||
# 补上 jdeps 无法检测的反射/SPI依赖
|
# 补上 jdeps 无法检测的反射/SPI依赖
|
||||||
RAW_MODULES="$RAW_MODULES,java.desktop,jdk.crypto.ec"
|
RAW_MODULES="$RAW_MODULES,java.desktop,java.scripting,jdk.crypto.ec"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "detected modules: $RAW_MODULES"
|
echo "detected modules: $RAW_MODULES"
|
||||||
@@ -215,6 +215,8 @@ jobs:
|
|||||||
ls -la "$JRE_BIN"/*.dll 2>/dev/null || echo "(无 .dll 文件)"
|
ls -la "$JRE_BIN"/*.dll 2>/dev/null || echo "(无 .dll 文件)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
"native-package/netdisk-fast-download/jre/bin/java" --list-modules | grep -q '^java.scripting@'
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 组装包目录
|
# 组装包目录
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ QQ交流群:1017480890
|
|||||||
<a href="https://vertx-china.github.io"><img src="https://img.shields.io/badge/vert.x-4.5.27-blue?style=flat"></a>
|
<a href="https://vertx-china.github.io"><img src="https://img.shields.io/badge/vert.x-4.5.27-blue?style=flat"></a>
|
||||||
<a href="https://raw.githubusercontent.com/qaiu/netdisk-fast-download/master/LICENSE"><img src="https://img.shields.io/github/license/qaiu/netdisk-fast-download?style=flat"></a>
|
<a href="https://raw.githubusercontent.com/qaiu/netdisk-fast-download/master/LICENSE"><img src="https://img.shields.io/github/license/qaiu/netdisk-fast-download?style=flat"></a>
|
||||||
<a href="https://github.com/qaiu/netdisk-fast-download/releases/"><img src="https://img.shields.io/github/v/release/qaiu/netdisk-fast-download?style=flat"></a>
|
<a href="https://github.com/qaiu/netdisk-fast-download/releases/"><img src="https://img.shields.io/github/v/release/qaiu/netdisk-fast-download?style=flat"></a>
|
||||||
|
<a href="https://github.com/QAIU/netdisk-fast-download">
|
||||||
|
<img src="https://img.shields.io/github/stars/QAIU/netdisk-fast-download?style=flat&logo=github" alt="GitHub Stars">
|
||||||
|
</a>
|
||||||
<a href="https://atomgit.com/QAIU/netdisk-fast-download"><img src="https://atomgit.com/QAIU/netdisk-fast-download/star/badge.svg" alt="AtomGit"></a>
|
<a href="https://atomgit.com/QAIU/netdisk-fast-download"><img src="https://atomgit.com/QAIU/netdisk-fast-download/star/badge.svg" alt="AtomGit"></a>
|
||||||
<a href="https://oosmetrics.com/repo/qaiu/netdisk-fast-download"><img src="https://api.oosmetrics.com/api/v1/badge/achievement/826aa27a-6e59-4de5-b7fa-cd189f484035.svg"></a>
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://trendshift.io/repositories/12101" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12101" alt="qaiu%2Fnetdisk-fast-download | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
<a href="https://trendshift.io/repositories/12101" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12101" alt="qaiu%2Fnetdisk-fast-download | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
</p>
|
</p>
|
||||||
@@ -16,10 +18,6 @@ QQ交流群:1017480890
|
|||||||

|

|
||||||
|
|
||||||
|
|
||||||
## 国内镜像
|
|
||||||
|
|
||||||
本项目同步托管于 **AtomGit**,国内访问更流畅:👉 [https://atomgit.com/QAIU/netdisk-fast-download](https://atomgit.com/QAIU/netdisk-fast-download)
|
|
||||||
|
|
||||||
|
|
||||||
## 介绍
|
## 介绍
|
||||||
> netdisk-fast-download网盘直链解析可以把云盘分享链接转为直链,可广泛应用于各类下载站,资源站,个人博客,图床,APP下载更新,视频点播等领域。支持市面各大主流云盘的文件分享以及文件夹分享链接,已支持蓝奏云/蓝奏云优享/移动云云空间/小飞机盘/亿方云/123云盘/Cloudreve等,支持加密分享,以及部分网盘文件夹分享。
|
> netdisk-fast-download网盘直链解析可以把云盘分享链接转为直链,可广泛应用于各类下载站,资源站,个人博客,图床,APP下载更新,视频点播等领域。支持市面各大主流云盘的文件分享以及文件夹分享链接,已支持蓝奏云/蓝奏云优享/移动云云空间/小飞机盘/亿方云/123云盘/Cloudreve等,支持加密分享,以及部分网盘文件夹分享。
|
||||||
@@ -40,12 +38,12 @@ curl -LOJ "https://lz.qaiu.top/parser?url=https://share.feijipan.com/s/Tk1F2kGQ&
|
|||||||
```shell
|
```shell
|
||||||
wget -O bilibili.mp4 "https://lz.qaiu.top/parser?url=https://share.feijipan.com/s/Tk1F2kGQ&pwd=1234"
|
wget -O bilibili.mp4 "https://lz.qaiu.top/parser?url=https://share.feijipan.com/s/Tk1F2kGQ&pwd=1234"
|
||||||
```
|
```
|
||||||
或者使用浏览器[直接访问](https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.top%2Fparser%3Furl%3Dhttps%3A%2F%2Fshare.feijipan.com%2Fs%2FTk1F2kGQ&name=bilibili.mp4&ext=mp4):
|
或者使用浏览器[直接访问](https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.top%2Fparser%3Furl%3Dhttps%3A%2F%2Fwww.ilanzou.com%2Fs%2FCDx6xKbT&name=bilibili.mp4&ext=mp4):
|
||||||
```
|
```
|
||||||
### 调用演示站下载:
|
### 调用演示站下载:
|
||||||
https://lz.qaiu.top/parser?url=https://share.feijipan.com/s/Tk1F2kGQ&pwd=1234
|
https://lz.qaiu.top/parser?url=https://www.ilanzou.com/s/CDx6xKbT&pwd=1234
|
||||||
### 调用演示站预览:
|
### 调用演示站预览:
|
||||||
https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.top%2Fparser%3Furl%3Dhttps%3A%2F%2Fshare.feijipan.com%2Fs%2FTk1F2kGQ&name=bilibili.mp4&ext=mp4
|
https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.top%2Fparser%3Furl%3Dhttps%3A%2F%2Fwww.ilanzou.com%2Fs%2FCDx6xKbT&name=bilibili.mp4&ext=mp4
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -71,7 +69,7 @@ https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.
|
|||||||
- [移动云云空间-ec](https://www.ecpan.cn/web)
|
- [移动云云空间-ec](https://www.ecpan.cn/web)
|
||||||
- [小飞机网盘-fj](https://www.feijipan.com/)
|
- [小飞机网盘-fj](https://www.feijipan.com/)
|
||||||
- [亿方云-fc](https://www.fangcloud.com/)
|
- [亿方云-fc](https://www.fangcloud.com/)
|
||||||
- [123云盘-ye](https://www.123pan.com/)
|
- [123云盘-ye](https://www.123pan.com/) ⚠️仅建议本地部署使用,需登录认证,公共/云端服务器风控严格建议自行部署(Windows 可直接用 run.bat 一键运行)
|
||||||
- ~[115网盘(失效)-p115](https://115.com/)~
|
- ~[115网盘(失效)-p115](https://115.com/)~
|
||||||
- [文叔叔-ws](https://www.wenshushu.cn/)
|
- [文叔叔-ws](https://www.wenshushu.cn/)
|
||||||
- [联想乐云-le](https://lecloud.lenovo.com/)
|
- [联想乐云-le](https://lecloud.lenovo.com/)
|
||||||
@@ -178,6 +176,7 @@ GET /json/getFileList?url={分享链接}&pwd={密码}
|
|||||||
| UC网盘(UC) | **必须** | 必须配置 Cookie 才能解析 |
|
| UC网盘(UC) | **必须** | 必须配置 Cookie 才能解析 |
|
||||||
| 小飞机网盘(FJ) | 可选 | 大文件(>100MB)需要认证 |
|
| 小飞机网盘(FJ) | 可选 | 大文件(>100MB)需要认证 |
|
||||||
| 蓝奏优享(IZ) | 可选 | 大文件需要认证 |
|
| 蓝奏优享(IZ) | 可选 | 大文件需要认证 |
|
||||||
|
| 123网盘(YE) | 可选 | 需要下载大文件/需要登录的分享时才需要认证,支持账号密码或 token/authorization |
|
||||||
|
|
||||||
**使用示例**:
|
**使用示例**:
|
||||||
```
|
```
|
||||||
@@ -198,6 +197,38 @@ GET /parser?url={分享链接}&pwd={密码}&auth={加密后的认证参数}
|
|||||||
- 目的:防止客户端伪造失败计数请求
|
- 目的:防止客户端伪造失败计数请求
|
||||||
- 建议:使用高强度随机字符串,且不要与 `authEncryptKey` 相同
|
- 建议:使用高强度随机字符串,且不要与 `authEncryptKey` 相同
|
||||||
|
|
||||||
|
#### `auth` 临时认证参数 与 `auths` 静态配置认证 的区别
|
||||||
|
|
||||||
|
本项目存在两种互相独立的认证配置方式,作用范围不同,不要混淆:
|
||||||
|
|
||||||
|
| 方式 | 配置位置 | 生效范围 | 适用场景 |
|
||||||
|
|------|---------|---------|---------|
|
||||||
|
| `auth` 临时认证参数 | 请求 URL 上的 `auth` 查询参数 | **仅当次请求**,优先级高于 app-dev.yml 中的静态配置 | 调用方按用户临时提供的账号/Cookie/token 解析,不同请求可携带不同认证信息 |
|
||||||
|
| `auths` 静态配置认证 | `app-dev.yml` 的 `auths.<网盘标识>` 节点 | **服务端长期生效**,所有未携带 `auth` 参数的请求都会复用 | 部署方自己长期配置一份账号,供所有请求默认使用 |
|
||||||
|
|
||||||
|
以 123网盘(`ye`)为例,`app-dev.yml` 中支持以下几种写法(三选一即可):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auths:
|
||||||
|
ye:
|
||||||
|
username: 你的123网盘账号
|
||||||
|
password: 你的123网盘密码
|
||||||
|
```
|
||||||
|
```yaml
|
||||||
|
auths:
|
||||||
|
ye:
|
||||||
|
token: 已登录后获取的 Authorization/AccessToken
|
||||||
|
```
|
||||||
|
```yaml
|
||||||
|
auths:
|
||||||
|
ye:
|
||||||
|
authorization: 已登录后获取的 Authorization/AccessToken # 与 token 等价,二选一
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ 注意:YAML 中 key 后面不写值(如 `authorization:` 空着)等同于没配置,不会生效,必须填入真实的账号密码或 token 内容。
|
||||||
|
|
||||||
|
如果只是临时调用一次,不想改动服务端配置,也可以用上面提到的 `auth` 参数临时传递(`authType` 可选 `password`/`accesstoken`/`authorization`),无需重启服务,仅本次请求生效。
|
||||||
|
|
||||||
### 特殊说明
|
### 特殊说明
|
||||||
|
|
||||||
- 移动云云空间的 `分享key` 取分享链接中的 `data` 参数值
|
- 移动云云空间的 `分享key` 取分享链接中的 `data` 参数值
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ public enum PanDomainTemplate {
|
|||||||
123795.com
|
123795.com
|
||||||
*/
|
*/
|
||||||
YE("123网盘",
|
YE("123网盘",
|
||||||
compile("https://www\\.(" +
|
compile("https://(?:[a-zA-Z\\d-]+\\.)*(" +
|
||||||
"123254\\.com|" +
|
"123254\\.com|" +
|
||||||
"123957\\.com|" +
|
"123957\\.com|" +
|
||||||
"123295\\.com|" +
|
"123295\\.com|" +
|
||||||
@@ -232,7 +232,7 @@ public enum PanDomainTemplate {
|
|||||||
"123635\\.com|" +
|
"123635\\.com|" +
|
||||||
"123242\\.com|" +
|
"123242\\.com|" +
|
||||||
"123795\\.com" +
|
"123795\\.com" +
|
||||||
")/s/(?<KEY>[a-zA-Z0-9_-]+)(?:\\.html)?"),
|
")/(?:(?:s|123pan)/|(?:[^/?#]+/)+)?(?<KEY>[a-zA-Z0-9]+-[a-zA-Z0-9]+|[a-zA-Z0-9_-]+)(?:\\.html)?(?:\\?.*)?"),
|
||||||
"https://www.123pan.com/s/{shareKey}",
|
"https://www.123pan.com/s/{shareKey}",
|
||||||
Ye2Tool.class),
|
Ye2Tool.class),
|
||||||
// https://www.ecpan.cn/web/#/yunpanProxy?path=%2F%23%2Fdrive%2Foutside&data={code}&isShare=1
|
// https://www.ecpan.cn/web/#/yunpanProxy?path=%2F%23%2Fdrive%2Foutside&data={code}&isShare=1
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package cn.qaiu.parser;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser token cache keyed by parser type and account identity.
|
||||||
|
*/
|
||||||
|
public final class TokenCache {
|
||||||
|
|
||||||
|
private static final Map<String, String> TOKENS = new ConcurrentHashMap<>();
|
||||||
|
private static final Map<String, Long> EXPIRES = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private TokenCache() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String key(String type, String accountId) {
|
||||||
|
return type + ":" + (StringUtils.isBlank(accountId) ? "_default" : accountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putToken(String key, String token) {
|
||||||
|
if (StringUtils.isBlank(key) || StringUtils.isBlank(token)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TOKENS.put(key, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getToken(String key) {
|
||||||
|
if (StringUtils.isBlank(key)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (isExpired(key)) {
|
||||||
|
TOKENS.remove(key);
|
||||||
|
EXPIRES.remove(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return TOKENS.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void putExpire(String key, long expireTimeMillis) {
|
||||||
|
if (StringUtils.isBlank(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
EXPIRES.put(key, expireTimeMillis);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isExpired(String key) {
|
||||||
|
Long expireTimeMillis = EXPIRES.get(key);
|
||||||
|
return expireTimeMillis != null && System.currentTimeMillis() > expireTimeMillis;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,10 @@ package cn.qaiu.parser.impl;
|
|||||||
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;
|
||||||
|
import cn.qaiu.parser.TokenCache;
|
||||||
import cn.qaiu.util.CommonUtils;
|
import cn.qaiu.util.CommonUtils;
|
||||||
import cn.qaiu.util.FileSizeConverter;
|
import cn.qaiu.util.FileSizeConverter;
|
||||||
|
import cn.qaiu.util.YeShareHostUtil;
|
||||||
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;
|
||||||
@@ -19,7 +21,11 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.*;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Random;
|
||||||
import java.util.zip.CRC32;
|
import java.util.zip.CRC32;
|
||||||
|
|
||||||
import static cn.qaiu.util.RandomStringGenerator.gen36String;
|
import static cn.qaiu.util.RandomStringGenerator.gen36String;
|
||||||
@@ -32,24 +38,22 @@ import static cn.qaiu.util.RandomStringGenerator.gen36String;
|
|||||||
*/
|
*/
|
||||||
public class Ye2Tool extends PanBase {
|
public class Ye2Tool extends PanBase {
|
||||||
|
|
||||||
public static final String SHARE_URL_PREFIX = "https://www.123pan.com/s/";
|
private static final String API_BASE = "https://api.123278.com";
|
||||||
public static final String FIRST_REQUEST_URL = SHARE_URL_PREFIX + "{key}.html";
|
private static final String GET_SHARE_INFO_URL = API_BASE + "/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 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 = API_BASE + "/b/api/file/download_info";
|
||||||
private static final String DOWNLOAD_API_URL = "https://www.123pan.com/b/api/file/download_info";
|
private static final String DOWNLOAD_API_V2_BASE = "https://api.123278.com";
|
||||||
private static final String BATCH_DOWNLOAD_API_URL = "https://www.123pan.com/b/api/file/batch_download_share_info";
|
private static final String DOWNLOAD_API_V2_PATH = "/b/api/v2/share/download/info";
|
||||||
|
private static final String BATCH_DOWNLOAD_API_URL = API_BASE + "/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 LOGIN_URL = "https://login.123pan.com/api/user/sign_in";
|
||||||
|
|
||||||
// 字符映射表
|
|
||||||
private static final String CHAR_MAP = "adefghlmyijnopkqrstubcvwsz";
|
private static final String CHAR_MAP = "adefghlmyijnopkqrstubcvwsz";
|
||||||
|
|
||||||
private final MultiMap header = MultiMap.caseInsensitiveMultiMap();
|
private final MultiMap header = MultiMap.caseInsensitiveMultiMap();
|
||||||
|
private final String cacheKey;
|
||||||
// Token管理
|
|
||||||
private static String ssoToken;
|
|
||||||
private static long tokenExpireTime = 0L; // 毫秒时间戳
|
|
||||||
|
|
||||||
public Ye2Tool(ShareLinkInfo shareLinkInfo) {
|
public Ye2Tool(ShareLinkInfo shareLinkInfo) {
|
||||||
super(shareLinkInfo);
|
super(shareLinkInfo);
|
||||||
|
this.cacheKey = TokenCache.key("ye2", resolveAccountId());
|
||||||
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("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("App-Version", "55");
|
||||||
header.set("Cache-Control", "no-cache");
|
header.set("Cache-Control", "no-cache");
|
||||||
@@ -65,16 +69,62 @@ public class Ye2Tool extends PanBase {
|
|||||||
header.set("Content-Type", "application/json");
|
header.set("Content-Type", "application/json");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private String resolveAccountId() {
|
||||||
* 判断 token 是否过期
|
String accountId = "_default";
|
||||||
*/
|
if (!shareLinkInfo.getOtherParam().containsKey("auths")) {
|
||||||
private boolean isTokenExpired() {
|
return accountId;
|
||||||
return System.currentTimeMillis() > tokenExpireTime - 60_000; // 提前1分钟刷新
|
}
|
||||||
|
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
|
||||||
|
if (auths.contains("_configId")) {
|
||||||
|
accountId = auths.get("_configId");
|
||||||
|
} else if (auths.contains("username")) {
|
||||||
|
accountId = auths.get("username");
|
||||||
|
} else {
|
||||||
|
String token = resolveProvidedToken(auths);
|
||||||
|
if (StringUtils.isNotEmpty(token)) {
|
||||||
|
accountId = token.substring(0, Math.min(16, token.length()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return accountId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算CRC32并转换为16进制字符串
|
* 从配置/临时认证参数中取出可直接使用的 Bearer token。
|
||||||
|
* 兼容两种 key:
|
||||||
|
* - token:URLParamUtil 处理 auth= 临时参数(authType=accesstoken/authorization)时写入的字段名
|
||||||
|
* - authorization:app-dev.yml 静态配置中更符合直觉的写法(auths.ye.authorization: xxx)
|
||||||
*/
|
*/
|
||||||
|
private String resolveProvidedToken(MultiMap auths) {
|
||||||
|
if (auths == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String token = auths.get("token");
|
||||||
|
if (StringUtils.isEmpty(token)) {
|
||||||
|
token = auths.get("authorization");
|
||||||
|
}
|
||||||
|
return stripBearerPrefix(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户/捐赠账号提供的 token 经常是从浏览器开发者工具里连同 "Bearer " 前缀一起复制过来的,
|
||||||
|
* 如果不做清理,后续 header.set("Authorization", "Bearer " + token) 会拼出
|
||||||
|
* "Bearer Bearer xxx",导致123网盘接口报错 tokenstring should not contain 'bearer '。
|
||||||
|
*/
|
||||||
|
private String stripBearerPrefix(String token) {
|
||||||
|
if (StringUtils.isBlank(token)) {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
String trimmed = token.trim();
|
||||||
|
if (trimmed.regionMatches(true, 0, "Bearer ", 0, 7)) {
|
||||||
|
trimmed = trimmed.substring(7).trim();
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTokenExpired() {
|
||||||
|
return TokenCache.isExpired(cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
private String crc32(String data) {
|
private String crc32(String data) {
|
||||||
CRC32 crc32 = new CRC32();
|
CRC32 crc32 = new CRC32();
|
||||||
crc32.update(data.getBytes());
|
crc32.update(data.getBytes());
|
||||||
@@ -82,105 +132,58 @@ public class Ye2Tool extends PanBase {
|
|||||||
return String.format("%08x", value);
|
return String.format("%08x", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 16进制转10进制
|
|
||||||
*/
|
|
||||||
private long hexToInt(String hexStr) {
|
private long hexToInt(String hexStr) {
|
||||||
return Long.parseLong(hexStr, 16);
|
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) {
|
private String encode123(String url, String way, String version, String timestamp) {
|
||||||
Random random = new Random();
|
Random random = new Random();
|
||||||
// 生成随机数 a = int(10000000 * random.randint(1, 10000000) / 10000)
|
|
||||||
int randomInt = random.nextInt(10000000) + 1;
|
int randomInt = random.nextInt(10000000) + 1;
|
||||||
long a = (10000000L * randomInt) / 10000;
|
long a = (10000000L * randomInt) / 10000;
|
||||||
|
|
||||||
// 将时间戳转换为时间格式
|
|
||||||
long timeLong = Long.parseLong(timestamp) / 1000;
|
long timeLong = Long.parseLong(timestamp) / 1000;
|
||||||
java.time.LocalDateTime dateTime = java.time.Instant.ofEpochSecond(timeLong)
|
java.time.LocalDateTime dateTime = java.time.Instant.ofEpochSecond(timeLong)
|
||||||
.atZone(java.time.ZoneId.systemDefault())
|
.atZone(java.time.ZoneId.systemDefault())
|
||||||
.toLocalDateTime();
|
.toLocalDateTime();
|
||||||
String timeStr = dateTime.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm"));
|
String timeStr = dateTime.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm"));
|
||||||
|
|
||||||
// 根据时间字符串生成g
|
|
||||||
StringBuilder g = new StringBuilder();
|
StringBuilder g = new StringBuilder();
|
||||||
for (char c : timeStr.toCharArray()) {
|
for (char c : timeStr.toCharArray()) {
|
||||||
int digit = Character.getNumericValue(c);
|
int digit = Character.getNumericValue(c);
|
||||||
if (digit == 0) {
|
if (digit == 0) {
|
||||||
g.append(CHAR_MAP.charAt(0));
|
g.append(CHAR_MAP.charAt(0));
|
||||||
} else {
|
} else {
|
||||||
// 数字1对应索引0,数字2对应索引1,以此类推
|
|
||||||
g.append(CHAR_MAP.charAt(digit - 1));
|
g.append(CHAR_MAP.charAt(digit - 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算y值(CRC32的十进制)
|
|
||||||
String y = String.valueOf(hexToInt(crc32(g.toString())));
|
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 finalCrcInput = String.format("%d|%d|%s|%s|%s|%s", timeLong, a, url, way, version, y);
|
||||||
String finalCrc = String.valueOf(hexToInt(crc32(finalCrcInput)));
|
String finalCrc = String.valueOf(hexToInt(crc32(finalCrcInput)));
|
||||||
|
|
||||||
// 返回加密后的URL参数
|
|
||||||
return String.format("?%s=%d-%d-%s", y, timeLong, a, finalCrc);
|
return String.format("?%s=%d-%d-%s", y, timeLong, a, finalCrc);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Future<String> parse() {
|
public Future<String> parse() {
|
||||||
Future<String> tokenFuture;
|
Future<String> tokenFuture = resolveTokenFuture();
|
||||||
|
|
||||||
// 检查是否直接提供了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 -> {
|
tokenFuture.onSuccess(token -> {
|
||||||
if (!token.equals("nologin")) {
|
if (!token.equals("nologin")) {
|
||||||
// 2. 设置 header
|
TokenCache.putToken(cacheKey, token);
|
||||||
ssoToken = token;
|
|
||||||
header.set("Authorization", "Bearer " + token);
|
header.set("Authorization", "Bearer " + token);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String dataKey = shareLinkInfo.getShareKey().replace(".html", "");
|
final String dataKey = YeShareHostUtil.normalizeShareKey(shareLinkInfo.getShareKey());
|
||||||
final String pwd = shareLinkInfo.getSharePassword();
|
final String pwd = shareLinkInfo.getSharePassword();
|
||||||
|
final String shareOrigin = resolveYeShareOrigin(dataKey);
|
||||||
|
final String shareReferer = buildShareReferer(shareOrigin, dataKey, pwd);
|
||||||
|
|
||||||
// 3. 获取分享信息
|
|
||||||
client.getAbs(UriTemplate.of(GET_SHARE_INFO_URL))
|
client.getAbs(UriTemplate.of(GET_SHARE_INFO_URL))
|
||||||
.setTemplateParam("shareKey", dataKey)
|
.setTemplateParam("shareKey", dataKey)
|
||||||
.setTemplateParam("pwd", StringUtils.isEmpty(pwd) ? "" : pwd)
|
.setTemplateParam("pwd", StringUtils.isEmpty(pwd) ? "" : pwd)
|
||||||
.setTemplateParam("ParentFileId", "0")
|
.setTemplateParam("ParentFileId", "0")
|
||||||
.putHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
.putHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||||
.putHeader("Referer", "https://www.123pan.com/")
|
.putHeader("Referer", shareReferer)
|
||||||
.putHeader("Origin", "https://www.123pan.com")
|
.putHeader("Origin", shareOrigin)
|
||||||
.send()
|
.send()
|
||||||
.onSuccess(res -> {
|
.onSuccess(res -> {
|
||||||
JsonObject shareInfoJson = asJson(res);
|
JsonObject shareInfoJson = asJson(res);
|
||||||
@@ -200,35 +203,47 @@ public class Ye2Tool extends PanBase {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取第一个文件信息
|
|
||||||
JsonObject fileInfo = data.getJsonArray("InfoList").getJsonObject(0);
|
JsonObject fileInfo = data.getJsonArray("InfoList").getJsonObject(0);
|
||||||
|
|
||||||
// 检查是否需要登录
|
|
||||||
if (token.equals("nologin")) {
|
if (token.equals("nologin")) {
|
||||||
fail("该分享需要登录才能下载,请提供账号密码或token");
|
fail("该分享需要登录才能下载,请配置认证信息");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断是否为文件夹: Type: 1为文件夹, 0为文件
|
|
||||||
if (fileInfo.getInteger("Type", 0) == 1) {
|
if (fileInfo.getInteger("Type", 0) == 1) {
|
||||||
// 4. 获取文件夹打包下载链接
|
|
||||||
getZipDownUrl(client, fileInfo);
|
getZipDownUrl(client, fileInfo);
|
||||||
} else {
|
} else {
|
||||||
// 4. 获取文件下载链接
|
|
||||||
getDownUrl(client, fileInfo);
|
getDownUrl(client, fileInfo);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.onFailure(this.handleFail(GET_SHARE_INFO_URL));
|
.onFailure(this.handleFail(GET_SHARE_INFO_URL));
|
||||||
}).onFailure(err -> {
|
}).onFailure(err -> fail("123盘解析异常: {}", err.getMessage()));
|
||||||
fail("登录获取token失败: {}", err.getMessage());
|
|
||||||
});
|
|
||||||
|
|
||||||
return promise.future();
|
return promise.future();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private Future<String> resolveTokenFuture() {
|
||||||
* 登录并获取token
|
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
|
||||||
*/
|
if (auths != null) {
|
||||||
|
// 当同时提供了用户名+密码时,优先走真实登录流程。
|
||||||
|
// 注:前端/auth参数中 authType=password|username_password 时,URLParamUtil 出于兼容旧解析器的目的
|
||||||
|
// 会把 authToken(用户名) 同时写入 token 字段,如果这里无条件信任 token,会把用户名当成
|
||||||
|
// Bearer token 使用,导致真实的账号密码登录被跳过。
|
||||||
|
boolean hasCredential = StringUtils.isNotEmpty(auths.get("username"))
|
||||||
|
&& StringUtils.isNotEmpty(auths.get("password"));
|
||||||
|
String providedToken = resolveProvidedToken(auths);
|
||||||
|
if (!hasCredential && StringUtils.isNotEmpty(providedToken)) {
|
||||||
|
TokenCache.putToken(cacheKey, providedToken);
|
||||||
|
return Future.succeededFuture(providedToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String cached = TokenCache.getToken(cacheKey);
|
||||||
|
if (cached == null || isTokenExpired()) {
|
||||||
|
return loginAndGetToken();
|
||||||
|
}
|
||||||
|
return Future.succeededFuture(cached);
|
||||||
|
}
|
||||||
|
|
||||||
private Future<String> loginAndGetToken() {
|
private Future<String> loginAndGetToken() {
|
||||||
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
|
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
|
||||||
if (auths == null) {
|
if (auths == null) {
|
||||||
@@ -275,16 +290,17 @@ public class Ye2Tool extends PanBase {
|
|||||||
promise.fail("未获取到token");
|
promise.fail("未获取到token");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ssoToken = data.getString("token");
|
String ssoToken = data.getString("token");
|
||||||
String expireStr = data.getString("expire");
|
String expireStr = data.getString("expire");
|
||||||
// 解析过期时间
|
long expireMs;
|
||||||
if (StringUtils.isNotEmpty(expireStr)) {
|
if (StringUtils.isNotEmpty(expireStr)) {
|
||||||
tokenExpireTime = OffsetDateTime.parse(expireStr)
|
expireMs = OffsetDateTime.parse(expireStr)
|
||||||
.toInstant().toEpochMilli();
|
.toInstant().toEpochMilli() - 60_000;
|
||||||
} else {
|
} else {
|
||||||
// 如果没有过期时间,默认1小时后过期
|
expireMs = System.currentTimeMillis() + 3600_000;
|
||||||
tokenExpireTime = System.currentTimeMillis() + 3600_000;
|
|
||||||
}
|
}
|
||||||
|
TokenCache.putToken(cacheKey, ssoToken);
|
||||||
|
TokenCache.putExpire(cacheKey, expireMs);
|
||||||
log.info("登录成功,token: {}", ssoToken);
|
log.info("登录成功,token: {}", ssoToken);
|
||||||
promise.complete(ssoToken);
|
promise.complete(ssoToken);
|
||||||
})
|
})
|
||||||
@@ -292,13 +308,34 @@ public class Ye2Tool extends PanBase {
|
|||||||
return promise.future();
|
return promise.future();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取下载链接(使用Android平台API)
|
|
||||||
*/
|
|
||||||
private void getDownUrl(WebClient client, JsonObject fileInfo) {
|
private void getDownUrl(WebClient client, JsonObject fileInfo) {
|
||||||
setFileInfo(fileInfo);
|
setFileInfo(fileInfo);
|
||||||
|
|
||||||
// 构建请求数据
|
String normalizedShareKey = YeShareHostUtil.normalizeShareKey(shareLinkInfo.getShareKey());
|
||||||
|
if (StringUtils.isNotEmpty(normalizedShareKey)) {
|
||||||
|
JsonObject v2Body = new JsonObject()
|
||||||
|
.put("ShareKey", normalizedShareKey)
|
||||||
|
.put("FileID", fileInfo.getInteger("FileId"))
|
||||||
|
.put("S3keyFlag", fileInfo.getString("S3KeyFlag"))
|
||||||
|
.put("Size", fileInfo.getLong("Size"))
|
||||||
|
.put("Etag", fileInfo.getString("Etag"));
|
||||||
|
requestShareV2Download(normalizedShareKey, v2Body).onSuccess(v2Url -> {
|
||||||
|
if (StringUtils.isNotEmpty(v2Url)) {
|
||||||
|
complete(v2Url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
requestLegacyDownUrl(client, fileInfo);
|
||||||
|
}).onFailure(err -> {
|
||||||
|
log.warn("Ye2 v2分享下载接口失败,回退旧接口: {}", err.getMessage());
|
||||||
|
requestLegacyDownUrl(client, fileInfo);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestLegacyDownUrl(client, fileInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestLegacyDownUrl(WebClient client, JsonObject fileInfo) {
|
||||||
JsonObject jsonObject = new JsonObject();
|
JsonObject jsonObject = new JsonObject();
|
||||||
jsonObject.put("driveId", 0);
|
jsonObject.put("driveId", 0);
|
||||||
jsonObject.put("etag", fileInfo.getString("Etag"));
|
jsonObject.put("etag", fileInfo.getString("Etag"));
|
||||||
@@ -308,7 +345,6 @@ public class Ye2Tool extends PanBase {
|
|||||||
jsonObject.put("size", fileInfo.getLong("Size"));
|
jsonObject.put("size", fileInfo.getLong("Size"));
|
||||||
jsonObject.put("type", 0);
|
jsonObject.put("type", 0);
|
||||||
|
|
||||||
// 使用encode123加密URL参数
|
|
||||||
String timestamp = String.valueOf(System.currentTimeMillis());
|
String timestamp = String.valueOf(System.currentTimeMillis());
|
||||||
String encryptedParams = encode123("/b/api/file/download_info", "android", "55", timestamp);
|
String encryptedParams = encode123("/b/api/file/download_info", "android", "55", timestamp);
|
||||||
String apiUrl = DOWNLOAD_API_URL + encryptedParams;
|
String apiUrl = DOWNLOAD_API_URL + encryptedParams;
|
||||||
@@ -318,21 +354,98 @@ public class Ye2Tool extends PanBase {
|
|||||||
HttpRequest<Buffer> bufferHttpRequest = client.postAbs(apiUrl);
|
HttpRequest<Buffer> bufferHttpRequest = client.postAbs(apiUrl);
|
||||||
bufferHttpRequest.putHeader("platform", "android");
|
bufferHttpRequest.putHeader("platform", "android");
|
||||||
bufferHttpRequest.putHeader("App-Version", "55");
|
bufferHttpRequest.putHeader("App-Version", "55");
|
||||||
bufferHttpRequest.putHeader("Authorization", "Bearer " + ssoToken);
|
bufferHttpRequest.putHeader("Authorization", "Bearer " + TokenCache.getToken(cacheKey));
|
||||||
bufferHttpRequest.putHeader("User-Agent", "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36");
|
bufferHttpRequest.putHeader("User-Agent", "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36");
|
||||||
bufferHttpRequest.putHeader("Content-Type", "application/json");
|
bufferHttpRequest.putHeader("Content-Type", "application/json");
|
||||||
|
|
||||||
bufferHttpRequest
|
bufferHttpRequest
|
||||||
.sendJsonObject(jsonObject)
|
.sendJsonObject(jsonObject)
|
||||||
.onSuccess(res2 -> {
|
.onSuccess(res2 -> handleDownloadUrlResponse(client, asJson(res2), "Ye2"))
|
||||||
JsonObject downURLJson = asJson(res2);
|
.onFailure(err -> fail("下载接口失败: " + err.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Future<String> requestShareV2Download(String shareKey, JsonObject body) {
|
||||||
|
Promise<String> promise = Promise.promise();
|
||||||
|
String timestamp = String.valueOf(System.currentTimeMillis());
|
||||||
|
String encryptedParams = encode123(DOWNLOAD_API_V2_PATH, "web", "3", timestamp);
|
||||||
|
String apiUrl = DOWNLOAD_API_V2_BASE + DOWNLOAD_API_V2_PATH + encryptedParams;
|
||||||
|
String shareOrigin = resolveYeShareOrigin(shareKey);
|
||||||
|
String shareReferer = buildShareReferer(shareOrigin, shareKey, shareLinkInfo.getSharePassword());
|
||||||
|
|
||||||
|
HttpRequest<Buffer> request = client.postAbs(apiUrl);
|
||||||
|
request.putHeader("Accept", "*/*");
|
||||||
|
request.putHeader("Authorization", "Bearer " + TokenCache.getToken(cacheKey));
|
||||||
|
request.putHeader("App-Version", "3");
|
||||||
|
request.putHeader("platform", "web");
|
||||||
|
request.putHeader("Origin", shareOrigin);
|
||||||
|
request.putHeader("Referer", shareReferer);
|
||||||
|
request.putHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36");
|
||||||
|
request.putHeader("Content-Type", "application/json;charset=UTF-8");
|
||||||
|
|
||||||
|
request.sendJsonObject(body).onSuccess(resp -> {
|
||||||
|
JsonObject json = asJson(resp);
|
||||||
|
if (json == null || json.getInteger("code", -1) != 0) {
|
||||||
|
promise.fail("v2接口返回异常: " + (json == null ? "null" : json.encode()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JsonObject data = json.getJsonObject("data", new JsonObject());
|
||||||
|
JsonArray dispatchList = data.getJsonArray("dispatchList", new JsonArray());
|
||||||
|
String downloadPath = data.getString("downloadPath");
|
||||||
|
if (StringUtils.isBlank(downloadPath)) {
|
||||||
|
promise.complete("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String prefix = "";
|
||||||
|
if (dispatchList.size() > 0) {
|
||||||
|
JsonObject firstDispatch = dispatchList.getJsonObject(0);
|
||||||
|
if (firstDispatch != null) {
|
||||||
|
prefix = firstDispatch.getString("prefix", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(prefix)) {
|
||||||
|
promise.complete(downloadPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String finalUrl = prefix.endsWith("/") ? prefix.substring(0, prefix.length() - 1) : prefix;
|
||||||
|
finalUrl += downloadPath.startsWith("/") ? downloadPath : "/" + downloadPath;
|
||||||
|
promise.complete(finalUrl);
|
||||||
|
}).onFailure(promise::fail);
|
||||||
|
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getZipDownUrl(WebClient client, JsonObject fileInfo) {
|
||||||
|
JsonObject jsonObject = new JsonObject();
|
||||||
|
jsonObject.put("shareKey", YeShareHostUtil.normalizeShareKey(shareLinkInfo.getShareKey()));
|
||||||
|
jsonObject.put("fileIdList", new JsonArray().add(JsonObject.of("fileId", fileInfo.getInteger("FileId"))));
|
||||||
|
|
||||||
|
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 " + TokenCache.getToken(cacheKey));
|
||||||
|
bufferHttpRequest.putHeader("User-Agent", "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36");
|
||||||
|
bufferHttpRequest.putHeader("Content-Type", "application/json");
|
||||||
|
|
||||||
|
bufferHttpRequest
|
||||||
|
.sendJsonObject(jsonObject)
|
||||||
|
.onSuccess(res2 -> handleDownloadUrlResponse(client, asJson(res2), "Ye2: 文件夹打包下载"))
|
||||||
|
.onFailure(err -> fail("文件夹打包下载接口失败: " + err.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleDownloadUrlResponse(WebClient client, JsonObject downURLJson, String failPrefix) {
|
||||||
try {
|
try {
|
||||||
if (downURLJson.getInteger("code") != 0) {
|
if (downURLJson.getInteger("code") != 0) {
|
||||||
fail("Ye2: downURLJson返回值异常->" + downURLJson);
|
fail(failPrefix + "返回值异常->" + downURLJson);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
fail("Ye2: downURLJson格式异常->" + downURLJson);
|
fail(failPrefix + "格式异常->" + downURLJson);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,7 +455,7 @@ public class Ye2Tool extends PanBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (StringUtils.isEmpty(downURL)) {
|
if (StringUtils.isEmpty(downURL)) {
|
||||||
fail("Ye2: 未获取到下载链接");
|
fail(failPrefix + "未获取到下载链接");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,7 +463,6 @@ public class Ye2Tool extends PanBase {
|
|||||||
Map<String, String> urlParams = CommonUtils.getURLParams(downURL);
|
Map<String, String> urlParams = CommonUtils.getURLParams(downURL);
|
||||||
String params = urlParams.get("params");
|
String params = urlParams.get("params");
|
||||||
if (StringUtils.isEmpty(params)) {
|
if (StringUtils.isEmpty(params)) {
|
||||||
// 如果没有params参数,直接使用downURL
|
|
||||||
complete(downURL);
|
complete(downURL);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -371,11 +483,11 @@ public class Ye2Tool extends PanBase {
|
|||||||
JsonObject res3Json = asJson(res3);
|
JsonObject res3Json = asJson(res3);
|
||||||
try {
|
try {
|
||||||
if (res3Json.getInteger("code") != 0) {
|
if (res3Json.getInteger("code") != 0) {
|
||||||
fail("Ye2: downUrl2返回值异常->" + res3Json);
|
fail(failPrefix + "重定向返回值异常->" + res3Json);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
fail("Ye2: downUrl2格式异常->" + downURLJson);
|
fail(failPrefix + "重定向格式异常->" + downURLJson);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String redirectUrl = res3Json.getJsonObject("data").getString("redirect_url");
|
String redirectUrl = res3Json.getJsonObject("data").getString("redirect_url");
|
||||||
@@ -386,112 +498,12 @@ public class Ye2Tool extends PanBase {
|
|||||||
}
|
}
|
||||||
}).onFailure(err -> fail("获取直链失败: " + err.getMessage()));
|
}).onFailure(err -> fail("获取直链失败: " + err.getMessage()));
|
||||||
} catch (MalformedURLException e) {
|
} catch (MalformedURLException e) {
|
||||||
// 如果解析失败,直接使用downURL
|
|
||||||
complete(downURL);
|
complete(downURL);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
fail("urlParams解析异常: " + e.getMessage());
|
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) {
|
void setFileInfo(JsonObject reqBodyJson) {
|
||||||
FileInfo fileInfo = new FileInfo();
|
FileInfo fileInfo = new FileInfo();
|
||||||
fileInfo.setFileId(reqBodyJson.getInteger("FileId").toString());
|
fileInfo.setFileId(reqBodyJson.getInteger("FileId").toString());
|
||||||
@@ -514,62 +526,39 @@ public class Ye2Tool extends PanBase {
|
|||||||
shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
|
shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析文件夹中的文件列表
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public Future<List<FileInfo>> parseFileList() {
|
public Future<List<FileInfo>> parseFileList() {
|
||||||
Promise<List<FileInfo>> promise = Promise.promise();
|
Promise<List<FileInfo>> promise = Promise.promise();
|
||||||
|
|
||||||
String shareKey = shareLinkInfo.getShareKey().replace(".html", "");
|
String shareKey = YeShareHostUtil.normalizeShareKey(shareLinkInfo.getShareKey());
|
||||||
String pwd = shareLinkInfo.getSharePassword();
|
String pwd = shareLinkInfo.getSharePassword();
|
||||||
String parentFileId = "0"; // 根目录的文件ID
|
String parentFileId = "0";
|
||||||
|
|
||||||
// 如果参数里的目录ID不为空,则直接解析目录
|
|
||||||
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
|
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
|
||||||
if (StringUtils.isNotBlank(dirId)) {
|
if (StringUtils.isNotBlank(dirId)) {
|
||||||
parentFileId = dirId;
|
parentFileId = dirId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保已登录
|
Future<String> tokenFuture = resolveTokenFuture();
|
||||||
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;
|
String finalParentFileId = parentFileId;
|
||||||
tokenFuture.onSuccess(token -> {
|
tokenFuture.onSuccess(token -> {
|
||||||
if (token.equals("nologin")) {
|
if (token.equals("nologin")) {
|
||||||
promise.fail("该分享需要登录才能访问,请提供账号密码或token");
|
promise.fail("该分享需要登录才能访问,请配置认证信息");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构造文件列表接口的URL
|
String normalizedShareKey = YeShareHostUtil.normalizeShareKey(shareKey);
|
||||||
|
String shareOrigin = resolveYeShareOrigin(normalizedShareKey);
|
||||||
|
String shareReferer = buildShareReferer(shareOrigin, normalizedShareKey, pwd);
|
||||||
|
|
||||||
client.getAbs(UriTemplate.of(GET_SHARE_INFO_URL))
|
client.getAbs(UriTemplate.of(GET_SHARE_INFO_URL))
|
||||||
.setTemplateParam("shareKey", shareKey)
|
.setTemplateParam("shareKey", shareKey)
|
||||||
.setTemplateParam("pwd", StringUtils.isEmpty(pwd) ? "" : pwd)
|
.setTemplateParam("pwd", StringUtils.isEmpty(pwd) ? "" : pwd)
|
||||||
.setTemplateParam("ParentFileId", finalParentFileId)
|
.setTemplateParam("ParentFileId", finalParentFileId)
|
||||||
.putHeader("Authorization", "Bearer " + token)
|
.putHeader("Authorization", "Bearer " + token)
|
||||||
.putHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
.putHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||||
.putHeader("Referer", "https://www.123pan.com/")
|
.putHeader("Referer", shareReferer)
|
||||||
.putHeader("Origin", "https://www.123pan.com")
|
.putHeader("Origin", shareOrigin)
|
||||||
.send().onSuccess(res -> {
|
.send().onSuccess(res -> {
|
||||||
JsonObject response = asJson(res);
|
JsonObject response = asJson(res);
|
||||||
if (response.getInteger("code") != 0) {
|
if (response.getInteger("code") != 0) {
|
||||||
@@ -585,13 +574,12 @@ public class Ye2Tool extends PanBase {
|
|||||||
JsonArray infoList = response.getJsonObject("data").getJsonArray("InfoList");
|
JsonArray infoList = response.getJsonObject("data").getJsonArray("InfoList");
|
||||||
List<FileInfo> result = new ArrayList<>();
|
List<FileInfo> result = new ArrayList<>();
|
||||||
|
|
||||||
// 遍历返回的文件和目录信息
|
|
||||||
for (int i = 0; i < infoList.size(); i++) {
|
for (int i = 0; i < infoList.size(); i++) {
|
||||||
JsonObject item = infoList.getJsonObject(i);
|
JsonObject item = infoList.getJsonObject(i);
|
||||||
FileInfo fileInfo = new FileInfo();
|
FileInfo fileInfo = new FileInfo();
|
||||||
|
|
||||||
// 构建下载参数
|
|
||||||
JsonObject postData = JsonObject.of()
|
JsonObject postData = JsonObject.of()
|
||||||
|
.put("shareKey", shareLinkInfo.getShareKey())
|
||||||
.put("driveId", 0)
|
.put("driveId", 0)
|
||||||
.put("etag", item.getString("Etag"))
|
.put("etag", item.getString("Etag"))
|
||||||
.put("fileId", item.getInteger("FileId"))
|
.put("fileId", item.getInteger("FileId"))
|
||||||
@@ -602,7 +590,7 @@ public class Ye2Tool extends PanBase {
|
|||||||
|
|
||||||
String param = CommonUtils.urlBase64Encode(postData.encode());
|
String param = CommonUtils.urlBase64Encode(postData.encode());
|
||||||
|
|
||||||
if (item.getInteger("Type") == 0) { // 文件
|
if (item.getInteger("Type") == 0) {
|
||||||
fileInfo.setFileName(item.getString("FileName"))
|
fileInfo.setFileName(item.getString("FileName"))
|
||||||
.setFileId(item.getInteger("FileId").toString())
|
.setFileId(item.getInteger("FileId").toString())
|
||||||
.setFileType("file")
|
.setFileType("file")
|
||||||
@@ -610,40 +598,20 @@ public class Ye2Tool extends PanBase {
|
|||||||
.setHash(item.getString("Etag"))
|
.setHash(item.getString("Etag"))
|
||||||
.setSizeStr(FileSizeConverter.convertToReadableSize(item.getLong("Size")));
|
.setSizeStr(FileSizeConverter.convertToReadableSize(item.getLong("Size")));
|
||||||
|
|
||||||
String createAt = item.getString("CreateAt");
|
setFileTimes(item, fileInfo);
|
||||||
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(),
|
fileInfo.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(),
|
||||||
shareLinkInfo.getType(), param))
|
shareLinkInfo.getType(), param))
|
||||||
.setPreviewUrl(String.format("%s/v2/viewUrl/%s/%s", getDomainName(),
|
.setPreviewUrl(String.format("%s/v2/viewUrl/%s/%s", getDomainName(),
|
||||||
shareLinkInfo.getType(), param));
|
shareLinkInfo.getType(), param));
|
||||||
result.add(fileInfo);
|
result.add(fileInfo);
|
||||||
} else if (item.getInteger("Type") == 1) { // 目录
|
} else if (item.getInteger("Type") == 1) {
|
||||||
fileInfo.setFileName(item.getString("FileName"))
|
fileInfo.setFileName(item.getString("FileName"))
|
||||||
.setFileId(item.getInteger("FileId").toString())
|
.setFileId(item.getInteger("FileId").toString())
|
||||||
.setFileType("folder")
|
.setFileType("folder")
|
||||||
.setSize(0L);
|
.setSize(0L);
|
||||||
|
|
||||||
String createAt = item.getString("CreateAt");
|
setFileTimes(item, fileInfo);
|
||||||
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(
|
fileInfo.setParserUrl(
|
||||||
String.format("%s/v2/getFileList?url=%s&dirId=%s&pwd=%s",
|
String.format("%s/v2/getFileList?url=%s&dirId=%s&pwd=%s",
|
||||||
@@ -657,48 +625,70 @@ public class Ye2Tool extends PanBase {
|
|||||||
}
|
}
|
||||||
promise.complete(result);
|
promise.complete(result);
|
||||||
}).onFailure(promise::fail);
|
}).onFailure(promise::fail);
|
||||||
}).onFailure(err -> promise.fail("登录获取token失败: " + err.getMessage()));
|
}).onFailure(err -> promise.fail("123盘解析异常: " + err.getMessage()));
|
||||||
|
|
||||||
return promise.future();
|
return promise.future();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 通过ID解析特定文件
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public Future<String> parseById() {
|
public Future<String> parseById() {
|
||||||
JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson");
|
JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson");
|
||||||
|
Future<String> tokenFuture = resolveTokenFuture();
|
||||||
// 确保已登录
|
|
||||||
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 -> {
|
tokenFuture.onSuccess(token -> {
|
||||||
if (token.equals("nologin")) {
|
if (token.equals("nologin")) {
|
||||||
fail("该分享需要登录才能下载,请提供账号密码或token");
|
fail("该分享需要登录才能下载,请配置认证信息");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用encode123加密URL参数
|
String rawShareKey = paramJson.getString("shareKey");
|
||||||
|
if (StringUtils.isBlank(rawShareKey)) {
|
||||||
|
rawShareKey = paramJson.getString("ShareKey");
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(rawShareKey)) {
|
||||||
|
rawShareKey = shareLinkInfo.getShareKey();
|
||||||
|
}
|
||||||
|
String normalizedShareKey = YeShareHostUtil.normalizeShareKey(rawShareKey);
|
||||||
|
if (StringUtils.isNotEmpty(normalizedShareKey)) {
|
||||||
|
JsonObject v2Body = new JsonObject()
|
||||||
|
.put("ShareKey", normalizedShareKey)
|
||||||
|
.put("FileID", paramJson.getInteger("fileId", paramJson.getInteger("FileID", 0)))
|
||||||
|
.put("S3keyFlag", paramJson.getString("s3keyFlag", paramJson.getString("S3keyFlag", "")))
|
||||||
|
.put("Size", paramJson.getLong("size", paramJson.getLong("Size", 0L)))
|
||||||
|
.put("Etag", paramJson.getString("etag", paramJson.getString("Etag", "")));
|
||||||
|
requestShareV2Download(normalizedShareKey, v2Body).onSuccess(v2Url -> {
|
||||||
|
if (StringUtils.isNotEmpty(v2Url)) {
|
||||||
|
complete(v2Url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
parseByIdLegacy(token, paramJson);
|
||||||
|
}).onFailure(err -> {
|
||||||
|
log.warn("Ye2 parseById v2接口失败,回退旧接口: {}", err.getMessage());
|
||||||
|
parseByIdLegacy(token, paramJson);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseByIdLegacy(token, paramJson);
|
||||||
|
}).onFailure(err -> fail("123盘解析异常: " + err.getMessage()));
|
||||||
|
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseByIdLegacy(String token, JsonObject paramJson) {
|
||||||
|
if (paramJson.containsKey("FileID") && !paramJson.containsKey("fileId")) {
|
||||||
|
paramJson.put("fileId", paramJson.getValue("FileID"));
|
||||||
|
}
|
||||||
|
if (paramJson.containsKey("S3keyFlag") && !paramJson.containsKey("s3keyFlag")) {
|
||||||
|
paramJson.put("s3keyFlag", paramJson.getValue("S3keyFlag"));
|
||||||
|
}
|
||||||
|
if (paramJson.containsKey("Size") && !paramJson.containsKey("size")) {
|
||||||
|
paramJson.put("size", paramJson.getValue("Size"));
|
||||||
|
}
|
||||||
|
if (paramJson.containsKey("Etag") && !paramJson.containsKey("etag")) {
|
||||||
|
paramJson.put("etag", paramJson.getValue("Etag"));
|
||||||
|
}
|
||||||
|
|
||||||
String timestamp = String.valueOf(System.currentTimeMillis());
|
String timestamp = String.valueOf(System.currentTimeMillis());
|
||||||
String encryptedParams = encode123("/b/api/file/download_info", "android", "55", timestamp);
|
String encryptedParams = encode123("/b/api/file/download_info", "android", "55", timestamp);
|
||||||
String apiUrl = DOWNLOAD_API_URL + encryptedParams;
|
String apiUrl = DOWNLOAD_API_URL + encryptedParams;
|
||||||
@@ -714,77 +704,57 @@ public class Ye2Tool extends PanBase {
|
|||||||
|
|
||||||
bufferHttpRequest
|
bufferHttpRequest
|
||||||
.sendJsonObject(paramJson)
|
.sendJsonObject(paramJson)
|
||||||
.onSuccess(res2 -> {
|
.onSuccess(res2 -> handleDownloadUrlResponse(client, asJson(res2), "Ye2"))
|
||||||
JsonObject downURLJson = asJson(res2);
|
.onFailure(err -> fail("下载接口失败: " + err.getMessage()));
|
||||||
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");
|
private void setFileTimes(JsonObject item, FileInfo fileInfo) {
|
||||||
if (StringUtils.isEmpty(downURL)) {
|
String createAt = item.getString("CreateAt");
|
||||||
downURL = downURLJson.getJsonObject("data").getString("DownloadURL");
|
if (StringUtils.isNotEmpty(createAt)) {
|
||||||
|
fileInfo.setCreateTime(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
|
||||||
|
.format(OffsetDateTime.parse(createAt).toLocalDateTime()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StringUtils.isEmpty(downURL)) {
|
String updateAt = item.getString("UpdateAt");
|
||||||
fail("Ye2: 未获取到下载链接");
|
if (StringUtils.isNotEmpty(updateAt)) {
|
||||||
return;
|
fileInfo.setUpdateTime(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
|
||||||
|
.format(OffsetDateTime.parse(updateAt).toLocalDateTime()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
private String resolveYeShareOrigin(String shareKey) {
|
||||||
Map<String, String> urlParams = CommonUtils.getURLParams(downURL);
|
String origin = extractYeShareOrigin(shareLinkInfo.getShareUrl());
|
||||||
String params = urlParams.get("params");
|
if (StringUtils.isNotBlank(origin)) {
|
||||||
if (StringUtils.isEmpty(params)) {
|
return origin;
|
||||||
// 如果没有params参数,直接使用downURL
|
}
|
||||||
complete(downURL);
|
origin = extractYeShareOrigin(shareLinkInfo.getStandardUrl());
|
||||||
return;
|
if (StringUtils.isNotBlank(origin)) {
|
||||||
|
return origin;
|
||||||
|
}
|
||||||
|
String uid = YeShareHostUtil.getNumericSubdomainIdByShareKey(shareKey);
|
||||||
|
if (StringUtils.isNotBlank(uid)) {
|
||||||
|
return "https://" + uid + ".share.123pan.cn";
|
||||||
|
}
|
||||||
|
return "https://www.123pan.com";
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] decodeByte = Base64.getDecoder().decode(params);
|
private String extractYeShareOrigin(String url) {
|
||||||
String downUrl2 = new String(decodeByte);
|
if (StringUtils.isBlank(url) || !url.matches("^https?://[a-zA-Z\\d-]+\\.(?:mshare|share)\\.123pan\\.cn(?:[:/].*)?$")) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
int idx = url.indexOf('/', url.indexOf("//") + 2);
|
||||||
|
return idx > 0 ? url.substring(0, idx) : url;
|
||||||
|
}
|
||||||
|
|
||||||
clientNoRedirects.getAbs(downUrl2).putHeaders(header).send().onSuccess(res3 -> {
|
private String buildShareReferer(String shareOrigin, String shareKey, String pwd) {
|
||||||
if (res3.statusCode() == 302 || res3.statusCode() == 301) {
|
String key = YeShareHostUtil.normalizeShareKey(shareKey);
|
||||||
String redirectUrl = res3.getHeader("Location");
|
if (StringUtils.isBlank(key)) {
|
||||||
if (StringUtils.isBlank(redirectUrl)) {
|
return shareOrigin + "/";
|
||||||
fail("重定向链接为空");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
complete(redirectUrl);
|
String referer = shareOrigin + "/123pan/" + key;
|
||||||
return;
|
if (StringUtils.isNotBlank(pwd)) {
|
||||||
|
referer += "?pwd=" + pwd;
|
||||||
}
|
}
|
||||||
JsonObject res3Json = asJson(res3);
|
return referer;
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package cn.qaiu.util;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 123分享链接子域工具:将分享 key 的前半段解码为数字 uid。
|
||||||
|
*/
|
||||||
|
public final class YeShareHostUtil {
|
||||||
|
|
||||||
|
private static final String CODE62 = "Tvd3hHA9QEkom14xpfaBJIMwgFYGPXn2sWCNORDr80KuUSl7bZcetizL5q6yVj";
|
||||||
|
private static final long MAX_SAFE_INTEGER = 9007199254740991L;
|
||||||
|
private static final Map<Character, Integer> DECODE_MAP = new HashMap<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
for (int i = 0; i < CODE62.length(); i++) {
|
||||||
|
DECODE_MAP.put(CODE62.charAt(i), i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private YeShareHostUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getNumericSubdomainIdByShareKey(String shareKey) {
|
||||||
|
String normalized = normalizeShareKey(shareKey);
|
||||||
|
if (StringUtils.isBlank(normalized)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
int split = normalized.indexOf('-');
|
||||||
|
if (split <= 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String encodedUid = normalized.substring(0, split);
|
||||||
|
Long uid = decodeBase62LittleEndian(encodedUid);
|
||||||
|
return uid == null ? "" : String.valueOf(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String normalizeShareKey(String shareKey) {
|
||||||
|
if (StringUtils.isBlank(shareKey)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String key = shareKey.trim();
|
||||||
|
int queryIndex = key.indexOf('?');
|
||||||
|
if (queryIndex >= 0) {
|
||||||
|
key = key.substring(0, queryIndex);
|
||||||
|
}
|
||||||
|
int slashIndex = key.lastIndexOf('/');
|
||||||
|
if (slashIndex >= 0 && slashIndex < key.length() - 1) {
|
||||||
|
key = key.substring(slashIndex + 1);
|
||||||
|
}
|
||||||
|
if (key.endsWith(".html")) {
|
||||||
|
key = key.substring(0, key.length() - 5);
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Long decodeBase62LittleEndian(String value) {
|
||||||
|
if (StringUtils.isBlank(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
long result = 0L;
|
||||||
|
for (int i = 0; i < value.length(); i++) {
|
||||||
|
Integer digit = DECODE_MAP.get(value.charAt(i));
|
||||||
|
if (digit == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
double weighted = digit * Math.pow(62, i);
|
||||||
|
if (!Double.isFinite(weighted)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
long next = result + (long) weighted;
|
||||||
|
if (next <= 0 || next > MAX_SAFE_INTEGER) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
result = next;
|
||||||
|
}
|
||||||
|
if (result <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package cn.qaiu.parser;
|
package cn.qaiu.parser;
|
||||||
|
|
||||||
import cn.qaiu.entity.ShareLinkInfo;
|
import cn.qaiu.entity.ShareLinkInfo;
|
||||||
|
import cn.qaiu.util.YeShareHostUtil;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@@ -157,6 +158,26 @@ public class PanDomainTemplateTest {
|
|||||||
assertEquals("somekey", m5.group("KEY"));
|
assertEquals("somekey", m5.group("KEY"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testYeRedirectPatternAndUidDecode() {
|
||||||
|
Pattern yePattern = PanDomainTemplate.YE.getPattern();
|
||||||
|
|
||||||
|
Matcher oldUrl = yePattern.matcher("https://www.123pan.com/s/lN7UVv-pbYJ");
|
||||||
|
assertTrue("YE should match old 123pan share URL", oldUrl.find());
|
||||||
|
assertEquals("lN7UVv-pbYJ", oldUrl.group("KEY"));
|
||||||
|
|
||||||
|
Matcher redirectedUrl = yePattern.matcher("https://1813382308.mshare.123pan.cn/123pan/lN7UVv-pbYJ");
|
||||||
|
assertTrue("YE should match redirected mshare URL", redirectedUrl.find());
|
||||||
|
assertEquals("lN7UVv-pbYJ", redirectedUrl.group("KEY"));
|
||||||
|
|
||||||
|
Matcher htmlUrl = yePattern.matcher("https://www.123278.com/s/lN7UVv-pbYJ.html?pwd=abcd");
|
||||||
|
assertTrue("YE should match html URL with query", htmlUrl.find());
|
||||||
|
assertEquals("lN7UVv-pbYJ", htmlUrl.group("KEY"));
|
||||||
|
|
||||||
|
assertEquals("1813382308", YeShareHostUtil.getNumericSubdomainIdByShareKey("lN7UVv-pbYJ"));
|
||||||
|
assertEquals("lN7UVv-pbYJ", YeShareHostUtil.normalizeShareKey("https://1813382308.mshare.123pan.cn/123pan/lN7UVv-pbYJ?pwd=abcd"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testLePatternFix() {
|
public void testLePatternFix() {
|
||||||
Pattern lePattern = PanDomainTemplate.LE.getPattern();
|
Pattern lePattern = PanDomainTemplate.LE.getPattern();
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<revision>0.3.4</revision>
|
<revision>0.4.0</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>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nfd-web",
|
"name": "nfd-web",
|
||||||
"version": "0.2.1",
|
"version": "0.4.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
|
|||||||
@@ -326,6 +326,10 @@
|
|||||||
<span>蓝奏优享 (IZ)</span>
|
<span>蓝奏优享 (IZ)</span>
|
||||||
<el-tag size="small" type="warning" style="margin-left: 8px">大文件</el-tag>
|
<el-tag size="small" type="warning" style="margin-left: 8px">大文件</el-tag>
|
||||||
</el-option>
|
</el-option>
|
||||||
|
<el-option label="123云盘 (YE)" value="YE">
|
||||||
|
<span>123云盘 (YE)</span>
|
||||||
|
<el-tag size="small" type="warning" style="margin-left: 8px">部分分享需登录</el-tag>
|
||||||
|
</el-option>
|
||||||
</el-option-group>
|
</el-option-group>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -767,7 +771,8 @@ export default {
|
|||||||
if (url.includes('drive.uc.cn') || url.includes('fast.uc.cn')) return 'UC'
|
if (url.includes('drive.uc.cn') || url.includes('fast.uc.cn')) return 'UC'
|
||||||
if (url.includes('feijipan.com') || url.includes('feijihe.com') || url.includes('xiaofeiyang.com')) return 'FJ'
|
if (url.includes('feijipan.com') || url.includes('feijihe.com') || url.includes('xiaofeiyang.com')) return 'FJ'
|
||||||
if (url.includes('ilanzou.com') || url.includes('lanzouv.com')) return 'IZ'
|
if (url.includes('ilanzou.com') || url.includes('lanzouv.com')) return 'IZ'
|
||||||
if (url.includes('123pan.com') || url.includes('123684.com') || url.includes('123865.com')) return 'YE'
|
// 123网盘域名较多(如 123pan.com/123pan.cn/share.123pan.cn/123684.com/123865.com 等数字域名),使用规则匹配代替枚举
|
||||||
|
if (/123\d{3}\.com|123panpay\.com|123pan\.(?:com|cn)/.test(url)) return 'YE'
|
||||||
return ''
|
return ''
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -789,7 +794,8 @@ export default {
|
|||||||
'QK': '夸克网盘必须配置 Cookie 才能解析和下载(登录后从浏览器开发者工具获取)',
|
'QK': '夸克网盘必须配置 Cookie 才能解析和下载(登录后从浏览器开发者工具获取)',
|
||||||
'UC': 'UC网盘必须配置 Cookie 才能解析和下载(登录后从浏览器开发者工具获取)',
|
'UC': 'UC网盘必须配置 Cookie 才能解析和下载(登录后从浏览器开发者工具获取)',
|
||||||
'FJ': '小飞机网盘大文件(>100MB)需要配置认证信息',
|
'FJ': '小飞机网盘大文件(>100MB)需要配置认证信息',
|
||||||
'IZ': '蓝奏优享大文件需要配置认证信息'
|
'IZ': '蓝奏优享大文件需要配置认证信息',
|
||||||
|
'YE': '123云盘部分分享(需要登录才能查看/下载)需要配置账号密码或 Authorization Token'
|
||||||
}
|
}
|
||||||
return hints[this.authConfig.panType] || '请选择网盘类型后配置认证信息'
|
return hints[this.authConfig.panType] || '请选择网盘类型后配置认证信息'
|
||||||
},
|
},
|
||||||
@@ -1230,10 +1236,11 @@ export default {
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
showClose: true
|
showClose: true
|
||||||
})
|
})
|
||||||
} else if (panType === 'fj' || panType === 'lz' || panType === 'iz' || panType === 'le') {
|
} else if (panType === 'fj' || panType === 'lz' || panType === 'iz' || panType === 'le' || panType === 'ye') {
|
||||||
// 小飞机、蓝奏、优享、联想乐云:提示大文件需要认证
|
// 小飞机、蓝奏、优享、联想乐云、123云盘:提示大文件/需登录分享需要认证
|
||||||
const hasAuth = this.allAuthConfigs[panType]?.cookie ||
|
const hasAuth = this.allAuthConfigs[panType]?.cookie ||
|
||||||
this.allAuthConfigs[panType]?.username ||
|
this.allAuthConfigs[panType]?.username ||
|
||||||
|
this.allAuthConfigs[panType]?.token ||
|
||||||
(this.donateAccountCounts.active[panType.toUpperCase()] || 0) > 0
|
(this.donateAccountCounts.active[panType.toUpperCase()] || 0) > 0
|
||||||
if (!hasAuth) {
|
if (!hasAuth) {
|
||||||
this.$message.info({
|
this.$message.info({
|
||||||
@@ -1612,12 +1619,14 @@ export default {
|
|||||||
|
|
||||||
this.donateSubmitting = true
|
this.donateSubmitting = true
|
||||||
try {
|
try {
|
||||||
|
// 只提交当前认证方式实际用到的字段,避免切换认证类型后遗留的用户名/密码脏数据被一起提交
|
||||||
|
const isPasswordAuth = this.donateConfig.authType === 'password'
|
||||||
const payload = {
|
const payload = {
|
||||||
panType: this.donateConfig.panType,
|
panType: this.donateConfig.panType,
|
||||||
authType: this.donateConfig.authType,
|
authType: this.donateConfig.authType,
|
||||||
username: this.donateConfig.username || '',
|
username: isPasswordAuth ? (this.donateConfig.username || '') : '',
|
||||||
password: this.donateConfig.password || '',
|
password: isPasswordAuth ? (this.donateConfig.password || '') : '',
|
||||||
token: this.donateConfig.token || '',
|
token: isPasswordAuth ? '' : (this.donateConfig.token || ''),
|
||||||
remark: this.donateConfig.remark || ''
|
remark: this.donateConfig.remark || ''
|
||||||
}
|
}
|
||||||
await axios.post(`${this.baseAPI}/v2/donateAccount`, payload)
|
await axios.post(`${this.baseAPI}/v2/donateAccount`, payload)
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
package cn.qaiu.lz.common.util;
|
||||||
|
|
||||||
|
import cn.qaiu.entity.ShareLinkInfo;
|
||||||
|
import cn.qaiu.lz.web.model.AuthParam;
|
||||||
|
import cn.qaiu.lz.web.service.DbService;
|
||||||
|
import cn.qaiu.parser.ParserCreate;
|
||||||
|
import cn.qaiu.vx.core.util.ConfigConstant;
|
||||||
|
import cn.qaiu.vx.core.util.VertxHolder;
|
||||||
|
import io.vertx.core.Future;
|
||||||
|
import io.vertx.core.MultiMap;
|
||||||
|
import io.vertx.core.http.HttpServerRequest;
|
||||||
|
import io.vertx.core.json.JsonObject;
|
||||||
|
import io.vertx.core.shareddata.LocalMap;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class ParserAuthUtil {
|
||||||
|
|
||||||
|
public static final String SKIP_CLIENT_LINKS = "_skipClientLinks";
|
||||||
|
public static final String TEMP_AUTH_ADDED = "__TEMP_AUTH_ADDED";
|
||||||
|
public static final String DONATED_ACCOUNT_TOKEN = "__AUTO_DONATED_ACCOUNT_TOKEN";
|
||||||
|
|
||||||
|
private ParserAuthUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JsonObject buildOtherParam(HttpServerRequest request, String auth, String requestOrigin) {
|
||||||
|
return buildOtherParam(request, auth, requestOrigin, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JsonObject buildOtherParam(HttpServerRequest request, String auth, String requestOrigin,
|
||||||
|
boolean skipClientLinks) {
|
||||||
|
JsonObject otherParam = JsonObject.of(
|
||||||
|
"UA", request.headers().get("user-agent"),
|
||||||
|
"_requestOrigin", requestOrigin
|
||||||
|
);
|
||||||
|
if (skipClientLinks) {
|
||||||
|
otherParam.put(SKIP_CLIENT_LINKS, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(auth)) {
|
||||||
|
AuthParam authParam = AuthParamCodec.decode(auth);
|
||||||
|
if (authParam != null && authParam.hasValidAuth()) {
|
||||||
|
otherParam.put("authType", authParam.getAuthType());
|
||||||
|
otherParam.put("authToken", authParam.getPrimaryCredential());
|
||||||
|
otherParam.put("authPassword", authParam.getPassword());
|
||||||
|
otherParam.put("authInfo1", authParam.getExt1());
|
||||||
|
otherParam.put("authInfo2", authParam.getExt2());
|
||||||
|
otherParam.put("authInfo3", authParam.getExt3());
|
||||||
|
otherParam.put("authInfo4", authParam.getExt4());
|
||||||
|
otherParam.put("authInfo5", authParam.getExt5());
|
||||||
|
if (StringUtils.isNotBlank(authParam.getDonatedAccountToken())) {
|
||||||
|
otherParam.put("donatedAccountToken", authParam.getDonatedAccountToken());
|
||||||
|
}
|
||||||
|
log.debug("已解码认证参数: authType={}", authParam.getAuthType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return otherParam;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Future<Void> applyAuthParamsAndDonatedFallback(ParserCreate parserCreate, JsonObject otherParam,
|
||||||
|
DbService dbService) {
|
||||||
|
JsonObject params = otherParam == null ? new JsonObject() : otherParam;
|
||||||
|
parserCreate.getShareLinkInfo().getOtherParam().putAll(params.getMap());
|
||||||
|
|
||||||
|
if (params.containsKey("authType") || params.containsKey("authToken")) {
|
||||||
|
log.debug("从otherParam中检测到临时认证参数");
|
||||||
|
URLParamUtil.addTempAuthParam(parserCreate,
|
||||||
|
params.getString("authType"),
|
||||||
|
params.getString("authToken"),
|
||||||
|
params.getString("authPassword"),
|
||||||
|
params.getString("authInfo1"),
|
||||||
|
params.getString("authInfo2"),
|
||||||
|
params.getString("authInfo3"),
|
||||||
|
params.getString("authInfo4"),
|
||||||
|
params.getString("authInfo5"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return applyDonatedAccountFallback(parserCreate, dbService);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void recordDonatedAccountFailureIfNeeded(DbService dbService, JsonObject otherParam,
|
||||||
|
Throwable cause) {
|
||||||
|
if (!isLikelyAuthFailure(cause) || otherParam == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String donatedAccountToken = otherParam.getString("donatedAccountToken");
|
||||||
|
if (StringUtils.isBlank(donatedAccountToken)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dbService.recordDonatedAccountFailureByToken(donatedAccountToken)
|
||||||
|
.onFailure(e -> log.warn("记录捐赠账号失败次数失败", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void recordAutoDonatedFailureIfNeeded(DbService dbService, ShareLinkInfo shareLinkInfo,
|
||||||
|
Throwable cause) {
|
||||||
|
if (shareLinkInfo == null || !isLikelyAuthFailure(cause)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object tokenObj = shareLinkInfo.getOtherParam().get(DONATED_ACCOUNT_TOKEN);
|
||||||
|
if (!(tokenObj instanceof String) || StringUtils.isBlank((String) tokenObj)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dbService.recordDonatedAccountFailureByToken((String) tokenObj)
|
||||||
|
.onFailure(e -> log.warn("记录自动捐赠账号失败次数失败", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Future<Void> applyDonatedAccountFallback(ParserCreate parserCreate, DbService dbService) {
|
||||||
|
ShareLinkInfo shareLinkInfo = parserCreate.getShareLinkInfo();
|
||||||
|
Map<String, Object> otherParam = shareLinkInfo.getOtherParam();
|
||||||
|
if (Boolean.TRUE.equals(otherParam.get(TEMP_AUTH_ADDED))) {
|
||||||
|
return Future.succeededFuture();
|
||||||
|
}
|
||||||
|
String type = shareLinkInfo.getType();
|
||||||
|
if (StringUtils.isBlank(type) || hasUsableStaticAuthConfig(type)) {
|
||||||
|
return Future.succeededFuture();
|
||||||
|
}
|
||||||
|
return dbService.getRandomDonatedAccount(type.toUpperCase())
|
||||||
|
.compose(res -> {
|
||||||
|
if (!Integer.valueOf(200).equals(res.getInteger("code"))) {
|
||||||
|
return Future.succeededFuture();
|
||||||
|
}
|
||||||
|
JsonObject data = res.getJsonObject("data");
|
||||||
|
if (data == null || data.isEmpty()) {
|
||||||
|
return Future.succeededFuture();
|
||||||
|
}
|
||||||
|
String username = data.getString("username");
|
||||||
|
String password = data.getString("password");
|
||||||
|
String token = data.getString("token");
|
||||||
|
if (StringUtils.isBlank(username) && StringUtils.isBlank(password) && StringUtils.isBlank(token)) {
|
||||||
|
return Future.succeededFuture();
|
||||||
|
}
|
||||||
|
MultiMap tempAuth = MultiMap.caseInsensitiveMultiMap();
|
||||||
|
if (StringUtils.isNotBlank(username)) {
|
||||||
|
tempAuth.set("username", username);
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(password)) {
|
||||||
|
tempAuth.set("password", password);
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(token)) {
|
||||||
|
tempAuth.set("token", token);
|
||||||
|
}
|
||||||
|
otherParam.put(ConfigConstant.AUTHS, tempAuth);
|
||||||
|
otherParam.put(TEMP_AUTH_ADDED, true);
|
||||||
|
String donatedAccountToken = data.getString("donatedAccountToken");
|
||||||
|
if (StringUtils.isNotBlank(donatedAccountToken)) {
|
||||||
|
otherParam.put(DONATED_ACCOUNT_TOKEN, donatedAccountToken);
|
||||||
|
}
|
||||||
|
log.debug("已自动应用捐赠账号: type={}", type);
|
||||||
|
return Future.<Void>succeededFuture();
|
||||||
|
})
|
||||||
|
.recover(err -> {
|
||||||
|
log.warn("自动获取捐赠账号失败: type={}", type, err);
|
||||||
|
return Future.succeededFuture();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasUsableStaticAuthConfig(String type) {
|
||||||
|
LocalMap<Object, Object> localMap = VertxHolder.getVertxInstance().sharedData()
|
||||||
|
.getLocalMap(ConfigConstant.LOCAL);
|
||||||
|
if (!localMap.containsKey(ConfigConstant.AUTHS)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JsonObject auths = (JsonObject) localMap.get(ConfigConstant.AUTHS);
|
||||||
|
JsonObject cfg = auths.getJsonObject(type);
|
||||||
|
if (cfg == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (String key : cfg.fieldNames()) {
|
||||||
|
Object value = cfg.getValue(key);
|
||||||
|
if (value != null && StringUtils.isNotBlank(value.toString())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isLikelyAuthFailure(Throwable cause) {
|
||||||
|
if (cause == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String msg = cause.getMessage();
|
||||||
|
if (StringUtils.isBlank(msg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String lower = msg.toLowerCase();
|
||||||
|
return lower.contains("auth")
|
||||||
|
|| lower.contains("token")
|
||||||
|
|| lower.contains("cookie")
|
||||||
|
|| lower.contains("password")
|
||||||
|
|| lower.contains("credential")
|
||||||
|
|| lower.contains("401")
|
||||||
|
|| lower.contains("403")
|
||||||
|
|| lower.contains("unauthorized")
|
||||||
|
|| lower.contains("forbidden")
|
||||||
|
|| lower.contains("expired")
|
||||||
|
|| lower.contains("登录")
|
||||||
|
|| lower.contains("认证");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -100,6 +100,11 @@ public class URLParamUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (localMap.containsKey(ConfigConstant.AUTHS)) {
|
if (localMap.containsKey(ConfigConstant.AUTHS)) {
|
||||||
|
// 如果本次请求已经通过 auth 临时参数(个人配置/捐赠账号)设置过认证信息,
|
||||||
|
// 则不要再用后台 app-dev.yml 的静态配置覆盖,否则临时认证会被静默清空/替换为空配置。
|
||||||
|
boolean tempAuthAdded = Boolean.TRUE.equals(
|
||||||
|
parserCreate.getShareLinkInfo().getOtherParam().get("__TEMP_AUTH_ADDED"));
|
||||||
|
if (!tempAuthAdded) {
|
||||||
JsonObject auths = (JsonObject) localMap.get(ConfigConstant.AUTHS);
|
JsonObject auths = (JsonObject) localMap.get(ConfigConstant.AUTHS);
|
||||||
if (auths.containsKey(type)) {
|
if (auths.containsKey(type)) {
|
||||||
// 需要处理引号
|
// 需要处理引号
|
||||||
@@ -117,6 +122,7 @@ public class URLParamUtil {
|
|||||||
parserCreate.getShareLinkInfo().getOtherParam().put(ConfigConstant.AUTHS, entries);
|
parserCreate.getShareLinkInfo().getOtherParam().put(ConfigConstant.AUTHS, entries);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName");
|
String linkPrefix = SharedDataUtil.getJsonConfig("server").getString("domainName");
|
||||||
if (StringUtils.isBlank(linkPrefix)) {
|
if (StringUtils.isBlank(linkPrefix)) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import cn.qaiu.entity.FileInfo;
|
|||||||
import cn.qaiu.entity.ShareLinkInfo;
|
import cn.qaiu.entity.ShareLinkInfo;
|
||||||
import cn.qaiu.lz.common.cache.CacheManager;
|
import cn.qaiu.lz.common.cache.CacheManager;
|
||||||
import cn.qaiu.lz.common.util.AuthParamCodec;
|
import cn.qaiu.lz.common.util.AuthParamCodec;
|
||||||
|
import cn.qaiu.lz.common.util.ParserAuthUtil;
|
||||||
import cn.qaiu.lz.common.util.URLParamUtil;
|
import cn.qaiu.lz.common.util.URLParamUtil;
|
||||||
import cn.qaiu.lz.web.model.AuthParam;
|
import cn.qaiu.lz.web.model.AuthParam;
|
||||||
import cn.qaiu.lz.web.model.CacheLinkInfo;
|
import cn.qaiu.lz.web.model.CacheLinkInfo;
|
||||||
@@ -159,7 +160,8 @@ public class ParserApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RouteMapping("/getFileList")
|
@RouteMapping("/getFileList")
|
||||||
public Future<List<FileInfo>> getFileList(HttpServerRequest request, String pwd, String dirId, String uuid) {
|
public Future<List<FileInfo>> getFileList(HttpServerRequest request, String pwd, String dirId, String uuid,
|
||||||
|
String auth) {
|
||||||
String url = URLParamUtil.parserParams(request);
|
String url = URLParamUtil.parserParams(request);
|
||||||
ParserCreate parserCreate;
|
ParserCreate parserCreate;
|
||||||
try {
|
try {
|
||||||
@@ -168,6 +170,7 @@ public class ParserApi {
|
|||||||
return Future.failedFuture(e);
|
return Future.failedFuture(e);
|
||||||
}
|
}
|
||||||
String linkPrefix = getLinkPrefix(request);
|
String linkPrefix = getLinkPrefix(request);
|
||||||
|
JsonObject otherParam = ParserAuthUtil.buildOtherParam(request, auth, linkPrefix);
|
||||||
parserCreate.getShareLinkInfo().getOtherParam().put("domainName", linkPrefix);
|
parserCreate.getShareLinkInfo().getOtherParam().put("domainName", linkPrefix);
|
||||||
parserCreate.getShareLinkInfo().getOtherParam().put("_requestOrigin", linkPrefix);
|
parserCreate.getShareLinkInfo().getOtherParam().put("_requestOrigin", linkPrefix);
|
||||||
if (StringUtils.isNotBlank(dirId)) {
|
if (StringUtils.isNotBlank(dirId)) {
|
||||||
@@ -176,13 +179,22 @@ public class ParserApi {
|
|||||||
if (StringUtils.isNotBlank(uuid)) {
|
if (StringUtils.isNotBlank(uuid)) {
|
||||||
parserCreate.getShareLinkInfo().getOtherParam().put("uuid", uuid);
|
parserCreate.getShareLinkInfo().getOtherParam().put("uuid", uuid);
|
||||||
}
|
}
|
||||||
|
return ParserAuthUtil.applyAuthParamsAndDonatedFallback(parserCreate, otherParam, dbService)
|
||||||
|
.compose(v -> {
|
||||||
|
URLParamUtil.addParam(parserCreate);
|
||||||
IPanTool tool = parserCreate.createTool();
|
IPanTool tool = parserCreate.createTool();
|
||||||
return IPanTool.closeAfter(tool, tool::parseFileList);
|
return IPanTool.closeAfter(tool, tool::parseFileList)
|
||||||
|
.onFailure(t -> {
|
||||||
|
ParserAuthUtil.recordDonatedAccountFailureIfNeeded(dbService, otherParam, t);
|
||||||
|
ParserAuthUtil.recordAutoDonatedFailureIfNeeded(dbService,
|
||||||
|
parserCreate.getShareLinkInfo(), t);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 目录解析下载文件
|
// 目录解析下载文件
|
||||||
// @RouteMapping("/getFileDownUrl/:type/:param")
|
// @RouteMapping("/getFileDownUrl/:type/:param")
|
||||||
public Future<String> getFileDownUrl(HttpServerRequest request, String type, String param) {
|
public Future<String> getFileDownUrl(HttpServerRequest request, String type, String param, String auth) {
|
||||||
ParserCreate parserCreate = ParserCreate.fromType(type).shareKey("-") // shareKey not null
|
ParserCreate parserCreate = ParserCreate.fromType(type).shareKey("-") // shareKey not null
|
||||||
.setShareLinkInfoPwd("-");
|
.setShareLinkInfoPwd("-");
|
||||||
|
|
||||||
@@ -198,17 +210,28 @@ public class ParserApi {
|
|||||||
|
|
||||||
// domainName
|
// domainName
|
||||||
String linkPrefix = getLinkPrefix(request);
|
String linkPrefix = getLinkPrefix(request);
|
||||||
|
JsonObject otherParam = ParserAuthUtil.buildOtherParam(request, auth, linkPrefix, true);
|
||||||
shareLinkInfo.getOtherParam().put("domainName", linkPrefix);
|
shareLinkInfo.getOtherParam().put("domainName", linkPrefix);
|
||||||
shareLinkInfo.getOtherParam().put("_requestOrigin", linkPrefix);
|
shareLinkInfo.getOtherParam().put("_requestOrigin", linkPrefix);
|
||||||
|
return ParserAuthUtil.applyAuthParamsAndDonatedFallback(parserCreate, otherParam, dbService)
|
||||||
|
.compose(v -> {
|
||||||
|
URLParamUtil.addParam(parserCreate);
|
||||||
IPanTool tool = parserCreate.createTool();
|
IPanTool tool = parserCreate.createTool();
|
||||||
return IPanTool.closeAfter(tool, tool::parseById);
|
return IPanTool.closeAfter(tool, tool::parseById)
|
||||||
|
.onFailure(t -> {
|
||||||
|
ParserAuthUtil.recordDonatedAccountFailureIfNeeded(dbService, otherParam, t);
|
||||||
|
ParserAuthUtil.recordAutoDonatedFailureIfNeeded(dbService,
|
||||||
|
parserCreate.getShareLinkInfo(), t);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@RouteMapping("/redirectUrl/:type/:param")
|
@RouteMapping("/redirectUrl/:type/:param")
|
||||||
public Future<Void> redirectUrl(HttpServerRequest request, HttpServerResponse response, String type, String param) {
|
public Future<Void> redirectUrl(HttpServerRequest request, HttpServerResponse response, String type, String param,
|
||||||
|
String auth) {
|
||||||
Promise<Void> promise = Promise.promise();
|
Promise<Void> promise = Promise.promise();
|
||||||
|
|
||||||
getFileDownUrl(request, type, param)
|
getFileDownUrl(request, type, param, auth)
|
||||||
.onSuccess(res -> {
|
.onSuccess(res -> {
|
||||||
ResponseUtil.redirect(response, res, promise);
|
ResponseUtil.redirect(response, res, promise);
|
||||||
})
|
})
|
||||||
@@ -283,11 +306,12 @@ public class ParserApi {
|
|||||||
|
|
||||||
|
|
||||||
@RouteMapping("/viewUrl/:type/:param")
|
@RouteMapping("/viewUrl/:type/:param")
|
||||||
public Future<Void> viewUrl(HttpServerRequest request, HttpServerResponse response, String type, String param) {
|
public Future<Void> viewUrl(HttpServerRequest request, HttpServerResponse response, String type, String param,
|
||||||
|
String auth) {
|
||||||
Promise<Void> promise = Promise.promise();
|
Promise<Void> promise = Promise.promise();
|
||||||
|
|
||||||
String viewPrefix = SharedDataUtil.getJsonConfig("server").getString("previewURL");
|
String viewPrefix = SharedDataUtil.getJsonConfig("server").getString("previewURL");
|
||||||
getFileDownUrl(request, type, param)
|
getFileDownUrl(request, type, param, auth)
|
||||||
.onSuccess(res -> {
|
.onSuccess(res -> {
|
||||||
String url = viewPrefix + URLEncoder.encode(res, StandardCharsets.UTF_8);
|
String url = viewPrefix + URLEncoder.encode(res, StandardCharsets.UTF_8);
|
||||||
ResponseUtil.redirect(response, url, promise);
|
ResponseUtil.redirect(response, url, promise);
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
package cn.qaiu.lz.web.controller;
|
package cn.qaiu.lz.web.controller;
|
||||||
|
|
||||||
import cn.qaiu.lz.common.util.AuthParamCodec;
|
import cn.qaiu.lz.common.util.ParserAuthUtil;
|
||||||
import cn.qaiu.lz.common.util.URLParamUtil;
|
import cn.qaiu.lz.common.util.URLParamUtil;
|
||||||
import cn.qaiu.lz.web.model.AuthParam;
|
|
||||||
import cn.qaiu.lz.web.model.CacheLinkInfo;
|
import cn.qaiu.lz.web.model.CacheLinkInfo;
|
||||||
import cn.qaiu.lz.web.service.CacheService;
|
import cn.qaiu.lz.web.service.CacheService;
|
||||||
import cn.qaiu.lz.web.service.DbService;
|
import cn.qaiu.lz.web.service.DbService;
|
||||||
@@ -29,7 +28,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
@RouteHandler("/")
|
@RouteHandler("/")
|
||||||
public class ServerApi {
|
public class ServerApi {
|
||||||
|
|
||||||
private static final String SKIP_CLIENT_LINKS = "_skipClientLinks";
|
private static final String SKIP_CLIENT_LINKS = ParserAuthUtil.SKIP_CLIENT_LINKS;
|
||||||
|
|
||||||
private final CacheService cacheService = AsyncServiceUtil.getAsyncServiceInstance(CacheService.class);
|
private final CacheService cacheService = AsyncServiceUtil.getAsyncServiceInstance(CacheService.class);
|
||||||
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
|
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
|
||||||
@@ -47,7 +46,7 @@ public class ServerApi {
|
|||||||
addCacheHeaders(response, res),
|
addCacheHeaders(response, res),
|
||||||
res.getDirectLink(), promise))
|
res.getDirectLink(), promise))
|
||||||
.onFailure(t -> {
|
.onFailure(t -> {
|
||||||
recordDonatedAccountFailureIfNeeded(otherParam, t);
|
ParserAuthUtil.recordDonatedAccountFailureIfNeeded(dbService, otherParam, t);
|
||||||
promise.tryFail(t);
|
promise.tryFail(t);
|
||||||
});
|
});
|
||||||
return promise.future();
|
return promise.future();
|
||||||
@@ -58,14 +57,14 @@ public class ServerApi {
|
|||||||
String url = URLParamUtil.parserParams(request);
|
String url = URLParamUtil.parserParams(request);
|
||||||
JsonObject otherParam = buildOtherParam(request, auth);
|
JsonObject otherParam = buildOtherParam(request, auth);
|
||||||
return cacheService.getCachedByShareUrlAndPwd(url, pwd, otherParam)
|
return cacheService.getCachedByShareUrlAndPwd(url, pwd, otherParam)
|
||||||
.onFailure(t -> recordDonatedAccountFailureIfNeeded(otherParam, t));
|
.onFailure(t -> ParserAuthUtil.recordDonatedAccountFailureIfNeeded(dbService, otherParam, t));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Future<CacheLinkInfo> parseJsonForRedirect(HttpServerRequest request, String pwd, String auth) {
|
public Future<CacheLinkInfo> parseJsonForRedirect(HttpServerRequest request, String pwd, String auth) {
|
||||||
String url = URLParamUtil.parserParams(request);
|
String url = URLParamUtil.parserParams(request);
|
||||||
JsonObject otherParam = buildOtherParam(request, auth, true);
|
JsonObject otherParam = buildOtherParam(request, auth, true);
|
||||||
return cacheService.getCachedByShareUrlAndPwd(url, pwd, otherParam)
|
return cacheService.getCachedByShareUrlAndPwd(url, pwd, otherParam)
|
||||||
.onFailure(t -> recordDonatedAccountFailureIfNeeded(otherParam, t));
|
.onFailure(t -> ParserAuthUtil.recordDonatedAccountFailureIfNeeded(dbService, otherParam, t));
|
||||||
}
|
}
|
||||||
|
|
||||||
@RouteMapping(value = "/json/:type/:key", method = RouteMethod.GET)
|
@RouteMapping(value = "/json/:type/:key", method = RouteMethod.GET)
|
||||||
@@ -146,66 +145,6 @@ public class ServerApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private JsonObject buildOtherParam(HttpServerRequest request, String auth, boolean skipClientLinks) {
|
private JsonObject buildOtherParam(HttpServerRequest request, String auth, boolean skipClientLinks) {
|
||||||
JsonObject otherParam = JsonObject.of("UA", request.headers().get("user-agent"), "_requestOrigin", resolveOrigin(request));
|
return ParserAuthUtil.buildOtherParam(request, auth, resolveOrigin(request), skipClientLinks);
|
||||||
if (skipClientLinks) {
|
|
||||||
otherParam.put(SKIP_CLIENT_LINKS, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解码认证参数
|
|
||||||
if (auth != null && !auth.isEmpty()) {
|
|
||||||
AuthParam authParam = AuthParamCodec.decode(auth);
|
|
||||||
if (authParam != null && authParam.hasValidAuth()) {
|
|
||||||
// 将认证参数放入 otherParam
|
|
||||||
otherParam.put("authType", authParam.getAuthType());
|
|
||||||
otherParam.put("authToken", authParam.getPrimaryCredential());
|
|
||||||
otherParam.put("authPassword", authParam.getPassword());
|
|
||||||
otherParam.put("authInfo1", authParam.getExt1());
|
|
||||||
otherParam.put("authInfo2", authParam.getExt2());
|
|
||||||
otherParam.put("authInfo3", authParam.getExt3());
|
|
||||||
otherParam.put("authInfo4", authParam.getExt4());
|
|
||||||
otherParam.put("authInfo5", authParam.getExt5());
|
|
||||||
if (authParam.getDonatedAccountToken() != null && !authParam.getDonatedAccountToken().isBlank()) {
|
|
||||||
otherParam.put("donatedAccountToken", authParam.getDonatedAccountToken());
|
|
||||||
}
|
|
||||||
log.debug("已解码认证参数: authType={}", authParam.getAuthType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return otherParam;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void recordDonatedAccountFailureIfNeeded(JsonObject otherParam, Throwable cause) {
|
|
||||||
if (!isLikelyAuthFailure(cause)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String donatedAccountToken = otherParam.getString("donatedAccountToken");
|
|
||||||
if (donatedAccountToken == null || donatedAccountToken.isBlank()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dbService.recordDonatedAccountFailureByToken(donatedAccountToken)
|
|
||||||
.onFailure(e -> log.warn("记录捐赠账号失败次数失败", e));
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isLikelyAuthFailure(Throwable cause) {
|
|
||||||
if (cause == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String msg = cause.getMessage();
|
|
||||||
if (msg == null || msg.isBlank()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String lower = msg.toLowerCase();
|
|
||||||
return lower.contains("auth")
|
|
||||||
|| lower.contains("token")
|
|
||||||
|| lower.contains("cookie")
|
|
||||||
|| lower.contains("password")
|
|
||||||
|| lower.contains("credential")
|
|
||||||
|| lower.contains("401")
|
|
||||||
|| lower.contains("403")
|
|
||||||
|| lower.contains("unauthorized")
|
|
||||||
|| lower.contains("forbidden")
|
|
||||||
|| lower.contains("expired")
|
|
||||||
|| lower.contains("登录")
|
|
||||||
|| lower.contains("认证");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,17 @@ import cn.qaiu.entity.ShareLinkInfo;
|
|||||||
import cn.qaiu.lz.common.cache.CacheConfigLoader;
|
import cn.qaiu.lz.common.cache.CacheConfigLoader;
|
||||||
import cn.qaiu.lz.common.cache.CacheManager;
|
import cn.qaiu.lz.common.cache.CacheManager;
|
||||||
import cn.qaiu.lz.common.cache.CacheTotalField;
|
import cn.qaiu.lz.common.cache.CacheTotalField;
|
||||||
|
import cn.qaiu.lz.common.util.ParserAuthUtil;
|
||||||
import cn.qaiu.lz.common.util.URLParamUtil;
|
import cn.qaiu.lz.common.util.URLParamUtil;
|
||||||
import cn.qaiu.lz.web.model.CacheLinkInfo;
|
import cn.qaiu.lz.web.model.CacheLinkInfo;
|
||||||
import cn.qaiu.lz.web.service.CacheService;
|
import cn.qaiu.lz.web.service.CacheService;
|
||||||
|
import cn.qaiu.lz.web.service.DbService;
|
||||||
import cn.qaiu.parser.IPanTool;
|
import cn.qaiu.parser.IPanTool;
|
||||||
import cn.qaiu.parser.ParserCreate;
|
import cn.qaiu.parser.ParserCreate;
|
||||||
import cn.qaiu.parser.clientlink.ClientLinkGeneratorFactory;
|
import cn.qaiu.parser.clientlink.ClientLinkGeneratorFactory;
|
||||||
import cn.qaiu.parser.clientlink.ClientLinkType;
|
import cn.qaiu.parser.clientlink.ClientLinkType;
|
||||||
import cn.qaiu.vx.core.annotaions.Service;
|
import cn.qaiu.vx.core.annotaions.Service;
|
||||||
|
import cn.qaiu.vx.core.util.AsyncServiceUtil;
|
||||||
import io.vertx.core.Future;
|
import io.vertx.core.Future;
|
||||||
import io.vertx.core.Promise;
|
import io.vertx.core.Promise;
|
||||||
import io.vertx.core.json.JsonObject;
|
import io.vertx.core.json.JsonObject;
|
||||||
@@ -27,9 +30,10 @@ import java.util.Map;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class CacheServiceImpl implements CacheService {
|
public class CacheServiceImpl implements CacheService {
|
||||||
|
|
||||||
private static final String SKIP_CLIENT_LINKS = "_skipClientLinks";
|
private static final String SKIP_CLIENT_LINKS = ParserAuthUtil.SKIP_CLIENT_LINKS;
|
||||||
|
|
||||||
private final CacheManager cacheManager = new CacheManager();
|
private final CacheManager cacheManager = new CacheManager();
|
||||||
|
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
|
||||||
|
|
||||||
static {
|
static {
|
||||||
// 服务类加载时注册缓存定时清理任务
|
// 服务类加载时注册缓存定时清理任务
|
||||||
@@ -71,7 +75,9 @@ public class CacheServiceImpl implements CacheService {
|
|||||||
promise.fail(cause);
|
promise.fail(cause);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
IPanTool.closeAfter(tool, tool::parse).onSuccess(redirectUrl -> {
|
IPanTool.closeAfter(tool, tool::parse).onFailure(err -> {
|
||||||
|
ParserAuthUtil.recordAutoDonatedFailureIfNeeded(dbService, shareLinkInfo, err);
|
||||||
|
}).onSuccess(redirectUrl -> {
|
||||||
// 使用 effectiveCacheDuration
|
// 使用 effectiveCacheDuration
|
||||||
long expires = System.currentTimeMillis() + effectiveCacheDuration * 60 * 1000L;
|
long expires = System.currentTimeMillis() + effectiveCacheDuration * 60 * 1000L;
|
||||||
result.setDirectLink(redirectUrl);
|
result.setDirectLink(redirectUrl);
|
||||||
@@ -278,8 +284,9 @@ public class CacheServiceImpl implements CacheService {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return Future.failedFuture(e);
|
return Future.failedFuture(e);
|
||||||
}
|
}
|
||||||
parserCreate.getShareLinkInfo().getOtherParam().putAll(otherParam.getMap());
|
ParserCreate finalParserCreate = parserCreate;
|
||||||
return getAndSaveCachedShareLink(parserCreate);
|
return ParserAuthUtil.applyAuthParamsAndDonatedFallback(finalParserCreate, otherParam, dbService)
|
||||||
|
.compose(v -> getAndSaveCachedShareLink(finalParserCreate));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -290,22 +297,8 @@ public class CacheServiceImpl implements CacheService {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return Future.failedFuture(e);
|
return Future.failedFuture(e);
|
||||||
}
|
}
|
||||||
parserCreate.getShareLinkInfo().getOtherParam().putAll(otherParam.getMap());
|
ParserCreate finalParserCreate = parserCreate;
|
||||||
|
return ParserAuthUtil.applyAuthParamsAndDonatedFallback(finalParserCreate, otherParam, dbService)
|
||||||
// 检查是否有临时认证参数
|
.compose(v -> getAndSaveCachedShareLink(finalParserCreate));
|
||||||
if (otherParam.containsKey("authType") || otherParam.containsKey("authToken")) {
|
|
||||||
log.debug("从otherParam中检测到临时认证参数");
|
|
||||||
URLParamUtil.addTempAuthParam(parserCreate,
|
|
||||||
otherParam.getString("authType"),
|
|
||||||
otherParam.getString("authToken"),
|
|
||||||
otherParam.getString("authPassword"),
|
|
||||||
otherParam.getString("authInfo1"),
|
|
||||||
otherParam.getString("authInfo2"),
|
|
||||||
otherParam.getString("authInfo3"),
|
|
||||||
otherParam.getString("authInfo4"),
|
|
||||||
otherParam.getString("authInfo5"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return getAndSaveCachedShareLink(parserCreate);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -325,9 +325,16 @@ public class DbServiceImpl implements DbService {
|
|||||||
public Future<JsonObject> saveDonatedAccount(JsonObject account) {
|
public Future<JsonObject> saveDonatedAccount(JsonObject account) {
|
||||||
JDBCPool client = JDBCPoolInit.instance().getPool();
|
JDBCPool client = JDBCPoolInit.instance().getPool();
|
||||||
|
|
||||||
Future<String> encryptedUsername = CryptoUtil.encrypt(account.getString("username"));
|
// 只保留当前认证方式实际用到的字段,避免调用方切换认证类型后遗留的用户名/密码脏数据
|
||||||
Future<String> encryptedPassword = CryptoUtil.encrypt(account.getString("password"));
|
// 被一并存入库中,导致后续解析时被误当作真实凭证使用(例如把废弃的用户名当手机号登录)。
|
||||||
Future<String> encryptedToken = CryptoUtil.encrypt(account.getString("token"));
|
boolean isPasswordAuth = "password".equalsIgnoreCase(account.getString("authType"));
|
||||||
|
String usernameToStore = isPasswordAuth ? account.getString("username") : null;
|
||||||
|
String passwordToStore = isPasswordAuth ? account.getString("password") : null;
|
||||||
|
String tokenToStore = isPasswordAuth ? null : account.getString("token");
|
||||||
|
|
||||||
|
Future<String> encryptedUsername = CryptoUtil.encrypt(usernameToStore);
|
||||||
|
Future<String> encryptedPassword = CryptoUtil.encrypt(passwordToStore);
|
||||||
|
Future<String> encryptedToken = CryptoUtil.encrypt(tokenToStore);
|
||||||
|
|
||||||
return ensureFailCountColumn(client).compose(v ->
|
return ensureFailCountColumn(client).compose(v ->
|
||||||
Future.all(encryptedUsername, encryptedPassword, encryptedToken).compose(compositeFuture -> {
|
Future.all(encryptedUsername, encryptedPassword, encryptedToken).compose(compositeFuture -> {
|
||||||
@@ -412,6 +419,14 @@ public class DbServiceImpl implements DbService {
|
|||||||
String password = passwordFuture.result();
|
String password = passwordFuture.result();
|
||||||
String token = tokenFuture.result();
|
String token = tokenFuture.result();
|
||||||
|
|
||||||
|
// 历史脏数据兜底:非 password 认证类型的账号不应该带用户名/密码
|
||||||
|
// (例如切换认证类型前遗留的表单数据),否则会被解析器误当作真实账号密码去登录。
|
||||||
|
boolean isPasswordAuth = "password".equalsIgnoreCase(row.getString("auth_type"));
|
||||||
|
if (!isPasswordAuth) {
|
||||||
|
username = null;
|
||||||
|
password = null;
|
||||||
|
}
|
||||||
|
|
||||||
// 如果解密后没有任何可用凭证,返回空对象,避免把密文当作明文认证参数下发给前端
|
// 如果解密后没有任何可用凭证,返回空对象,避免把密文当作明文认证参数下发给前端
|
||||||
if (StringUtils.isBlank(username) && StringUtils.isBlank(password) && StringUtils.isBlank(token)) {
|
if (StringUtils.isBlank(username) && StringUtils.isBlank(password) && StringUtils.isBlank(token)) {
|
||||||
log.warn("random donated account has no usable credential after decrypt, accountId={}", row.getLong("id"));
|
log.warn("random donated account has no usable credential after decrypt, accountId={}", row.getLong("id"));
|
||||||
|
|||||||
Reference in New Issue
Block a user