mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2026-02-04 20:36:18 +00:00
Compare commits
2 Commits
copilot/up
...
ff08615d1e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff08615d1e | ||
|
|
4a6c3a1f90 |
13
README.md
13
README.md
@@ -40,8 +40,6 @@ https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.
|
|||||||
|
|
||||||
**JavaScript解析器文档:** [JavaScript解析器开发指南](parser/doc/JAVASCRIPT_PARSER_GUIDE.md) | [自定义解析器扩展指南](parser/doc/CUSTOM_PARSER_GUIDE.md) | [快速开始](parser/doc/CUSTOM_PARSER_QUICKSTART.md)
|
**JavaScript解析器文档:** [JavaScript解析器开发指南](parser/doc/JAVASCRIPT_PARSER_GUIDE.md) | [自定义解析器扩展指南](parser/doc/CUSTOM_PARSER_GUIDE.md) | [快速开始](parser/doc/CUSTOM_PARSER_QUICKSTART.md)
|
||||||
|
|
||||||
**Playground访问控制:** [配置指南](web-service/doc/PLAYGROUND_ACCESS_CONTROL.md) - 了解如何配置演练场的访问权限
|
|
||||||
|
|
||||||
## 预览地址
|
## 预览地址
|
||||||
[预览地址1](https://lz.qaiu.top)
|
[预览地址1](https://lz.qaiu.top)
|
||||||
[预览地址2](https://lzzz.qaiu.top)
|
[预览地址2](https://lzzz.qaiu.top)
|
||||||
@@ -61,7 +59,7 @@ main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/
|
|||||||
|
|
||||||
- [蓝奏云-lz](https://pc.woozooo.com/)
|
- [蓝奏云-lz](https://pc.woozooo.com/)
|
||||||
- [蓝奏云优享-iz](https://www.ilanzou.com/)
|
- [蓝奏云优享-iz](https://www.ilanzou.com/)
|
||||||
- ~[奶牛快传-cow(即将停服)](https://cowtransfer.com/)~
|
- [奶牛快传-cow](https://cowtransfer.com/)
|
||||||
- [移动云云空间-ec](https://www.ecpan.cn/web)
|
- [移动云云空间-ec](https://www.ecpan.cn/web)
|
||||||
- [小飞机网盘-fj](https://www.feijipan.com/)
|
- [小飞机网盘-fj](https://www.feijipan.com/)
|
||||||
- [亿方云-fc](https://www.fangcloud.com/)
|
- [亿方云-fc](https://www.fangcloud.com/)
|
||||||
@@ -473,10 +471,11 @@ Core模块集成Vert.x实现类似spring的注解式路由API
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
### 关于专属版
|
### 关于赞助定制专属版
|
||||||
99元, 提供对小飞机,蓝奏优享大文件解析的支持, 提供天翼云盘,移动云盘,联通云盘的解析支持
|
1. 专属版提供对小飞机,蓝奏优享大文件解析的支持, 提供天翼云盘/移动云盘/联通云盘的解析支持。
|
||||||
199元, 包含部署服务, 需提供宝塔环境
|
2. 可提供托管服务:包含部署服务和云服务器环境。
|
||||||
可以提供功能定制开发, 添加以下任意一个联系方式详谈:
|
3. 可提供功能定制开发。
|
||||||
|
您可能需要提供一定的资金赞助支持定制专属版, 请添加以下任意一个联系方式详谈赞助模式:
|
||||||
<p>qq: 197575894</p>
|
<p>qq: 197575894</p>
|
||||||
<p>wechat: imcoding_</p>
|
<p>wechat: imcoding_</p>
|
||||||
|
|
||||||
|
|||||||
270
TESTING_GUIDE.md
270
TESTING_GUIDE.md
@@ -1,270 +0,0 @@
|
|||||||
# Playground Access Control - Testing Guide
|
|
||||||
|
|
||||||
## Quick Test Scenarios
|
|
||||||
|
|
||||||
### Scenario 1: Disabled Mode (Default)
|
|
||||||
**Configuration:**
|
|
||||||
```yaml
|
|
||||||
playground:
|
|
||||||
enabled: false
|
|
||||||
password: ""
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected Behavior:**
|
|
||||||
1. Navigate to `/playground`
|
|
||||||
2. Should see: "Playground未开启,请联系管理员在配置中启用此功能"
|
|
||||||
3. All API endpoints (`/v2/playground/*`) should return error
|
|
||||||
|
|
||||||
**API Test:**
|
|
||||||
```bash
|
|
||||||
curl http://localhost:6400/v2/playground/status
|
|
||||||
# Expected: {"code":200,"msg":"success","success":true,"data":{"enabled":false,"needPassword":false,"authed":false}}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Scenario 2: Password-Protected Mode
|
|
||||||
**Configuration:**
|
|
||||||
```yaml
|
|
||||||
playground:
|
|
||||||
enabled: true
|
|
||||||
password: "test123"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected Behavior:**
|
|
||||||
1. Navigate to `/playground`
|
|
||||||
2. Should see password input form with lock icon
|
|
||||||
3. Enter wrong password → Error message: "密码错误"
|
|
||||||
4. Enter correct password "test123" → Success, editor loads
|
|
||||||
5. Refresh page → Should remain authenticated
|
|
||||||
|
|
||||||
**API Tests:**
|
|
||||||
```bash
|
|
||||||
# Check status
|
|
||||||
curl http://localhost:6400/v2/playground/status
|
|
||||||
# Expected: {"enabled":true,"needPassword":true,"authed":false}
|
|
||||||
|
|
||||||
# Login with wrong password
|
|
||||||
curl -X POST http://localhost:6400/v2/playground/login \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"password":"wrong"}'
|
|
||||||
# Expected: {"code":500,"msg":"密码错误","success":false}
|
|
||||||
|
|
||||||
# Login with correct password
|
|
||||||
curl -X POST http://localhost:6400/v2/playground/login \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"password":"test123"}'
|
|
||||||
# Expected: {"code":200,"msg":"登录成功","success":true}
|
|
||||||
|
|
||||||
# Try to access without login (should fail)
|
|
||||||
curl http://localhost:6400/v2/playground/test \
|
|
||||||
-X POST \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"jsCode":"function parse(){return \"test\";}","shareUrl":"http://test.com"}'
|
|
||||||
# Expected: Error response
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Scenario 3: Public Access Mode
|
|
||||||
**Configuration:**
|
|
||||||
```yaml
|
|
||||||
playground:
|
|
||||||
enabled: true
|
|
||||||
password: ""
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected Behavior:**
|
|
||||||
1. Navigate to `/playground`
|
|
||||||
2. Should directly load the editor (no password prompt)
|
|
||||||
3. All features work immediately
|
|
||||||
|
|
||||||
**API Test:**
|
|
||||||
```bash
|
|
||||||
curl http://localhost:6400/v2/playground/status
|
|
||||||
# Expected: {"enabled":true,"needPassword":false,"authed":true}
|
|
||||||
```
|
|
||||||
|
|
||||||
⚠️ **Warning**: Only use this mode in localhost or secure internal network!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Full Feature Tests
|
|
||||||
|
|
||||||
### 1. Status Endpoint
|
|
||||||
```bash
|
|
||||||
curl http://localhost:6400/v2/playground/status
|
|
||||||
```
|
|
||||||
|
|
||||||
Should return JSON with:
|
|
||||||
- `enabled`: boolean
|
|
||||||
- `needPassword`: boolean
|
|
||||||
- `authed`: boolean
|
|
||||||
|
|
||||||
### 2. Login Endpoint (when password is set)
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:6400/v2/playground/login \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"password":"YOUR_PASSWORD"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Test Script Execution (after authentication)
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:6400/v2/playground/test \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"jsCode": "function parse(shareLinkInfo, http, logger) { return \"http://example.com/file.zip\"; }",
|
|
||||||
"shareUrl": "https://example.com/share/123",
|
|
||||||
"pwd": "",
|
|
||||||
"method": "parse"
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Get Types Definition
|
|
||||||
```bash
|
|
||||||
curl http://localhost:6400/v2/playground/types.js
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Parser Management (after authentication)
|
|
||||||
```bash
|
|
||||||
# List parsers
|
|
||||||
curl http://localhost:6400/v2/playground/parsers
|
|
||||||
|
|
||||||
# Get parser by ID
|
|
||||||
curl http://localhost:6400/v2/playground/parsers/1
|
|
||||||
|
|
||||||
# Delete parser
|
|
||||||
curl -X DELETE http://localhost:6400/v2/playground/parsers/1
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## UI Testing Checklist
|
|
||||||
|
|
||||||
### When Disabled
|
|
||||||
- [ ] Page shows "Playground未开启" message
|
|
||||||
- [ ] No editor visible
|
|
||||||
- [ ] Clean, centered layout
|
|
||||||
|
|
||||||
### When Password Protected (Not Authenticated)
|
|
||||||
- [ ] Password input form visible
|
|
||||||
- [ ] Lock icon displayed
|
|
||||||
- [ ] Can toggle password visibility
|
|
||||||
- [ ] Enter key submits form
|
|
||||||
- [ ] Error message shows for wrong password
|
|
||||||
- [ ] Success message and editor loads on correct password
|
|
||||||
|
|
||||||
### When Password Protected (Authenticated)
|
|
||||||
- [ ] Editor loads immediately on page refresh
|
|
||||||
- [ ] All features work (run, save, format, etc.)
|
|
||||||
- [ ] Can execute tests
|
|
||||||
- [ ] Can save/load parsers
|
|
||||||
|
|
||||||
### When Public Access
|
|
||||||
- [ ] Editor loads immediately
|
|
||||||
- [ ] All features work without authentication
|
|
||||||
- [ ] No password prompt visible
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Configuration Examples
|
|
||||||
|
|
||||||
### Production (Recommended)
|
|
||||||
```yaml
|
|
||||||
playground:
|
|
||||||
enabled: false
|
|
||||||
password: ""
|
|
||||||
```
|
|
||||||
|
|
||||||
### Development Team (Public Network)
|
|
||||||
```yaml
|
|
||||||
playground:
|
|
||||||
enabled: true
|
|
||||||
password: "SecureP@ssw0rd2024!"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Local Development
|
|
||||||
```yaml
|
|
||||||
playground:
|
|
||||||
enabled: true
|
|
||||||
password: ""
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Common Issues
|
|
||||||
|
|
||||||
### Issue: "Failed to extract session ID from cookie"
|
|
||||||
**Cause**: Cookie parsing error
|
|
||||||
**Solution**: This is logged as a warning and falls back to IP-based identification
|
|
||||||
|
|
||||||
### Issue: Editor doesn't load after correct password
|
|
||||||
**Cause**: Frontend state not updated
|
|
||||||
**Solution**: Check browser console for errors, ensure initPlayground() is called
|
|
||||||
|
|
||||||
### Issue: Authentication lost on page refresh
|
|
||||||
**Cause**: Server restarted (in-memory session storage)
|
|
||||||
**Solution**: Expected behavior - re-enter password after server restart
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Security Verification
|
|
||||||
|
|
||||||
### 1. Default Security
|
|
||||||
- [ ] Default config has `enabled: false`
|
|
||||||
- [ ] Cannot access playground without enabling
|
|
||||||
- [ ] No unintended API exposure
|
|
||||||
|
|
||||||
### 2. Password Protection
|
|
||||||
- [ ] Wrong password rejected
|
|
||||||
- [ ] Session persists across requests
|
|
||||||
- [ ] Different clients have independent sessions
|
|
||||||
|
|
||||||
### 3. API Protection
|
|
||||||
- [ ] All playground endpoints check authentication
|
|
||||||
- [ ] Status endpoint accessible without auth (returns state only)
|
|
||||||
- [ ] Login endpoint accessible without auth (for authentication)
|
|
||||||
- [ ] All other endpoints require authentication when password is set
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Performance Testing
|
|
||||||
|
|
||||||
### Load Test
|
|
||||||
```bash
|
|
||||||
# Test status endpoint
|
|
||||||
ab -n 1000 -c 10 http://localhost:6400/v2/playground/status
|
|
||||||
```
|
|
||||||
|
|
||||||
### Session Management Test
|
|
||||||
```bash
|
|
||||||
# Create multiple concurrent sessions
|
|
||||||
for i in {1..10}; do
|
|
||||||
curl -X POST http://localhost:6400/v2/playground/login \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"password":"test123"}' &
|
|
||||||
done
|
|
||||||
wait
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Cleanup
|
|
||||||
|
|
||||||
After testing, remember to:
|
|
||||||
1. Set `enabled: false` in production
|
|
||||||
2. Use strong passwords if enabling in public networks
|
|
||||||
3. Monitor access logs
|
|
||||||
4. Regularly review created parsers
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Documentation References
|
|
||||||
|
|
||||||
- Full documentation: `web-service/doc/PLAYGROUND_ACCESS_CONTROL.md`
|
|
||||||
- Main README: `README.md` (Playground Access Control section)
|
|
||||||
- Configuration file: `web-service/src/main/resources/app-dev.yml`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Last Updated: 2025-12-07
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
package cn.qaiu.vx.core.verticle.conf;
|
|
||||||
|
|
||||||
import io.vertx.core.json.JsonObject;
|
|
||||||
import io.vertx.core.json.JsonArray;
|
|
||||||
import io.vertx.core.json.impl.JsonUtil;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.Base64;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converter and mapper for {@link cn.qaiu.vx.core.verticle.conf.HttpProxyConf}.
|
|
||||||
* NOTE: This class has been automatically generated from the {@link cn.qaiu.vx.core.verticle.conf.HttpProxyConf} original class using Vert.x codegen.
|
|
||||||
*/
|
|
||||||
public class HttpProxyConfConverter {
|
|
||||||
|
|
||||||
|
|
||||||
private static final Base64.Decoder BASE64_DECODER = JsonUtil.BASE64_DECODER;
|
|
||||||
private static final Base64.Encoder BASE64_ENCODER = JsonUtil.BASE64_ENCODER;
|
|
||||||
|
|
||||||
static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json, HttpProxyConf obj) {
|
|
||||||
for (java.util.Map.Entry<String, Object> member : json) {
|
|
||||||
switch (member.getKey()) {
|
|
||||||
case "password":
|
|
||||||
if (member.getValue() instanceof String) {
|
|
||||||
obj.setPassword((String)member.getValue());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "port":
|
|
||||||
if (member.getValue() instanceof Number) {
|
|
||||||
obj.setPort(((Number)member.getValue()).intValue());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "preProxyOptions":
|
|
||||||
if (member.getValue() instanceof JsonObject) {
|
|
||||||
obj.setPreProxyOptions(new io.vertx.core.net.ProxyOptions((io.vertx.core.json.JsonObject)member.getValue()));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "timeout":
|
|
||||||
if (member.getValue() instanceof Number) {
|
|
||||||
obj.setTimeout(((Number)member.getValue()).intValue());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "username":
|
|
||||||
if (member.getValue() instanceof String) {
|
|
||||||
obj.setUsername((String)member.getValue());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void toJson(HttpProxyConf obj, JsonObject json) {
|
|
||||||
toJson(obj, json.getMap());
|
|
||||||
}
|
|
||||||
|
|
||||||
static void toJson(HttpProxyConf obj, java.util.Map<String, Object> json) {
|
|
||||||
if (obj.getPassword() != null) {
|
|
||||||
json.put("password", obj.getPassword());
|
|
||||||
}
|
|
||||||
if (obj.getPort() != null) {
|
|
||||||
json.put("port", obj.getPort());
|
|
||||||
}
|
|
||||||
if (obj.getPreProxyOptions() != null) {
|
|
||||||
json.put("preProxyOptions", obj.getPreProxyOptions().toJson());
|
|
||||||
}
|
|
||||||
if (obj.getTimeout() != null) {
|
|
||||||
json.put("timeout", obj.getTimeout());
|
|
||||||
}
|
|
||||||
if (obj.getUsername() != null) {
|
|
||||||
json.put("username", obj.getUsername());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,36 +4,6 @@ import axios from 'axios';
|
|||||||
* 演练场API服务
|
* 演练场API服务
|
||||||
*/
|
*/
|
||||||
export const playgroundApi = {
|
export const playgroundApi = {
|
||||||
/**
|
|
||||||
* 获取Playground状态
|
|
||||||
* @returns {Promise} 状态信息 {enabled, needPassword, authed}
|
|
||||||
*/
|
|
||||||
async getStatus() {
|
|
||||||
try {
|
|
||||||
const response = await axios.get('/v2/playground/status');
|
|
||||||
if (response.data && response.data.data) {
|
|
||||||
return response.data.data;
|
|
||||||
}
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(error.response?.data?.msg || error.message || '获取状态失败');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Playground登录
|
|
||||||
* @param {string} password - 密码
|
|
||||||
* @returns {Promise} 登录结果
|
|
||||||
*/
|
|
||||||
async login(password) {
|
|
||||||
try {
|
|
||||||
const response = await axios.post('/v2/playground/login', { password });
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(error.response?.data?.msg || error.message || '登录失败');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 测试执行JavaScript代码
|
* 测试执行JavaScript代码
|
||||||
* @param {string} jsCode - JavaScript代码
|
* @param {string} jsCode - JavaScript代码
|
||||||
|
|||||||
@@ -1,64 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="playgroundContainer" class="playground-container" :class="{ 'dark-theme': isDarkMode, 'fullscreen': isFullscreen }">
|
<div ref="playgroundContainer" class="playground-container" :class="{ 'dark-theme': isDarkMode, 'fullscreen': isFullscreen }">
|
||||||
<!-- 加载状态 -->
|
<el-card class="playground-card">
|
||||||
<el-card v-if="statusLoading" class="playground-card" v-loading="true" element-loading-text="正在加载...">
|
|
||||||
<div style="height: 400px;"></div>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<!-- Playground未开启 -->
|
|
||||||
<el-card v-else-if="!enabled" class="playground-card">
|
|
||||||
<el-empty description="Playground未开启">
|
|
||||||
<template #extra>
|
|
||||||
<p style="color: #909399; font-size: 14px; margin-top: 10px;">
|
|
||||||
Playground功能目前未启用,请联系管理员在配置中开启此功能。
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
</el-empty>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<!-- 需要密码但未认证 -->
|
|
||||||
<el-card v-else-if="needPassword && !authed" class="playground-card">
|
|
||||||
<div class="password-container">
|
|
||||||
<h2>🔒 Playground访问认证</h2>
|
|
||||||
<p style="color: #909399; margin: 20px 0;">
|
|
||||||
此Playground需要密码访问,请输入密码后继续使用。
|
|
||||||
</p>
|
|
||||||
<el-form @submit.prevent="submitPassword" style="max-width: 400px; margin: 0 auto;">
|
|
||||||
<el-form-item>
|
|
||||||
<el-input
|
|
||||||
v-model="password"
|
|
||||||
type="password"
|
|
||||||
placeholder="请输入访问密码"
|
|
||||||
size="large"
|
|
||||||
show-password
|
|
||||||
clearable
|
|
||||||
@keyup.enter="submitPassword"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<el-icon><Lock /></el-icon>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="authError" style="margin-bottom: 10px;">
|
|
||||||
<el-alert type="error" :title="authError" :closable="false" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
size="large"
|
|
||||||
style="width: 100%;"
|
|
||||||
:loading="authenticating"
|
|
||||||
@click="submitPassword"
|
|
||||||
>
|
|
||||||
{{ authenticating ? '验证中...' : '验证并进入' }}
|
|
||||||
</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<!-- 已启用且已认证(或公开模式) -->
|
|
||||||
<el-card v-else class="playground-card">
|
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
@@ -531,7 +473,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue';
|
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
import { Lock } from '@element-plus/icons-vue';
|
|
||||||
import { useMagicKeys, useFullscreen, useEventListener } from '@vueuse/core';
|
import { useMagicKeys, useFullscreen, useEventListener } from '@vueuse/core';
|
||||||
import { Splitpanes, Pane } from 'splitpanes';
|
import { Splitpanes, Pane } from 'splitpanes';
|
||||||
import 'splitpanes/dist/splitpanes.css';
|
import 'splitpanes/dist/splitpanes.css';
|
||||||
@@ -546,8 +487,7 @@ export default {
|
|||||||
MonacoEditor,
|
MonacoEditor,
|
||||||
JsonViewer,
|
JsonViewer,
|
||||||
Splitpanes,
|
Splitpanes,
|
||||||
Pane,
|
Pane
|
||||||
Lock
|
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const editorRef = ref(null);
|
const editorRef = ref(null);
|
||||||
@@ -565,15 +505,6 @@ export default {
|
|||||||
const loadingList = ref(false);
|
const loadingList = ref(false);
|
||||||
const publishDialogVisible = ref(false);
|
const publishDialogVisible = ref(false);
|
||||||
const publishing = ref(false);
|
const publishing = ref(false);
|
||||||
|
|
||||||
// Playground状态相关
|
|
||||||
const statusLoading = ref(true);
|
|
||||||
const enabled = ref(false);
|
|
||||||
const needPassword = ref(false);
|
|
||||||
const authed = ref(false);
|
|
||||||
const password = ref('');
|
|
||||||
const authError = ref('');
|
|
||||||
const authenticating = ref(false);
|
|
||||||
const publishForm = ref({
|
const publishForm = ref({
|
||||||
jsCode: ''
|
jsCode: ''
|
||||||
});
|
});
|
||||||
@@ -725,63 +656,6 @@ function parseById(shareLinkInfo, http, logger) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取Playground状态
|
|
||||||
const fetchStatus = async () => {
|
|
||||||
try {
|
|
||||||
const result = await playgroundApi.getStatus();
|
|
||||||
enabled.value = result.enabled;
|
|
||||||
needPassword.value = result.needPassword;
|
|
||||||
authed.value = result.authed;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取Playground状态失败:', error);
|
|
||||||
ElMessage.error('获取Playground状态失败: ' + error.message);
|
|
||||||
// 默认为未启用
|
|
||||||
enabled.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 提交密码
|
|
||||||
const submitPassword = async () => {
|
|
||||||
if (!password.value) {
|
|
||||||
authError.value = '请输入密码';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
authError.value = '';
|
|
||||||
authenticating.value = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await playgroundApi.login(password.value);
|
|
||||||
if (result.success || result.code === 200) {
|
|
||||||
authed.value = true;
|
|
||||||
ElMessage.success('认证成功');
|
|
||||||
// 初始化Playground
|
|
||||||
await nextTick();
|
|
||||||
await initPlayground();
|
|
||||||
} else {
|
|
||||||
authError.value = result.msg || '密码错误';
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Playground登录失败:', error);
|
|
||||||
authError.value = error.message || '登录失败';
|
|
||||||
} finally {
|
|
||||||
authenticating.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 初始化Playground(加载编辑器等)
|
|
||||||
const initPlayground = async () => {
|
|
||||||
await initMonacoTypes();
|
|
||||||
|
|
||||||
// 加载保存的代码
|
|
||||||
const saved = localStorage.getItem('playground_code');
|
|
||||||
if (saved) {
|
|
||||||
jsCode.value = saved;
|
|
||||||
} else {
|
|
||||||
jsCode.value = exampleCode;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 代码变化处理
|
// 代码变化处理
|
||||||
const onCodeChange = (value) => {
|
const onCodeChange = (value) => {
|
||||||
jsCode.value = value;
|
jsCode.value = value;
|
||||||
@@ -1282,19 +1156,14 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
checkDarkMode();
|
checkDarkMode();
|
||||||
|
await initMonacoTypes();
|
||||||
|
|
||||||
// 首先获取Playground状态
|
// 加载保存的代码
|
||||||
await fetchStatus();
|
const saved = localStorage.getItem('playground_code');
|
||||||
statusLoading.value = false;
|
if (saved) {
|
||||||
|
jsCode.value = saved;
|
||||||
// 如果未启用,直接返回
|
} else {
|
||||||
if (!enabled.value) {
|
jsCode.value = exampleCode;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果不需要密码或已认证,初始化Playground
|
|
||||||
if (!needPassword.value || authed.value) {
|
|
||||||
await initPlayground();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载保存的主题
|
// 加载保存的主题
|
||||||
@@ -1375,17 +1244,6 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
|
|||||||
helpCollapseActive,
|
helpCollapseActive,
|
||||||
consoleLogs,
|
consoleLogs,
|
||||||
clearConsoleLogs,
|
clearConsoleLogs,
|
||||||
// Playground状态相关
|
|
||||||
statusLoading,
|
|
||||||
enabled,
|
|
||||||
needPassword,
|
|
||||||
authed,
|
|
||||||
password,
|
|
||||||
authError,
|
|
||||||
authenticating,
|
|
||||||
fetchStatus,
|
|
||||||
submitPassword,
|
|
||||||
initPlayground,
|
|
||||||
// 新增功能
|
// 新增功能
|
||||||
collapsedPanels,
|
collapsedPanels,
|
||||||
togglePanel,
|
togglePanel,
|
||||||
@@ -1408,27 +1266,6 @@ curl "${baseUrl}/json/parser?url=${encodeURIComponent(exampleUrl)}"</pre>
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Password container styles */
|
|
||||||
.password-container {
|
|
||||||
text-align: center;
|
|
||||||
padding: 60px 20px;
|
|
||||||
min-height: 400px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.password-container h2 {
|
|
||||||
font-size: 28px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark-theme .password-container {
|
|
||||||
color: rgba(255, 255, 255, 0.85);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* API示例对话框样式 */
|
/* API示例对话框样式 */
|
||||||
.api-example-dialog {
|
.api-example-dialog {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
|
|||||||
@@ -1,293 +0,0 @@
|
|||||||
# Playground 访问控制配置指南
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
Playground 演练场是一个用于编写、测试和发布 JavaScript 解析脚本的在线开发环境。为了提升安全性,现在支持灵活的访问控制配置。
|
|
||||||
|
|
||||||
## 配置说明
|
|
||||||
|
|
||||||
在 `web-service/src/main/resources/app-dev.yml` 文件中添加以下配置:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# Playground演练场配置
|
|
||||||
playground:
|
|
||||||
# 是否启用Playground,默认关闭
|
|
||||||
enabled: false
|
|
||||||
# 访问密码,可选。仅在enabled=true时生效
|
|
||||||
# 为空时表示公开访问,不需要密码
|
|
||||||
password: ""
|
|
||||||
```
|
|
||||||
|
|
||||||
### 配置参数
|
|
||||||
|
|
||||||
#### `enabled`
|
|
||||||
- **类型**: `boolean`
|
|
||||||
- **默认值**: `false`
|
|
||||||
- **说明**: 控制 Playground 功能是否启用
|
|
||||||
- `false`: Playground 完全关闭,页面和所有相关 API 均不可访问
|
|
||||||
- `true`: Playground 启用,可以正常使用
|
|
||||||
|
|
||||||
#### `password`
|
|
||||||
- **类型**: `string`
|
|
||||||
- **默认值**: `""` (空字符串)
|
|
||||||
- **说明**: 访问密码(仅在 `enabled = true` 时生效)
|
|
||||||
- 空字符串或 `null`: 公开访问模式,无需密码
|
|
||||||
- 非空字符串: 需要输入正确密码才能访问
|
|
||||||
|
|
||||||
## 访问模式
|
|
||||||
|
|
||||||
### 1. 完全禁用模式 (enabled = false)
|
|
||||||
|
|
||||||
这是**默认且推荐的生产环境配置**。
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
playground:
|
|
||||||
enabled: false
|
|
||||||
password: ""
|
|
||||||
```
|
|
||||||
|
|
||||||
**行为**:
|
|
||||||
- `/playground` 页面显示"Playground未开启"提示
|
|
||||||
- 所有 Playground 相关 API(`/v2/playground/**`)返回错误提示
|
|
||||||
- 最安全的模式,适合生产环境
|
|
||||||
|
|
||||||
**适用场景**:
|
|
||||||
- 生产环境部署
|
|
||||||
- 不需要使用 Playground 功能的情况
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. 密码保护模式 (enabled = true, password 非空)
|
|
||||||
|
|
||||||
这是**公网环境的推荐配置**。
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
playground:
|
|
||||||
enabled: true
|
|
||||||
password: "your_strong_password_here"
|
|
||||||
```
|
|
||||||
|
|
||||||
**行为**:
|
|
||||||
- 访问 `/playground` 页面时会显示密码输入框
|
|
||||||
- 需要输入正确密码才能进入编辑器
|
|
||||||
- 密码验证通过后,在当前会话中保持已登录状态
|
|
||||||
- 会话基于客户端 IP 或 Cookie 进行识别
|
|
||||||
|
|
||||||
**适用场景**:
|
|
||||||
- 需要在公网环境使用 Playground
|
|
||||||
- 多人共享访问,但需要访问控制
|
|
||||||
- 团队协作开发环境
|
|
||||||
|
|
||||||
**示例**:
|
|
||||||
```yaml
|
|
||||||
playground:
|
|
||||||
enabled: true
|
|
||||||
password: "MySecure@Password123"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. 公开访问模式 (enabled = true, password 为空)
|
|
||||||
|
|
||||||
⚠️ **仅建议在本地开发或内网环境使用**。
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
playground:
|
|
||||||
enabled: true
|
|
||||||
password: ""
|
|
||||||
```
|
|
||||||
|
|
||||||
**行为**:
|
|
||||||
- Playground 对所有访问者开放
|
|
||||||
- 无需输入密码即可使用所有功能
|
|
||||||
- 页面加载后直接显示编辑器
|
|
||||||
|
|
||||||
**适用场景**:
|
|
||||||
- 本地开发环境(`localhost`)
|
|
||||||
- 完全隔离的内网环境
|
|
||||||
- 个人使用且不暴露在公网
|
|
||||||
|
|
||||||
**⚠️ 安全警告**:
|
|
||||||
> **强烈不建议在公网环境下使用此配置!**
|
|
||||||
>
|
|
||||||
> 公开访问模式允许任何人:
|
|
||||||
> - 执行任意 JavaScript 代码(虽然有沙箱限制)
|
|
||||||
> - 发布解析器脚本到数据库
|
|
||||||
> - 查看、修改、删除已有的解析器
|
|
||||||
> - 可能导致服务器资源被滥用
|
|
||||||
>
|
|
||||||
> 如果必须在公网环境开启 Playground,请务必:
|
|
||||||
> 1. 设置一个足够复杂的密码
|
|
||||||
> 2. 定期更换密码
|
|
||||||
> 3. 通过防火墙或网关限制访问来源(IP 白名单)
|
|
||||||
> 4. 启用访问日志监控
|
|
||||||
> 5. 考虑使用 HTTPS 加密传输
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 技术实现
|
|
||||||
|
|
||||||
### 后端实现
|
|
||||||
|
|
||||||
#### 状态检查 API
|
|
||||||
```
|
|
||||||
GET /v2/playground/status
|
|
||||||
```
|
|
||||||
|
|
||||||
返回:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"needPassword": true,
|
|
||||||
"authed": false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 登录 API
|
|
||||||
```
|
|
||||||
POST /v2/playground/login
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"password": "your_password"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 认证机制
|
|
||||||
- 使用 Vert.x 的 `SharedData` 存储认证状态
|
|
||||||
- 基于客户端 IP 或 Cookie 中的 session ID 识别用户
|
|
||||||
- 密码验证通过后在 `playground_auth` Map 中记录
|
|
||||||
|
|
||||||
#### 受保护的端点
|
|
||||||
所有以下端点都需要通过访问控制检查:
|
|
||||||
- `POST /v2/playground/test` - 执行测试
|
|
||||||
- `GET /v2/playground/types.js` - 获取类型定义
|
|
||||||
- `GET /v2/playground/parsers` - 获取解析器列表
|
|
||||||
- `POST /v2/playground/parsers` - 保存解析器
|
|
||||||
- `PUT /v2/playground/parsers/:id` - 更新解析器
|
|
||||||
- `DELETE /v2/playground/parsers/:id` - 删除解析器
|
|
||||||
- `GET /v2/playground/parsers/:id` - 获取解析器详情
|
|
||||||
|
|
||||||
### 前端实现
|
|
||||||
|
|
||||||
#### 状态检查流程
|
|
||||||
1. 页面加载时调用 `/v2/playground/status`
|
|
||||||
2. 根据返回的状态显示不同界面:
|
|
||||||
- `enabled = false`: 显示"未开启"提示
|
|
||||||
- `enabled = true & needPassword = true & !authed`: 显示密码输入框
|
|
||||||
- `enabled = true & (!needPassword || authed)`: 加载编辑器
|
|
||||||
|
|
||||||
#### 密码输入界面
|
|
||||||
- 密码输入框(支持显示/隐藏密码)
|
|
||||||
- 验证按钮
|
|
||||||
- 错误提示
|
|
||||||
- 支持回车键提交
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 配置示例
|
|
||||||
|
|
||||||
### 示例 1: 生产环境(推荐)
|
|
||||||
```yaml
|
|
||||||
playground:
|
|
||||||
enabled: false
|
|
||||||
password: ""
|
|
||||||
```
|
|
||||||
|
|
||||||
### 示例 2: 公网开发环境
|
|
||||||
```yaml
|
|
||||||
playground:
|
|
||||||
enabled: true
|
|
||||||
password: "Str0ng!P@ssw0rd#2024"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 示例 3: 本地开发
|
|
||||||
```yaml
|
|
||||||
playground:
|
|
||||||
enabled: true
|
|
||||||
password: ""
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 常见问题
|
|
||||||
|
|
||||||
### Q1: 忘记密码怎么办?
|
|
||||||
**A**: 直接修改 `app-dev.yml` 配置文件中的 `password` 值,然后重启服务即可。
|
|
||||||
|
|
||||||
### Q2: 可以动态修改密码吗?
|
|
||||||
**A**: 目前需要修改配置文件并重启服务。密码在服务启动时加载。
|
|
||||||
|
|
||||||
### Q3: 多个用户可以同时使用吗?
|
|
||||||
**A**: 可以。密码验证通过后,每个客户端都会保持独立的认证状态。
|
|
||||||
|
|
||||||
### Q4: 公开模式下有什么安全限制吗?
|
|
||||||
**A**:
|
|
||||||
- 代码执行有大小限制(128KB)
|
|
||||||
- JavaScript 在 Nashorn 沙箱中运行
|
|
||||||
- 最多创建 100 个解析器
|
|
||||||
- 但仍然建议在内网或本地使用
|
|
||||||
|
|
||||||
### Q5: 密码会明文传输吗?
|
|
||||||
**A**: 如果使用 HTTP,是的。强烈建议配置 HTTPS 以加密传输。
|
|
||||||
|
|
||||||
### Q6: 会话会过期吗?
|
|
||||||
**A**: 当前实现基于内存存储,服务重启后需要重新登录。单个会话不会主动过期。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 安全建议
|
|
||||||
|
|
||||||
### 🔒 强密码要求
|
|
||||||
如果启用密码保护,请确保密码符合以下要求:
|
|
||||||
- 长度至少 12 位
|
|
||||||
- 包含大小写字母、数字和特殊字符
|
|
||||||
- 避免使用常见单词或生日等个人信息
|
|
||||||
- 定期更换密码(建议每季度一次)
|
|
||||||
|
|
||||||
### 🌐 网络安全措施
|
|
||||||
- 在生产环境使用 HTTPS
|
|
||||||
- 配置防火墙或网关限制访问来源
|
|
||||||
- 使用反向代理(如 Nginx)添加额外的安全层
|
|
||||||
- 启用访问日志和监控
|
|
||||||
|
|
||||||
### 📝 最佳实践
|
|
||||||
1. **默认禁用**: 除非必要,保持 `enabled: false`
|
|
||||||
2. **密码保护**: 公网环境务必设置密码
|
|
||||||
3. **访问控制**: 结合 IP 白名单限制访问
|
|
||||||
4. **定期审计**: 检查已创建的解析器脚本
|
|
||||||
5. **监控告警**: 设置异常访问告警机制
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 迁移指南
|
|
||||||
|
|
||||||
### 从旧版本升级
|
|
||||||
|
|
||||||
如果你的系统之前没有 Playground 访问控制,升级后:
|
|
||||||
|
|
||||||
1. **默认行为**: Playground 将被禁用(`enabled: false`)
|
|
||||||
2. **如需启用**: 在配置文件中添加上述配置
|
|
||||||
3. **兼容性**: 完全向后兼容,不影响其他功能
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 更新日志
|
|
||||||
|
|
||||||
### v0.1.8
|
|
||||||
- 添加 Playground 访问控制功能
|
|
||||||
- 支持三种访问模式:禁用、密码保护、公开访问
|
|
||||||
- 前端添加密码输入界面
|
|
||||||
- 所有 Playground API 受保护
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 技术支持
|
|
||||||
|
|
||||||
如有问题,请访问:
|
|
||||||
- GitHub Issues: https://github.com/qaiu/netdisk-fast-download/issues
|
|
||||||
- 项目文档: https://github.com/qaiu/netdisk-fast-download
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**最后更新**: 2025-12-07
|
|
||||||
@@ -3,7 +3,6 @@ package cn.qaiu.lz;
|
|||||||
import cn.qaiu.WebClientVertxInit;
|
import cn.qaiu.WebClientVertxInit;
|
||||||
import cn.qaiu.db.pool.JDBCPoolInit;
|
import cn.qaiu.db.pool.JDBCPoolInit;
|
||||||
import cn.qaiu.lz.common.cache.CacheConfigLoader;
|
import cn.qaiu.lz.common.cache.CacheConfigLoader;
|
||||||
import cn.qaiu.lz.common.config.PlaygroundConfig;
|
|
||||||
import cn.qaiu.lz.common.interceptorImpl.RateLimiter;
|
import cn.qaiu.lz.common.interceptorImpl.RateLimiter;
|
||||||
import cn.qaiu.vx.core.Deploy;
|
import cn.qaiu.vx.core.Deploy;
|
||||||
import cn.qaiu.vx.core.util.ConfigConstant;
|
import cn.qaiu.vx.core.util.ConfigConstant;
|
||||||
@@ -89,10 +88,5 @@ public class AppMain {
|
|||||||
JsonObject auths = jsonObject.getJsonObject(ConfigConstant.AUTHS);
|
JsonObject auths = jsonObject.getJsonObject(ConfigConstant.AUTHS);
|
||||||
localMap.put(ConfigConstant.AUTHS, auths);
|
localMap.put(ConfigConstant.AUTHS, auths);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Playground配置
|
|
||||||
if (jsonObject.containsKey("playground")) {
|
|
||||||
PlaygroundConfig.init(jsonObject.getJsonObject("playground"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
package cn.qaiu.lz.common.config;
|
|
||||||
|
|
||||||
import io.vertx.core.json.JsonObject;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Playground配置加载器
|
|
||||||
*
|
|
||||||
* @author <a href="https://qaiu.top">QAIU</a>
|
|
||||||
*/
|
|
||||||
public class PlaygroundConfig {
|
|
||||||
private static boolean enabled = false;
|
|
||||||
private static String password = "";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化配置
|
|
||||||
* @param config 配置对象
|
|
||||||
*/
|
|
||||||
public static void init(JsonObject config) {
|
|
||||||
if (config == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
enabled = config.getBoolean("enabled", false);
|
|
||||||
password = config.getString("password", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否启用Playground
|
|
||||||
*/
|
|
||||||
public static boolean isEnabled() {
|
|
||||||
return enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取密码
|
|
||||||
*/
|
|
||||||
public static String getPassword() {
|
|
||||||
return password;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否需要密码
|
|
||||||
*/
|
|
||||||
public static boolean hasPassword() {
|
|
||||||
return StringUtils.isNotBlank(password);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package cn.qaiu.lz.web.controller;
|
package cn.qaiu.lz.web.controller;
|
||||||
|
|
||||||
import cn.qaiu.entity.ShareLinkInfo;
|
import cn.qaiu.entity.ShareLinkInfo;
|
||||||
import cn.qaiu.lz.common.config.PlaygroundConfig;
|
|
||||||
import cn.qaiu.lz.web.model.PlaygroundTestResp;
|
import cn.qaiu.lz.web.model.PlaygroundTestResp;
|
||||||
import cn.qaiu.lz.web.service.DbService;
|
import cn.qaiu.lz.web.service.DbService;
|
||||||
import cn.qaiu.parser.ParserCreate;
|
import cn.qaiu.parser.ParserCreate;
|
||||||
@@ -14,7 +13,6 @@ import cn.qaiu.vx.core.enums.RouteMethod;
|
|||||||
import cn.qaiu.vx.core.model.JsonResult;
|
import cn.qaiu.vx.core.model.JsonResult;
|
||||||
import cn.qaiu.vx.core.util.AsyncServiceUtil;
|
import cn.qaiu.vx.core.util.AsyncServiceUtil;
|
||||||
import cn.qaiu.vx.core.util.ResponseUtil;
|
import cn.qaiu.vx.core.util.ResponseUtil;
|
||||||
import cn.qaiu.vx.core.util.VertxHolder;
|
|
||||||
import io.vertx.core.Future;
|
import io.vertx.core.Future;
|
||||||
import io.vertx.core.Promise;
|
import io.vertx.core.Promise;
|
||||||
import io.vertx.core.http.HttpServerRequest;
|
import io.vertx.core.http.HttpServerRequest;
|
||||||
@@ -44,179 +42,8 @@ public class PlaygroundApi {
|
|||||||
|
|
||||||
private static final int MAX_PARSER_COUNT = 100;
|
private static final int MAX_PARSER_COUNT = 100;
|
||||||
private static final int MAX_CODE_LENGTH = 128 * 1024; // 128KB 代码长度限制
|
private static final int MAX_CODE_LENGTH = 128 * 1024; // 128KB 代码长度限制
|
||||||
private static final String AUTH_SESSION_KEY = "playground_authed_";
|
|
||||||
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
|
private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取Playground状态
|
|
||||||
*
|
|
||||||
* @param ctx 路由上下文
|
|
||||||
* @return 状态信息
|
|
||||||
*/
|
|
||||||
@RouteMapping(value = "/status", method = RouteMethod.GET)
|
|
||||||
public Future<JsonObject> getStatus(RoutingContext ctx) {
|
|
||||||
Promise<JsonObject> promise = Promise.promise();
|
|
||||||
|
|
||||||
try {
|
|
||||||
boolean enabled = PlaygroundConfig.isEnabled();
|
|
||||||
boolean needPassword = enabled && PlaygroundConfig.hasPassword();
|
|
||||||
boolean authed = false;
|
|
||||||
|
|
||||||
if (enabled) {
|
|
||||||
if (!needPassword) {
|
|
||||||
// 公开模式,无需认证
|
|
||||||
authed = true;
|
|
||||||
} else {
|
|
||||||
// 检查是否已认证
|
|
||||||
String clientId = getClientId(ctx.request());
|
|
||||||
authed = isAuthenticated(clientId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonObject result = new JsonObject()
|
|
||||||
.put("enabled", enabled)
|
|
||||||
.put("needPassword", needPassword)
|
|
||||||
.put("authed", authed);
|
|
||||||
|
|
||||||
promise.complete(JsonResult.data(result).toJsonObject());
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("获取Playground状态失败", e);
|
|
||||||
promise.complete(JsonResult.error("获取状态失败: " + e.getMessage()).toJsonObject());
|
|
||||||
}
|
|
||||||
|
|
||||||
return promise.future();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Playground登录
|
|
||||||
*
|
|
||||||
* @param ctx 路由上下文
|
|
||||||
* @return 登录结果
|
|
||||||
*/
|
|
||||||
@RouteMapping(value = "/login", method = RouteMethod.POST)
|
|
||||||
public Future<JsonObject> login(RoutingContext ctx) {
|
|
||||||
Promise<JsonObject> promise = Promise.promise();
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!PlaygroundConfig.isEnabled()) {
|
|
||||||
promise.complete(JsonResult.error("Playground未开启").toJsonObject());
|
|
||||||
return promise.future();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!PlaygroundConfig.hasPassword()) {
|
|
||||||
// 无密码模式,直接标记为已认证
|
|
||||||
String clientId = getClientId(ctx.request());
|
|
||||||
setAuthenticated(clientId, true);
|
|
||||||
promise.complete(JsonResult.success("登录成功").toJsonObject());
|
|
||||||
return promise.future();
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonObject body = ctx.body().asJsonObject();
|
|
||||||
String password = body.getString("password");
|
|
||||||
|
|
||||||
if (StringUtils.equals(password, PlaygroundConfig.getPassword())) {
|
|
||||||
String clientId = getClientId(ctx.request());
|
|
||||||
setAuthenticated(clientId, true);
|
|
||||||
promise.complete(JsonResult.success("登录成功").toJsonObject());
|
|
||||||
} else {
|
|
||||||
promise.complete(JsonResult.error("密码错误").toJsonObject());
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Playground登录失败", e);
|
|
||||||
promise.complete(JsonResult.error("登录失败: " + e.getMessage()).toJsonObject());
|
|
||||||
}
|
|
||||||
|
|
||||||
return promise.future();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查Playground是否启用和已认证
|
|
||||||
*/
|
|
||||||
private void ensurePlaygroundAccess(RoutingContext ctx) {
|
|
||||||
if (!PlaygroundConfig.isEnabled()) {
|
|
||||||
throw new IllegalStateException("Playground未开启");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PlaygroundConfig.hasPassword()) {
|
|
||||||
String clientId = getClientId(ctx.request());
|
|
||||||
if (!isAuthenticated(clientId)) {
|
|
||||||
throw new IllegalStateException("未授权访问Playground");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取客户端唯一标识
|
|
||||||
*/
|
|
||||||
private String getClientId(HttpServerRequest request) {
|
|
||||||
// 优先使用Cookie中的session id,否则使用IP
|
|
||||||
String sessionId = extractSessionIdFromCookie(request);
|
|
||||||
if (sessionId != null) {
|
|
||||||
return sessionId;
|
|
||||||
}
|
|
||||||
return getClientIp(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从Cookie中提取session ID
|
|
||||||
*/
|
|
||||||
private String extractSessionIdFromCookie(HttpServerRequest request) {
|
|
||||||
String cookie = request.getHeader("Cookie");
|
|
||||||
if (cookie == null || !cookie.contains("playground_session=")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final String SESSION_KEY = "playground_session=";
|
|
||||||
int startIndex = cookie.indexOf(SESSION_KEY);
|
|
||||||
if (startIndex == -1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
startIndex += SESSION_KEY.length();
|
|
||||||
int endIndex = cookie.indexOf(";", startIndex);
|
|
||||||
|
|
||||||
if (endIndex != -1) {
|
|
||||||
return cookie.substring(startIndex, endIndex).trim();
|
|
||||||
} else {
|
|
||||||
return cookie.substring(startIndex).trim();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("Failed to extract session ID from cookie", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查是否已认证
|
|
||||||
*/
|
|
||||||
private boolean isAuthenticated(String clientId) {
|
|
||||||
try {
|
|
||||||
Boolean authed = (Boolean) VertxHolder.getVertxInstance()
|
|
||||||
.sharedData()
|
|
||||||
.getLocalMap("playground_auth")
|
|
||||||
.get(AUTH_SESSION_KEY + clientId);
|
|
||||||
return authed != null && authed;
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("检查认证状态失败", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置认证状态
|
|
||||||
*/
|
|
||||||
private void setAuthenticated(String clientId, boolean authed) {
|
|
||||||
try {
|
|
||||||
VertxHolder.getVertxInstance()
|
|
||||||
.sharedData()
|
|
||||||
.getLocalMap("playground_auth")
|
|
||||||
.put(AUTH_SESSION_KEY + clientId, authed);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("设置认证状态失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 测试执行JavaScript代码
|
* 测试执行JavaScript代码
|
||||||
*
|
*
|
||||||
@@ -228,9 +55,6 @@ public class PlaygroundApi {
|
|||||||
Promise<JsonObject> promise = Promise.promise();
|
Promise<JsonObject> promise = Promise.promise();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 检查访问权限
|
|
||||||
ensurePlaygroundAccess(ctx);
|
|
||||||
|
|
||||||
JsonObject body = ctx.body().asJsonObject();
|
JsonObject body = ctx.body().asJsonObject();
|
||||||
String jsCode = body.getString("jsCode");
|
String jsCode = body.getString("jsCode");
|
||||||
String shareUrl = body.getString("shareUrl");
|
String shareUrl = body.getString("shareUrl");
|
||||||
@@ -396,33 +220,24 @@ public class PlaygroundApi {
|
|||||||
* 获取types.js文件内容
|
* 获取types.js文件内容
|
||||||
*
|
*
|
||||||
* @param response HTTP响应
|
* @param response HTTP响应
|
||||||
* @param ctx 路由上下文
|
|
||||||
*/
|
*/
|
||||||
@RouteMapping(value = "/types.js", method = RouteMethod.GET)
|
@RouteMapping(value = "/types.js", method = RouteMethod.GET)
|
||||||
public void getTypesJs(HttpServerResponse response, RoutingContext ctx) {
|
public void getTypesJs(HttpServerResponse response) {
|
||||||
try {
|
try (InputStream inputStream = getClass().getClassLoader()
|
||||||
// 检查访问权限
|
.getResourceAsStream("custom-parsers/types.js")) {
|
||||||
ensurePlaygroundAccess(ctx);
|
|
||||||
|
|
||||||
InputStream inputStream = getClass().getClassLoader()
|
|
||||||
.getResourceAsStream("custom-parsers/types.js");
|
|
||||||
|
|
||||||
if (inputStream == null) {
|
if (inputStream == null) {
|
||||||
ResponseUtil.fireJsonResultResponse(response, JsonResult.error("types.js文件不存在"));
|
ResponseUtil.fireJsonResultResponse(response, JsonResult.error("types.js文件不存在"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try (inputStream) {
|
String content = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))
|
||||||
String content = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))
|
.lines()
|
||||||
.lines()
|
.collect(Collectors.joining("\n"));
|
||||||
.collect(Collectors.joining("\n"));
|
|
||||||
|
response.putHeader("Content-Type", "text/javascript; charset=utf-8")
|
||||||
|
.end(content);
|
||||||
|
|
||||||
response.putHeader("Content-Type", "text/javascript; charset=utf-8")
|
|
||||||
.end(content);
|
|
||||||
}
|
|
||||||
} catch (IllegalStateException e) {
|
|
||||||
log.error("访问Playground失败", e);
|
|
||||||
ResponseUtil.fireJsonResultResponse(response, JsonResult.error(e.getMessage()));
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("读取types.js失败", e);
|
log.error("读取types.js失败", e);
|
||||||
ResponseUtil.fireJsonResultResponse(response, JsonResult.error("读取types.js失败: " + e.getMessage()));
|
ResponseUtil.fireJsonResultResponse(response, JsonResult.error("读取types.js失败: " + e.getMessage()));
|
||||||
@@ -433,15 +248,8 @@ public class PlaygroundApi {
|
|||||||
* 获取解析器列表
|
* 获取解析器列表
|
||||||
*/
|
*/
|
||||||
@RouteMapping(value = "/parsers", method = RouteMethod.GET)
|
@RouteMapping(value = "/parsers", method = RouteMethod.GET)
|
||||||
public Future<JsonObject> getParserList(RoutingContext ctx) {
|
public Future<JsonObject> getParserList() {
|
||||||
try {
|
return dbService.getPlaygroundParserList();
|
||||||
// 检查访问权限
|
|
||||||
ensurePlaygroundAccess(ctx);
|
|
||||||
return dbService.getPlaygroundParserList();
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("获取解析器列表失败", e);
|
|
||||||
return Future.succeededFuture(JsonResult.error(e.getMessage()).toJsonObject());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -452,9 +260,6 @@ public class PlaygroundApi {
|
|||||||
Promise<JsonObject> promise = Promise.promise();
|
Promise<JsonObject> promise = Promise.promise();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 检查访问权限
|
|
||||||
ensurePlaygroundAccess(ctx);
|
|
||||||
|
|
||||||
JsonObject body = ctx.body().asJsonObject();
|
JsonObject body = ctx.body().asJsonObject();
|
||||||
String jsCode = body.getString("jsCode");
|
String jsCode = body.getString("jsCode");
|
||||||
|
|
||||||
@@ -554,9 +359,6 @@ public class PlaygroundApi {
|
|||||||
Promise<JsonObject> promise = Promise.promise();
|
Promise<JsonObject> promise = Promise.promise();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 检查访问权限
|
|
||||||
ensurePlaygroundAccess(ctx);
|
|
||||||
|
|
||||||
JsonObject body = ctx.body().asJsonObject();
|
JsonObject body = ctx.body().asJsonObject();
|
||||||
String jsCode = body.getString("jsCode");
|
String jsCode = body.getString("jsCode");
|
||||||
|
|
||||||
@@ -608,30 +410,16 @@ public class PlaygroundApi {
|
|||||||
* 删除解析器
|
* 删除解析器
|
||||||
*/
|
*/
|
||||||
@RouteMapping(value = "/parsers/:id", method = RouteMethod.DELETE)
|
@RouteMapping(value = "/parsers/:id", method = RouteMethod.DELETE)
|
||||||
public Future<JsonObject> deleteParser(RoutingContext ctx, Long id) {
|
public Future<JsonObject> deleteParser(Long id) {
|
||||||
try {
|
return dbService.deletePlaygroundParser(id);
|
||||||
// 检查访问权限
|
|
||||||
ensurePlaygroundAccess(ctx);
|
|
||||||
return dbService.deletePlaygroundParser(id);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("删除解析器失败", e);
|
|
||||||
return Future.succeededFuture(JsonResult.error(e.getMessage()).toJsonObject());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据ID获取解析器
|
* 根据ID获取解析器
|
||||||
*/
|
*/
|
||||||
@RouteMapping(value = "/parsers/:id", method = RouteMethod.GET)
|
@RouteMapping(value = "/parsers/:id", method = RouteMethod.GET)
|
||||||
public Future<JsonObject> getParserById(RoutingContext ctx, Long id) {
|
public Future<JsonObject> getParserById(Long id) {
|
||||||
try {
|
return dbService.getPlaygroundParserById(id);
|
||||||
// 检查访问权限
|
|
||||||
ensurePlaygroundAccess(ctx);
|
|
||||||
return dbService.getPlaygroundParserById(id);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("获取解析器失败", e);
|
|
||||||
return Future.succeededFuture(JsonResult.error(e.getMessage()).toJsonObject());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -101,12 +101,4 @@ auths:
|
|||||||
# 123网盘:配置用户名密码
|
# 123网盘:配置用户名密码
|
||||||
ye:
|
ye:
|
||||||
username:
|
username:
|
||||||
password:
|
password:
|
||||||
|
|
||||||
# Playground演练场配置
|
|
||||||
playground:
|
|
||||||
# 是否启用Playground,默认关闭
|
|
||||||
enabled: false
|
|
||||||
# 访问密码,可选。仅在enabled=true时生效
|
|
||||||
# 为空时表示公开访问,不需要密码
|
|
||||||
password: ""
|
|
||||||
Reference in New Issue
Block a user