mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2026-06-10 23:47:29 +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
|
sdkTest.log
|
||||||
app.yml
|
app.yml
|
||||||
app-local.yml
|
app-local.yml
|
||||||
|
secret.yml
|
||||||
|
|
||||||
|
|
||||||
#some local files
|
#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 && \
|
RUN unzip netdisk-fast-download-bin.zip && \
|
||||||
mv netdisk-fast-download/* ./ && \
|
mv netdisk-fast-download/* ./ && \
|
||||||
rm netdisk-fast-download-bin.zip && \
|
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>
|
<dependency>
|
||||||
<groupId>org.postgresql</groupId>
|
<groupId>org.postgresql</groupId>
|
||||||
<artifactId>postgresql</artifactId>
|
<artifactId>postgresql</artifactId>
|
||||||
<version>42.7.3</version>
|
<version>42.7.11</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|||||||
@@ -127,8 +127,9 @@ public class RouterHandlerFactory implements BaseHttpApi {
|
|||||||
// 错误请求处理
|
// 错误请求处理
|
||||||
mainRouter.errorHandler(405, ctx -> doFireJsonResultResponse(ctx, JsonResult
|
mainRouter.errorHandler(405, ctx -> doFireJsonResultResponse(ctx, JsonResult
|
||||||
.error("Method Not Allowed", 405)));
|
.error("Method Not Allowed", 405)));
|
||||||
mainRouter.errorHandler(404, ctx -> ctx.response().setStatusCode(404).setChunked(true)
|
mainRouter.errorHandler(404, ctx -> {
|
||||||
.end("Internal server error: 404 not found"));
|
ctx.response().setStatusCode(404).end("404 not found");
|
||||||
|
});
|
||||||
|
|
||||||
return mainRouter;
|
return mainRouter;
|
||||||
}
|
}
|
||||||
@@ -179,8 +180,9 @@ public class RouterHandlerFactory implements BaseHttpApi {
|
|||||||
if (ctx.statusCode() == 503 || ctx.failure() == null) {
|
if (ctx.statusCode() == 503 || ctx.failure() == null) {
|
||||||
doFireJsonResultResponse(ctx, JsonResult.error("未知异常, 请联系管理员"), 503);
|
doFireJsonResultResponse(ctx, JsonResult.error("未知异常, 请联系管理员"), 503);
|
||||||
} else {
|
} else {
|
||||||
ctx.failure().printStackTrace();
|
LOGGER.error("路由处理失败", ctx.failure());
|
||||||
doFireJsonResultResponse(ctx, JsonResult.error(ctx.failure().getMessage()), 500);
|
String msg = ctx.failure() != null ? ctx.failure().getMessage() : "未知异常";
|
||||||
|
doFireJsonResultResponse(ctx, JsonResult.error(msg), 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (method.isAnnotationPresent(SockRouteMapper.class)) {
|
} else if (method.isAnnotationPresent(SockRouteMapper.class)) {
|
||||||
@@ -198,7 +200,7 @@ public class RouterHandlerFactory implements BaseHttpApi {
|
|||||||
try {
|
try {
|
||||||
ReflectionUtil.invokeWithArguments(method, instance, sock);
|
ReflectionUtil.invokeWithArguments(method, instance, sock);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
e.printStackTrace();
|
LOGGER.error("WebSocket处理异常", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (url.endsWith("*")) {
|
if (url.endsWith("*")) {
|
||||||
@@ -322,7 +324,7 @@ public class RouterHandlerFactory implements BaseHttpApi {
|
|||||||
parameterValueList.put(k, entity);
|
parameterValueList.put(k, entity);
|
||||||
}
|
}
|
||||||
} catch (ClassNotFoundException e) {
|
} catch (ClassNotFoundException e) {
|
||||||
e.printStackTrace();
|
LOGGER.error("实体类绑定异常: {}", typeName, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -365,7 +367,7 @@ public class RouterHandlerFactory implements BaseHttpApi {
|
|||||||
Object entity = ParamUtil.multiMapToEntity(queryParams, aClass);
|
Object entity = ParamUtil.multiMapToEntity(queryParams, aClass);
|
||||||
parameterValueList.put(k, entity);
|
parameterValueList.put(k, entity);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
LOGGER.error("参数绑定异常: {}", v.getRight().getName(), e);
|
||||||
}
|
}
|
||||||
} else if (parameterValueList.get(k) == null
|
} else if (parameterValueList.get(k) == null
|
||||||
&& JsonObject.class.getName().equals(v.getRight().getName())) {
|
&& JsonObject.class.getName().equals(v.getRight().getName())) {
|
||||||
@@ -408,22 +410,19 @@ public class RouterHandlerFactory implements BaseHttpApi {
|
|||||||
doFireJsonResultResponse(ctx, JsonResult.data(null));
|
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 {
|
} else {
|
||||||
doFireJsonResultResponse(ctx, JsonResult.data(data));
|
doFireJsonResultResponse(ctx, JsonResult.data(data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
e.printStackTrace();
|
LOGGER.error("请求处理异常", e);
|
||||||
String err = e.getMessage();
|
String msg = e.getMessage() != null ? e.getMessage() : "服务器内部错误";
|
||||||
if (e.getCause() != null) {
|
doFireJsonResultResponse(ctx, JsonResult.error(msg), 500);
|
||||||
if (e.getCause() instanceof InvocationTargetException) {
|
|
||||||
err = ((InvocationTargetException) e.getCause()).getTargetException().getMessage();
|
|
||||||
} else {
|
|
||||||
err = e.getCause().getMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
doFireJsonResultResponse(ctx, JsonResult.error(err), 500);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package cn.qaiu.vx.core.util;
|
package cn.qaiu.vx.core.util;
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* vertx 上下文外的本地容器 为不在vertx线程的方法传递数据
|
* vertx 上下文外的本地容器 为不在vertx线程的方法传递数据
|
||||||
@@ -10,11 +10,10 @@ import java.util.Map;
|
|||||||
* @author <a href="https://qaiu.top">QAIU</a>
|
* @author <a href="https://qaiu.top">QAIU</a>
|
||||||
*/
|
*/
|
||||||
public class LocalConstant {
|
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) {
|
public static Map<String, Object> put(String k, Object v) {
|
||||||
if (LOCAL_CONST.containsKey(k)) return LOCAL_CONST;
|
LOCAL_CONST.putIfAbsent(k, v);
|
||||||
LOCAL_CONST.put(k, v);
|
|
||||||
return LOCAL_CONST;
|
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>
|
<groupId>cn.qaiu</groupId>
|
||||||
<artifactId>parser</artifactId>
|
<artifactId>parser</artifactId>
|
||||||
<version>10.2.5</version>
|
<version>${parserVersion}</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<name>cn.qaiu:parser</name>
|
<name>cn.qaiu:parser</name>
|
||||||
@@ -35,9 +35,9 @@
|
|||||||
</developers>
|
</developers>
|
||||||
|
|
||||||
<scm>
|
<scm>
|
||||||
<connection>scm:git:https://github.com/qaiu/netdisk-fast-download.git</connection>
|
<connection>scm:git:https://github.com/${github.owner}/${github.repo}.git</connection>
|
||||||
<developerConnection>scm:git:ssh://git@github.com:qaiu/netdisk-fast-download.git</developerConnection>
|
<developerConnection>scm:git:ssh://git@github.com:${github.owner}/${github.repo}.git</developerConnection>
|
||||||
<url>https://github.com/qaiu/netdisk-fast-download</url>
|
<url>https://github.com/${github.owner}/${github.repo}</url>
|
||||||
</scm>
|
</scm>
|
||||||
|
|
||||||
<distributionManagement>
|
<distributionManagement>
|
||||||
@@ -52,20 +52,19 @@
|
|||||||
</distributionManagement>
|
</distributionManagement>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<revision>0.2.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>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
|
||||||
<!-- Versions -->
|
<!-- Versions -->
|
||||||
<vertx.version>4.5.24</vertx.version>
|
<vertx.version>4.5.27</vertx.version>
|
||||||
<org.reflections.version>0.10.2</org.reflections.version>
|
<org.reflections.version>0.10.2</org.reflections.version>
|
||||||
<lombok.version>1.18.38</lombok.version>
|
<lombok.version>1.18.38</lombok.version>
|
||||||
<slf4j.version>2.0.16</slf4j.version>
|
<slf4j.version>2.0.16</slf4j.version>
|
||||||
<commons-lang3.version>3.18.0</commons-lang3.version>
|
<commons-lang3.version>3.18.0</commons-lang3.version>
|
||||||
<jackson.version>2.18.6</jackson.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>
|
<junit.version>4.13.2</junit.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
@@ -124,6 +123,41 @@
|
|||||||
<build>
|
<build>
|
||||||
<plugins>
|
<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>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<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 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 final Object EXECUTOR_LOCK = new Object();
|
||||||
|
|
||||||
private static String FETCH_RUNTIME_JS = null;
|
private static String FETCH_RUNTIME_JS = null;
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import java.security.spec.X509EncodedKeySpec;
|
|||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HexFormat;
|
import java.util.HexFormat;
|
||||||
import java.util.Random;
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@@ -299,7 +298,7 @@ public class AESUtils {
|
|||||||
//length用户要求产生字符串的长度
|
//length用户要求产生字符串的长度
|
||||||
public static String getRandomString(int length){
|
public static String getRandomString(int length){
|
||||||
String str="abcdefghijklmnopqrstuvwxyz0123456789";
|
String str="abcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
Random random=new Random();
|
SecureRandom random=new SecureRandom();
|
||||||
StringBuilder sb=new StringBuilder();
|
StringBuilder sb=new StringBuilder();
|
||||||
for(int i=0;i<length;i++){
|
for(int i=0;i<length;i++){
|
||||||
int number=random.nextInt(36);
|
int number=random.nextInt(36);
|
||||||
|
|||||||
9
pom.xml
9
pom.xml
@@ -25,16 +25,17 @@
|
|||||||
|
|
||||||
<packageDirectory>${project.basedir}/web-service/target/package</packageDirectory>
|
<packageDirectory>${project.basedir}/web-service/target/package</packageDirectory>
|
||||||
|
|
||||||
<!-- Vert.x 4.5.24 已包含安全修复,无需单独指定 Netty 版本 -->
|
<!-- Vert.x 4.5.27 包含安全修复,无需单独指定 Netty 版本 -->
|
||||||
<vertx.version>4.5.24</vertx.version>
|
<vertx.version>4.5.27</vertx.version>
|
||||||
<org.reflections.version>0.10.2</org.reflections.version>
|
<org.reflections.version>0.10.2</org.reflections.version>
|
||||||
<lombok.version>1.18.38</lombok.version>
|
<lombok.version>1.18.38</lombok.version>
|
||||||
<slf4j.version>2.0.16</slf4j.version>
|
<slf4j.version>2.0.16</slf4j.version>
|
||||||
<commons-lang3.version>3.18.0</commons-lang3.version>
|
<commons-lang3.version>3.18.0</commons-lang3.version>
|
||||||
<commons-beanutils2.version>2.0.0</commons-beanutils2.version>
|
<commons-beanutils2.version>2.0.0</commons-beanutils2.version>
|
||||||
|
<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.18</logback.version>
|
<logback.version>1.5.32</logback.version>
|
||||||
<junit.version>4.13.2</junit.version>
|
<junit.version>4.13.2</junit.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.qaiu</groupId>
|
<groupId>cn.qaiu</groupId>
|
||||||
<artifactId>parser</artifactId>
|
<artifactId>parser</artifactId>
|
||||||
<version>10.2.5</version>
|
<version>${parserVersion}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|||||||
@@ -5,15 +5,15 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
"dev": "vue-cli-service serve",
|
"dev": "vue-cli-service serve",
|
||||||
"build": "vue-cli-service build && node scripts/compress-vs.js",
|
"build": "node scripts/sync-version.js && vue-cli-service build && node scripts/compress-vs.js",
|
||||||
"build:no-compress": "vue-cli-service build",
|
"build:no-compress": "node scripts/sync-version.js && vue-cli-service build",
|
||||||
"lint": "vue-cli-service lint"
|
"lint": "vue-cli-service lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@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.13.5",
|
"axios": "1.16.1",
|
||||||
"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",
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ if (item) {
|
|||||||
const darkMode = ref(item)
|
const darkMode = ref(item)
|
||||||
|
|
||||||
watch(darkMode, (newValue) => {
|
watch(darkMode, (newValue) => {
|
||||||
console.log(`darkMode: ${newValue}`)
|
|
||||||
window.localStorage.setItem("darkMode", newValue);
|
window.localStorage.setItem("darkMode", newValue);
|
||||||
|
|
||||||
// 发射主题变化事件
|
// 发射主题变化事件
|
||||||
|
|||||||
@@ -293,24 +293,6 @@ export default {
|
|||||||
return clientConfig[type]?.downloadUrl || '#'
|
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 getOSInfo = () => {
|
||||||
const userAgent = navigator.userAgent.toLowerCase()
|
const userAgent = navigator.userAgent.toLowerCase()
|
||||||
@@ -369,7 +351,7 @@ export default {
|
|||||||
copyToClipboard(link)
|
copyToClipboard(link)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
window.open(link, '_blank')
|
window.open(link, '_blank', 'noopener,noreferrer')
|
||||||
ElMessage.success('正在唤起迅雷下载')
|
ElMessage.success('正在唤起迅雷下载')
|
||||||
break
|
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) => {
|
const formatFileSize = (bytes) => {
|
||||||
if (!bytes) return '未知'
|
if (!bytes) return '未知'
|
||||||
@@ -440,9 +415,7 @@ export default {
|
|||||||
getTextareaRows,
|
getTextareaRows,
|
||||||
goBack,
|
goBack,
|
||||||
getClientLogo,
|
getClientLogo,
|
||||||
downloadClient,
|
|
||||||
handleImageError,
|
handleImageError,
|
||||||
shouldShowDownloadButton,
|
|
||||||
getClientSupportsCookie,
|
getClientSupportsCookie,
|
||||||
goToAuthConfig
|
goToAuthConfig
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,11 +19,11 @@
|
|||||||
</el-dialog> -->
|
</el-dialog> -->
|
||||||
<!-- 顶部反馈栏(小号、灰色、无红边框) -->
|
<!-- 顶部反馈栏(小号、灰色、无红边框) -->
|
||||||
<div class="feedback-bar">
|
<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>
|
<i class="fas fa-bug feedback-icon"></i>
|
||||||
反馈
|
反馈
|
||||||
</a>
|
</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>
|
<i class="fab fa-github feedback-icon"></i>
|
||||||
源码
|
源码
|
||||||
</a>
|
</a>
|
||||||
@@ -73,9 +73,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 项目简介移到卡片内 -->
|
<!-- 项目简介移到卡片内 -->
|
||||||
<div class="project-intro">
|
<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 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>文件夹解析支持:蓝奏云、蓝奏云优享、小飞机盘、123云盘</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
<!-- 开关按钮,控制是否自动读取剪切板 -->
|
<!-- 开关按钮,控制是否自动读取剪切板 -->
|
||||||
<el-switch v-model="autoReadClipboard" active-text="自动识别剪切板"></el-switch>
|
<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 #prepend>分享链接</template>
|
||||||
<template #append v-if="!autoReadClipboard">
|
<template #append v-if="!autoReadClipboard">
|
||||||
<el-button @click="getPaste(true)">读取剪切板</el-button>
|
<el-button @click="getPaste(true)">读取剪切板</el-button>
|
||||||
@@ -595,11 +595,10 @@ import fileTypeUtils from '@/utils/fileTypeUtils'
|
|||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { playgroundApi } from '@/utils/playgroundApi'
|
import { playgroundApi } from '@/utils/playgroundApi'
|
||||||
import { testConnection, autoDetect, addDownload, getConfig, saveConfig } from '@/utils/downloaderService'
|
import { testConnection, autoDetect, addDownload, getConfig, saveConfig } from '@/utils/downloaderService'
|
||||||
|
import { PREVIEW_BASE_URL } from '@/utils/constants'
|
||||||
export const previewBaseUrl = 'https://nfd-parser.github.io/nfd-preview/preview.html?src=';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'Home',
|
||||||
components: { DarkMode, DirectoryTree, DownloadDialog },
|
components: { DarkMode, DirectoryTree, DownloadDialog },
|
||||||
mixins: [fileTypeUtils],
|
mixins: [fileTypeUtils],
|
||||||
data() {
|
data() {
|
||||||
@@ -617,7 +616,7 @@ export default {
|
|||||||
parseResult: {},
|
parseResult: {},
|
||||||
downloadUrl: null,
|
downloadUrl: null,
|
||||||
directLink: '',
|
directLink: '',
|
||||||
previewBaseUrl,
|
previewBaseUrl: PREVIEW_BASE_URL,
|
||||||
|
|
||||||
// 功能结果
|
// 功能结果
|
||||||
markdownText: '',
|
markdownText: '',
|
||||||
@@ -714,6 +713,12 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
githubRepoUrl() {
|
||||||
|
return process.env.VUE_APP_GITHUB_REPO_URL
|
||||||
|
},
|
||||||
|
projectVersion() {
|
||||||
|
return process.env.VUE_APP_VERSION || '0.0.0'
|
||||||
|
},
|
||||||
// 检查是否配置了认证信息(针对当前链接的网盘类型)
|
// 检查是否配置了认证信息(针对当前链接的网盘类型)
|
||||||
hasAuthConfig() {
|
hasAuthConfig() {
|
||||||
const panType = this.getCurrentPanType()
|
const panType = this.getCurrentPanType()
|
||||||
@@ -959,18 +964,16 @@ export default {
|
|||||||
// 优先使用个人配置
|
// 优先使用个人配置
|
||||||
if (this.allAuthConfigs[panType]) {
|
if (this.allAuthConfigs[panType]) {
|
||||||
config = this.allAuthConfigs[panType]
|
config = this.allAuthConfigs[panType]
|
||||||
console.log(`[认证] 使用个人配置: ${this.getPanDisplayName(panType)}`)
|
|
||||||
} else {
|
} else {
|
||||||
// 从后端随机获取捐赠账号(后端已加密,直接使用 encryptedAuth)
|
// 从后端随机获取捐赠账号(后端已加密,直接使用 encryptedAuth)
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${this.baseAPI}/v2/randomAuth`, { params: { panType } })
|
const response = await axios.get(`${this.baseAPI}/v2/randomAuth`, { params: { panType } })
|
||||||
const encryptedAuth = response.data?.data?.encryptedAuth
|
const encryptedAuth = response.data?.data?.encryptedAuth
|
||||||
if (encryptedAuth) {
|
if (encryptedAuth) {
|
||||||
console.log(`[认证] 使用捐赠账号: ${this.getPanDisplayName(panType)}`)
|
|
||||||
return encryptedAuth
|
return encryptedAuth
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`[认证] 无可用捐赠账号: ${this.getPanDisplayName(panType)}`)
|
// no available donated account
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@@ -1091,17 +1094,45 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 识别并转换短链输入(如 lz:shareKey@pwd)
|
// 识别并转换短链输入(如 lz:shareKey@pwd),或从文本中提取链接
|
||||||
normalizeShortcutInput() {
|
normalizeShortcutInput() {
|
||||||
const shortInfo = this.expandShortFormat(this.link)
|
if (!this.link) return
|
||||||
if (!shortInfo) return
|
const trimmed = this.link.trim()
|
||||||
|
if (!trimmed) return
|
||||||
|
|
||||||
this.link = shortInfo.link
|
// 已经是直接链接,跳过
|
||||||
if (!this.password && shortInfo.pwd) {
|
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) return
|
||||||
this.password = shortInfo.pwd
|
|
||||||
|
// 尝试短格式
|
||||||
|
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
|
params.auth = authParam
|
||||||
}
|
}
|
||||||
const response = await axios.get(`${this.baseAPI}${endpoint}`, { params })
|
const response = await axios.get(`${this.baseAPI}${endpoint}`, { params })
|
||||||
|
|
||||||
if (response.data.code === 200) {
|
if (response.data.code === 200) {
|
||||||
// this.$message.success(response.data.msg || '操作成功')
|
// this.$message.success(response.data.msg || '操作成功')
|
||||||
return response.data
|
return response.data
|
||||||
} else {
|
} else {
|
||||||
// 在页面右下角显示一个“查看详情”按钮 可以查看原json
|
// 在页面右下角显示一个”查看详情”按钮 可以查看原json
|
||||||
this.errorDetail = response?.data
|
this.errorDetail = response?.data
|
||||||
this.errorButtonVisible = true
|
this.errorButtonVisible = true
|
||||||
throw new Error(response.data.msg || '操作失败')
|
throw new Error(response.data.msg || '操作失败')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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 || '网络错误')
|
this.$message.error(error.message || '网络错误')
|
||||||
throw error
|
throw error
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1309,7 +1347,7 @@ export default {
|
|||||||
// 文件点击处理
|
// 文件点击处理
|
||||||
handleFileClick(file) {
|
handleFileClick(file) {
|
||||||
if (file.parserUrl) {
|
if (file.parserUrl) {
|
||||||
window.open(file.parserUrl, '_blank')
|
window.open(file.parserUrl, '_blank', 'noopener,noreferrer')
|
||||||
} else {
|
} else {
|
||||||
this.$message.warning('该文件暂无下载链接')
|
this.$message.warning('该文件暂无下载链接')
|
||||||
}
|
}
|
||||||
@@ -1319,7 +1357,6 @@ export default {
|
|||||||
async getPaste(isManual = false) {
|
async getPaste(isManual = false) {
|
||||||
try {
|
try {
|
||||||
const text = await navigator.clipboard.readText()
|
const text = await navigator.clipboard.readText()
|
||||||
console.log('获取到的文本内容是:', text)
|
|
||||||
|
|
||||||
const shortInfo = this.expandShortFormat(text)
|
const shortInfo = this.expandShortFormat(text)
|
||||||
if (shortInfo) {
|
if (shortInfo) {
|
||||||
@@ -1364,7 +1401,9 @@ export default {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('读取剪切板失败:', error)
|
console.error('读取剪切板失败:', error)
|
||||||
this.$message.error('读取剪切板失败,请检查浏览器权限')
|
if (isManual) {
|
||||||
|
this.$message.warning('读取剪切板失败,请手动粘贴链接到输入框')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1439,7 +1478,7 @@ export default {
|
|||||||
错误信息:${JSON.stringify(this.errorDetail, null, 2)}`;
|
错误信息:${JSON.stringify(this.errorDetail, null, 2)}`;
|
||||||
navigator.clipboard.writeText(text).then(() => {
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
this.$message.success('已复制分享信息和错误详情');
|
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(() => {
|
}).catch(() => {
|
||||||
this.$message.error('复制失败');
|
this.$message.error('复制失败');
|
||||||
});
|
});
|
||||||
@@ -1786,19 +1825,26 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 监听窗口焦点事件
|
// 监听窗口焦点事件
|
||||||
window.addEventListener('focus', () => {
|
this._onFocusHandler = () => {
|
||||||
if (this.autoReadClipboard) {
|
if (this.autoReadClipboard) {
|
||||||
this.hasClipboardSuccessTip = false // 聚焦时重置,只提示一次
|
this.hasClipboardSuccessTip = false // 聚焦时重置,只提示一次
|
||||||
this.getPaste()
|
this.getPaste()
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
window.addEventListener('focus', this._onFocusHandler)
|
||||||
|
|
||||||
// 首次打开页面弹出风险提示
|
// 首次打开页面弹出风险提示
|
||||||
if (!window.localStorage.getItem('nfd_risk_ack')) {
|
if (!window.localStorage.getItem('nfd_risk_ack')) {
|
||||||
this.showRiskDialog = true
|
this.showRiskDialog = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
beforeUnmount() {
|
||||||
|
if (this._onFocusHandler) {
|
||||||
|
window.removeEventListener('focus', this._onFocusHandler)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
downloadUrl(val) {
|
downloadUrl(val) {
|
||||||
if (!val) {
|
if (!val) {
|
||||||
|
|||||||
@@ -653,22 +653,22 @@
|
|||||||
<p>更多详细信息,请参考 GitHub 仓库文档:</p>
|
<p>更多详细信息,请参考 GitHub 仓库文档:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<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 解析器开发指南
|
JavaScript 解析器开发指南
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -858,6 +858,7 @@ export default {
|
|||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const githubRepoUrl = process.env.VUE_APP_GITHUB_REPO_URL;
|
||||||
|
|
||||||
// 语言常量
|
// 语言常量
|
||||||
const LANGUAGE = {
|
const LANGUAGE = {
|
||||||
@@ -1178,7 +1179,7 @@ function parseById(shareLinkInfo, http, logger) {
|
|||||||
|
|
||||||
// 新窗口打开首页
|
// 新窗口打开首页
|
||||||
const goHomeInNewWindow = () => {
|
const goHomeInNewWindow = () => {
|
||||||
window.open('/', '_blank');
|
window.open('/', '_blank', 'noopener,noreferrer');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 检查是否有未保存的文件
|
// 检查是否有未保存的文件
|
||||||
@@ -1758,7 +1759,6 @@ function parseFileList(shareLinkInfo, http, logger) {
|
|||||||
testParams.value.method
|
testParams.value.method
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('测试结果:', result);
|
|
||||||
testResult.value = result;
|
testResult.value = result;
|
||||||
|
|
||||||
// 将日志添加到控制台
|
// 将日志添加到控制台
|
||||||
@@ -1820,10 +1820,8 @@ function parseFileList(shareLinkInfo, http, logger) {
|
|||||||
loadingList.value = true;
|
loadingList.value = true;
|
||||||
try {
|
try {
|
||||||
const result = await playgroundApi.getParserList();
|
const result = await playgroundApi.getParserList();
|
||||||
console.log('获取解析器列表响应:', result);
|
|
||||||
// 检查响应格式
|
// 检查响应格式
|
||||||
if (result.code === 200 || result.success) {
|
if (result.code === 200 || result.success) {
|
||||||
console.log('列表数据:', result.data);
|
|
||||||
parserList.value = result.data || [];
|
parserList.value = result.data || [];
|
||||||
} else if (result.data && Array.isArray(result.data)) {
|
} else if (result.data && Array.isArray(result.data)) {
|
||||||
// 如果data直接是数组
|
// 如果data直接是数组
|
||||||
@@ -1857,7 +1855,6 @@ function parseFileList(shareLinkInfo, http, logger) {
|
|||||||
try {
|
try {
|
||||||
const codeToPublish = currentCode.value;
|
const codeToPublish = currentCode.value;
|
||||||
const result = await playgroundApi.saveParser(codeToPublish);
|
const result = await playgroundApi.saveParser(codeToPublish);
|
||||||
console.log('保存解析器响应:', result);
|
|
||||||
// 检查响应格式
|
// 检查响应格式
|
||||||
if (result.code === 200 || result.success) {
|
if (result.code === 200 || result.success) {
|
||||||
// 从响应或代码中提取type信息
|
// 从响应或代码中提取type信息
|
||||||
@@ -2223,6 +2220,8 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
|
|||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let themeObserver = null;
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 初始化移动端检测
|
// 初始化移动端检测
|
||||||
updateIsMobile();
|
updateIsMobile();
|
||||||
@@ -2249,10 +2248,10 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
|
|||||||
const html = document.documentElement;
|
const html = document.documentElement;
|
||||||
if (html && html.classList) {
|
if (html && html.classList) {
|
||||||
try {
|
try {
|
||||||
const observer = new MutationObserver(() => {
|
themeObserver = new MutationObserver(() => {
|
||||||
checkDarkMode();
|
checkDarkMode();
|
||||||
});
|
});
|
||||||
observer.observe(html, {
|
themeObserver.observe(html, {
|
||||||
attributes: true,
|
attributes: true,
|
||||||
attributeFilter: ['class', 'data-theme']
|
attributeFilter: ['class', 'data-theme']
|
||||||
});
|
});
|
||||||
@@ -2269,9 +2268,11 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
|
|||||||
window.removeEventListener('resize', updateIsMobile);
|
window.removeEventListener('resize', updateIsMobile);
|
||||||
// 移除页面关闭/刷新前的提示
|
// 移除页面关闭/刷新前的提示
|
||||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||||
|
themeObserver?.disconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
githubRepoUrl,
|
||||||
LANGUAGE,
|
LANGUAGE,
|
||||||
editorRef,
|
editorRef,
|
||||||
jsCode,
|
jsCode,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import fileTypeUtils from '@/utils/fileTypeUtils'
|
import fileTypeUtils from '@/utils/fileTypeUtils'
|
||||||
import { previewBaseUrl } from '@/views/Home.vue'
|
import { PREVIEW_BASE_URL } from '@/utils/constants'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ShowFile',
|
name: 'ShowFile',
|
||||||
@@ -44,7 +44,7 @@ export default {
|
|||||||
downloadUrl: '',
|
downloadUrl: '',
|
||||||
shareUrl: '', // 添加原始分享链接
|
shareUrl: '', // 添加原始分享链接
|
||||||
fileTypeUtils,
|
fileTypeUtils,
|
||||||
previewBaseUrl
|
previewBaseUrl: PREVIEW_BASE_URL
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -73,7 +73,7 @@ export default {
|
|||||||
this.parseResult = res.data
|
this.parseResult = res.data
|
||||||
this.downloadUrl = res.data.data?.directLink
|
this.downloadUrl = res.data.data?.directLink
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.error = '解析失败'
|
this.error = e.response?.data?.msg || e.response?.data?.error || '解析失败'
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export default {
|
|||||||
const res = await axios.get('/v2/getFileList', { params: { url: this.url } })
|
const res = await axios.get('/v2/getFileList', { params: { url: this.url } })
|
||||||
this.directoryData = res.data.data || []
|
this.directoryData = res.data.data || []
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.error = '目录解析失败'
|
this.error = e.response?.data?.msg || e.response?.data?.error || '目录解析失败'
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import java.math.BigDecimal;
|
|||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class RateLimiter {
|
public class RateLimiter {
|
||||||
@@ -51,12 +52,12 @@ public class RateLimiter {
|
|||||||
return new RequestInfo(1, currentTime);
|
return new RequestInfo(1, currentTime);
|
||||||
} else {
|
} else {
|
||||||
// 增加计数器
|
// 增加计数器
|
||||||
requestInfo.count++;
|
requestInfo.count.incrementAndGet();
|
||||||
return requestInfo;
|
return requestInfo;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (info.count > MAX_REQUESTS) {
|
if (info.count.get() > MAX_REQUESTS) {
|
||||||
// 超过限制
|
// 超过限制
|
||||||
// 计算剩余时间
|
// 计算剩余时间
|
||||||
long remainingTime = TIME_WINDOW - (System.currentTimeMillis() - info.timestamp);
|
long remainingTime = TIME_WINDOW - (System.currentTimeMillis() - info.timestamp);
|
||||||
@@ -71,11 +72,11 @@ public class RateLimiter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static class RequestInfo {
|
private static class RequestInfo {
|
||||||
volatile int count;
|
final AtomicInteger count;
|
||||||
volatile long timestamp;
|
volatile long timestamp;
|
||||||
|
|
||||||
RequestInfo(int count, long time) {
|
RequestInfo(int count, long time) {
|
||||||
this.count = count;
|
this.count = new AtomicInteger(count);
|
||||||
this.timestamp = time;
|
this.timestamp = time;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import javax.crypto.Mac;
|
|||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -93,9 +94,10 @@ public class JwtUtil {
|
|||||||
String encodedPayload = parts[1];
|
String encodedPayload = parts[1];
|
||||||
String signature = parts[2];
|
String signature = parts[2];
|
||||||
|
|
||||||
// 验证签名
|
// 验证签名(使用常量时间比较防止时序攻击)
|
||||||
String expectedSignature = hmacSha256(encodedHeader + "." + encodedPayload, SECRET_KEY);
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,8 +80,10 @@ public class PasswordUtil {
|
|||||||
byte[] calculatedHash = md.digest(plainPassword.getBytes(StandardCharsets.UTF_8));
|
byte[] calculatedHash = md.digest(plainPassword.getBytes(StandardCharsets.UTF_8));
|
||||||
String calculatedHashBase64 = Base64.getEncoder().encodeToString(calculatedHash);
|
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) {
|
} catch (Exception e) {
|
||||||
// 如果发生异常(例如格式不正确),返回false
|
// 如果发生异常(例如格式不正确),返回false
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import java.io.BufferedReader;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
@@ -129,8 +130,11 @@ public class PlaygroundApi {
|
|||||||
return promise.future();
|
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();
|
String token = config.generateToken();
|
||||||
JsonObject tokenData = new JsonObject().put("token", token);
|
JsonObject tokenData = new JsonObject().put("token", token);
|
||||||
promise.complete(JsonResult.data(tokenData).toJsonObject());
|
promise.complete(JsonResult.data(tokenData).toJsonObject());
|
||||||
@@ -299,7 +303,6 @@ public class PlaygroundApi {
|
|||||||
}).onFailure(e -> {
|
}).onFailure(e -> {
|
||||||
long executionTime = System.currentTimeMillis() - startTime;
|
long executionTime = System.currentTimeMillis() - startTime;
|
||||||
String errorMessage = e.getMessage();
|
String errorMessage = e.getMessage();
|
||||||
String stackTrace = getStackTrace(e);
|
|
||||||
|
|
||||||
log.error("演练场执行失败", e);
|
log.error("演练场执行失败", e);
|
||||||
|
|
||||||
@@ -317,7 +320,6 @@ public class PlaygroundApi {
|
|||||||
PlaygroundTestResp response = PlaygroundTestResp.builder()
|
PlaygroundTestResp response = PlaygroundTestResp.builder()
|
||||||
.success(false)
|
.success(false)
|
||||||
.error(errorMessage)
|
.error(errorMessage)
|
||||||
.stackTrace(stackTrace)
|
|
||||||
.executionTime(executionTime)
|
.executionTime(executionTime)
|
||||||
.logs(respLogs)
|
.logs(respLogs)
|
||||||
.build();
|
.build();
|
||||||
@@ -328,14 +330,12 @@ public class PlaygroundApi {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
long executionTime = System.currentTimeMillis() - startTime;
|
long executionTime = System.currentTimeMillis() - startTime;
|
||||||
String errorMessage = e.getMessage();
|
String errorMessage = e.getMessage();
|
||||||
String stackTrace = getStackTrace(e);
|
|
||||||
|
|
||||||
log.error("演练场初始化失败", e);
|
log.error("演练场初始化失败", e);
|
||||||
|
|
||||||
PlaygroundTestResp response = PlaygroundTestResp.builder()
|
PlaygroundTestResp response = PlaygroundTestResp.builder()
|
||||||
.success(false)
|
.success(false)
|
||||||
.error(errorMessage)
|
.error(errorMessage)
|
||||||
.stackTrace(stackTrace)
|
|
||||||
.executionTime(executionTime)
|
.executionTime(executionTime)
|
||||||
.logs(new ArrayList<>())
|
.logs(new ArrayList<>())
|
||||||
.build();
|
.build();
|
||||||
@@ -346,8 +346,7 @@ public class PlaygroundApi {
|
|||||||
log.error("解析请求参数失败", e);
|
log.error("解析请求参数失败", e);
|
||||||
promise.complete(JsonObject.mapFrom(PlaygroundTestResp.builder()
|
promise.complete(JsonObject.mapFrom(PlaygroundTestResp.builder()
|
||||||
.success(false)
|
.success(false)
|
||||||
.error("解析请求参数失败: " + e.getMessage())
|
.error("解析请求参数失败")
|
||||||
.stackTrace(getStackTrace(e))
|
|
||||||
.build()));
|
.build()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -696,18 +695,5 @@ public class PlaygroundApi {
|
|||||||
}
|
}
|
||||||
return ip;
|
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) {
|
if (rows.size() == 0) {
|
||||||
promise.complete(new JsonObject()
|
promise.complete(new JsonObject()
|
||||||
.put("success", false)
|
.put("success", false)
|
||||||
.put("message", "用户不存在"));
|
.put("message", "用户名或密码错误"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Row row = rows.iterator().next();
|
Row row = rows.iterator().next();
|
||||||
SysUser existUser = rowToUser(row);
|
SysUser existUser = rowToUser(row);
|
||||||
|
|
||||||
// 验证密码
|
// 验证密码
|
||||||
if (!PasswordUtil.checkPassword(user.getPassword(), existUser.getPassword())) {
|
if (!PasswordUtil.checkPassword(user.getPassword(), existUser.getPassword())) {
|
||||||
promise.complete(new JsonObject()
|
promise.complete(new JsonObject()
|
||||||
.put("success", false)
|
.put("success", false)
|
||||||
.put("message", "密码错误"));
|
.put("message", "用户名或密码错误"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +169,7 @@ public class UserServiceImpl implements UserService {
|
|||||||
log.error("登录查询失败", err);
|
log.error("登录查询失败", err);
|
||||||
promise.complete(new JsonObject()
|
promise.complete(new JsonObject()
|
||||||
.put("success", false)
|
.put("success", false)
|
||||||
.put("message", "登录失败: " + err.getMessage()));
|
.put("message", "登录失败,请稍后重试"));
|
||||||
});
|
});
|
||||||
|
|
||||||
return promise.future();
|
return promise.future();
|
||||||
@@ -189,10 +189,10 @@ public class UserServiceImpl implements UserService {
|
|||||||
.execute(Tuple.of(username))
|
.execute(Tuple.of(username))
|
||||||
.onSuccess(rows -> {
|
.onSuccess(rows -> {
|
||||||
if (rows.size() == 0) {
|
if (rows.size() == 0) {
|
||||||
promise.fail("用户不存在");
|
promise.fail("用户名或密码错误");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Row row = rows.iterator().next();
|
Row row = rows.iterator().next();
|
||||||
SysUser user = rowToUser(row);
|
SysUser user = rowToUser(row);
|
||||||
promise.complete(filterSensitiveInfo(user));
|
promise.complete(filterSensitiveInfo(user));
|
||||||
@@ -296,10 +296,10 @@ public class UserServiceImpl implements UserService {
|
|||||||
.execute(Tuple.of(user.getUsername()))
|
.execute(Tuple.of(user.getUsername()))
|
||||||
.onSuccess(rows -> {
|
.onSuccess(rows -> {
|
||||||
if (rows.size() == 0) {
|
if (rows.size() == 0) {
|
||||||
promise.fail("用户不存在");
|
promise.fail("用户名或密码错误");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Row row = rows.iterator().next();
|
Row row = rows.iterator().next();
|
||||||
SysUser existUser = rowToUser(row);
|
SysUser existUser = rowToUser(row);
|
||||||
|
|
||||||
@@ -406,7 +406,7 @@ public class UserServiceImpl implements UserService {
|
|||||||
.onFailure(err -> {
|
.onFailure(err -> {
|
||||||
promise.complete(new JsonObject()
|
promise.complete(new JsonObject()
|
||||||
.put("success", false)
|
.put("success", false)
|
||||||
.put("message", "用户不存在"));
|
.put("message", "认证失败,请重新登录"));
|
||||||
});
|
});
|
||||||
|
|
||||||
return promise.future();
|
return promise.future();
|
||||||
|
|||||||
Reference in New Issue
Block a user