mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2026-07-14 15:31:14 +00:00
Compare commits
51 Commits
v3.0.0
...
ff400d3be3
| Author | SHA1 | Date | |
|---|---|---|---|
| ff400d3be3 | |||
| 2f7304ab2d | |||
| 0df01ba3d5 | |||
| 710e454fd0 | |||
| c46dfa00a0 | |||
| 9a3ea05023 | |||
| 66d7a62d3a | |||
| f1b6cd3e18 | |||
| 189d1477a8 | |||
| 9c3945f45a | |||
| 77c7d6c5d6 | |||
| ab3009e9cc | |||
| 1c2291f9cf | |||
| efb135ee48 | |||
| 0699c4a127 | |||
| 33cef5f8e1 | |||
| 32d467b6d9 | |||
| 6dfa770137 | |||
| 0dfee8ab22 | |||
| 85fe910f25 | |||
| 6c60b0116f | |||
| 8dfcf510f6 | |||
| be1ed3d46d | |||
| 1fca578c07 | |||
| a83665ac44 | |||
| 21e8a370c3 | |||
| 3dd4dd139b | |||
| afe2046bc8 | |||
| 6d24388690 | |||
| 0b024a849a | |||
| 8745dc3567 | |||
| 1f4c7019d4 | |||
| 255e7b2fb5 | |||
| 7419e536cf | |||
| 74df000287 | |||
| 2e0127d609 | |||
| 2b9168e8df | |||
| 7bb3cf7c51 | |||
| 2ce5058be0 | |||
| a5fc41f152 | |||
| 3245a27156 | |||
| ce0fbf65aa | |||
| eb87c3d865 | |||
| 990a90e461 | |||
| 45cb255a4d | |||
| 5e8b8a3575 | |||
| c317e53c24 | |||
| 29b59d8450 | |||
| 97b7e2f86e | |||
| 2f55294b58 | |||
| 2161190d9a |
@@ -60,6 +60,7 @@ jobs:
|
||||
- name: Update dependency graph
|
||||
uses: advanced-security/maven-dependency-submission-action@v3
|
||||
if: github.event_name != 'pull_request'
|
||||
continue-on-error: true
|
||||
with:
|
||||
ignore-maven-wrapper: true
|
||||
|
||||
@@ -97,5 +98,5 @@ jobs:
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
tags: |
|
||||
ghcr.io/qaiu/netdisk-fast-download:${{ steps.tag.outputs.tag }}
|
||||
ghcr.io/qaiu/netdisk-fast-download:latest
|
||||
ghcr.io/${{ github.repository }}:${{ steps.tag.outputs.tag }}
|
||||
ghcr.io/${{ github.repository }}:latest
|
||||
|
||||
@@ -3,27 +3,26 @@ QQ交流群:1017480890
|
||||
<p align="center">
|
||||
<a href="https://github.com/qaiu/netdisk-fast-download/actions/workflows/maven.yml"><img src="https://img.shields.io/github/actions/workflow/status/qaiu/netdisk-fast-download/maven.yml?branch=v0.1.9b8a&style=flat"></a>
|
||||
<a href="https://www.oracle.com/cn/java/technologies/downloads"><img src="https://img.shields.io/badge/jdk-%3E%3D17-blue"></a>
|
||||
<a href="https://vertx-china.github.io"><img src="https://img.shields.io/badge/vert.x-4.5.22-blue?style=flat"></a>
|
||||
<a href="https://vertx-china.github.io"><img src="https://img.shields.io/badge/vert.x-4.5.24-blue?style=flat"></a>
|
||||
<a href="https://raw.githubusercontent.com/qaiu/netdisk-fast-download/master/LICENSE"><img src="https://img.shields.io/github/license/qaiu/netdisk-fast-download?style=flat"></a>
|
||||
<a href="https://github.com/qaiu/netdisk-fast-download/releases/"><img src="https://img.shields.io/github/v/release/qaiu/netdisk-fast-download?style=flat"></a>
|
||||
<a href="https://atomgit.com/QAIU/netdisk-fast-download"><img src="https://atomgit.com/QAIU/netdisk-fast-download/star/badge.svg" alt="AtomGit"></a>
|
||||
<a href="https://oosmetrics.com/repo/qaiu/netdisk-fast-download"><img src="https://api.oosmetrics.com/api/v1/badge/achievement/826aa27a-6e59-4de5-b7fa-cd189f484035.svg"></a>
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/12101" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12101" alt="qaiu%2Fnetdisk-fast-download | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
</p>
|
||||
|
||||
<div align="center" style="display:flex; justify-content:center; gap:10px; align-items:flex-start;">
|
||||
<img
|
||||
src="https://github.com/user-attachments/assets/bf266d0a-aaf8-4772-9231-e38a4b7bb6cb"
|
||||
alt="image1"
|
||||
style="width:300px; max-width:300px; flex:none;"
|
||||
>
|
||||
<img
|
||||
src="https://github.com/user-attachments/assets/bb7a85f0-c256-4b4a-a11b-3ceb55afc302"
|
||||
alt="image2"
|
||||
style="width:300px; max-width:300px; flex:none;"
|
||||
>
|
||||
</div>
|
||||
|
||||
> netdisk-fast-download网盘直链解析可以把云盘分享链接转为直链,可广泛应用于各类下载站,资源站,个人博客,图床,APP下载更新,视频点播等领域。支持市面各大主流云盘的文件分享以及文件夹分享链接,已支持蓝奏云/蓝奏云优享/奶牛快传/移动云云空间/小飞机盘/亿方云/123云盘/Cloudreve等,支持加密分享,以及部分网盘文件夹分享。
|
||||

|
||||
|
||||
|
||||
## 国内镜像
|
||||
|
||||
本项目同步托管于 **AtomGit**,国内访问更流畅:👉 [https://atomgit.com/QAIU/netdisk-fast-download](https://atomgit.com/QAIU/netdisk-fast-download)
|
||||
|
||||
|
||||
## 介绍
|
||||
> netdisk-fast-download网盘直链解析可以把云盘分享链接转为直链,可广泛应用于各类下载站,资源站,个人博客,图床,APP下载更新,视频点播等领域。支持市面各大主流云盘的文件分享以及文件夹分享链接,已支持蓝奏云/蓝奏云优享/移动云云空间/小飞机盘/亿方云/123云盘/Cloudreve等,支持加密分享,以及部分网盘文件夹分享。
|
||||
|
||||
[官方文档](https://nfd-parser.github.io/)
|
||||
[API接入](https://nfdparser.apifox.cn/)
|
||||
@@ -62,7 +61,6 @@ https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.
|
||||
**注意⚠️请不要过度依赖 lz.qaiu.top,建议本地搭建或者云服务器自行搭建。请求量过多的话服务器可能会被云盘厂商限制,遇到解析失败的分享链接不要着急提issues,请先检查分享是否有效。**
|
||||
|
||||
## 网盘支持情况:
|
||||
> 20230905 奶牛云直链做了防盗链,需加入请求头:Referer: https://cowtransfer.com/
|
||||
> 20230824 123云盘解析大文件(>100MB)失效,需要登录
|
||||
> 20230722 UC网盘解析失效,需要登录
|
||||
|
||||
@@ -95,6 +93,13 @@ https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.
|
||||
- Onedrive-pod
|
||||
- Dropbox-pdp
|
||||
- iCloud-pic
|
||||
### 专业版提供
|
||||
- 迅雷网盘-xl
|
||||
- [夸克云盘-qk](https://pan.quark.cn/)
|
||||
- [UC云盘-uc](https://fast.uc.cn/)
|
||||
- [移动云盘-p139](https://yun.139.com/)
|
||||
- [联通云盘-pwo](https://pan.wo.cn/)
|
||||
- [天翼云盘-p189](https://cloud.189.cn/)
|
||||
|
||||
## API接口
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import org.slf4j.LoggerFactory;
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
*/
|
||||
public class JDBCPoolInit {
|
||||
public class JDBCPoolInit implements AutoCloseable {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(JDBCPoolInit.class);
|
||||
|
||||
@@ -101,4 +101,16 @@ public class JDBCPoolInit {
|
||||
synchronized public JDBCPool getPool() {
|
||||
return pool;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭连接池,释放数据库资源
|
||||
*/
|
||||
@Override
|
||||
public synchronized void close() {
|
||||
if (pool != null) {
|
||||
pool.close();
|
||||
LOGGER.info("数据库连接池已关闭: URL={}", url);
|
||||
pool = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,11 @@ public final class Deploy {
|
||||
// 读取yml配置
|
||||
ConfigUtil.readYamlConfig(path.toString(), tempVertx)
|
||||
.onSuccess(this::readConf)
|
||||
.onFailure(Throwable::printStackTrace);
|
||||
.onFailure(err -> {
|
||||
LOGGER.error("读取配置文件失败: {}", err.getMessage(), err);
|
||||
LockSupport.unpark(mainThread);
|
||||
System.exit(-1);
|
||||
});
|
||||
LockSupport.park();
|
||||
deployVerticle();
|
||||
}
|
||||
@@ -137,6 +141,17 @@ public final class Deploy {
|
||||
vertxOptions.getWorkerPoolSize());
|
||||
var vertx = Vertx.vertx(vertxOptions);
|
||||
VertxHolder.init(vertx);
|
||||
|
||||
// 注册 ShutdownHook,确保进程退出时优雅关闭资源
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
LOGGER.info("JVM shutting down, closing Vert.x...");
|
||||
try {
|
||||
vertx.close().toCompletionStage().toCompletableFuture().get(10, java.util.concurrent.TimeUnit.SECONDS);
|
||||
LOGGER.info("Vert.x closed successfully");
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Vert.x close error or timeout", e);
|
||||
}
|
||||
}));
|
||||
//配置保存在共享数据中
|
||||
var sharedData = vertx.sharedData();
|
||||
LocalMap<String, Object> localMap = sharedData.getLocalMap(LOCAL);
|
||||
|
||||
@@ -36,16 +36,20 @@ public final class ParamUtil {
|
||||
|
||||
public static MultiMap paramsToMap(String paramString) {
|
||||
MultiMap entries = MultiMap.caseInsensitiveMultiMap();
|
||||
if (paramString == null) return entries;
|
||||
if (paramString == null || paramString.isEmpty()) return entries;
|
||||
String[] params = paramString.split("&");
|
||||
if (params.length == 0) return entries;
|
||||
for (String param : params) {
|
||||
String[] kv = param.split("=");
|
||||
if (param == null || param.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String[] kv = param.split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
entries.set(kv[0], kv[1]);
|
||||
} else {
|
||||
} else if (kv.length == 1) {
|
||||
entries.set(kv[0], "");
|
||||
}
|
||||
// kv.length == 0 时(空字符串),跳过
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
@@ -241,7 +241,7 @@ public final class ReflectionUtil {
|
||||
public static boolean isBasicTypeArray(CtClass ctClass) {
|
||||
if (!ctClass.isArray()) {
|
||||
return false;
|
||||
} else return (ctClass.getName().matches("^(boolen|char|byte|short|int|long|float|double|String)\\[]$"));
|
||||
} else return (ctClass.getName().matches("^(boolean|char|byte|short|int|long|float|double|String)\\[]$"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -129,16 +129,25 @@ public class HttpProxyVerticle extends AbstractVerticle {
|
||||
clientRequest.response().setStatusCode(403).end();
|
||||
return;
|
||||
}
|
||||
String[] split = new String(Base64.getDecoder().decode(s.replace("Basic ", ""))).split(":");
|
||||
if (split.length > 1) {
|
||||
// TODO
|
||||
String username = proxyServerConf.getString("username");
|
||||
String password = proxyServerConf.getString("password");
|
||||
if (!split[0].equals(username) || !split[1].equals(password)) {
|
||||
LOGGER.info("-----auth failed------\nusername: {}\npassword: {}", username, password);
|
||||
String[] split;
|
||||
try {
|
||||
split = new String(Base64.getDecoder().decode(s.replace("Basic ", ""))).split(":");
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOGGER.warn("Proxy-Authorization header is not valid Base64");
|
||||
clientRequest.response().setStatusCode(403).end();
|
||||
return;
|
||||
}
|
||||
if (split.length <= 1) {
|
||||
LOGGER.warn("Proxy-Authorization header format invalid: missing username:password separator");
|
||||
clientRequest.response().setStatusCode(403).end();
|
||||
return;
|
||||
}
|
||||
String username = proxyServerConf.getString("username");
|
||||
String password = proxyServerConf.getString("password");
|
||||
if (!split[0].equals(username) || !split[1].equals(password)) {
|
||||
LOGGER.info("-----auth failed------\nusername: {}", split[0]);
|
||||
clientRequest.response().setStatusCode(403).end();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -351,7 +351,7 @@ public class ReverseProxyVerticle extends AbstractVerticle {
|
||||
String host = url.getHost();
|
||||
int port = url.getPort();
|
||||
if (port == -1) {
|
||||
port = 80;
|
||||
port = 443;
|
||||
}
|
||||
String originPath = url.getPath();
|
||||
LOGGER.info("path {}, originPath {}, to {}:{}", path, originPath, host, port);
|
||||
|
||||
@@ -5,11 +5,15 @@ import cn.qaiu.vx.core.base.BaseAsyncService;
|
||||
import cn.qaiu.vx.core.util.ReflectionUtil;
|
||||
import io.vertx.core.AbstractVerticle;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.core.eventbus.MessageConsumer;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.serviceproxy.ServiceBinder;
|
||||
import org.reflections.Reflections;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@@ -24,6 +28,7 @@ public class ServiceVerticle extends AbstractVerticle {
|
||||
Logger LOGGER = LoggerFactory.getLogger(ServiceVerticle.class);
|
||||
private static final AtomicInteger ID = new AtomicInteger(1);
|
||||
private static final Set<Class<?>> handlers;
|
||||
private final List<MessageConsumer<JsonObject>> consumers = new ArrayList<>();
|
||||
|
||||
static {
|
||||
Reflections reflections = ReflectionUtil.getReflections();
|
||||
@@ -39,7 +44,10 @@ public class ServiceVerticle extends AbstractVerticle {
|
||||
try {
|
||||
serviceNames.append(asyncService.getName()).append("|");
|
||||
BaseAsyncService asInstance = (BaseAsyncService) ReflectionUtil.newWithNoParam(asyncService);
|
||||
binder.setAddress(asInstance.getAddress()).register(asInstance.getAsyncInterfaceClass(), asInstance);
|
||||
String address = asInstance.getAddress();
|
||||
MessageConsumer<JsonObject> consumer = binder.setAddress(address)
|
||||
.register(asInstance.getAsyncInterfaceClass(), asInstance);
|
||||
consumers.add(consumer);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to register service: {}", asyncService.getName(), e);
|
||||
}
|
||||
@@ -49,4 +57,19 @@ public class ServiceVerticle extends AbstractVerticle {
|
||||
}
|
||||
startPromise.complete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop(Promise<Void> stopPromise) {
|
||||
int count = consumers.size();
|
||||
consumers.forEach(consumer -> {
|
||||
try {
|
||||
consumer.unregister();
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Failed to unregister service consumer at address: {}", consumer.address(), e);
|
||||
}
|
||||
});
|
||||
consumers.clear();
|
||||
LOGGER.info("ServiceVerticle stopped, unregistered {} services", count);
|
||||
stopPromise.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ public class HttpProxyConf {
|
||||
public HttpProxyConf() {
|
||||
this.username = DEFAULT_USERNAME;
|
||||
this.password = DEFAULT_PASSWORD;
|
||||
this.timeout = DEFAULT_PORT;
|
||||
this.port = DEFAULT_PORT;
|
||||
this.timeout = DEFAULT_TIMEOUT;
|
||||
this.preProxyOptions = new ProxyOptions();
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
app.version=${project.version}
|
||||
build=${build.timestamp}
|
||||
build=${maven.build.timestamp}
|
||||
|
||||
@@ -40,28 +40,34 @@ public abstract class PanBase implements IPanTool {
|
||||
protected Promise<String> promise = Promise.promise();
|
||||
|
||||
/**
|
||||
* Http client
|
||||
* 共享的 WebClient 实例(线程安全,避免每请求创建导致资源泄漏)
|
||||
*/
|
||||
protected WebClient client = WebClient.create(WebClientVertxInit.get(),
|
||||
private static final WebClient SHARED_CLIENT = WebClient.create(WebClientVertxInit.get(),
|
||||
new WebClientOptions());
|
||||
private static final WebClient SHARED_CLIENT_NO_REDIRECTS = WebClient.create(WebClientVertxInit.get(),
|
||||
new WebClientOptions().setFollowRedirects(false));
|
||||
private static final WebClient SHARED_CLIENT_DISABLE_UA = WebClient.create(WebClientVertxInit.get(),
|
||||
new WebClientOptions().setUserAgentEnabled(false));
|
||||
|
||||
/**
|
||||
* Http client session (会话管理, 带cookie请求)
|
||||
* Http client (默认使用共享实例,代理模式下使用独立实例)
|
||||
*/
|
||||
protected WebClient client = SHARED_CLIENT;
|
||||
|
||||
/**
|
||||
* Http client session (会话管理, 带cookie请求, 每实例独立)
|
||||
*/
|
||||
protected WebClientSession clientSession = WebClientSession.create(client);
|
||||
|
||||
/**
|
||||
* Http client 不自动跳转
|
||||
*/
|
||||
protected WebClient clientNoRedirects = WebClient.create(WebClientVertxInit.get(),
|
||||
new WebClientOptions().setFollowRedirects(false));
|
||||
protected WebClient clientNoRedirects = SHARED_CLIENT_NO_REDIRECTS;
|
||||
|
||||
/**
|
||||
* Http client disable UserAgent
|
||||
*/
|
||||
protected WebClient clientDisableUA = WebClient.create(WebClientVertxInit.get()
|
||||
, new WebClientOptions().setUserAgentEnabled(false)
|
||||
);
|
||||
protected WebClient clientDisableUA = SHARED_CLIENT_DISABLE_UA;
|
||||
|
||||
protected ShareLinkInfo shareLinkInfo;
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ public class JsHttpClient {
|
||||
};
|
||||
|
||||
public JsHttpClient() {
|
||||
this.client = WebClient.create(WebClientVertxInit.get(), new WebClientOptions());;
|
||||
this.client = WebClient.create(WebClientVertxInit.get(), new WebClientOptions());
|
||||
this.clientSession = WebClientSession.create(client);
|
||||
this.headers = MultiMap.caseInsensitiveMultiMap();
|
||||
// 设置默认的Accept-Encoding头以支持压缩响应
|
||||
@@ -677,4 +677,13 @@ public class JsHttpClient {
|
||||
return buffer.length();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭 WebClient 释放连接池资源
|
||||
*/
|
||||
public void close() {
|
||||
if (client != null) {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,11 +29,12 @@ import java.util.stream.Collectors;
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/10/17
|
||||
*/
|
||||
public class JsParserExecutor implements IPanTool {
|
||||
public class JsParserExecutor implements IPanTool, AutoCloseable {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(JsParserExecutor.class);
|
||||
|
||||
private static final WorkerExecutor EXECUTOR = WebClientVertxInit.get().createSharedWorkerExecutor("parser-executor", 32);
|
||||
private static WorkerExecutor EXECUTOR;
|
||||
private static final Object EXECUTOR_LOCK = new Object();
|
||||
|
||||
private static String FETCH_RUNTIME_JS = null;
|
||||
|
||||
@@ -146,12 +147,57 @@ public class JsParserExecutor implements IPanTool {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源(ScriptEngine 和 HttpClient),避免内存泄漏
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
if (httpClient != null) {
|
||||
httpClient.close();
|
||||
}
|
||||
// 清除 ScriptEngine 持有的 Java 对象引用,帮助 GC 回收
|
||||
if (engine != null) {
|
||||
engine.put("http", null);
|
||||
engine.put("logger", null);
|
||||
engine.put("shareLinkInfo", null);
|
||||
engine.put("JavaFetch", null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭全局 WorkerExecutor(应在应用关闭时调用)
|
||||
*/
|
||||
public static void shutdownExecutor() {
|
||||
synchronized (EXECUTOR_LOCK) {
|
||||
if (EXECUTOR != null) {
|
||||
EXECUTOR.close();
|
||||
EXECUTOR = null;
|
||||
log.info("JsParserExecutor WorkerExecutor 已关闭");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建 WorkerExecutor(懒加载)
|
||||
*/
|
||||
private static WorkerExecutor getExecutor() {
|
||||
if (EXECUTOR != null) {
|
||||
return EXECUTOR;
|
||||
}
|
||||
synchronized (EXECUTOR_LOCK) {
|
||||
if (EXECUTOR == null) {
|
||||
EXECUTOR = WebClientVertxInit.get().createSharedWorkerExecutor("parser-executor", 32);
|
||||
}
|
||||
return EXECUTOR;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<String> parse() {
|
||||
jsLogger.info("开始执行JavaScript解析器: {}", config.getType());
|
||||
|
||||
// 使用executeBlocking在工作线程上执行,避免阻塞EventLoop线程
|
||||
return EXECUTOR.executeBlocking(() -> {
|
||||
return getExecutor().executeBlocking(() -> {
|
||||
// 直接调用全局parse函数
|
||||
Object parseFunction = engine.get("parse");
|
||||
if (parseFunction == null) {
|
||||
@@ -173,7 +219,7 @@ public class JsParserExecutor implements IPanTool {
|
||||
} else {
|
||||
throw new RuntimeException("parse函数类型错误");
|
||||
}
|
||||
});
|
||||
}).onComplete(ar -> close());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -181,7 +227,7 @@ public class JsParserExecutor implements IPanTool {
|
||||
jsLogger.info("开始执行JavaScript文件列表解析: {}", config.getType());
|
||||
|
||||
// 使用executeBlocking在工作线程上执行,避免阻塞EventLoop线程
|
||||
return EXECUTOR.executeBlocking(() -> {
|
||||
return getExecutor().executeBlocking(() -> {
|
||||
// 直接调用全局parseFileList函数
|
||||
Object parseFileListFunction = engine.get("parseFileList");
|
||||
if (parseFileListFunction == null) {
|
||||
@@ -206,7 +252,7 @@ public class JsParserExecutor implements IPanTool {
|
||||
} else {
|
||||
throw new RuntimeException("parseFileList函数类型错误");
|
||||
}
|
||||
});
|
||||
}).onComplete(ar -> close());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -214,7 +260,7 @@ public class JsParserExecutor implements IPanTool {
|
||||
jsLogger.info("开始执行JavaScript按ID解析: {}", config.getType());
|
||||
|
||||
// 使用executeBlocking在工作线程上执行,避免阻塞EventLoop线程
|
||||
return EXECUTOR.executeBlocking(() -> {
|
||||
return getExecutor().executeBlocking(() -> {
|
||||
// 直接调用全局parseById函数
|
||||
Object parseByIdFunction = engine.get("parseById");
|
||||
if (parseByIdFunction == null) {
|
||||
@@ -237,7 +283,7 @@ public class JsParserExecutor implements IPanTool {
|
||||
} else {
|
||||
throw new RuntimeException("parseById函数类型错误");
|
||||
}
|
||||
});
|
||||
}).onComplete(ar -> close());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -81,8 +81,8 @@ public class LzTool extends PanBase {
|
||||
}
|
||||
|
||||
private void doParser(String html, String pwd, String sUrl) {
|
||||
// 检测是否为目录分享链接 (含 /s/、/b/ 路径段或 b0 开头的路径段)
|
||||
if (sUrl.matches(".*/(s|b)/[^/]+.*") || sUrl.matches(".*/b0[^/]+.*")) {
|
||||
// 检测是否为目录分享链接 (含 /s/、/b/ 路径段或 b 开头的路径段)
|
||||
if (sUrl.matches(".*/(s|b)/[^/]+.*") || sUrl.matches(".*/b[^/]+.*")) {
|
||||
fail("该链接为蓝奏云目录分享,请使用目录解析接口");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -21,11 +21,11 @@ import static cn.qaiu.util.AESUtils.encrypt;
|
||||
*/
|
||||
public class JsExecUtils {
|
||||
private static final Invocable inv;
|
||||
private static final ScriptEngineManager ENGINE_MANAGER = new ScriptEngineManager();
|
||||
|
||||
// 初始化脚本引擎
|
||||
static {
|
||||
ScriptEngineManager engineManager = new ScriptEngineManager();
|
||||
ScriptEngine engine = engineManager.getEngineByName("JavaScript"); // 得到脚本引擎
|
||||
ScriptEngine engine = ENGINE_MANAGER.getEngineByName("JavaScript"); // 得到脚本引擎
|
||||
|
||||
try {
|
||||
engine.eval(JsContent.ye123);
|
||||
@@ -45,12 +45,11 @@ public class JsExecUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用执行蓝奏云js文件
|
||||
* 调用执行蓝奏云js文件(每次动态JS代码,无法复用引擎)
|
||||
*/
|
||||
public static ScriptObjectMirror executeDynamicJs(String jsText, String funName) throws ScriptException,
|
||||
NoSuchMethodException {
|
||||
ScriptEngineManager engineManager = new ScriptEngineManager();
|
||||
ScriptEngine engine = engineManager.getEngineByName("JavaScript"); // 得到脚本引擎
|
||||
ScriptEngine engine = ENGINE_MANAGER.getEngineByName("JavaScript"); // 得到脚本引擎
|
||||
engine.eval(JsContent.lz + "\n" + jsText);
|
||||
Invocable inv = (Invocable) engine;
|
||||
//调用js中的函数
|
||||
@@ -63,12 +62,11 @@ public class JsExecUtils {
|
||||
|
||||
|
||||
/**
|
||||
* 调用执行蓝奏云js文件
|
||||
* 调用执行js文件(使用缓存的 ScriptEngineManager 创建新引擎实例)
|
||||
*/
|
||||
public static Object executeOtherJs(String jsText, String funName, Object ... args) throws ScriptException,
|
||||
NoSuchMethodException {
|
||||
ScriptEngineManager engineManager = new ScriptEngineManager();
|
||||
ScriptEngine engine = engineManager.getEngineByName("JavaScript"); // 得到脚本引擎
|
||||
ScriptEngine engine = ENGINE_MANAGER.getEngineByName("JavaScript"); // 得到脚本引擎
|
||||
engine.eval(jsText);
|
||||
Invocable inv = (Invocable) engine;
|
||||
//调用js中的函数
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
## 测试配置
|
||||
|
||||
### 小飞机网盘 ✅
|
||||
- **用户名**: 15764091073
|
||||
- **用户名**: 15x
|
||||
- **URL**: https://share.feijipan.com/s/ZWYoZ31c
|
||||
- **文件**: 资源.rar (1.13 GB)
|
||||
- **认证方式**: username/password
|
||||
@@ -32,7 +32,7 @@
|
||||
```
|
||||
=== 测试小飞机网盘解析(带认证)===
|
||||
分享链接: https://share.feijipan.com/s/ZWYoZ31c
|
||||
用户名: 15764091073
|
||||
用户名: 15x
|
||||
密码: ******
|
||||
|
||||
开始解析...
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
// ==FetchRuntime==
|
||||
// @name Fetch API Polyfill for ES5
|
||||
// @description Fetch API and Promise implementation for ES5 JavaScript engines
|
||||
// @version 1.0.0
|
||||
// @author QAIU
|
||||
// ==============
|
||||
|
||||
/**
|
||||
* Simple Promise implementation compatible with ES5
|
||||
* Supports basic Promise functionality needed for fetch API
|
||||
*/
|
||||
function SimplePromise(executor) {
|
||||
var state = 'pending';
|
||||
var value;
|
||||
var handlers = [];
|
||||
var self = this;
|
||||
|
||||
function resolve(result) {
|
||||
if (state !== 'pending') return;
|
||||
state = 'fulfilled';
|
||||
value = result;
|
||||
handlers.forEach(handle);
|
||||
handlers = [];
|
||||
}
|
||||
|
||||
function reject(err) {
|
||||
if (state !== 'pending') return;
|
||||
state = 'rejected';
|
||||
value = err;
|
||||
handlers.forEach(handle);
|
||||
handlers = [];
|
||||
}
|
||||
|
||||
function handle(handler) {
|
||||
if (state === 'pending') {
|
||||
handlers.push(handler);
|
||||
} else {
|
||||
setTimeout(function() {
|
||||
if (state === 'fulfilled' && typeof handler.onFulfilled === 'function') {
|
||||
try {
|
||||
var result = handler.onFulfilled(value);
|
||||
if (result && typeof result.then === 'function') {
|
||||
result.then(handler.resolve, handler.reject);
|
||||
} else {
|
||||
handler.resolve(result);
|
||||
}
|
||||
} catch (e) {
|
||||
handler.reject(e);
|
||||
}
|
||||
}
|
||||
if (state === 'rejected' && typeof handler.onRejected === 'function') {
|
||||
try {
|
||||
var result = handler.onRejected(value);
|
||||
if (result && typeof result.then === 'function') {
|
||||
result.then(handler.resolve, handler.reject);
|
||||
} else {
|
||||
handler.resolve(result);
|
||||
}
|
||||
} catch (e) {
|
||||
handler.reject(e);
|
||||
}
|
||||
} else if (state === 'rejected' && !handler.onRejected) {
|
||||
handler.reject(value);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
this.then = function(onFulfilled, onRejected) {
|
||||
return new SimplePromise(function(resolveNext, rejectNext) {
|
||||
handle({
|
||||
onFulfilled: onFulfilled,
|
||||
onRejected: onRejected,
|
||||
resolve: resolveNext,
|
||||
reject: rejectNext
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this['catch'] = function(onRejected) {
|
||||
return this.then(null, onRejected);
|
||||
};
|
||||
|
||||
this['finally'] = function(onFinally) {
|
||||
return this.then(
|
||||
function(value) {
|
||||
return SimplePromise.resolve(onFinally()).then(function() {
|
||||
return value;
|
||||
});
|
||||
},
|
||||
function(reason) {
|
||||
return SimplePromise.resolve(onFinally()).then(function() {
|
||||
throw reason;
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
executor(resolve, reject);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Static methods
|
||||
SimplePromise.resolve = function(value) {
|
||||
if (value && typeof value.then === 'function') {
|
||||
return value;
|
||||
}
|
||||
return new SimplePromise(function(resolve) {
|
||||
resolve(value);
|
||||
});
|
||||
};
|
||||
|
||||
SimplePromise.reject = function(reason) {
|
||||
return new SimplePromise(function(resolve, reject) {
|
||||
reject(reason);
|
||||
});
|
||||
};
|
||||
|
||||
SimplePromise.all = function(promises) {
|
||||
return new SimplePromise(function(resolve, reject) {
|
||||
var results = [];
|
||||
var remaining = promises.length;
|
||||
|
||||
if (remaining === 0) {
|
||||
resolve(results);
|
||||
return;
|
||||
}
|
||||
|
||||
function handleResult(index, value) {
|
||||
results[index] = value;
|
||||
remaining--;
|
||||
if (remaining === 0) {
|
||||
resolve(results);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < promises.length; i++) {
|
||||
(function(index) {
|
||||
var promise = promises[index];
|
||||
if (promise && typeof promise.then === 'function') {
|
||||
promise.then(
|
||||
function(value) { handleResult(index, value); },
|
||||
reject
|
||||
);
|
||||
} else {
|
||||
handleResult(index, promise);
|
||||
}
|
||||
})(i);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
SimplePromise.race = function(promises) {
|
||||
return new SimplePromise(function(resolve, reject) {
|
||||
if (promises.length === 0) {
|
||||
// Per spec, Promise.race with empty array stays pending forever
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < promises.length; i++) {
|
||||
var promise = promises[i];
|
||||
if (promise && typeof promise.then === 'function') {
|
||||
promise.then(resolve, reject);
|
||||
} else {
|
||||
resolve(promise);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Make Promise global if not already defined
|
||||
if (typeof Promise === 'undefined') {
|
||||
var Promise = SimplePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response object that mimics the Fetch API Response
|
||||
*/
|
||||
function FetchResponse(jsHttpResponse) {
|
||||
this._jsResponse = jsHttpResponse;
|
||||
this.status = jsHttpResponse.statusCode();
|
||||
this.ok = this.status >= 200 && this.status < 300;
|
||||
|
||||
// Map HTTP status codes to standard status text
|
||||
var statusTexts = {
|
||||
200: 'OK',
|
||||
201: 'Created',
|
||||
204: 'No Content',
|
||||
301: 'Moved Permanently',
|
||||
302: 'Found',
|
||||
304: 'Not Modified',
|
||||
400: 'Bad Request',
|
||||
401: 'Unauthorized',
|
||||
403: 'Forbidden',
|
||||
404: 'Not Found',
|
||||
405: 'Method Not Allowed',
|
||||
408: 'Request Timeout',
|
||||
409: 'Conflict',
|
||||
410: 'Gone',
|
||||
500: 'Internal Server Error',
|
||||
501: 'Not Implemented',
|
||||
502: 'Bad Gateway',
|
||||
503: 'Service Unavailable',
|
||||
504: 'Gateway Timeout'
|
||||
};
|
||||
|
||||
this.statusText = statusTexts[this.status] || (this.ok ? 'OK' : 'Error');
|
||||
this.headers = {
|
||||
get: function(name) {
|
||||
return jsHttpResponse.header(name);
|
||||
},
|
||||
has: function(name) {
|
||||
return jsHttpResponse.header(name) !== null;
|
||||
},
|
||||
entries: function() {
|
||||
var headerMap = jsHttpResponse.headers();
|
||||
var entries = [];
|
||||
for (var key in headerMap) {
|
||||
if (headerMap.hasOwnProperty(key)) {
|
||||
entries.push([key, headerMap[key]]);
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
FetchResponse.prototype.text = function() {
|
||||
var body = this._jsResponse.body();
|
||||
return SimplePromise.resolve(body || '');
|
||||
};
|
||||
|
||||
FetchResponse.prototype.json = function() {
|
||||
var self = this;
|
||||
return this.text().then(function(text) {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (e) {
|
||||
throw new Error('Invalid JSON: ' + e.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
FetchResponse.prototype.arrayBuffer = function() {
|
||||
var bytes = this._jsResponse.bodyBytes();
|
||||
return SimplePromise.resolve(bytes);
|
||||
};
|
||||
|
||||
FetchResponse.prototype.blob = function() {
|
||||
// Blob not supported in ES5, return bytes
|
||||
return this.arrayBuffer();
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch API implementation using JavaFetch bridge
|
||||
* @param {string} url - Request URL
|
||||
* @param {Object} options - Fetch options (method, headers, body, etc.)
|
||||
* @returns {Promise<FetchResponse>}
|
||||
*/
|
||||
function fetch(url, options) {
|
||||
return new SimplePromise(function(resolve, reject) {
|
||||
try {
|
||||
// Parse options
|
||||
options = options || {};
|
||||
var method = (options.method || 'GET').toUpperCase();
|
||||
var headers = options.headers || {};
|
||||
var body = options.body;
|
||||
|
||||
// Prepare request options for JavaFetch
|
||||
var requestOptions = {
|
||||
method: method,
|
||||
headers: {}
|
||||
};
|
||||
|
||||
// Convert headers to simple object
|
||||
if (headers) {
|
||||
if (typeof headers.forEach === 'function') {
|
||||
// Headers object
|
||||
headers.forEach(function(value, key) {
|
||||
requestOptions.headers[key] = value;
|
||||
});
|
||||
} else if (typeof headers === 'object') {
|
||||
// Plain object
|
||||
for (var key in headers) {
|
||||
if (headers.hasOwnProperty(key)) {
|
||||
requestOptions.headers[key] = headers[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add body if present
|
||||
if (body !== undefined && body !== null) {
|
||||
if (typeof body === 'string') {
|
||||
requestOptions.body = body;
|
||||
} else if (typeof body === 'object') {
|
||||
// Assume JSON
|
||||
requestOptions.body = JSON.stringify(body);
|
||||
if (!requestOptions.headers['Content-Type'] && !requestOptions.headers['content-type']) {
|
||||
requestOptions.headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call JavaFetch bridge
|
||||
var jsHttpResponse = JavaFetch.fetch(url, requestOptions);
|
||||
|
||||
// Create Response object
|
||||
var response = new FetchResponse(jsHttpResponse);
|
||||
resolve(response);
|
||||
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Export for global use
|
||||
if (typeof window !== 'undefined') {
|
||||
window.fetch = fetch;
|
||||
window.Promise = Promise;
|
||||
} else if (typeof global !== 'undefined') {
|
||||
global.fetch = fetch;
|
||||
global.Promise = Promise;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 69 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 302 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 482 KiB |
@@ -5,6 +5,15 @@ const axiosInstance = axios.create({
|
||||
withCredentials: true // 重要:允许跨域请求携带cookie
|
||||
});
|
||||
|
||||
// 请求拦截器:将存储的Token添加到Authorization请求头
|
||||
axiosInstance.interceptors.request.use(config => {
|
||||
const token = localStorage.getItem('playground_token');
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
/**
|
||||
* 演练场API服务
|
||||
*/
|
||||
@@ -30,7 +39,12 @@ export const playgroundApi = {
|
||||
async login(password) {
|
||||
try {
|
||||
const response = await axiosInstance.post('/v2/playground/login', { password });
|
||||
return response.data;
|
||||
const data = response.data;
|
||||
// 登录成功时从响应中提取并保存Token
|
||||
if ((data.code === 200 || data.success) && data.data?.token) {
|
||||
localStorage.setItem('playground_token', data.data.token);
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.error || error.message || '登录失败');
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
</div>
|
||||
<div class="demo-basic--circle">
|
||||
<div class="block" style="text-align: center;">
|
||||
<img :height="150" src="../../public/images/lanzou111.png" alt="lz">
|
||||
<img :height="150" src="../../public/images/logo01.png" alt="lz">
|
||||
</div>
|
||||
</div>
|
||||
<!-- 项目简介移到卡片内 -->
|
||||
|
||||
@@ -1146,10 +1146,11 @@ function parseById(shareLinkInfo, http, logger) {
|
||||
const isAuthed = res.data.authed || res.data.public;
|
||||
authed.value = isAuthed;
|
||||
|
||||
// 如果后端session已失效,清除localStorage
|
||||
// 如果后端认证已失效,清除localStorage中的认证信息
|
||||
if (!isAuthed && savedAuth === 'true') {
|
||||
localStorage.removeItem('playground_authed');
|
||||
localStorage.removeItem('playground_auth_time');
|
||||
localStorage.removeItem('playground_token');
|
||||
}
|
||||
|
||||
return isAuthed;
|
||||
|
||||
@@ -200,6 +200,51 @@ public class CacheManager {
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期缓存记录,防止数据库无限增长
|
||||
* @return 删除的行数
|
||||
*/
|
||||
public Future<Integer> cleanupExpiredCache() {
|
||||
String sql = "DELETE FROM cache_link_info WHERE expiration > 0 AND expiration < #{now}";
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("now", System.currentTimeMillis());
|
||||
Promise<Integer> promise = Promise.promise();
|
||||
SqlTemplate.forUpdate(jdbcPool, sql)
|
||||
.execute(params)
|
||||
.onSuccess(res -> {
|
||||
int deleted = res.rowCount();
|
||||
if (deleted > 0) {
|
||||
LOGGER.info("清理过期缓存记录 {} 条", deleted);
|
||||
}
|
||||
promise.complete(deleted);
|
||||
})
|
||||
.onFailure(e -> {
|
||||
LOGGER.error("清理过期缓存失败", e);
|
||||
promise.fail(e);
|
||||
});
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册定时清理过期缓存任务(每小时执行一次)
|
||||
* 应在应用启动后调用
|
||||
*/
|
||||
public static void registerPeriodicCleanup() {
|
||||
try {
|
||||
io.vertx.core.Vertx vertx = cn.qaiu.vx.core.util.VertxHolder.getVertxInstance();
|
||||
vertx.setPeriodic(3600_000, 3600_000, id -> {
|
||||
try {
|
||||
new CacheManager().cleanupExpiredCache();
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("定时清理缓存任务跳过(数据库可能未就绪)", e);
|
||||
}
|
||||
});
|
||||
LOGGER.info("缓存定时清理任务已注册(每小时执行)");
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("注册缓存定时清理任务失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
public Future<Map<String, Integer>> getShareKeyTotal(String shareKey) {
|
||||
String sql = """
|
||||
SELECT `share_key`, SUM(cache_hit_total) AS hit_total, SUM(api_parser_total) AS parser_total
|
||||
|
||||
@@ -28,7 +28,7 @@ public class RateLimiter {
|
||||
MAX_REQUESTS, TIME_WINDOW, PATH_REG);
|
||||
}
|
||||
|
||||
synchronized public static Future<Void> checkRateLimit(HttpServerRequest request) {
|
||||
public static Future<Void> checkRateLimit(HttpServerRequest request) {
|
||||
Promise<Void> promise = Promise.promise();
|
||||
if (!request.path().matches(PATH_REG)) {
|
||||
// 如果请求路径不匹配正则,则不进行限流
|
||||
@@ -38,7 +38,13 @@ public class RateLimiter {
|
||||
|
||||
String ip = request.remoteAddress().host();
|
||||
|
||||
ipRequestMap.compute(ip, (key, requestInfo) -> {
|
||||
// 定期清理过期条目,防止 Map 无限增长
|
||||
if (ipRequestMap.size() > 1000) {
|
||||
long now = System.currentTimeMillis();
|
||||
ipRequestMap.entrySet().removeIf(entry -> now - entry.getValue().timestamp > TIME_WINDOW);
|
||||
}
|
||||
|
||||
RequestInfo info = ipRequestMap.compute(ip, (key, requestInfo) -> {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
if (requestInfo == null || currentTime - requestInfo.timestamp > TIME_WINDOW) {
|
||||
// 初始化或重置计数器
|
||||
@@ -50,7 +56,6 @@ public class RateLimiter {
|
||||
}
|
||||
});
|
||||
|
||||
RequestInfo info = ipRequestMap.get(ip);
|
||||
if (info.count > MAX_REQUESTS) {
|
||||
// 超过限制
|
||||
// 计算剩余时间
|
||||
@@ -66,8 +71,8 @@ public class RateLimiter {
|
||||
}
|
||||
|
||||
private static class RequestInfo {
|
||||
int count;
|
||||
long timestamp;
|
||||
volatile int count;
|
||||
volatile long timestamp;
|
||||
|
||||
RequestInfo(int count, long time) {
|
||||
this.count = count;
|
||||
|
||||
@@ -4,6 +4,10 @@ import io.vertx.core.json.JsonObject;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* JS演练场配置
|
||||
*
|
||||
@@ -13,11 +17,21 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@Slf4j
|
||||
public class PlaygroundConfig {
|
||||
|
||||
/** Token有效期:24小时 */
|
||||
private static final long TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000L;
|
||||
|
||||
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||
|
||||
/**
|
||||
* 单例实例
|
||||
*/
|
||||
private static PlaygroundConfig instance;
|
||||
|
||||
/**
|
||||
* 已颁发的认证Token及其创建时间
|
||||
*/
|
||||
private final Map<String, Long> validTokens = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 是否启用演练场
|
||||
* 默认false,不启用
|
||||
@@ -42,6 +56,39 @@ public class PlaygroundConfig {
|
||||
private PlaygroundConfig() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成并存储一个新的认证Token,同时清理过期Token
|
||||
*/
|
||||
public String generateToken() {
|
||||
// 清理过期Token,防止Map无限增长
|
||||
long now = System.currentTimeMillis();
|
||||
validTokens.entrySet().removeIf(e -> now - e.getValue() > TOKEN_EXPIRY_MS);
|
||||
// 使用SecureRandom生成32字节的密码学安全Token
|
||||
byte[] bytes = new byte[32];
|
||||
SECURE_RANDOM.nextBytes(bytes);
|
||||
StringBuilder sb = new StringBuilder(64);
|
||||
for (byte b : bytes) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
String token = sb.toString();
|
||||
validTokens.put(token, now);
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验Token是否合法且未过期
|
||||
*/
|
||||
public boolean validateToken(String token) {
|
||||
if (token == null || token.isEmpty()) return false;
|
||||
Long createdAt = validTokens.get(token);
|
||||
if (createdAt == null) return false;
|
||||
if (System.currentTimeMillis() - createdAt > TOKEN_EXPIRY_MS) {
|
||||
validTokens.remove(token);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*/
|
||||
|
||||
@@ -21,7 +21,6 @@ import io.vertx.core.http.HttpServerRequest;
|
||||
import io.vertx.core.http.HttpServerResponse;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.ext.web.RoutingContext;
|
||||
import io.vertx.ext.web.Session;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
@@ -47,7 +46,6 @@ public class PlaygroundApi {
|
||||
|
||||
private static final int MAX_PARSER_COUNT = 100;
|
||||
private static final int MAX_CODE_LENGTH = 128 * 1024; // 128KB 代码长度限制
|
||||
private static final String SESSION_AUTH_KEY = "playgroundAuthed";
|
||||
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
|
||||
|
||||
/**
|
||||
@@ -74,14 +72,14 @@ public class PlaygroundApi {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 否则检查Session中的认证状态
|
||||
Session session = ctx.session();
|
||||
if (session == null) {
|
||||
return false;
|
||||
// 检查 Authorization: Bearer <token> 请求头
|
||||
String authHeader = ctx.request().getHeader("Authorization");
|
||||
if (authHeader != null && authHeader.startsWith("Bearer ") && authHeader.length() > 7) {
|
||||
String token = authHeader.substring(7).trim();
|
||||
return config.validateToken(token);
|
||||
}
|
||||
|
||||
Boolean authed = session.get(SESSION_AUTH_KEY);
|
||||
return authed != null && authed;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,12 +114,8 @@ public class PlaygroundApi {
|
||||
try {
|
||||
PlaygroundConfig config = PlaygroundConfig.getInstance();
|
||||
|
||||
// 如果是公开模式,直接成功
|
||||
// 如果是公开模式,直接成功(不需要token)
|
||||
if (config.isPublic()) {
|
||||
Session session = ctx.session();
|
||||
if (session != null) {
|
||||
session.put(SESSION_AUTH_KEY, true);
|
||||
}
|
||||
promise.complete(JsonResult.success("公开模式,无需密码").toJsonObject());
|
||||
return promise.future();
|
||||
}
|
||||
@@ -137,11 +131,9 @@ public class PlaygroundApi {
|
||||
|
||||
// 验证密码
|
||||
if (config.getPassword().equals(password)) {
|
||||
Session session = ctx.session();
|
||||
if (session != null) {
|
||||
session.put(SESSION_AUTH_KEY, true);
|
||||
}
|
||||
promise.complete(JsonResult.success("登录成功").toJsonObject());
|
||||
String token = config.generateToken();
|
||||
JsonObject tokenData = new JsonObject().put("token", token);
|
||||
promise.complete(JsonResult.data(tokenData).toJsonObject());
|
||||
} else {
|
||||
promise.complete(JsonResult.error("密码错误").toJsonObject());
|
||||
}
|
||||
|
||||
@@ -29,6 +29,11 @@ public class CacheServiceImpl implements CacheService {
|
||||
|
||||
private final CacheManager cacheManager = new CacheManager();
|
||||
|
||||
static {
|
||||
// 服务类加载时注册缓存定时清理任务
|
||||
CacheManager.registerPeriodicCleanup();
|
||||
}
|
||||
|
||||
private Future<CacheLinkInfo> getAndSaveCachedShareLink(ParserCreate parserCreate) {
|
||||
|
||||
// 认证、域名相关(检查是否已经添加过参数,避免重复调用)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 要激活的配置: app-配置名称.yml
|
||||
active: local
|
||||
active: dev
|
||||
# 控制台输出的版权文字
|
||||
copyright: QAIU
|
||||
|
||||
Reference in New Issue
Block a user