mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2026-08-26 19:42:02 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a5240fcbc | |||
| ecd365767a | |||
| f358140974 | |||
| 541c21f963 | |||
| bdd253383d | |||
| 0fb53d5159 | |||
| b9ff408bdd | |||
| aeb4394cf8 | |||
| 8d419d3265 | |||
| b70e30796f | |||
| d3ef521773 | |||
| 2969c92d3d | |||
| 2fde421169 | |||
| 3494271150 | |||
| 675d930f86 | |||
| 363c603bbb |
+13
-26
@@ -158,37 +158,19 @@ jobs:
|
|||||||
path: web-service/target/package
|
path: web-service/target/package
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# jdeps 分析 → 确定所需 JDK 模块
|
# 选择 JDK 模块
|
||||||
# ============================================================
|
# ============================================================
|
||||||
- name: 分析所需 JDK 模块(jdeps)
|
- name: 选择完整 JDK 模块集合
|
||||||
run: |
|
run: |
|
||||||
MAIN_JAR="web-service/target/package/netdisk-fast-download.jar"
|
# 123 网盘/自定义 JS 解析器会通过 javax.script、Nashorn 和 SPI/反射加载模块,
|
||||||
LIB_DIR="web-service/target/package/lib"
|
# jdeps 无法稳定识别这些运行时依赖。这里优先保证原生包可运行,使用完整
|
||||||
|
# JDK 模块集合生成运行时,避免 Windows 精简 JRE 缺 java.scripting/jdk.dynalink 等模块。
|
||||||
CP=""
|
printf 'JDK_MODULES=ALL-MODULE-PATH\n' >> $GITHUB_ENV
|
||||||
for jar in "$LIB_DIR"/*.jar; do
|
|
||||||
CP="$CP${CP:+:}$jar"
|
|
||||||
done
|
|
||||||
|
|
||||||
RAW_MODULES=$(jdeps --print-module-deps --ignore-missing-deps --multi-release 17 \
|
|
||||||
--class-path "$CP" "$MAIN_JAR" 2>/dev/null | head -n 1 | tr -d '\r\n' || true)
|
|
||||||
|
|
||||||
if [ -z "$RAW_MODULES" ] || [[ "$RAW_MODULES" == *"Missing"* ]] || [[ "$RAW_MODULES" == *"Error"* ]]; then
|
|
||||||
# ⚠️ 回退列表:若项目新增了需要 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"
|
|
||||||
echo "jdeps 分析失败,使用回退模块列表"
|
|
||||||
else
|
|
||||||
# 补上 jdeps 无法检测的反射/SPI依赖
|
|
||||||
RAW_MODULES="$RAW_MODULES,java.desktop,jdk.crypto.ec"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "detected modules: $RAW_MODULES"
|
|
||||||
printf 'JDK_MODULES=%s\n' "$RAW_MODULES" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# jlink 生成精简 JRE
|
# jlink 生成完整模块 JRE
|
||||||
# ============================================================
|
# ============================================================
|
||||||
- name: 生成精简 JRE(jlink)
|
- name: 生成完整模块 JRE(jlink)
|
||||||
run: |
|
run: |
|
||||||
jlink \
|
jlink \
|
||||||
--module-path "$JAVA_HOME/jmods" \
|
--module-path "$JAVA_HOME/jmods" \
|
||||||
@@ -215,6 +197,11 @@ jobs:
|
|||||||
ls -la "$JRE_BIN"/*.dll 2>/dev/null || echo "(无 .dll 文件)"
|
ls -la "$JRE_BIN"/*.dll 2>/dev/null || echo "(无 .dll 文件)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "=== 关键 JDK 模块校验 ==="
|
||||||
|
"native-package/netdisk-fast-download/jre/bin/java" --list-modules | grep -q '^java.scripting@'
|
||||||
|
"native-package/netdisk-fast-download/jre/bin/java" --list-modules | grep -q '^jdk.dynalink@'
|
||||||
|
"native-package/netdisk-fast-download/jre/bin/java" --list-modules | grep -q '^jdk.unsupported@'
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 组装包目录
|
# 组装包目录
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
@@ -531,7 +531,13 @@ Core模块集成Vert.x实现类似spring的注解式路由API
|
|||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
[](https://star-history.com/#qaiu/netdisk-fast-download&Date)
|
<a href="https://www.star-history.com/?repos=qaiu%2Fnetdisk-fast-download&type=date&legend=bottom-right">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=qaiu/netdisk-fast-download&type=date&theme=dark&legend=bottom-right&sealed_token=dfQO_dJcTqcPkEnM7SfxRyHoFbV5Ah4LxoEhdlheMn4T2YLEV_WETxFZexeAbWN5OmNyYuycWan2d42PAFbw0CuU4oCTKgehfErFJ9eVl2CyVpP_4xrdQw" />
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=qaiu/netdisk-fast-download&type=date&legend=bottom-right&sealed_token=dfQO_dJcTqcPkEnM7SfxRyHoFbV5Ah4LxoEhdlheMn4T2YLEV_WETxFZexeAbWN5OmNyYuycWan2d42PAFbw0CuU4oCTKgehfErFJ9eVl2CyVpP_4xrdQw" />
|
||||||
|
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=qaiu/netdisk-fast-download&type=date&legend=bottom-right&sealed_token=dfQO_dJcTqcPkEnM7SfxRyHoFbV5Ah4LxoEhdlheMn4T2YLEV_WETxFZexeAbWN5OmNyYuycWan2d42PAFbw0CuU4oCTKgehfErFJ9eVl2CyVpP_4xrdQw" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
|
||||||
## **免责声明**
|
## **免责声明**
|
||||||
- 用户在使用本项目时,应自行承担风险,并确保其行为符合当地法律法规。开发者不对用户因使用本项目而导致的任何后果负责。
|
- 用户在使用本项目时,应自行承担风险,并确保其行为符合当地法律法规。开发者不对用户因使用本项目而导致的任何后果负责。
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.postgresql</groupId>
|
<groupId>org.postgresql</groupId>
|
||||||
<artifactId>postgresql</artifactId>
|
<artifactId>postgresql</artifactId>
|
||||||
<version>42.7.11</version>
|
<version>42.7.12</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -201,6 +204,76 @@ public abstract class PanBase implements IPanTool, Closeable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSRF 防护: 校验通用自定义域名解析器 (CE/Ce4/Kd/Other 等) 即将请求的目标主机,
|
||||||
|
* 拒绝解析到回环/内网/链路本地/组播等非公网地址的域名, 阻止攻击者通过可控 DNS
|
||||||
|
* 记录 (或直接填写内网域名) 让服务端向内网/云元数据接口发起请求。
|
||||||
|
* <p>
|
||||||
|
* 必须在子类 parse() 中构造出 baseUrl/发起任何 clientSession 请求之前调用。
|
||||||
|
*
|
||||||
|
* @param url 从 shareLinkInfo.getShareUrl() 解析出的 URL
|
||||||
|
* @throws IOException 当主机无法解析或解析结果落入禁止的地址段时抛出
|
||||||
|
*/
|
||||||
|
protected static void assertPublicHost(URL url) throws IOException {
|
||||||
|
String host = url.getHost();
|
||||||
|
InetAddress[] addresses;
|
||||||
|
try {
|
||||||
|
addresses = InetAddress.getAllByName(host);
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
throw new IOException("无法解析目标主机: " + host, e);
|
||||||
|
}
|
||||||
|
if (addresses.length == 0) {
|
||||||
|
throw new IOException("无法解析目标主机: " + host);
|
||||||
|
}
|
||||||
|
for (InetAddress addr : addresses) {
|
||||||
|
if (isDisallowedAddress(addr)) {
|
||||||
|
throw new IOException("目标地址不允许访问(内网/回环/链路本地/组播): "
|
||||||
|
+ host + " -> " + addr.getHostAddress());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断地址是否落入禁止访问的范围: 0.0.0.0/8, 10/8, 127/8, 169.254/16(含云元数据
|
||||||
|
* 169.254.169.254), 172.16/12, 192.168/16, 100.64/10(CGNAT), 224/4~255/4(组播/保留),
|
||||||
|
* 以及对应的 IPv6 回环/链路本地/唯一本地地址(fc00::/7)/组播地址; IPv4-映射的 IPv6
|
||||||
|
* 地址(::ffff:a.b.c.d) 会先还原为 IPv4 再判断,避免绕过。
|
||||||
|
*/
|
||||||
|
private static boolean isDisallowedAddress(InetAddress addr) {
|
||||||
|
byte[] bytes = addr.getAddress();
|
||||||
|
if (bytes.length == 16 && isIPv4Mapped(bytes)) {
|
||||||
|
byte[] v4 = new byte[4];
|
||||||
|
System.arraycopy(bytes, 12, v4, 0, 4);
|
||||||
|
bytes = v4;
|
||||||
|
}
|
||||||
|
if (bytes.length == 4) {
|
||||||
|
int b0 = bytes[0] & 0xFF;
|
||||||
|
int b1 = bytes[1] & 0xFF;
|
||||||
|
if (b0 == 0) return true; // 0.0.0.0/8
|
||||||
|
if (b0 == 10) return true; // 10.0.0.0/8
|
||||||
|
if (b0 == 127) return true; // 127.0.0.0/8 loopback
|
||||||
|
if (b0 == 169 && b1 == 254) return true; // 169.254.0.0/16 (含云元数据 169.254.169.254)
|
||||||
|
if (b0 == 172 && b1 >= 16 && b1 <= 31) return true; // 172.16.0.0/12
|
||||||
|
if (b0 == 192 && b1 == 168) return true; // 192.168.0.0/16
|
||||||
|
if (b0 == 100 && b1 >= 64 && b1 <= 127) return true;// 100.64.0.0/10 CGNAT
|
||||||
|
return b0 >= 224; // 224.0.0.0/4 组播 + 240.0.0.0/4 保留
|
||||||
|
}
|
||||||
|
// IPv6
|
||||||
|
if (addr.isAnyLocalAddress() || addr.isLoopbackAddress()
|
||||||
|
|| addr.isLinkLocalAddress() || addr.isSiteLocalAddress()
|
||||||
|
|| addr.isMulticastAddress()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return bytes.length == 16 && (bytes[0] & 0xFE) == 0xFC; // fc00::/7 unique local
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isIPv4Mapped(byte[] b) {
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
if (b[i] != 0) return false;
|
||||||
|
}
|
||||||
|
return (b[10] & 0xFF) == 0xFF && (b[11] & 0xFF) == 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
protected String baseMsg() {
|
protected String baseMsg() {
|
||||||
if (shareLinkInfo.getShareUrl() != null) {
|
if (shareLinkInfo.getShareUrl() != null) {
|
||||||
return shareLinkInfo.getPanName() + "-" + shareLinkInfo.getType() + ": url=" + shareLinkInfo.getShareUrl();
|
return shareLinkInfo.getPanName() + "-" + shareLinkInfo.getType() + ": url=" + shareLinkInfo.getShareUrl();
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ public class Ce4Tool extends PanBase {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
URL url = new URL(shareLinkInfo.getShareUrl());
|
URL url = new URL(shareLinkInfo.getShareUrl());
|
||||||
|
assertPublicHost(url);
|
||||||
String baseUrl = url.getProtocol() + "://" + url.getHost();
|
String baseUrl = url.getProtocol() + "://" + url.getHost();
|
||||||
// 如果有端口,拼接上端口
|
// 如果有端口,拼接上端口
|
||||||
if (url.getPort() != -1) {
|
if (url.getPort() != -1) {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ public class CeTool extends PanBase {
|
|||||||
String pwd = shareLinkInfo.getSharePassword();
|
String pwd = shareLinkInfo.getSharePassword();
|
||||||
try {
|
try {
|
||||||
URL url = new URL(shareLinkInfo.getShareUrl());
|
URL url = new URL(shareLinkInfo.getShareUrl());
|
||||||
|
assertPublicHost(url);
|
||||||
String baseUrl = url.getProtocol() + "://" + url.getHost();
|
String baseUrl = url.getProtocol() + "://" + url.getHost();
|
||||||
// 如果有端口,拼接上端口
|
// 如果有端口,拼接上端口
|
||||||
if (url.getPort() != -1) {
|
if (url.getPort() != -1) {
|
||||||
|
|||||||
@@ -99,10 +99,26 @@ public class Ye2Tool extends PanBase {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String token = auths.get("token");
|
String token = auths.get("token");
|
||||||
if (StringUtils.isNotEmpty(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;
|
return token;
|
||||||
}
|
}
|
||||||
return auths.get("authorization");
|
String trimmed = token.trim();
|
||||||
|
if (trimmed.regionMatches(true, 0, "Bearer ", 0, 7)) {
|
||||||
|
trimmed = trimmed.substring(7).trim();
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isTokenExpired() {
|
private boolean isTokenExpired() {
|
||||||
@@ -625,7 +641,14 @@ public class Ye2Tool extends PanBase {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String normalizedShareKey = YeShareHostUtil.normalizeShareKey(shareLinkInfo.getShareKey());
|
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)) {
|
if (StringUtils.isNotEmpty(normalizedShareKey)) {
|
||||||
JsonObject v2Body = new JsonObject()
|
JsonObject v2Body = new JsonObject()
|
||||||
.put("ShareKey", normalizedShareKey)
|
.put("ShareKey", normalizedShareKey)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<revision>0.3.7</revision>
|
<revision>0.4.1</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>
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
<parserVersion>10.2.5</parserVersion>
|
<parserVersion>10.2.5</parserVersion>
|
||||||
<jackson.version>2.18.6</jackson.version>
|
<jackson.version>2.18.6</jackson.version>
|
||||||
<!-- Logback 最新稳定版 -->
|
<!-- Logback 最新稳定版 -->
|
||||||
<logback.version>1.5.32</logback.version>
|
<logback.version>1.5.33</logback.version>
|
||||||
<junit.version>4.13.2</junit.version>
|
<junit.version>4.13.2</junit.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nfd-web",
|
"name": "nfd-web",
|
||||||
"version": "0.3.7",
|
"version": "0.4.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"@element-plus/icons-vue": "^2.3.1",
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
"@monaco-editor/loader": "^1.4.0",
|
"@monaco-editor/loader": "^1.4.0",
|
||||||
"@vueuse/core": "^11.2.0",
|
"@vueuse/core": "^11.2.0",
|
||||||
"axios": "1.16.1",
|
"axios": "1.18.0",
|
||||||
"clipboard": "^2.0.11",
|
"clipboard": "^2.0.11",
|
||||||
"core-js": "^3.8.3",
|
"core-js": "^3.8.3",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -386,6 +386,16 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "uuid123"
|
"example": "uuid123"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stoken",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"description": "分享 token,用于子目录解析时复用认证状态",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "OASBe5qM0pg2VvvyLM..."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
|
|||||||
@@ -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("认证");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,9 +68,9 @@ public class URLParamUtil {
|
|||||||
boolean firstParam = !decodedUrl.contains("?");
|
boolean firstParam = !decodedUrl.contains("?");
|
||||||
|
|
||||||
for (String paramName : params.names()) {
|
for (String paramName : params.names()) {
|
||||||
// 忽略 "url", "pwd", "dirId", "uuid", "auth" 参数(这些参数单独处理,不应拼接到分享URL中)
|
// 忽略 "url", "pwd", "dirId", "uuid", "auth", "stoken" 参数(这些参数单独处理,不应拼接到分享URL中)
|
||||||
if (!paramName.equals("url") && !paramName.equals("pwd") && !paramName.equals("dirId")
|
if (!paramName.equals("url") && !paramName.equals("pwd") && !paramName.equals("dirId")
|
||||||
&& !paramName.equals("uuid") && !paramName.equals("auth")) {
|
&& !paramName.equals("uuid") && !paramName.equals("auth") && !paramName.equals("stoken")) {
|
||||||
if (firstParam) {
|
if (firstParam) {
|
||||||
urlBuilder.append("?");
|
urlBuilder.append("?");
|
||||||
firstParam = false;
|
firstParam = false;
|
||||||
@@ -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 stoken, String auth) {
|
||||||
String url = URLParamUtil.parserParams(request);
|
String url = URLParamUtil.parserParams(request);
|
||||||
ParserCreate parserCreate;
|
ParserCreate parserCreate;
|
||||||
try {
|
try {
|
||||||
@@ -168,21 +170,34 @@ 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)) {
|
||||||
parserCreate.getShareLinkInfo().getOtherParam().put("dirId", dirId);
|
parserCreate.getShareLinkInfo().getOtherParam().put("dirId", dirId);
|
||||||
}
|
}
|
||||||
|
if (StringUtils.isNotBlank(stoken)) {
|
||||||
|
parserCreate.getShareLinkInfo().getOtherParam().put("stoken", stoken);
|
||||||
|
}
|
||||||
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 +213,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 +309,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