mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2026-06-10 15:37:28 +00:00
fix(security): 安全漏洞修复与依赖升级
- 升级 Vert.x 4.5.24 → 4.5.27, postgresql 42.7.3 → 42.7.11, logback 1.5.18 → 1.5.32, axios 1.13.5 → 1.16.1 - 修复 JWT 签名验证和密码比较的时序攻击漏洞 (MessageDigest.isEqual) - 修复 AESUtils 使用不安全 Random 改为 SecureRandom - 修复登录用户枚举和异常信息泄露,统一错误提示 - 修复 RateLimiter count++ 非原子操作 (AtomicInteger) - 修复 JsParserExecutor DCL 模式缺少 volatile - 修复 Token 日志泄露,仅打印前8字符 - 修复 Playground 密码时序攻击和堆栈泄露 - 所有 window.open 添加 noopener,noreferrer - LocalConstant 改用 ConcurrentHashMap 保证线程安全 - Dockerfile 添加非 root 用户运行,secret.yml 加入 .gitignore
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -31,6 +31,7 @@ target/
|
||||
sdkTest.log
|
||||
app.yml
|
||||
app-local.yml
|
||||
secret.yml
|
||||
|
||||
|
||||
#some local files
|
||||
|
||||
11
Dockerfile
11
Dockerfile
@@ -10,8 +10,13 @@ COPY ./web-service/target/netdisk-fast-download-bin.zip .
|
||||
RUN unzip netdisk-fast-download-bin.zip && \
|
||||
mv netdisk-fast-download/* ./ && \
|
||||
rm netdisk-fast-download-bin.zip && \
|
||||
chmod +x run.sh
|
||||
chmod +x run.sh && \
|
||||
mkdir -p db logs
|
||||
|
||||
EXPOSE 6400 6401
|
||||
COPY ./docker-entrypoint.sh /docker-entrypoint.sh
|
||||
RUN chmod +x /docker-entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["sh", "run.sh"]
|
||||
EXPOSE 6401
|
||||
|
||||
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.3</version>
|
||||
<version>42.7.11</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -127,8 +127,9 @@ public class RouterHandlerFactory implements BaseHttpApi {
|
||||
// 错误请求处理
|
||||
mainRouter.errorHandler(405, ctx -> doFireJsonResultResponse(ctx, JsonResult
|
||||
.error("Method Not Allowed", 405)));
|
||||
mainRouter.errorHandler(404, ctx -> ctx.response().setStatusCode(404).setChunked(true)
|
||||
.end("Internal server error: 404 not found"));
|
||||
mainRouter.errorHandler(404, ctx -> {
|
||||
ctx.response().setStatusCode(404).end("404 not found");
|
||||
});
|
||||
|
||||
return mainRouter;
|
||||
}
|
||||
@@ -179,8 +180,9 @@ public class RouterHandlerFactory implements BaseHttpApi {
|
||||
if (ctx.statusCode() == 503 || ctx.failure() == null) {
|
||||
doFireJsonResultResponse(ctx, JsonResult.error("未知异常, 请联系管理员"), 503);
|
||||
} else {
|
||||
ctx.failure().printStackTrace();
|
||||
doFireJsonResultResponse(ctx, JsonResult.error(ctx.failure().getMessage()), 500);
|
||||
LOGGER.error("路由处理失败", ctx.failure());
|
||||
String msg = ctx.failure() != null ? ctx.failure().getMessage() : "未知异常";
|
||||
doFireJsonResultResponse(ctx, JsonResult.error(msg), 500);
|
||||
}
|
||||
});
|
||||
} else if (method.isAnnotationPresent(SockRouteMapper.class)) {
|
||||
@@ -198,7 +200,7 @@ public class RouterHandlerFactory implements BaseHttpApi {
|
||||
try {
|
||||
ReflectionUtil.invokeWithArguments(method, instance, sock);
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
LOGGER.error("WebSocket处理异常", e);
|
||||
}
|
||||
});
|
||||
if (url.endsWith("*")) {
|
||||
@@ -322,7 +324,7 @@ public class RouterHandlerFactory implements BaseHttpApi {
|
||||
parameterValueList.put(k, entity);
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
LOGGER.error("实体类绑定异常: {}", typeName, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -365,7 +367,7 @@ public class RouterHandlerFactory implements BaseHttpApi {
|
||||
Object entity = ParamUtil.multiMapToEntity(queryParams, aClass);
|
||||
parameterValueList.put(k, entity);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
LOGGER.error("参数绑定异常: {}", v.getRight().getName(), e);
|
||||
}
|
||||
} else if (parameterValueList.get(k) == null
|
||||
&& JsonObject.class.getName().equals(v.getRight().getName())) {
|
||||
@@ -408,22 +410,19 @@ public class RouterHandlerFactory implements BaseHttpApi {
|
||||
doFireJsonResultResponse(ctx, JsonResult.data(null));
|
||||
}
|
||||
|
||||
}).onFailure(e -> doFireJsonResultResponse(ctx, JsonResult.error(e.getMessage()), 500));
|
||||
}).onFailure(e -> {
|
||||
LOGGER.error("请求处理失败", e);
|
||||
String msg = e.getMessage() != null ? e.getMessage() : "服务器内部错误";
|
||||
doFireJsonResultResponse(ctx, JsonResult.error(msg), 500);
|
||||
});
|
||||
} else {
|
||||
doFireJsonResultResponse(ctx, JsonResult.data(data));
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
String err = e.getMessage();
|
||||
if (e.getCause() != null) {
|
||||
if (e.getCause() instanceof InvocationTargetException) {
|
||||
err = ((InvocationTargetException) e.getCause()).getTargetException().getMessage();
|
||||
} else {
|
||||
err = e.getCause().getMessage();
|
||||
}
|
||||
}
|
||||
doFireJsonResultResponse(ctx, JsonResult.error(err), 500);
|
||||
LOGGER.error("请求处理异常", e);
|
||||
String msg = e.getMessage() != null ? e.getMessage() : "服务器内部错误";
|
||||
doFireJsonResultResponse(ctx, JsonResult.error(msg), 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package cn.qaiu.vx.core.util;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* vertx 上下文外的本地容器 为不在vertx线程的方法传递数据
|
||||
@@ -10,11 +10,10 @@ import java.util.Map;
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
*/
|
||||
public class LocalConstant {
|
||||
private static final Map<String, Object> LOCAL_CONST = new HashMap<>();
|
||||
private static final Map<String, Object> LOCAL_CONST = new ConcurrentHashMap<>();
|
||||
|
||||
public static Map<String, Object> put(String k, Object v) {
|
||||
if (LOCAL_CONST.containsKey(k)) return LOCAL_CONST;
|
||||
LOCAL_CONST.put(k, v);
|
||||
LOCAL_CONST.putIfAbsent(k, v);
|
||||
return LOCAL_CONST;
|
||||
}
|
||||
|
||||
|
||||
9
docker-entrypoint.sh
Normal file
9
docker-entrypoint.sh
Normal file
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Fix permissions on volume-mounted directories (runs as root)
|
||||
chown -R appuser:appgroup /app/db /app/logs /app/resources 2>/dev/null || true
|
||||
|
||||
# Run Java directly - entrypoint is PID 1, exec makes Java PID 1
|
||||
# Docker SIGTERM goes directly to Java, triggering ShutdownHook
|
||||
exec java -Xmx${JVM_XMX:-512M} ${JVM_OPTS} -Duser.timezone=${TZ:-Asia/Shanghai} -jar /app/netdisk-fast-download.jar
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<groupId>cn.qaiu</groupId>
|
||||
<artifactId>parser</artifactId>
|
||||
<version>10.2.5</version>
|
||||
<version>${parserVersion}</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>cn.qaiu:parser</name>
|
||||
@@ -35,9 +35,9 @@
|
||||
</developers>
|
||||
|
||||
<scm>
|
||||
<connection>scm:git:https://github.com/qaiu/netdisk-fast-download.git</connection>
|
||||
<developerConnection>scm:git:ssh://git@github.com:qaiu/netdisk-fast-download.git</developerConnection>
|
||||
<url>https://github.com/qaiu/netdisk-fast-download</url>
|
||||
<connection>scm:git:https://github.com/${github.owner}/${github.repo}.git</connection>
|
||||
<developerConnection>scm:git:ssh://git@github.com:${github.owner}/${github.repo}.git</developerConnection>
|
||||
<url>https://github.com/${github.owner}/${github.repo}</url>
|
||||
</scm>
|
||||
|
||||
<distributionManagement>
|
||||
@@ -52,20 +52,19 @@
|
||||
</distributionManagement>
|
||||
|
||||
<properties>
|
||||
<revision>0.2.1</revision>
|
||||
<java.version>17</java.version>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
||||
<!-- Versions -->
|
||||
<vertx.version>4.5.24</vertx.version>
|
||||
<vertx.version>4.5.27</vertx.version>
|
||||
<org.reflections.version>0.10.2</org.reflections.version>
|
||||
<lombok.version>1.18.38</lombok.version>
|
||||
<slf4j.version>2.0.16</slf4j.version>
|
||||
<commons-lang3.version>3.18.0</commons-lang3.version>
|
||||
<jackson.version>2.18.6</jackson.version>
|
||||
<logback.version>1.5.19</logback.version>
|
||||
<logback.version>1.5.32</logback.version>
|
||||
<junit.version>4.13.2</junit.version>
|
||||
</properties>
|
||||
|
||||
@@ -124,6 +123,41 @@
|
||||
<build>
|
||||
<plugins>
|
||||
|
||||
<!-- 从 git remote origin 自动识别 GitHub 仓库地址 -->
|
||||
<plugin>
|
||||
<groupId>org.codehaus.gmavenplus</groupId>
|
||||
<artifactId>gmavenplus-plugin</artifactId>
|
||||
<version>4.1.1</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.groovy</groupId>
|
||||
<artifactId>groovy</artifactId>
|
||||
<version>4.0.24</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>initialize</phase>
|
||||
<goals><goal>execute</goal></goals>
|
||||
<configuration>
|
||||
<scripts>
|
||||
<script>
|
||||
def url = 'git remote get-url origin'.execute().text.trim()
|
||||
def m = (url =~ 'github\\.com[:/]([^/]+)/([^/.]+?)(?:\\.git)?$')
|
||||
if (m.find()) {
|
||||
project.properties.setProperty('github.owner', m.group(1))
|
||||
project.properties.setProperty('github.repo', m.group(2))
|
||||
} else {
|
||||
project.properties.setProperty('github.owner', 'qaiu')
|
||||
project.properties.setProperty('github.repo', 'netdisk-fast-download')
|
||||
}
|
||||
</script>
|
||||
</scripts>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<!-- 编译 -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
|
||||
@@ -33,7 +33,7 @@ public class JsParserExecutor implements IPanTool, AutoCloseable {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(JsParserExecutor.class);
|
||||
|
||||
private static WorkerExecutor EXECUTOR;
|
||||
private static volatile WorkerExecutor EXECUTOR;
|
||||
private static final Object EXECUTOR_LOCK = new Object();
|
||||
|
||||
private static String FETCH_RUNTIME_JS = null;
|
||||
|
||||
@@ -14,7 +14,6 @@ import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Random;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -299,7 +298,7 @@ public class AESUtils {
|
||||
//length用户要求产生字符串的长度
|
||||
public static String getRandomString(int length){
|
||||
String str="abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
Random random=new Random();
|
||||
SecureRandom random=new SecureRandom();
|
||||
StringBuilder sb=new StringBuilder();
|
||||
for(int i=0;i<length;i++){
|
||||
int number=random.nextInt(36);
|
||||
|
||||
9
pom.xml
9
pom.xml
@@ -25,16 +25,17 @@
|
||||
|
||||
<packageDirectory>${project.basedir}/web-service/target/package</packageDirectory>
|
||||
|
||||
<!-- Vert.x 4.5.24 已包含安全修复,无需单独指定 Netty 版本 -->
|
||||
<vertx.version>4.5.24</vertx.version>
|
||||
<!-- Vert.x 4.5.27 包含安全修复,无需单独指定 Netty 版本 -->
|
||||
<vertx.version>4.5.27</vertx.version>
|
||||
<org.reflections.version>0.10.2</org.reflections.version>
|
||||
<lombok.version>1.18.38</lombok.version>
|
||||
<slf4j.version>2.0.16</slf4j.version>
|
||||
<commons-lang3.version>3.18.0</commons-lang3.version>
|
||||
<commons-beanutils2.version>2.0.0</commons-beanutils2.version>
|
||||
<parserVersion>10.2.5</parserVersion>
|
||||
<jackson.version>2.18.6</jackson.version>
|
||||
<!-- Logback 最新稳定版 -->
|
||||
<logback.version>1.5.18</logback.version>
|
||||
<logback.version>1.5.32</logback.version>
|
||||
<junit.version>4.13.2</junit.version>
|
||||
</properties>
|
||||
|
||||
@@ -74,7 +75,7 @@
|
||||
<dependency>
|
||||
<groupId>cn.qaiu</groupId>
|
||||
<artifactId>parser</artifactId>
|
||||
<version>10.2.5</version>
|
||||
<version>${parserVersion}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
@@ -5,15 +5,15 @@
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"dev": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build && node scripts/compress-vs.js",
|
||||
"build:no-compress": "vue-cli-service build",
|
||||
"build": "node scripts/sync-version.js && vue-cli-service build && node scripts/compress-vs.js",
|
||||
"build:no-compress": "node scripts/sync-version.js && vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@monaco-editor/loader": "^1.4.0",
|
||||
"@vueuse/core": "^11.2.0",
|
||||
"axios": "1.13.5",
|
||||
"axios": "1.16.1",
|
||||
"clipboard": "^2.0.11",
|
||||
"core-js": "^3.8.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
|
||||
@@ -36,7 +36,6 @@ if (item) {
|
||||
const darkMode = ref(item)
|
||||
|
||||
watch(darkMode, (newValue) => {
|
||||
console.log(`darkMode: ${newValue}`)
|
||||
window.localStorage.setItem("darkMode", newValue);
|
||||
|
||||
// 发射主题变化事件
|
||||
|
||||
@@ -293,24 +293,6 @@ export default {
|
||||
return clientConfig[type]?.downloadUrl || '#'
|
||||
}
|
||||
|
||||
// 判断是否应该显示下载客户端按钮
|
||||
const shouldShowDownloadButton = (type) => {
|
||||
const os = getOSInfo()
|
||||
switch (type) {
|
||||
case 'CURL':
|
||||
// cURL 在 Windows 上可能需要安装
|
||||
return os === 'windows'
|
||||
case 'ARIA2':
|
||||
// Aria2 需要手动安装
|
||||
return true
|
||||
case 'THUNDER':
|
||||
// 迅雷主要在 Windows 上使用
|
||||
return os === 'windows'
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取操作系统信息
|
||||
const getOSInfo = () => {
|
||||
const userAgent = navigator.userAgent.toLowerCase()
|
||||
@@ -369,7 +351,7 @@ export default {
|
||||
copyToClipboard(link)
|
||||
return
|
||||
}
|
||||
window.open(link, '_blank')
|
||||
window.open(link, '_blank', 'noopener,noreferrer')
|
||||
ElMessage.success('正在唤起迅雷下载')
|
||||
break
|
||||
|
||||
@@ -383,13 +365,6 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
// 下载客户端
|
||||
const downloadClient = (type) => {
|
||||
const url = getClientDownloadUrl(type)
|
||||
window.open(url, '_blank')
|
||||
ElMessage.success(`正在跳转到 ${getClientDisplayName(type)} 下载页面`)
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (bytes) => {
|
||||
if (!bytes) return '未知'
|
||||
@@ -440,9 +415,7 @@ export default {
|
||||
getTextareaRows,
|
||||
goBack,
|
||||
getClientLogo,
|
||||
downloadClient,
|
||||
handleImageError,
|
||||
shouldShowDownloadButton,
|
||||
getClientSupportsCookie,
|
||||
goToAuthConfig
|
||||
}
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
</el-dialog> -->
|
||||
<!-- 顶部反馈栏(小号、灰色、无红边框) -->
|
||||
<div class="feedback-bar">
|
||||
<a href="https://github.com/qaiu/netdisk-fast-download/issues" target="_blank" rel="noopener" class="feedback-link mini">
|
||||
<a :href="githubRepoUrl + '/issues'" target="_blank" rel="noopener" class="feedback-link mini">
|
||||
<i class="fas fa-bug feedback-icon"></i>
|
||||
反馈
|
||||
</a>
|
||||
<a href="https://github.com/qaiu/netdisk-fast-download" target="_blank" rel="noopener" class="feedback-link mini">
|
||||
<a :href="githubRepoUrl" target="_blank" rel="noopener" class="feedback-link mini">
|
||||
<i class="fab fa-github feedback-icon"></i>
|
||||
源码
|
||||
</a>
|
||||
@@ -73,9 +73,9 @@
|
||||
</div>
|
||||
<!-- 项目简介移到卡片内 -->
|
||||
<div class="project-intro">
|
||||
<div class="intro-title">NFD网盘直链解析0.3.0</div>
|
||||
<div class="intro-title">NFD网盘直链解析 {{ projectVersion }}</div>
|
||||
<div class="intro-desc">
|
||||
<div>支持网盘:蓝奏云、蓝奏云优享、小飞机盘、123云盘、iCloud、移动云空间、联想乐云、QQ闪传等 <el-link style="color:#606cf5" href="https://github.com/qaiu/netdisk-fast-download?tab=readme-ov-file#%E7%BD%91%E7%9B%98%E6%94%AF%E6%8C%81%E6%83%85%E5%86%B5" target="_blank"> >> </el-link></div>
|
||||
<div>支持网盘:蓝奏云、蓝奏云优享、小飞机盘、123云盘、iCloud、移动云空间、联想乐云、QQ闪传等 <el-link style="color:#606cf5" :href="githubRepoUrl + '?tab=readme-ov-file#%E7%BD%91%E7%9B%98%E6%94%AF%E6%8C%81%E6%83%85%E5%86%B5'" target="_blank"> >> </el-link></div>
|
||||
<div>文件夹解析支持:蓝奏云、蓝奏云优享、小飞机盘、123云盘</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,7 +90,7 @@
|
||||
<!-- 开关按钮,控制是否自动读取剪切板 -->
|
||||
<el-switch v-model="autoReadClipboard" active-text="自动识别剪切板"></el-switch>
|
||||
|
||||
<el-input placeholder="请粘贴分享链接(http://或https://)" v-model="link" id="url">
|
||||
<el-input placeholder="请粘贴分享链接(http://或https://)" v-model="link" id="url" @paste="onPaste">
|
||||
<template #prepend>分享链接</template>
|
||||
<template #append v-if="!autoReadClipboard">
|
||||
<el-button @click="getPaste(true)">读取剪切板</el-button>
|
||||
@@ -595,11 +595,10 @@ import fileTypeUtils from '@/utils/fileTypeUtils'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { playgroundApi } from '@/utils/playgroundApi'
|
||||
import { testConnection, autoDetect, addDownload, getConfig, saveConfig } from '@/utils/downloaderService'
|
||||
|
||||
export const previewBaseUrl = 'https://nfd-parser.github.io/nfd-preview/preview.html?src=';
|
||||
import { PREVIEW_BASE_URL } from '@/utils/constants'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
name: 'Home',
|
||||
components: { DarkMode, DirectoryTree, DownloadDialog },
|
||||
mixins: [fileTypeUtils],
|
||||
data() {
|
||||
@@ -617,7 +616,7 @@ export default {
|
||||
parseResult: {},
|
||||
downloadUrl: null,
|
||||
directLink: '',
|
||||
previewBaseUrl,
|
||||
previewBaseUrl: PREVIEW_BASE_URL,
|
||||
|
||||
// 功能结果
|
||||
markdownText: '',
|
||||
@@ -714,6 +713,12 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
githubRepoUrl() {
|
||||
return process.env.VUE_APP_GITHUB_REPO_URL
|
||||
},
|
||||
projectVersion() {
|
||||
return process.env.VUE_APP_VERSION || '0.0.0'
|
||||
},
|
||||
// 检查是否配置了认证信息(针对当前链接的网盘类型)
|
||||
hasAuthConfig() {
|
||||
const panType = this.getCurrentPanType()
|
||||
@@ -959,18 +964,16 @@ export default {
|
||||
// 优先使用个人配置
|
||||
if (this.allAuthConfigs[panType]) {
|
||||
config = this.allAuthConfigs[panType]
|
||||
console.log(`[认证] 使用个人配置: ${this.getPanDisplayName(panType)}`)
|
||||
} else {
|
||||
// 从后端随机获取捐赠账号(后端已加密,直接使用 encryptedAuth)
|
||||
try {
|
||||
const response = await axios.get(`${this.baseAPI}/v2/randomAuth`, { params: { panType } })
|
||||
const encryptedAuth = response.data?.data?.encryptedAuth
|
||||
if (encryptedAuth) {
|
||||
console.log(`[认证] 使用捐赠账号: ${this.getPanDisplayName(panType)}`)
|
||||
return encryptedAuth
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`[认证] 无可用捐赠账号: ${this.getPanDisplayName(panType)}`)
|
||||
// no available donated account
|
||||
}
|
||||
return ''
|
||||
}
|
||||
@@ -1091,17 +1094,45 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
// 识别并转换短链输入(如 lz:shareKey@pwd)
|
||||
// 识别并转换短链输入(如 lz:shareKey@pwd),或从文本中提取链接
|
||||
normalizeShortcutInput() {
|
||||
const shortInfo = this.expandShortFormat(this.link)
|
||||
if (!shortInfo) return
|
||||
if (!this.link) return
|
||||
const trimmed = this.link.trim()
|
||||
if (!trimmed) return
|
||||
|
||||
this.link = shortInfo.link
|
||||
if (!this.password && shortInfo.pwd) {
|
||||
this.password = shortInfo.pwd
|
||||
// 已经是直接链接,跳过
|
||||
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) return
|
||||
|
||||
// 尝试短格式
|
||||
const shortInfo = this.expandShortFormat(trimmed)
|
||||
if (shortInfo) {
|
||||
this.link = shortInfo.link
|
||||
if (!this.password && shortInfo.pwd) {
|
||||
this.password = shortInfo.pwd
|
||||
}
|
||||
this.$message.success(`已识别短格式并自动转换,网盘类型: ${shortInfo.name}`)
|
||||
this.updateDirectLink()
|
||||
return
|
||||
}
|
||||
this.$message.success(`已识别短格式并自动转换,网盘类型: ${shortInfo.name}`)
|
||||
this.updateDirectLink()
|
||||
|
||||
// 从文本中自动提取链接
|
||||
const linkInfo = parserUrl.parseLink(trimmed)
|
||||
if (linkInfo.link) {
|
||||
this.link = linkInfo.link
|
||||
const pwd = parserUrl.parsePwd(trimmed)
|
||||
if (!this.password && pwd) {
|
||||
this.password = pwd
|
||||
}
|
||||
this.$message.success(`已从文本中识别到 ${linkInfo.name} 分享链接`)
|
||||
this.updateDirectLink()
|
||||
}
|
||||
},
|
||||
|
||||
// 粘贴事件:从粘贴的文本中自动提取链接
|
||||
onPaste(e) {
|
||||
this.$nextTick(() => {
|
||||
this.normalizeShortcutInput()
|
||||
})
|
||||
},
|
||||
|
||||
// 清除结果
|
||||
@@ -1126,17 +1157,24 @@ export default {
|
||||
params.auth = authParam
|
||||
}
|
||||
const response = await axios.get(`${this.baseAPI}${endpoint}`, { params })
|
||||
|
||||
|
||||
if (response.data.code === 200) {
|
||||
// this.$message.success(response.data.msg || '操作成功')
|
||||
return response.data
|
||||
} else {
|
||||
// 在页面右下角显示一个“查看详情”按钮 可以查看原json
|
||||
// 在页面右下角显示一个”查看详情”按钮 可以查看原json
|
||||
this.errorDetail = response?.data
|
||||
this.errorButtonVisible = true
|
||||
throw new Error(response.data.msg || '操作失败')
|
||||
}
|
||||
} catch (error) {
|
||||
// HTTP 非2xx时,从响应体中提取后端返回的错误信息
|
||||
if (error.response?.data?.msg) {
|
||||
this.errorDetail = error.response.data
|
||||
this.errorButtonVisible = true
|
||||
this.$message.error(error.response.data.msg)
|
||||
throw new Error(error.response.data.msg)
|
||||
}
|
||||
this.$message.error(error.message || '网络错误')
|
||||
throw error
|
||||
} finally {
|
||||
@@ -1309,7 +1347,7 @@ export default {
|
||||
// 文件点击处理
|
||||
handleFileClick(file) {
|
||||
if (file.parserUrl) {
|
||||
window.open(file.parserUrl, '_blank')
|
||||
window.open(file.parserUrl, '_blank', 'noopener,noreferrer')
|
||||
} else {
|
||||
this.$message.warning('该文件暂无下载链接')
|
||||
}
|
||||
@@ -1319,7 +1357,6 @@ export default {
|
||||
async getPaste(isManual = false) {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText()
|
||||
console.log('获取到的文本内容是:', text)
|
||||
|
||||
const shortInfo = this.expandShortFormat(text)
|
||||
if (shortInfo) {
|
||||
@@ -1364,7 +1401,9 @@ export default {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('读取剪切板失败:', error)
|
||||
this.$message.error('读取剪切板失败,请检查浏览器权限')
|
||||
if (isManual) {
|
||||
this.$message.warning('读取剪切板失败,请手动粘贴链接到输入框')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1439,7 +1478,7 @@ export default {
|
||||
错误信息:${JSON.stringify(this.errorDetail, null, 2)}`;
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
this.$message.success('已复制分享信息和错误详情');
|
||||
window.open('https://github.com/qaiu/netdisk-fast-download/issues/new', '_blank');
|
||||
window.open(`${this.githubRepoUrl}/issues/new`, '_blank', 'noopener,noreferrer');
|
||||
}).catch(() => {
|
||||
this.$message.error('复制失败');
|
||||
});
|
||||
@@ -1786,19 +1825,26 @@ export default {
|
||||
}
|
||||
|
||||
// 监听窗口焦点事件
|
||||
window.addEventListener('focus', () => {
|
||||
this._onFocusHandler = () => {
|
||||
if (this.autoReadClipboard) {
|
||||
this.hasClipboardSuccessTip = false // 聚焦时重置,只提示一次
|
||||
this.getPaste()
|
||||
}
|
||||
})
|
||||
}
|
||||
window.addEventListener('focus', this._onFocusHandler)
|
||||
|
||||
// 首次打开页面弹出风险提示
|
||||
if (!window.localStorage.getItem('nfd_risk_ack')) {
|
||||
this.showRiskDialog = true
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
beforeUnmount() {
|
||||
if (this._onFocusHandler) {
|
||||
window.removeEventListener('focus', this._onFocusHandler)
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
downloadUrl(val) {
|
||||
if (!val) {
|
||||
|
||||
@@ -653,22 +653,22 @@
|
||||
<p>更多详细信息,请参考 GitHub 仓库文档:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://github.com/qaiu/netdisk-fast-download/blob/main/parser/doc/JAVASCRIPT_PARSER_GUIDE.md" target="_blank" rel="noopener noreferrer">
|
||||
<a :href="githubRepoUrl + '/blob/main/parser/doc/JAVASCRIPT_PARSER_GUIDE.md'" target="_blank" rel="noopener noreferrer">
|
||||
JavaScript 解析器开发指南
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/qaiu/netdisk-fast-download/blob/main/parser/doc/CUSTOM_PARSER_GUIDE.md" target="_blank" rel="noopener noreferrer">
|
||||
<a :href="githubRepoUrl + '/blob/main/parser/doc/CUSTOM_PARSER_GUIDE.md'" target="_blank" rel="noopener noreferrer">
|
||||
自定义解析器扩展指南
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/qaiu/netdisk-fast-download/blob/main/parser/doc/CUSTOM_PARSER_QUICKSTART.md" target="_blank" rel="noopener noreferrer">
|
||||
<a :href="githubRepoUrl + '/blob/main/parser/doc/CUSTOM_PARSER_QUICKSTART.md'" target="_blank" rel="noopener noreferrer">
|
||||
快速开始教程
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/qaiu/netdisk-fast-download/blob/main/parser/README.md" target="_blank" rel="noopener noreferrer">
|
||||
<a :href="githubRepoUrl + '/blob/main/parser/README.md'" target="_blank" rel="noopener noreferrer">
|
||||
解析器模块文档
|
||||
</a>
|
||||
</li>
|
||||
@@ -858,6 +858,7 @@ export default {
|
||||
},
|
||||
setup() {
|
||||
const router = useRouter();
|
||||
const githubRepoUrl = process.env.VUE_APP_GITHUB_REPO_URL;
|
||||
|
||||
// 语言常量
|
||||
const LANGUAGE = {
|
||||
@@ -1178,7 +1179,7 @@ function parseById(shareLinkInfo, http, logger) {
|
||||
|
||||
// 新窗口打开首页
|
||||
const goHomeInNewWindow = () => {
|
||||
window.open('/', '_blank');
|
||||
window.open('/', '_blank', 'noopener,noreferrer');
|
||||
};
|
||||
|
||||
// 检查是否有未保存的文件
|
||||
@@ -1758,7 +1759,6 @@ function parseFileList(shareLinkInfo, http, logger) {
|
||||
testParams.value.method
|
||||
);
|
||||
|
||||
console.log('测试结果:', result);
|
||||
testResult.value = result;
|
||||
|
||||
// 将日志添加到控制台
|
||||
@@ -1820,10 +1820,8 @@ function parseFileList(shareLinkInfo, http, logger) {
|
||||
loadingList.value = true;
|
||||
try {
|
||||
const result = await playgroundApi.getParserList();
|
||||
console.log('获取解析器列表响应:', result);
|
||||
// 检查响应格式
|
||||
if (result.code === 200 || result.success) {
|
||||
console.log('列表数据:', result.data);
|
||||
parserList.value = result.data || [];
|
||||
} else if (result.data && Array.isArray(result.data)) {
|
||||
// 如果data直接是数组
|
||||
@@ -1857,7 +1855,6 @@ function parseFileList(shareLinkInfo, http, logger) {
|
||||
try {
|
||||
const codeToPublish = currentCode.value;
|
||||
const result = await playgroundApi.saveParser(codeToPublish);
|
||||
console.log('保存解析器响应:', result);
|
||||
// 检查响应格式
|
||||
if (result.code === 200 || result.success) {
|
||||
// 从响应或代码中提取type信息
|
||||
@@ -2223,6 +2220,8 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
|
||||
}, 100);
|
||||
};
|
||||
|
||||
let themeObserver = null;
|
||||
|
||||
onMounted(async () => {
|
||||
// 初始化移动端检测
|
||||
updateIsMobile();
|
||||
@@ -2249,10 +2248,10 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
|
||||
const html = document.documentElement;
|
||||
if (html && html.classList) {
|
||||
try {
|
||||
const observer = new MutationObserver(() => {
|
||||
themeObserver = new MutationObserver(() => {
|
||||
checkDarkMode();
|
||||
});
|
||||
observer.observe(html, {
|
||||
themeObserver.observe(html, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class', 'data-theme']
|
||||
});
|
||||
@@ -2269,9 +2268,11 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
|
||||
window.removeEventListener('resize', updateIsMobile);
|
||||
// 移除页面关闭/刷新前的提示
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
themeObserver?.disconnect();
|
||||
});
|
||||
|
||||
return {
|
||||
githubRepoUrl,
|
||||
LANGUAGE,
|
||||
editorRef,
|
||||
jsCode,
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import fileTypeUtils from '@/utils/fileTypeUtils'
|
||||
import { previewBaseUrl } from '@/views/Home.vue'
|
||||
import { PREVIEW_BASE_URL } from '@/utils/constants'
|
||||
|
||||
export default {
|
||||
name: 'ShowFile',
|
||||
@@ -44,7 +44,7 @@ export default {
|
||||
downloadUrl: '',
|
||||
shareUrl: '', // 添加原始分享链接
|
||||
fileTypeUtils,
|
||||
previewBaseUrl
|
||||
previewBaseUrl: PREVIEW_BASE_URL
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -73,7 +73,7 @@ export default {
|
||||
this.parseResult = res.data
|
||||
this.downloadUrl = res.data.data?.directLink
|
||||
} catch (e) {
|
||||
this.error = '解析失败'
|
||||
this.error = e.response?.data?.msg || e.response?.data?.error || '解析失败'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export default {
|
||||
const res = await axios.get('/v2/getFileList', { params: { url: this.url } })
|
||||
this.directoryData = res.data.data || []
|
||||
} catch (e) {
|
||||
this.error = '目录解析失败'
|
||||
this.error = e.response?.data?.msg || e.response?.data?.error || '目录解析失败'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@Slf4j
|
||||
public class RateLimiter {
|
||||
@@ -51,12 +52,12 @@ public class RateLimiter {
|
||||
return new RequestInfo(1, currentTime);
|
||||
} else {
|
||||
// 增加计数器
|
||||
requestInfo.count++;
|
||||
requestInfo.count.incrementAndGet();
|
||||
return requestInfo;
|
||||
}
|
||||
});
|
||||
|
||||
if (info.count > MAX_REQUESTS) {
|
||||
if (info.count.get() > MAX_REQUESTS) {
|
||||
// 超过限制
|
||||
// 计算剩余时间
|
||||
long remainingTime = TIME_WINDOW - (System.currentTimeMillis() - info.timestamp);
|
||||
@@ -71,11 +72,11 @@ public class RateLimiter {
|
||||
}
|
||||
|
||||
private static class RequestInfo {
|
||||
volatile int count;
|
||||
final AtomicInteger count;
|
||||
volatile long timestamp;
|
||||
|
||||
RequestInfo(int count, long time) {
|
||||
this.count = count;
|
||||
this.count = new AtomicInteger(count);
|
||||
this.timestamp = time;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -93,9 +94,10 @@ public class JwtUtil {
|
||||
String encodedPayload = parts[1];
|
||||
String signature = parts[2];
|
||||
|
||||
// 验证签名
|
||||
// 验证签名(使用常量时间比较防止时序攻击)
|
||||
String expectedSignature = hmacSha256(encodedHeader + "." + encodedPayload, SECRET_KEY);
|
||||
if (!expectedSignature.equals(signature)) {
|
||||
if (!MessageDigest.isEqual(expectedSignature.getBytes(StandardCharsets.UTF_8),
|
||||
signature.getBytes(StandardCharsets.UTF_8))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -80,8 +80,10 @@ public class PasswordUtil {
|
||||
byte[] calculatedHash = md.digest(plainPassword.getBytes(StandardCharsets.UTF_8));
|
||||
String calculatedHashBase64 = Base64.getEncoder().encodeToString(calculatedHash);
|
||||
|
||||
// 比较计算出的哈希值和存储的哈希值
|
||||
return hashBase64.equals(calculatedHashBase64);
|
||||
// 比较计算出的哈希值和存储的哈希值(使用常量时间比较防止时序攻击)
|
||||
return MessageDigest.isEqual(
|
||||
hashBase64.getBytes(StandardCharsets.UTF_8),
|
||||
calculatedHashBase64.getBytes(StandardCharsets.UTF_8));
|
||||
} catch (Exception e) {
|
||||
// 如果发生异常(例如格式不正确),返回false
|
||||
return false;
|
||||
|
||||
@@ -28,6 +28,7 @@ import java.io.BufferedReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
@@ -129,8 +130,11 @@ public class PlaygroundApi {
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if (config.getPassword().equals(password)) {
|
||||
// 验证密码(使用常量时间比较防止时序攻击)
|
||||
String storedPassword = config.getPassword();
|
||||
if (storedPassword != null && MessageDigest.isEqual(
|
||||
storedPassword.getBytes(StandardCharsets.UTF_8),
|
||||
password.getBytes(StandardCharsets.UTF_8))) {
|
||||
String token = config.generateToken();
|
||||
JsonObject tokenData = new JsonObject().put("token", token);
|
||||
promise.complete(JsonResult.data(tokenData).toJsonObject());
|
||||
@@ -299,7 +303,6 @@ public class PlaygroundApi {
|
||||
}).onFailure(e -> {
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
String errorMessage = e.getMessage();
|
||||
String stackTrace = getStackTrace(e);
|
||||
|
||||
log.error("演练场执行失败", e);
|
||||
|
||||
@@ -317,7 +320,6 @@ public class PlaygroundApi {
|
||||
PlaygroundTestResp response = PlaygroundTestResp.builder()
|
||||
.success(false)
|
||||
.error(errorMessage)
|
||||
.stackTrace(stackTrace)
|
||||
.executionTime(executionTime)
|
||||
.logs(respLogs)
|
||||
.build();
|
||||
@@ -328,14 +330,12 @@ public class PlaygroundApi {
|
||||
} catch (Exception e) {
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
String errorMessage = e.getMessage();
|
||||
String stackTrace = getStackTrace(e);
|
||||
|
||||
log.error("演练场初始化失败", e);
|
||||
|
||||
PlaygroundTestResp response = PlaygroundTestResp.builder()
|
||||
.success(false)
|
||||
.error(errorMessage)
|
||||
.stackTrace(stackTrace)
|
||||
.executionTime(executionTime)
|
||||
.logs(new ArrayList<>())
|
||||
.build();
|
||||
@@ -346,8 +346,7 @@ public class PlaygroundApi {
|
||||
log.error("解析请求参数失败", e);
|
||||
promise.complete(JsonObject.mapFrom(PlaygroundTestResp.builder()
|
||||
.success(false)
|
||||
.error("解析请求参数失败: " + e.getMessage())
|
||||
.stackTrace(getStackTrace(e))
|
||||
.error("解析请求参数失败")
|
||||
.build()));
|
||||
}
|
||||
|
||||
@@ -696,18 +695,5 @@ public class PlaygroundApi {
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取异常堆栈信息
|
||||
*/
|
||||
private String getStackTrace(Throwable throwable) {
|
||||
if (throwable == null) {
|
||||
return "";
|
||||
}
|
||||
java.io.StringWriter sw = new java.io.StringWriter();
|
||||
java.io.PrintWriter pw = new java.io.PrintWriter(sw);
|
||||
throwable.printStackTrace(pw);
|
||||
return sw.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -125,18 +125,18 @@ public class UserServiceImpl implements UserService {
|
||||
if (rows.size() == 0) {
|
||||
promise.complete(new JsonObject()
|
||||
.put("success", false)
|
||||
.put("message", "用户不存在"));
|
||||
.put("message", "用户名或密码错误"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Row row = rows.iterator().next();
|
||||
SysUser existUser = rowToUser(row);
|
||||
|
||||
|
||||
// 验证密码
|
||||
if (!PasswordUtil.checkPassword(user.getPassword(), existUser.getPassword())) {
|
||||
promise.complete(new JsonObject()
|
||||
.put("success", false)
|
||||
.put("message", "密码错误"));
|
||||
.put("message", "用户名或密码错误"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ public class UserServiceImpl implements UserService {
|
||||
log.error("登录查询失败", err);
|
||||
promise.complete(new JsonObject()
|
||||
.put("success", false)
|
||||
.put("message", "登录失败: " + err.getMessage()));
|
||||
.put("message", "登录失败,请稍后重试"));
|
||||
});
|
||||
|
||||
return promise.future();
|
||||
@@ -189,10 +189,10 @@ public class UserServiceImpl implements UserService {
|
||||
.execute(Tuple.of(username))
|
||||
.onSuccess(rows -> {
|
||||
if (rows.size() == 0) {
|
||||
promise.fail("用户不存在");
|
||||
promise.fail("用户名或密码错误");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Row row = rows.iterator().next();
|
||||
SysUser user = rowToUser(row);
|
||||
promise.complete(filterSensitiveInfo(user));
|
||||
@@ -296,10 +296,10 @@ public class UserServiceImpl implements UserService {
|
||||
.execute(Tuple.of(user.getUsername()))
|
||||
.onSuccess(rows -> {
|
||||
if (rows.size() == 0) {
|
||||
promise.fail("用户不存在");
|
||||
promise.fail("用户名或密码错误");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Row row = rows.iterator().next();
|
||||
SysUser existUser = rowToUser(row);
|
||||
|
||||
@@ -406,7 +406,7 @@ public class UserServiceImpl implements UserService {
|
||||
.onFailure(err -> {
|
||||
promise.complete(new JsonObject()
|
||||
.put("success", false)
|
||||
.put("message", "用户不存在"));
|
||||
.put("message", "认证失败,请重新登录"));
|
||||
});
|
||||
|
||||
return promise.future();
|
||||
|
||||
Reference in New Issue
Block a user