Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcf3e865b6 | ||
|
|
c16e147931 | ||
|
|
f623654151 | ||
|
|
9311124602 | ||
|
|
ed719255b7 | ||
|
|
dadfd4c78d | ||
|
|
72aa2846e5 | ||
|
|
4f0815a5f9 | ||
|
|
b4c37f33b0 | ||
|
|
9fffac0fb7 | ||
|
|
fb9a62f2a7 | ||
|
|
843350300f | ||
|
|
0f0039f2d4 | ||
|
|
9781dddfaf | ||
|
|
9ec2d6598e | ||
|
|
dfcfc7634a | ||
|
|
48b2f27090 | ||
|
|
e800ad1774 | ||
|
|
c34f1ed682 | ||
|
|
70cf1a7306 | ||
|
|
26889f5b38 | ||
|
|
ca876a8951 | ||
|
|
11e52e0ffc | ||
|
|
11bf99355d | ||
|
|
0468f52050 | ||
|
|
3207510c91 | ||
|
|
8e2ea02a62 | ||
|
|
24b8cf19aa | ||
|
|
16bc615a52 | ||
|
|
ee148fe6c3 | ||
|
|
ca4a32422c | ||
|
|
ec1e051252 | ||
|
|
bc912056cd | ||
|
|
25a21e284b | ||
|
|
2e7d5fb1ce | ||
|
|
67c651c804 | ||
|
|
0e0ac8b016 | ||
|
|
45ed4d0d6b | ||
|
|
b1c63f6713 | ||
|
|
7dd31c8f5a | ||
|
|
79eddf7fb2 | ||
|
|
46acf4123a | ||
|
|
65ff630868 | ||
|
|
2541996b18 | ||
|
|
82f17ad875 | ||
|
|
8ee1d94471 | ||
|
|
7970a4f093 | ||
|
|
acf0ff6d96 | ||
|
|
6fc1283519 | ||
|
|
de1055b959 | ||
|
|
f89522cb88 | ||
|
|
91241d08da | ||
|
|
3ee31047e9 | ||
|
|
30ddd53f19 | ||
|
|
d293831bf9 | ||
|
|
3d9990ddea | ||
|
|
54dbb024e1 | ||
|
|
63154085f7 | ||
|
|
b73f1be3dd | ||
|
|
5ad91654cc | ||
|
|
76139350a9 | ||
|
|
3494ce88a1 | ||
|
|
d6e9cc990b | ||
|
|
4d53b13220 | ||
|
|
397181429e | ||
|
|
e21e02a233 | ||
|
|
cd816ae065 | ||
|
|
e7b1a74a8d | ||
|
|
5b5c805768 | ||
|
|
1a205a8e8a | ||
|
|
39f33e99a7 | ||
|
|
722961ac93 | ||
|
|
f346b55688 | ||
|
|
6154764091 | ||
|
|
2fd2c65d64 | ||
|
|
e86c3db45f | ||
|
|
aea162a616 | ||
|
|
c701603bb1 | ||
|
|
cb428300cd | ||
|
|
747f53b997 | ||
|
|
d9a8462cf6 |
234
README.md
Normal file
234
README.md
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
# DNS服务器项目
|
||||||
|
|
||||||
|
## 项目简介
|
||||||
|
|
||||||
|
这是一个基于Go语言开发的DNS服务器,具有屏蔽规则管理、查询日志记录和统计、Web控制台等功能。该服务器可以拦截特定域名的DNS查询,提供实时的查询统计和日志记录,并通过Web控制台进行管理。
|
||||||
|
|
||||||
|
### 技术栈
|
||||||
|
|
||||||
|
- Go语言
|
||||||
|
- Gorilla Mux (HTTP路由)
|
||||||
|
- Gorilla WebSocket (实时通信)
|
||||||
|
- Chart.js (数据可视化)
|
||||||
|
- Tailwind CSS (样式框架)
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
### 1. DNS查询处理
|
||||||
|
- 支持UDP和TCP协议
|
||||||
|
- 支持常见DNS查询类型(A, AAAA, CNAME, MX, NS, TXT等)
|
||||||
|
- 高性能查询处理
|
||||||
|
|
||||||
|
### 2. 屏蔽规则管理
|
||||||
|
- 支持域名规则和正则表达式规则
|
||||||
|
- 支持规则例外
|
||||||
|
- 支持远程规则列表
|
||||||
|
- 支持本地规则管理
|
||||||
|
|
||||||
|
### 3. 查询日志记录和统计
|
||||||
|
- 实时记录DNS查询日志
|
||||||
|
- 支持日志持久化到文件
|
||||||
|
- 提供查询统计和趋势分析
|
||||||
|
- 支持日志搜索和过滤
|
||||||
|
- 支持日志排序
|
||||||
|
|
||||||
|
### 4. Web控制台
|
||||||
|
- 直观的仪表盘
|
||||||
|
- 实时统计数据
|
||||||
|
- 图表可视化
|
||||||
|
- 规则管理界面
|
||||||
|
- 查询日志详情页面
|
||||||
|
- 支持分页和自定义记录数量
|
||||||
|
|
||||||
|
### 5. WebSocket实时更新
|
||||||
|
- 实时更新统计数据
|
||||||
|
- 实时更新图表
|
||||||
|
- 支持连接状态管理
|
||||||
|
|
||||||
|
### 6. 查询日志持久化
|
||||||
|
- 将查询日志保存到`querylog.json`文件
|
||||||
|
- 定期自动保存
|
||||||
|
- 服务器重启后自动加载
|
||||||
|
|
||||||
|
## 安装步骤
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
|
||||||
|
- Go 1.18或更高版本
|
||||||
|
- Linux或Windows操作系统
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go mod download
|
||||||
|
```
|
||||||
|
|
||||||
|
### 编译和运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 编译
|
||||||
|
go build -o dns-server main.go
|
||||||
|
|
||||||
|
# 运行
|
||||||
|
./dns-server
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
### 主要配置项
|
||||||
|
|
||||||
|
- `ListenPort`: DNS服务器监听端口,默认53
|
||||||
|
- `HTTPPort`: HTTP控制台监听端口,默认8080
|
||||||
|
- `StatsFile`: 统计数据保存文件,默认`data/stats.json`
|
||||||
|
- `SaveInterval`: 自动保存间隔(秒),默认300
|
||||||
|
- `MaxQueryLogs`: 最大保存日志数量,默认1000
|
||||||
|
|
||||||
|
### 配置文件格式
|
||||||
|
|
||||||
|
配置文件使用JSON格式,位于`config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ListenPort": 53,
|
||||||
|
"HTTPPort": 8080,
|
||||||
|
"StatsFile": "data/stats.json",
|
||||||
|
"SaveInterval": 300,
|
||||||
|
"MaxQueryLogs": 1000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 启动服务器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./dns-server
|
||||||
|
```
|
||||||
|
|
||||||
|
### 访问Web控制台
|
||||||
|
|
||||||
|
在浏览器中访问:
|
||||||
|
```
|
||||||
|
http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### 管理屏蔽规则
|
||||||
|
|
||||||
|
1. 登录Web控制台
|
||||||
|
2. 点击左侧菜单中的"屏蔽管理"
|
||||||
|
3. 在"本地规则管理"中添加或删除规则
|
||||||
|
4. 在"远程黑名单管理"中添加或删除远程规则列表
|
||||||
|
|
||||||
|
### 查看查询日志
|
||||||
|
|
||||||
|
1. 登录Web控制台
|
||||||
|
2. 点击左侧菜单中的"查询日志"
|
||||||
|
3. 查看日志统计和趋势
|
||||||
|
4. 使用搜索和过滤功能查找特定日志
|
||||||
|
5. 点击列头进行排序
|
||||||
|
6. 使用刷新按钮手动刷新日志
|
||||||
|
|
||||||
|
## API文档
|
||||||
|
|
||||||
|
### 主要API端点
|
||||||
|
|
||||||
|
#### 1. DNS查询
|
||||||
|
```
|
||||||
|
GET /api/query?domain=example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 屏蔽规则管理
|
||||||
|
```
|
||||||
|
GET /api/shield/rules
|
||||||
|
POST /api/shield/rules
|
||||||
|
DELETE /api/shield/rules/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Hosts管理
|
||||||
|
```
|
||||||
|
GET /api/hosts
|
||||||
|
POST /api/hosts
|
||||||
|
DELETE /api/hosts/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. 查询日志
|
||||||
|
```
|
||||||
|
GET /api/logs/stats
|
||||||
|
GET /api/logs/query
|
||||||
|
GET /api/logs/count
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. WebSocket
|
||||||
|
```
|
||||||
|
ws://localhost:8080/ws/stats
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发说明
|
||||||
|
|
||||||
|
### 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
/root/dnsbak/
|
||||||
|
├── config/ # 配置文件
|
||||||
|
├── data/ # 数据文件
|
||||||
|
├── dns/ # DNS服务器相关代码
|
||||||
|
├── http/ # HTTP服务器相关代码
|
||||||
|
├── shield/ # 屏蔽规则管理
|
||||||
|
├── static/ # 静态资源
|
||||||
|
│ ├── css/ # CSS文件
|
||||||
|
│ ├── js/ # JavaScript文件
|
||||||
|
│ └── index.html # 主页面
|
||||||
|
├── main.go # 入口文件
|
||||||
|
├── go.mod # Go模块文件
|
||||||
|
└── go.sum # Go依赖校验文件
|
||||||
|
```
|
||||||
|
|
||||||
|
### 开发流程
|
||||||
|
|
||||||
|
1. 克隆仓库
|
||||||
|
2. 安装依赖
|
||||||
|
3. 开发功能
|
||||||
|
4. 编译和测试
|
||||||
|
5. 提交代码
|
||||||
|
|
||||||
|
### 测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
## 贡献
|
||||||
|
|
||||||
|
欢迎提交Issue和Pull Request!
|
||||||
|
|
||||||
|
## 联系方式
|
||||||
|
|
||||||
|
如有问题或建议,请通过以下方式联系:
|
||||||
|
|
||||||
|
- Email: wxf26054@live.cn
|
||||||
|
- Git: https://gitea.amazehome.xyz/AMAZEHOME/dns-server
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
### v1.0.0 (2025-11-30)
|
||||||
|
- 初始版本
|
||||||
|
- 实现基本DNS服务器功能
|
||||||
|
- 实现屏蔽规则管理
|
||||||
|
- 实现查询日志记录和统计
|
||||||
|
- 实现Web控制台
|
||||||
|
- 实现WebSocket实时更新
|
||||||
|
- 实现查询日志持久化
|
||||||
|
|
||||||
|
### v1.0.1 (2025-11-30)
|
||||||
|
- 修复搜索和过滤功能
|
||||||
|
- 优化查询日志显示
|
||||||
|
- 修复样式间隔问题
|
||||||
|
- 添加查询日志刷新按钮
|
||||||
|
|
||||||
|
## 致谢
|
||||||
|
|
||||||
|
感谢所有为该项目做出贡献的开源项目和开发者!
|
||||||
81
ReadMe.md
81
ReadMe.md
@@ -1,81 +0,0 @@
|
|||||||
# DNS服务器项目介绍
|
|
||||||
## 项目概述
|
|
||||||
这是一个基于Go语言开发的高性能DNS服务器,具备域名屏蔽、Hosts管理、统计分析和远程规则管理等功能。服务器支持通过Web界面进行管理配置,同时能够自动更新和缓存远程规则列表。
|
|
||||||
|
|
||||||
## 技术架构
|
|
||||||
### 核心组件
|
|
||||||
1. DNS服务模块 ( `server.go` )
|
|
||||||
- 基于 github.com/miekg/dns 库实现高性能DNS查询处理
|
|
||||||
- 支持配置上游DNS服务器进行递归查询
|
|
||||||
- 实现域名屏蔽、统计数据收集等核心功能
|
|
||||||
|
|
||||||
2. 屏蔽管理系统 ( `manager.go` )
|
|
||||||
- 管理本地和远程屏蔽规则
|
|
||||||
- 支持规则缓存、自动更新和统计
|
|
||||||
- 实现域名和正则表达式规则的解析和匹配
|
|
||||||
|
|
||||||
3. HTTP控制台 ( `server.go` )
|
|
||||||
- 提供Web管理界面
|
|
||||||
- 实现REST API用于配置管理和数据查询
|
|
||||||
|
|
||||||
4. 配置管理 ( `config.go` )
|
|
||||||
- 定义配置结构和加载功能
|
|
||||||
- 支持JSON格式配置文件
|
|
||||||
|
|
||||||
## 主要功能特性
|
|
||||||
### 1. 域名屏蔽系统
|
|
||||||
- 支持本地规则文件和远程规则URL
|
|
||||||
- 多种屏蔽方式:NXDOMAIN、refused、emptyIP、customIP
|
|
||||||
- 支持域名精确匹配和正则表达式匹配
|
|
||||||
- 远程规则自动缓存和更新机制
|
|
||||||
### 2. Hosts管理
|
|
||||||
- 支持自定义Hosts映射
|
|
||||||
- 提供Web界面管理Hosts条目
|
|
||||||
- 自动保存Hosts配置
|
|
||||||
### 3. 统计分析功能
|
|
||||||
- 记录屏蔽域名统计信息
|
|
||||||
- 记录解析域名统计信息
|
|
||||||
- 提供按小时统计的屏蔽数据
|
|
||||||
- 支持查询最常屏蔽和解析的域名
|
|
||||||
### 4. 远程规则管理
|
|
||||||
- 支持添加多个远程规则URL
|
|
||||||
- 自动定期更新远程规则
|
|
||||||
- 本地缓存机制确保规则可用性
|
|
||||||
- Web界面可视化管理
|
|
||||||
### 5. 管理界面
|
|
||||||
- 提供直观的Web控制台
|
|
||||||
- 支持查看服务器状态和统计信息
|
|
||||||
- 规则管理和配置修改
|
|
||||||
- DNS查询测试工具
|
|
||||||
## 项目结构
|
|
||||||
```
|
|
||||||
/root/dns/
|
|
||||||
├── config/ # 配置管理
|
|
||||||
├── data/ # 数据目录(包含缓存和统计)
|
|
||||||
│ └── remote_rules/ # 远程规则缓存
|
|
||||||
├── dns/ # DNS服务器核心
|
|
||||||
├── http/ # HTTP控制台
|
|
||||||
├── logger/ # 日志系统
|
|
||||||
├── shield/ # 屏蔽规则管理
|
|
||||||
├── static/ # 静态Web文件
|
|
||||||
├── main.go # 程序入口
|
|
||||||
└── config.json # 配置文件
|
|
||||||
```
|
|
||||||
## 配置项说明
|
|
||||||
主要配置文件 `config.json` 包含以下部分:
|
|
||||||
|
|
||||||
- DNS配置 :端口、上游DNS服务器、超时设置等
|
|
||||||
- HTTP配置 :控制台端口、主机绑定等
|
|
||||||
- 屏蔽配置 :规则文件路径、远程规则URL、更新间隔等
|
|
||||||
- 日志配置 :日志文件路径、级别设置等
|
|
||||||
## 使用场景
|
|
||||||
1. 网络内容过滤(广告、恶意网站屏蔽)
|
|
||||||
2. 本地DNS缓存加速
|
|
||||||
3. 企业/家庭网络DNS管理
|
|
||||||
4. 开发测试环境DNS重定向
|
|
||||||
## 技术栈
|
|
||||||
- 语言 :Go
|
|
||||||
- DNS库 :github.com/miekg/dns
|
|
||||||
- 日志库 :github.com/sirupsen/logrus
|
|
||||||
- Web前端 :HTML/CSS/JavaScript
|
|
||||||
该DNS服务器具有高性能、功能全面、易于配置等特点,适用于需要精确控制DNS查询结果的各种网络环境。
|
|
||||||
81
Readme.md
81
Readme.md
@@ -1,81 +0,0 @@
|
|||||||
# DNS服务器项目介绍
|
|
||||||
## 项目概述
|
|
||||||
这是一个基于Go语言开发的高性能DNS服务器,具备域名屏蔽、Hosts管理、统计分析和远程规则管理等功能。服务器支持通过Web界面进行管理配置,同时能够自动更新和缓存远程规则列表。
|
|
||||||
|
|
||||||
## 技术架构
|
|
||||||
### 核心组件
|
|
||||||
1. DNS服务模块 ( `server.go` )
|
|
||||||
- 基于 github.com/miekg/dns 库实现高性能DNS查询处理
|
|
||||||
- 支持配置上游DNS服务器进行递归查询
|
|
||||||
- 实现域名屏蔽、统计数据收集等核心功能
|
|
||||||
|
|
||||||
2. 屏蔽管理系统 ( `manager.go` )
|
|
||||||
- 管理本地和远程屏蔽规则
|
|
||||||
- 支持规则缓存、自动更新和统计
|
|
||||||
- 实现域名和正则表达式规则的解析和匹配
|
|
||||||
|
|
||||||
3. HTTP控制台 ( `server.go` )
|
|
||||||
- 提供Web管理界面
|
|
||||||
- 实现REST API用于配置管理和数据查询
|
|
||||||
|
|
||||||
4. 配置管理 ( `config.go` )
|
|
||||||
- 定义配置结构和加载功能
|
|
||||||
- 支持JSON格式配置文件
|
|
||||||
|
|
||||||
## 主要功能特性
|
|
||||||
### 1. 域名屏蔽系统
|
|
||||||
- 支持本地规则文件和远程规则URL
|
|
||||||
- 多种屏蔽方式:NXDOMAIN、refused、emptyIP、customIP
|
|
||||||
- 支持域名精确匹配和正则表达式匹配
|
|
||||||
- 远程规则自动缓存和更新机制
|
|
||||||
### 2. Hosts管理
|
|
||||||
- 支持自定义Hosts映射
|
|
||||||
- 提供Web界面管理Hosts条目
|
|
||||||
- 自动保存Hosts配置
|
|
||||||
### 3. 统计分析功能
|
|
||||||
- 记录屏蔽域名统计信息
|
|
||||||
- 记录解析域名统计信息
|
|
||||||
- 提供按小时统计的屏蔽数据
|
|
||||||
- 支持查询最常屏蔽和解析的域名
|
|
||||||
### 4. 远程规则管理
|
|
||||||
- 支持添加多个远程规则URL
|
|
||||||
- 自动定期更新远程规则
|
|
||||||
- 本地缓存机制确保规则可用性
|
|
||||||
- Web界面可视化管理
|
|
||||||
### 5. 管理界面
|
|
||||||
- 提供直观的Web控制台
|
|
||||||
- 支持查看服务器状态和统计信息
|
|
||||||
- 规则管理和配置修改
|
|
||||||
- DNS查询测试工具
|
|
||||||
## 项目结构
|
|
||||||
```
|
|
||||||
/root/dns/
|
|
||||||
├── config/ # 配置管理
|
|
||||||
├── data/ # 数据目录(包含缓存和统计)
|
|
||||||
│ └── remote_rules/ # 远程规则缓存
|
|
||||||
├── dns/ # DNS服务器核心
|
|
||||||
├── http/ # HTTP控制台
|
|
||||||
├── logger/ # 日志系统
|
|
||||||
├── shield/ # 屏蔽规则管理
|
|
||||||
├── static/ # 静态Web文件
|
|
||||||
├── main.go # 程序入口
|
|
||||||
└── config.json # 配置文件
|
|
||||||
```
|
|
||||||
## 配置项说明
|
|
||||||
主要配置文件 `config.json` 包含以下部分:
|
|
||||||
|
|
||||||
- DNS配置 :端口、上游DNS服务器、超时设置等
|
|
||||||
- HTTP配置 :控制台端口、主机绑定等
|
|
||||||
- 屏蔽配置 :规则文件路径、远程规则URL、更新间隔等
|
|
||||||
- 日志配置 :日志文件路径、级别设置等
|
|
||||||
## 使用场景
|
|
||||||
1. 网络内容过滤(广告、恶意网站屏蔽)
|
|
||||||
2. 本地DNS缓存加速
|
|
||||||
3. 企业/家庭网络DNS管理
|
|
||||||
4. 开发测试环境DNS重定向
|
|
||||||
## 技术栈
|
|
||||||
- 语言 :Go
|
|
||||||
- DNS库 :github.com/miekg/dns
|
|
||||||
- 日志库 :github.com/sirupsen/logrus
|
|
||||||
- Web前端 :HTML/CSS/JavaScript
|
|
||||||
该DNS服务器具有高性能、功能全面、易于配置等特点,适用于需要精确控制DNS查询结果的各种网络环境。
|
|
||||||
96
config.json
96
config.json
@@ -6,30 +6,110 @@
|
|||||||
"223.6.6.6:53"
|
"223.6.6.6:53"
|
||||||
],
|
],
|
||||||
"timeout": 5000,
|
"timeout": 5000,
|
||||||
"statsFile": "./data/stats.json",
|
"statsFile": "data/stats.json",
|
||||||
"saveInterval": 300
|
"saveInterval": 300
|
||||||
},
|
},
|
||||||
"http": {
|
"http": {
|
||||||
"port": 8080,
|
"port": 8080,
|
||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
"enableAPI": true
|
"enableAPI": true,
|
||||||
|
"username": "admin",
|
||||||
|
"password": "admin"
|
||||||
},
|
},
|
||||||
"shield": {
|
"shield": {
|
||||||
"localRulesFile": "data/rules.txt",
|
"localRulesFile": "data/rules.txt",
|
||||||
"remoteRules": [
|
"blacklists": [
|
||||||
"https://example.com/rules.txt",
|
{
|
||||||
"https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/costomize.txt"
|
"name": "AdGuard DNS filter",
|
||||||
|
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/filter.txt",
|
||||||
|
"enabled": true,
|
||||||
|
"lastUpdateTime": "2025-11-28T16:13:03.564Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Adaway Default Blocklist",
|
||||||
|
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/adaway.txt",
|
||||||
|
"enabled": true,
|
||||||
|
"lastUpdateTime": "2025-11-28T15:36:43.086Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CHN-anti-AD",
|
||||||
|
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/list/easylist.txt",
|
||||||
|
"enabled": true,
|
||||||
|
"lastUpdateTime": "2025-11-28T15:26:24.833Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "My GitHub Rules",
|
||||||
|
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/costomize.txt",
|
||||||
|
"enabled": true,
|
||||||
|
"lastUpdateTime": "2025-11-29T17:05:40.283Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CNList",
|
||||||
|
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/list/china.list",
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "大圣净化",
|
||||||
|
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/dsjh.txt",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Hate \u0026 Junk",
|
||||||
|
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hate-and-junk-extended.txt",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "My Gitlab Hosts",
|
||||||
|
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hosts/costomize.txt",
|
||||||
|
"enabled": true,
|
||||||
|
"lastUpdateTime": "2025-11-29T17:11:28.130Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Anti Remote Requests",
|
||||||
|
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hosts/anti-remoterequests.txt",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "URL-Based.txt",
|
||||||
|
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/url-based-adguard.txt",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "My Gitlab A/T Rules",
|
||||||
|
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/ads-and-trackers.txt",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "My Gitlab Malware List",
|
||||||
|
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/malware.txt",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hosts",
|
||||||
|
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/costomize.txt",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AWAvenue-Ads-Rule",
|
||||||
|
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/rules/AWAvenue-Ads-Rule.txt",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "诈骗域名",
|
||||||
|
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/cheat.txt",
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"updateInterval": 60,
|
"updateInterval": 3600,
|
||||||
"hostsFile": "data/hosts.txt",
|
"hostsFile": "data/hosts.txt",
|
||||||
"blockMethod": "NXDOMAIN",
|
"blockMethod": "NXDOMAIN",
|
||||||
"customBlockIP": "",
|
"customBlockIP": "",
|
||||||
"statsFile": "./data/shield_stats.json",
|
"statsFile": "./data/shield_stats.json",
|
||||||
"statsSaveInterval": 60,
|
"statsSaveInterval": 60,
|
||||||
"remoteRulesCacheDir": "./data/remote_rules"
|
"remoteRulesCacheDir": "data/remote_rules"
|
||||||
},
|
},
|
||||||
"log": {
|
"log": {
|
||||||
"file": "dns-server.log",
|
"file": "logs/dns-server.log",
|
||||||
"level": "debug",
|
"level": "debug",
|
||||||
"maxSize": 100,
|
"maxSize": 100,
|
||||||
"maxBackups": 10,
|
"maxBackups": 10,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type DNSConfig struct {
|
|||||||
Timeout int `json:"timeout"`
|
Timeout int `json:"timeout"`
|
||||||
StatsFile string `json:"statsFile"` // 统计数据持久化文件
|
StatsFile string `json:"statsFile"` // 统计数据持久化文件
|
||||||
SaveInterval int `json:"saveInterval"` // 数据保存间隔(秒)
|
SaveInterval int `json:"saveInterval"` // 数据保存间隔(秒)
|
||||||
|
CacheTTL int `json:"cacheTTL"` // DNS缓存过期时间(分钟)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPConfig HTTP控制台配置
|
// HTTPConfig HTTP控制台配置
|
||||||
@@ -19,6 +20,8 @@ type HTTPConfig struct {
|
|||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
EnableAPI bool `json:"enableAPI"`
|
EnableAPI bool `json:"enableAPI"`
|
||||||
|
Username string `json:"username"` // 登录用户名
|
||||||
|
Password string `json:"password"` // 登录密码
|
||||||
}
|
}
|
||||||
|
|
||||||
// BlacklistEntry 黑名单条目
|
// BlacklistEntry 黑名单条目
|
||||||
@@ -86,12 +89,23 @@ func LoadConfig(path string) (*Config, error) {
|
|||||||
if config.DNS.SaveInterval == 0 {
|
if config.DNS.SaveInterval == 0 {
|
||||||
config.DNS.SaveInterval = 300 // 默认5分钟保存一次
|
config.DNS.SaveInterval = 300 // 默认5分钟保存一次
|
||||||
}
|
}
|
||||||
|
// 默认DNS缓存TTL为30分钟
|
||||||
|
if config.DNS.CacheTTL == 0 {
|
||||||
|
config.DNS.CacheTTL = 30 // 默认30分钟
|
||||||
|
}
|
||||||
if config.HTTP.Port == 0 {
|
if config.HTTP.Port == 0 {
|
||||||
config.HTTP.Port = 8080
|
config.HTTP.Port = 8080
|
||||||
}
|
}
|
||||||
if config.HTTP.Host == "" {
|
if config.HTTP.Host == "" {
|
||||||
config.HTTP.Host = "0.0.0.0"
|
config.HTTP.Host = "0.0.0.0"
|
||||||
}
|
}
|
||||||
|
// 默认用户名和密码,如果未配置则使用admin/admin
|
||||||
|
if config.HTTP.Username == "" {
|
||||||
|
config.HTTP.Username = "admin"
|
||||||
|
}
|
||||||
|
if config.HTTP.Password == "" {
|
||||||
|
config.HTTP.Password = "admin"
|
||||||
|
}
|
||||||
if config.Shield.UpdateInterval == 0 {
|
if config.Shield.UpdateInterval == 0 {
|
||||||
config.Shield.UpdateInterval = 3600
|
config.Shield.UpdateInterval = 3600
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
dns-server
Executable file
BIN
dns-server
Executable file
Binary file not shown.
127
dns/cache.go
Normal file
127
dns/cache.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNSCacheItem 表示缓存中的DNS响应项
|
||||||
|
type DNSCacheItem struct {
|
||||||
|
Response *dns.Msg // DNS响应消息
|
||||||
|
Expiry time.Time // 过期时间
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNSCache DNS缓存结构
|
||||||
|
type DNSCache struct {
|
||||||
|
cache map[string]*DNSCacheItem // 缓存映射表
|
||||||
|
mutex sync.RWMutex // 读写锁,保护缓存
|
||||||
|
defaultTTL time.Duration // 默认缓存TTL
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSCache 创建新的DNS缓存实例
|
||||||
|
func NewDNSCache(defaultTTL time.Duration) *DNSCache {
|
||||||
|
cache := &DNSCache{
|
||||||
|
cache: make(map[string]*DNSCacheItem),
|
||||||
|
defaultTTL: defaultTTL,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动缓存清理协程
|
||||||
|
go cache.startCleanupLoop()
|
||||||
|
|
||||||
|
return cache
|
||||||
|
}
|
||||||
|
|
||||||
|
// cacheKey 生成缓存键
|
||||||
|
func cacheKey(qName string, qType uint16) string {
|
||||||
|
return qName + "|" + dns.TypeToString[qType]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set 设置缓存项
|
||||||
|
func (c *DNSCache) Set(qName string, qType uint16, response *dns.Msg, ttl time.Duration) {
|
||||||
|
if ttl <= 0 {
|
||||||
|
ttl = c.defaultTTL
|
||||||
|
}
|
||||||
|
|
||||||
|
key := cacheKey(qName, qType)
|
||||||
|
item := &DNSCacheItem{
|
||||||
|
Response: response.Copy(), // 复制响应以避免外部修改
|
||||||
|
Expiry: time.Now().Add(ttl),
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mutex.Lock()
|
||||||
|
c.cache[key] = item
|
||||||
|
c.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取缓存项
|
||||||
|
func (c *DNSCache) Get(qName string, qType uint16) (*dns.Msg, bool) {
|
||||||
|
key := cacheKey(qName, qType)
|
||||||
|
|
||||||
|
c.mutex.RLock()
|
||||||
|
item, found := c.cache[key]
|
||||||
|
if !found {
|
||||||
|
c.mutex.RUnlock()
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否过期
|
||||||
|
if time.Now().After(item.Expiry) {
|
||||||
|
c.mutex.RUnlock()
|
||||||
|
// 过期了,删除缓存项(在写锁中)
|
||||||
|
c.delete(key)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回缓存的响应副本
|
||||||
|
response := item.Response.Copy()
|
||||||
|
c.mutex.RUnlock()
|
||||||
|
|
||||||
|
return response, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete 删除缓存项
|
||||||
|
func (c *DNSCache) delete(key string) {
|
||||||
|
c.mutex.Lock()
|
||||||
|
delete(c.cache, key)
|
||||||
|
c.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear 清空缓存
|
||||||
|
func (c *DNSCache) Clear() {
|
||||||
|
c.mutex.Lock()
|
||||||
|
c.cache = make(map[string]*DNSCacheItem)
|
||||||
|
c.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size 获取缓存大小
|
||||||
|
func (c *DNSCache) Size() int {
|
||||||
|
c.mutex.RLock()
|
||||||
|
defer c.mutex.RUnlock()
|
||||||
|
return len(c.cache)
|
||||||
|
}
|
||||||
|
|
||||||
|
// startCleanupLoop 启动定期清理过期缓存的协程
|
||||||
|
func (c *DNSCache) startCleanupLoop() {
|
||||||
|
ticker := time.NewTicker(time.Minute * 5) // 每5分钟清理一次
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
c.cleanupExpired()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupExpired 清理过期的缓存项
|
||||||
|
func (c *DNSCache) cleanupExpired() {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
for key, item := range c.cache {
|
||||||
|
if now.After(item.Expiry) {
|
||||||
|
delete(c.cache, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
964
dns/server.go
964
dns/server.go
File diff suppressed because it is too large
Load Diff
1
go.mod
1
go.mod
@@ -5,6 +5,7 @@ go 1.23.0
|
|||||||
toolchain go1.24.10
|
toolchain go1.24.10
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/gorilla/websocket v1.5.1
|
||||||
github.com/miekg/dns v1.1.68
|
github.com/miekg/dns v1.1.68
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
)
|
)
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -3,6 +3,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||||
|
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
# DNS Server Hosts File
|
|
||||||
# Generated by DNS Server
|
|
||||||
|
|
||||||
::1 localhost
|
|
||||||
ad.qq.com 127.0.0.1
|
|
||||||
ad.qq.com 0.0.0.0
|
|
||||||
1011
http/server.go
1011
http/server.go
File diff suppressed because it is too large
Load Diff
1574
index.html
1574
index.html
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,10 @@
|
|||||||
package logger
|
package logger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@@ -15,7 +17,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// InitLogger 初始化日志系统
|
// InitLogger 初始化日志系统
|
||||||
func InitLogger(logFile, level string, maxSize, maxBackups, maxAge int) error {
|
func InitLogger(logFile, level string, maxSize, maxBackups, maxAge int, _ bool) error {
|
||||||
logMutex.Lock()
|
logMutex.Lock()
|
||||||
defer logMutex.Unlock()
|
defer logMutex.Unlock()
|
||||||
|
|
||||||
@@ -30,21 +32,42 @@ func InitLogger(logFile, level string, maxSize, maxBackups, maxAge int) error {
|
|||||||
FullTimestamp: true,
|
FullTimestamp: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 创建日志目录
|
||||||
|
if logFile != "" {
|
||||||
|
logDir := filepath.Dir(logFile)
|
||||||
|
if logDir != "." {
|
||||||
|
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("创建日志目录失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 设置输出目标
|
// 设置输出目标
|
||||||
|
outputTargets := []io.Writer{}
|
||||||
|
|
||||||
if logFile != "" {
|
if logFile != "" {
|
||||||
// 使用标准库打开文件,支持追加写入
|
// 使用标准库打开文件,支持追加写入
|
||||||
file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Warn("无法打开日志文件,将使用标准输出", "error", err)
|
// 无法打开日志文件时,回退到标准输出
|
||||||
log.SetOutput(os.Stdout)
|
log.Println("无法打开日志文件,将使用标准输出:", err)
|
||||||
} else {
|
} else {
|
||||||
// 同时输出到文件和标准输出
|
outputTargets = append(outputTargets, file)
|
||||||
log.SetOutput(io.MultiWriter(file, os.Stdout))
|
defer file.Sync() // 确保日志内容被写入磁盘
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
log.SetOutput(os.Stdout)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 无论是否指定日志文件,都同时输出到标准输出
|
||||||
|
if len(outputTargets) > 0 {
|
||||||
|
outputTargets = append(outputTargets, os.Stdout)
|
||||||
|
} else {
|
||||||
|
// 如果没有指定日志文件,仅使用标准输出
|
||||||
|
outputTargets = append(outputTargets, os.Stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置日志输出
|
||||||
|
log.SetOutput(io.MultiWriter(outputTargets...))
|
||||||
|
|
||||||
// 设置日志级别
|
// 设置日志级别
|
||||||
logLevel, err := logrus.ParseLevel(level)
|
logLevel, err := logrus.ParseLevel(level)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -61,13 +84,20 @@ func Close() {
|
|||||||
logMutex.Lock()
|
logMutex.Lock()
|
||||||
defer logMutex.Unlock()
|
defer logMutex.Unlock()
|
||||||
|
|
||||||
if !initialized {
|
if !initialized || log == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行日志刷新
|
// 执行日志刷新
|
||||||
log.Warn("日志系统已关闭")
|
log.Warn("日志系统已关闭")
|
||||||
|
|
||||||
|
// 确保日志被写入磁盘
|
||||||
|
if loggerOutput, ok := log.Out.(*os.File); ok {
|
||||||
|
loggerOutput.Sync()
|
||||||
|
}
|
||||||
|
|
||||||
initialized = false
|
initialized = false
|
||||||
|
log = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info 记录信息级别日志
|
// Info 记录信息级别日志
|
||||||
@@ -122,6 +152,20 @@ func Warn(msg string, fields ...interface{}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fatal 记录致命级别日志并退出程序
|
||||||
|
func Fatal(msg string, fields ...interface{}) {
|
||||||
|
if !initialized {
|
||||||
|
// 如果日志系统未初始化,使用标准库log
|
||||||
|
log.Fatal(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fields) > 0 {
|
||||||
|
log.WithFields(toFields(fields)).Fatal(msg)
|
||||||
|
} else {
|
||||||
|
log.Fatal(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// toFields 将键值对转换为logrus字段
|
// toFields 将键值对转换为logrus字段
|
||||||
func toFields(keyValues []interface{}) logrus.Fields {
|
func toFields(keyValues []interface{}) logrus.Fields {
|
||||||
fields := make(logrus.Fields)
|
fields := make(logrus.Fields)
|
||||||
|
|||||||
169
main.go
169
main.go
@@ -1,3 +1,14 @@
|
|||||||
|
// DNS Server API
|
||||||
|
// @title DNS Server API
|
||||||
|
// @version 1.0
|
||||||
|
// @description DNS服务器API文档
|
||||||
|
// @termsOfService http://swagger.io/terms/
|
||||||
|
// @contact.name API Support
|
||||||
|
// @contact.email support@example.com
|
||||||
|
// @license.name Apache 2.0
|
||||||
|
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
// @host localhost:8080
|
||||||
|
// @BasePath /api
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -6,6 +17,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"dns-server/config"
|
"dns-server/config"
|
||||||
@@ -15,19 +27,156 @@ import (
|
|||||||
"dns-server/shield"
|
"dns-server/shield"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// createDefaultConfig 创建默认配置文件
|
||||||
|
func createDefaultConfig(configFile string) error {
|
||||||
|
// 默认配置内容
|
||||||
|
defaultConfig := `{
|
||||||
|
"dns": {
|
||||||
|
"port": 53,
|
||||||
|
"upstreamDNS": [
|
||||||
|
"223.5.5.5:53",
|
||||||
|
"223.6.6.6:53"
|
||||||
|
],
|
||||||
|
"timeout": 5000,
|
||||||
|
"statsFile": "./data/stats.json",
|
||||||
|
"saveInterval": 300
|
||||||
|
},
|
||||||
|
"http": {
|
||||||
|
"port": 8080,
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"enableAPI": true,
|
||||||
|
"username": "admin",
|
||||||
|
"password": "admin"
|
||||||
|
},
|
||||||
|
"shield": {
|
||||||
|
"localRulesFile": "data/rules.txt",
|
||||||
|
"blacklists": [
|
||||||
|
{
|
||||||
|
"name": "AdGuard DNS filter",
|
||||||
|
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/filter.txt",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Adaway Default Blocklist",
|
||||||
|
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/adaway.txt",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CHN-anti-AD",
|
||||||
|
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/list/easylist.txt",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "My GitHub Rules",
|
||||||
|
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/costomize.txt",
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"updateInterval": 3600,
|
||||||
|
"hostsFile": "data/hosts.txt",
|
||||||
|
"blockMethod": "NXDOMAIN",
|
||||||
|
"customBlockIP": "",
|
||||||
|
"statsFile": "./data/shield_stats.json",
|
||||||
|
"statsSaveInterval": 60,
|
||||||
|
"remoteRulesCacheDir": "./data/remote_rules"
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
"file": "logs/dns-server.log",
|
||||||
|
"level": "debug",
|
||||||
|
"maxSize": 100,
|
||||||
|
"maxBackups": 10,
|
||||||
|
"maxAge": 30
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
// 写入默认配置到文件
|
||||||
|
return os.WriteFile(configFile, []byte(defaultConfig), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// createRequiredFiles 创建所需的文件和文件夹
|
||||||
|
func createRequiredFiles(cfg *config.Config) error {
|
||||||
|
// 创建数据文件夹
|
||||||
|
dataDir := "./data"
|
||||||
|
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("创建数据文件夹失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建远程规则缓存文件夹
|
||||||
|
if err := os.MkdirAll(cfg.Shield.RemoteRulesCacheDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("创建远程规则缓存文件夹失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建日志文件夹
|
||||||
|
logDir := filepath.Dir(cfg.Log.File)
|
||||||
|
if logDir != "." {
|
||||||
|
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("创建日志文件夹失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建本地规则文件
|
||||||
|
if _, err := os.Stat(cfg.Shield.LocalRulesFile); os.IsNotExist(err) {
|
||||||
|
if err := os.WriteFile(cfg.Shield.LocalRulesFile, []byte("# 本地规则文件\n# 格式:域名\n# 例如:example.com\n"), 0644); err != nil {
|
||||||
|
return fmt.Errorf("创建本地规则文件失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建Hosts文件
|
||||||
|
if _, err := os.Stat(cfg.Shield.HostsFile); os.IsNotExist(err) {
|
||||||
|
if err := os.WriteFile(cfg.Shield.HostsFile, []byte("# Hosts文件\n# 格式:IP 域名\n# 例如:127.0.0.1 localhost\n"), 0644); err != nil {
|
||||||
|
return fmt.Errorf("创建Hosts文件失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建统计数据文件
|
||||||
|
if _, err := os.Stat(cfg.DNS.StatsFile); os.IsNotExist(err) {
|
||||||
|
if err := os.WriteFile(cfg.DNS.StatsFile, []byte("{}"), 0644); err != nil {
|
||||||
|
return fmt.Errorf("创建统计数据文件失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建Shield统计数据文件
|
||||||
|
if _, err := os.Stat(cfg.Shield.StatsFile); os.IsNotExist(err) {
|
||||||
|
if err := os.WriteFile(cfg.Shield.StatsFile, []byte("{}"), 0644); err != nil {
|
||||||
|
return fmt.Errorf("创建Shield统计数据文件失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// 解析命令行参数
|
// 命令行参数解析
|
||||||
configPath := flag.String("config", "config.json", "配置文件路径")
|
var configFile string
|
||||||
|
flag.StringVar(&configFile, "config", "config.json", "配置文件路径")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
// 检查配置文件是否存在,如果不存在则创建默认配置文件
|
||||||
|
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||||
|
log.Printf("配置文件 %s 不存在,正在创建默认配置文件...", configFile)
|
||||||
|
if err := createDefaultConfig(configFile); err != nil {
|
||||||
|
log.Fatalf("创建默认配置文件失败: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("默认配置文件 %s 创建成功", configFile)
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化配置
|
// 初始化配置
|
||||||
cfg, err := config.LoadConfig(*configPath)
|
var cfg *config.Config
|
||||||
|
var err error
|
||||||
|
cfg, err = config.LoadConfig(configFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("加载配置失败: %v", err)
|
log.Fatalf("加载配置失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建所需的文件和文件夹
|
||||||
|
log.Println("正在创建所需的文件和文件夹...")
|
||||||
|
if err := createRequiredFiles(cfg); err != nil {
|
||||||
|
log.Fatalf("创建所需文件和文件夹失败: %v", err)
|
||||||
|
}
|
||||||
|
log.Println("所需文件和文件夹创建成功")
|
||||||
|
|
||||||
// 初始化日志系统
|
// 初始化日志系统
|
||||||
if err := logger.InitLogger(cfg.Log.File, cfg.Log.Level, 0, 0, 0); err != nil {
|
if err := logger.InitLogger(cfg.Log.File, cfg.Log.Level, 0, 0, 0, false); err != nil {
|
||||||
log.Fatalf("初始化日志系统失败: %v", err)
|
log.Fatalf("初始化日志系统失败: %v", err)
|
||||||
}
|
}
|
||||||
defer logger.Close()
|
defer logger.Close()
|
||||||
@@ -61,14 +210,16 @@ func main() {
|
|||||||
logger.Info(fmt.Sprintf("DNS服务器已启动,监听端口: %d", cfg.DNS.Port))
|
logger.Info(fmt.Sprintf("DNS服务器已启动,监听端口: %d", cfg.DNS.Port))
|
||||||
logger.Info(fmt.Sprintf("HTTP控制台已启动,监听端口: %d", cfg.HTTP.Port))
|
logger.Info(fmt.Sprintf("HTTP控制台已启动,监听端口: %d", cfg.HTTP.Port))
|
||||||
|
|
||||||
// 等待退出信号
|
// 监听信号
|
||||||
sigChan := make(chan os.Signal, 1)
|
sigCh := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||||
<-sigChan
|
<-sigCh
|
||||||
|
|
||||||
|
// 清理资源
|
||||||
logger.Info("正在关闭服务...")
|
logger.Info("正在关闭服务...")
|
||||||
dnsServer.Stop()
|
dnsServer.Stop()
|
||||||
httpServer.Stop()
|
httpServer.Stop()
|
||||||
shieldManager.StopAutoUpdate()
|
shieldManager.StopAutoUpdate()
|
||||||
logger.Info("所有服务已关闭")
|
|
||||||
|
logger.Info("服务已关闭")
|
||||||
}
|
}
|
||||||
|
|||||||
18
package.json
Normal file
18
package.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "dns-server-console",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "DNS服务器Web控制台",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"tailwindcss": "^3.3.3",
|
||||||
|
"font-awesome": "^4.7.0",
|
||||||
|
"chart.js": "^4.4.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {},
|
||||||
|
"keywords": ["dns", "server", "console", "web"],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC"
|
||||||
|
}
|
||||||
@@ -19,12 +19,6 @@ import (
|
|||||||
"dns-server/logger"
|
"dns-server/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// regexRule 正则规则结构,包含编译后的表达式和原始字符串
|
|
||||||
type regexRule struct {
|
|
||||||
pattern *regexp.Regexp
|
|
||||||
original string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShieldStatsData 用于持久化的Shield统计数据
|
// ShieldStatsData 用于持久化的Shield统计数据
|
||||||
type ShieldStatsData struct {
|
type ShieldStatsData struct {
|
||||||
BlockedDomainsCount map[string]int `json:"blockedDomainsCount"`
|
BlockedDomainsCount map[string]int `json:"blockedDomainsCount"`
|
||||||
@@ -32,40 +26,60 @@ type ShieldStatsData struct {
|
|||||||
LastSaved time.Time `json:"lastSaved"`
|
LastSaved time.Time `json:"lastSaved"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// regexRule 正则规则结构,包含编译后的表达式和原始字符串
|
||||||
|
type regexRule struct {
|
||||||
|
pattern *regexp.Regexp
|
||||||
|
original string
|
||||||
|
isLocal bool // 是否为本地规则
|
||||||
|
source string // 规则来源
|
||||||
|
}
|
||||||
|
|
||||||
// ShieldManager 屏蔽管理器
|
// ShieldManager 屏蔽管理器
|
||||||
type ShieldManager struct {
|
type ShieldManager struct {
|
||||||
config *config.ShieldConfig
|
config *config.ShieldConfig
|
||||||
domainRules map[string]bool
|
domainRules map[string]bool
|
||||||
domainExceptions map[string]bool
|
domainExceptions map[string]bool
|
||||||
regexRules []regexRule
|
domainRulesIsLocal map[string]bool // 标记域名规则是否为本地规则
|
||||||
regexExceptions []regexRule
|
domainExceptionsIsLocal map[string]bool // 标记域名排除规则是否为本地规则
|
||||||
hostsMap map[string]string
|
domainRulesSource map[string]string // 标记域名规则来源
|
||||||
blockedDomainsCount map[string]int
|
domainExceptionsSource map[string]string // 标记域名排除规则来源
|
||||||
resolvedDomainsCount map[string]int
|
domainRulesOriginal map[string]string // 存储域名规则的原始字符串
|
||||||
rulesMutex sync.RWMutex
|
domainExceptionsOriginal map[string]string // 存储域名排除规则的原始字符串
|
||||||
updateCtx context.Context
|
regexRules []regexRule
|
||||||
updateCancel context.CancelFunc
|
regexExceptions []regexRule
|
||||||
updateRunning bool
|
hostsMap map[string]string
|
||||||
localRulesCount int // 本地规则数量
|
blockedDomainsCount map[string]int
|
||||||
remoteRulesCount int // 远程规则数量
|
resolvedDomainsCount map[string]int
|
||||||
|
rulesMutex sync.RWMutex
|
||||||
|
updateCtx context.Context
|
||||||
|
updateCancel context.CancelFunc
|
||||||
|
updateRunning bool
|
||||||
|
localRulesCount int // 本地规则数量
|
||||||
|
remoteRulesCount int // 远程规则数量
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewShieldManager 创建屏蔽管理器实例
|
// NewShieldManager 创建屏蔽管理器实例
|
||||||
func NewShieldManager(config *config.ShieldConfig) *ShieldManager {
|
func NewShieldManager(config *config.ShieldConfig) *ShieldManager {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
manager := &ShieldManager{
|
manager := &ShieldManager{
|
||||||
config: config,
|
config: config,
|
||||||
domainRules: make(map[string]bool),
|
domainRules: make(map[string]bool),
|
||||||
domainExceptions: make(map[string]bool),
|
domainExceptions: make(map[string]bool),
|
||||||
regexRules: []regexRule{},
|
domainRulesIsLocal: make(map[string]bool),
|
||||||
regexExceptions: []regexRule{},
|
domainExceptionsIsLocal: make(map[string]bool),
|
||||||
hostsMap: make(map[string]string),
|
domainRulesSource: make(map[string]string),
|
||||||
blockedDomainsCount: make(map[string]int),
|
domainExceptionsSource: make(map[string]string),
|
||||||
resolvedDomainsCount: make(map[string]int),
|
domainRulesOriginal: make(map[string]string),
|
||||||
updateCtx: ctx,
|
domainExceptionsOriginal: make(map[string]string),
|
||||||
updateCancel: cancel,
|
regexRules: []regexRule{},
|
||||||
localRulesCount: 0,
|
regexExceptions: []regexRule{},
|
||||||
remoteRulesCount: 0,
|
hostsMap: make(map[string]string),
|
||||||
|
blockedDomainsCount: make(map[string]int),
|
||||||
|
resolvedDomainsCount: make(map[string]int),
|
||||||
|
updateCtx: ctx,
|
||||||
|
updateCancel: cancel,
|
||||||
|
localRulesCount: 0,
|
||||||
|
remoteRulesCount: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载已保存的计数数据
|
// 加载已保存的计数数据
|
||||||
@@ -82,6 +96,12 @@ func (m *ShieldManager) LoadRules() error {
|
|||||||
// 清空现有规则
|
// 清空现有规则
|
||||||
m.domainRules = make(map[string]bool)
|
m.domainRules = make(map[string]bool)
|
||||||
m.domainExceptions = make(map[string]bool)
|
m.domainExceptions = make(map[string]bool)
|
||||||
|
m.domainRulesIsLocal = make(map[string]bool)
|
||||||
|
m.domainExceptionsIsLocal = make(map[string]bool)
|
||||||
|
m.domainRulesSource = make(map[string]string)
|
||||||
|
m.domainExceptionsSource = make(map[string]string)
|
||||||
|
m.domainRulesOriginal = make(map[string]string)
|
||||||
|
m.domainExceptionsOriginal = make(map[string]string)
|
||||||
m.regexRules = []regexRule{}
|
m.regexRules = []regexRule{}
|
||||||
m.regexExceptions = []regexRule{}
|
m.regexExceptions = []regexRule{}
|
||||||
m.hostsMap = make(map[string]string)
|
m.hostsMap = make(map[string]string)
|
||||||
@@ -134,7 +154,7 @@ func (m *ShieldManager) loadLocalRules() error {
|
|||||||
if line == "" || strings.HasPrefix(line, "#") {
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
m.parseRule(line)
|
m.parseRule(line, true, "本地规则") // 本地规则,isLocal=true,来源为"本地规则"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新本地规则计数
|
// 更新本地规则计数
|
||||||
@@ -191,7 +211,7 @@ func (m *ShieldManager) fetchRemoteRules(url string) error {
|
|||||||
// 尝试从缓存加载
|
// 尝试从缓存加载
|
||||||
hasLoadedFromCache := false
|
hasLoadedFromCache := false
|
||||||
if !m.shouldUpdateCache(cacheFile) {
|
if !m.shouldUpdateCache(cacheFile) {
|
||||||
if err := m.loadCachedRules(cacheFile); err == nil {
|
if err := m.loadCachedRules(cacheFile, url); err == nil {
|
||||||
logger.Info("从缓存加载远程规则", "url", url)
|
logger.Info("从缓存加载远程规则", "url", url)
|
||||||
hasLoadedFromCache = true
|
hasLoadedFromCache = true
|
||||||
}
|
}
|
||||||
@@ -236,14 +256,14 @@ func (m *ShieldManager) fetchRemoteRules(url string) error {
|
|||||||
if line == "" || strings.HasPrefix(line, "#") {
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
m.parseRule(line)
|
m.parseRule(line, false, url) // 远程规则,isLocal=false,来源为URL
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadCachedRules 从缓存文件加载规则
|
// loadCachedRules 从缓存文件加载规则
|
||||||
func (m *ShieldManager) loadCachedRules(filePath string) error {
|
func (m *ShieldManager) loadCachedRules(filePath string, source string) error {
|
||||||
file, err := os.Open(filePath)
|
file, err := os.Open(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -265,7 +285,7 @@ func (m *ShieldManager) loadCachedRules(filePath string) error {
|
|||||||
if line == "" || strings.HasPrefix(line, "#") {
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
m.parseRule(line)
|
m.parseRule(line, false, source) // 远程规则,isLocal=false,来源为URL
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新远程规则计数
|
// 更新远程规则计数
|
||||||
@@ -318,7 +338,10 @@ func (m *ShieldManager) loadHosts() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parseRule 解析规则行
|
// parseRule 解析规则行
|
||||||
func (m *ShieldManager) parseRule(line string) {
|
func (m *ShieldManager) parseRule(line string, isLocal bool, source string) {
|
||||||
|
// 保存原始规则用于后续使用
|
||||||
|
originalLine := line
|
||||||
|
|
||||||
// 处理注释
|
// 处理注释
|
||||||
if strings.HasPrefix(line, "!") || strings.HasPrefix(line, "#") || line == "" {
|
if strings.HasPrefix(line, "!") || strings.HasPrefix(line, "#") || line == "" {
|
||||||
return
|
return
|
||||||
@@ -343,12 +366,12 @@ func (m *ShieldManager) parseRule(line string) {
|
|||||||
case strings.HasPrefix(line, "||") && strings.HasSuffix(line, "^"):
|
case strings.HasPrefix(line, "||") && strings.HasSuffix(line, "^"):
|
||||||
// AdGuardHome域名规则格式: ||example.com^
|
// AdGuardHome域名规则格式: ||example.com^
|
||||||
domain := strings.TrimSuffix(strings.TrimPrefix(line, "||"), "^")
|
domain := strings.TrimSuffix(strings.TrimPrefix(line, "||"), "^")
|
||||||
m.addDomainRule(domain, !isException)
|
m.addDomainRule(domain, !isException, isLocal, source, originalLine)
|
||||||
|
|
||||||
case strings.HasPrefix(line, "||"):
|
case strings.HasPrefix(line, "||"):
|
||||||
// 精确域名匹配规则
|
// 精确域名匹配规则
|
||||||
domain := strings.TrimPrefix(line, "||")
|
domain := strings.TrimPrefix(line, "||")
|
||||||
m.addDomainRule(domain, !isException)
|
m.addDomainRule(domain, !isException, isLocal, source, originalLine)
|
||||||
|
|
||||||
case strings.HasPrefix(line, "*"):
|
case strings.HasPrefix(line, "*"):
|
||||||
// 通配符规则,转换为正则表达式
|
// 通配符规则,转换为正则表达式
|
||||||
@@ -356,15 +379,17 @@ func (m *ShieldManager) parseRule(line string) {
|
|||||||
pattern = "^" + pattern + "$"
|
pattern = "^" + pattern + "$"
|
||||||
if re, err := regexp.Compile(pattern); err == nil {
|
if re, err := regexp.Compile(pattern); err == nil {
|
||||||
// 保存原始规则字符串
|
// 保存原始规则字符串
|
||||||
m.addRegexRule(re, line, !isException)
|
m.addRegexRule(re, originalLine, !isException, isLocal, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasPrefix(line, "/") && strings.HasSuffix(line, "/"):
|
case strings.HasPrefix(line, "/") && strings.HasSuffix(line, "/"):
|
||||||
// 正则表达式规则
|
// 正则表达式匹配规则:/regex/ 格式,不区分大小写
|
||||||
pattern := strings.TrimPrefix(strings.TrimSuffix(line, "/"), "/")
|
pattern := strings.TrimPrefix(strings.TrimSuffix(line, "/"), "/")
|
||||||
if re, err := regexp.Compile(pattern); err == nil {
|
// 编译为不区分大小写的正则表达式,确保能匹配域名中任意位置
|
||||||
|
// 对于像 /domain/ 这样的规则,应该匹配包含 domain 字符串的任何域名
|
||||||
|
if re, err := regexp.Compile("(?i).*" + regexp.QuoteMeta(pattern) + ".*"); err == nil {
|
||||||
// 保存原始规则字符串
|
// 保存原始规则字符串
|
||||||
m.addRegexRule(re, line, !isException)
|
m.addRegexRule(re, originalLine, !isException, isLocal, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasPrefix(line, "|") && strings.HasSuffix(line, "|"):
|
case strings.HasPrefix(line, "|") && strings.HasSuffix(line, "|"):
|
||||||
@@ -373,7 +398,7 @@ func (m *ShieldManager) parseRule(line string) {
|
|||||||
// 将URL模式转换为正则表达式
|
// 将URL模式转换为正则表达式
|
||||||
pattern := "^" + regexp.QuoteMeta(urlPattern) + "$"
|
pattern := "^" + regexp.QuoteMeta(urlPattern) + "$"
|
||||||
if re, err := regexp.Compile(pattern); err == nil {
|
if re, err := regexp.Compile(pattern); err == nil {
|
||||||
m.addRegexRule(re, line, !isException)
|
m.addRegexRule(re, originalLine, !isException, isLocal, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasPrefix(line, "|"):
|
case strings.HasPrefix(line, "|"):
|
||||||
@@ -381,7 +406,7 @@ func (m *ShieldManager) parseRule(line string) {
|
|||||||
urlPattern := strings.TrimPrefix(line, "|")
|
urlPattern := strings.TrimPrefix(line, "|")
|
||||||
pattern := "^" + regexp.QuoteMeta(urlPattern)
|
pattern := "^" + regexp.QuoteMeta(urlPattern)
|
||||||
if re, err := regexp.Compile(pattern); err == nil {
|
if re, err := regexp.Compile(pattern); err == nil {
|
||||||
m.addRegexRule(re, line, !isException)
|
m.addRegexRule(re, originalLine, !isException, isLocal, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasSuffix(line, "|"):
|
case strings.HasSuffix(line, "|"):
|
||||||
@@ -389,12 +414,12 @@ func (m *ShieldManager) parseRule(line string) {
|
|||||||
urlPattern := strings.TrimSuffix(line, "|")
|
urlPattern := strings.TrimSuffix(line, "|")
|
||||||
pattern := regexp.QuoteMeta(urlPattern) + "$"
|
pattern := regexp.QuoteMeta(urlPattern) + "$"
|
||||||
if re, err := regexp.Compile(pattern); err == nil {
|
if re, err := regexp.Compile(pattern); err == nil {
|
||||||
m.addRegexRule(re, line, !isException)
|
m.addRegexRule(re, originalLine, !isException, isLocal, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// 默认作为普通域名规则
|
// 默认作为普通域名规则
|
||||||
m.addDomainRule(line, !isException)
|
m.addDomainRule(line, !isException, isLocal, source, originalLine)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,42 +443,65 @@ func (m *ShieldManager) parseRuleOptions(optionsStr string) map[string]string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// addDomainRule 添加域名规则,支持是否为阻止规则
|
// addDomainRule 添加域名规则,支持是否为阻止规则
|
||||||
func (m *ShieldManager) addDomainRule(domain string, block bool) {
|
func (m *ShieldManager) addDomainRule(domain string, block bool, isLocal bool, source string, original string) {
|
||||||
if block {
|
if block {
|
||||||
m.domainRules[domain] = true
|
// 如果是远程规则,检查是否已经存在本地规则,如果存在则不覆盖
|
||||||
// 添加所有子域名的匹配支持
|
if !isLocal {
|
||||||
parts := strings.Split(domain, ".")
|
if _, exists := m.domainRulesIsLocal[domain]; exists && m.domainRulesIsLocal[domain] {
|
||||||
if len(parts) > 1 {
|
// 已经存在本地规则,不覆盖
|
||||||
// 为二级域名和顶级域名添加规则
|
return
|
||||||
for i := 0; i < len(parts)-1; i++ {
|
|
||||||
subdomain := strings.Join(parts[i:], ".")
|
|
||||||
m.domainRules[subdomain] = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
m.domainRules[domain] = true
|
||||||
|
m.domainRulesIsLocal[domain] = isLocal
|
||||||
|
m.domainRulesSource[domain] = source
|
||||||
|
m.domainRulesOriginal[domain] = original
|
||||||
} else {
|
} else {
|
||||||
// 添加到排除规则
|
// 添加到排除规则
|
||||||
m.domainExceptions[domain] = true
|
// 如果是远程规则,检查是否已经存在本地规则,如果存在则不覆盖
|
||||||
// 为子域名也添加排除规则
|
if !isLocal {
|
||||||
parts := strings.Split(domain, ".")
|
if _, exists := m.domainExceptionsIsLocal[domain]; exists && m.domainExceptionsIsLocal[domain] {
|
||||||
if len(parts) > 1 {
|
// 已经存在本地规则,不覆盖
|
||||||
for i := 0; i < len(parts)-1; i++ {
|
return
|
||||||
subdomain := strings.Join(parts[i:], ".")
|
|
||||||
m.domainExceptions[subdomain] = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
m.domainExceptions[domain] = true
|
||||||
|
m.domainExceptionsIsLocal[domain] = isLocal
|
||||||
|
m.domainExceptionsSource[domain] = source
|
||||||
|
m.domainExceptionsOriginal[domain] = original
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// addRegexRule 添加正则表达式规则,支持是否为阻止规则
|
// addRegexRule 添加正则表达式规则,支持是否为阻止规则
|
||||||
func (m *ShieldManager) addRegexRule(re *regexp.Regexp, original string, block bool) {
|
func (m *ShieldManager) addRegexRule(re *regexp.Regexp, original string, block bool, isLocal bool, source string) {
|
||||||
rule := regexRule{
|
rule := regexRule{
|
||||||
pattern: re,
|
pattern: re,
|
||||||
original: original,
|
original: original,
|
||||||
|
isLocal: isLocal,
|
||||||
|
source: source,
|
||||||
}
|
}
|
||||||
if block {
|
if block {
|
||||||
|
// 如果是远程规则,检查是否已经存在相同的本地规则,如果存在则不添加
|
||||||
|
if !isLocal {
|
||||||
|
for _, existingRule := range m.regexRules {
|
||||||
|
if existingRule.original == original && existingRule.isLocal {
|
||||||
|
// 已经存在相同的本地规则,不添加
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
m.regexRules = append(m.regexRules, rule)
|
m.regexRules = append(m.regexRules, rule)
|
||||||
} else {
|
} else {
|
||||||
// 添加到排除规则
|
// 添加到排除规则
|
||||||
|
// 如果是远程规则,检查是否已经存在相同的本地规则,如果存在则不添加
|
||||||
|
if !isLocal {
|
||||||
|
for _, existingRule := range m.regexExceptions {
|
||||||
|
if existingRule.original == original && existingRule.isLocal {
|
||||||
|
// 已经存在相同的本地规则,不添加
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
m.regexExceptions = append(m.regexExceptions, rule)
|
m.regexExceptions = append(m.regexExceptions, rule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -471,15 +519,16 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
|||||||
}
|
}
|
||||||
|
|
||||||
result := map[string]interface{}{
|
result := map[string]interface{}{
|
||||||
"domain": domain,
|
"domain": domain,
|
||||||
"blocked": false,
|
"blocked": false,
|
||||||
"blockRule": "",
|
"blockRule": "",
|
||||||
"blockRuleType": "",
|
"blockRuleType": "",
|
||||||
"excluded": false,
|
"blocksource": "",
|
||||||
"excludeRule": "",
|
"excluded": false,
|
||||||
|
"excludeRule": "",
|
||||||
"excludeRuleType": "",
|
"excludeRuleType": "",
|
||||||
"hasHosts": false,
|
"hasHosts": false,
|
||||||
"hostsIP": "",
|
"hostsIP": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查hosts记录
|
// 检查hosts记录
|
||||||
@@ -491,8 +540,9 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
|||||||
// 检查域名排除规则
|
// 检查域名排除规则
|
||||||
if m.domainExceptions[domain] {
|
if m.domainExceptions[domain] {
|
||||||
result["excluded"] = true
|
result["excluded"] = true
|
||||||
result["excludeRule"] = domain
|
result["excludeRule"] = m.domainExceptionsOriginal[domain]
|
||||||
result["excludeRuleType"] = "exact_domain"
|
result["excludeRuleType"] = "exact_domain"
|
||||||
|
result["blocksource"] = m.domainExceptionsSource[domain]
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,8 +552,9 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
|||||||
subdomain := strings.Join(parts[i:], ".")
|
subdomain := strings.Join(parts[i:], ".")
|
||||||
if m.domainExceptions[subdomain] {
|
if m.domainExceptions[subdomain] {
|
||||||
result["excluded"] = true
|
result["excluded"] = true
|
||||||
result["excludeRule"] = subdomain
|
result["excludeRule"] = m.domainExceptionsOriginal[subdomain]
|
||||||
result["excludeRuleType"] = "subdomain"
|
result["excludeRuleType"] = "subdomain"
|
||||||
|
result["blocksource"] = m.domainExceptionsSource[subdomain]
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -514,16 +565,18 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
|||||||
result["excluded"] = true
|
result["excluded"] = true
|
||||||
result["excludeRule"] = re.original
|
result["excludeRule"] = re.original
|
||||||
result["excludeRuleType"] = "regex"
|
result["excludeRuleType"] = "regex"
|
||||||
|
result["blocksource"] = re.source
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查阻止规则
|
// 检查阻止规则 - 先检查精确域名匹配,再检查子域名匹配
|
||||||
// 检查精确域名匹配
|
// 检查精确域名匹配
|
||||||
if m.domainRules[domain] {
|
if m.domainRules[domain] {
|
||||||
result["blocked"] = true
|
result["blocked"] = true
|
||||||
result["blockRule"] = domain
|
result["blockRule"] = m.domainRulesOriginal[domain]
|
||||||
result["blockRuleType"] = "exact_domain"
|
result["blockRuleType"] = "exact_domain"
|
||||||
|
result["blocksource"] = m.domainRulesSource[domain]
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -533,8 +586,9 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
|||||||
subdomain := strings.Join(parts[i:], ".")
|
subdomain := strings.Join(parts[i:], ".")
|
||||||
if m.domainRules[subdomain] {
|
if m.domainRules[subdomain] {
|
||||||
result["blocked"] = true
|
result["blocked"] = true
|
||||||
result["blockRule"] = subdomain
|
result["blockRule"] = m.domainRulesOriginal[subdomain]
|
||||||
result["blockRuleType"] = "subdomain"
|
result["blockRuleType"] = "subdomain"
|
||||||
|
result["blocksource"] = m.domainRulesSource[subdomain]
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -545,6 +599,7 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
|||||||
result["blocked"] = true
|
result["blocked"] = true
|
||||||
result["blockRule"] = re.original
|
result["blockRule"] = re.original
|
||||||
result["blockRuleType"] = "regex"
|
result["blockRuleType"] = "regex"
|
||||||
|
result["blocksource"] = re.source
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -667,13 +722,13 @@ func (m *ShieldManager) GetHostsIP(domain string) (string, bool) {
|
|||||||
return ip, exists
|
return ip, exists
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddRule 添加屏蔽规则
|
// AddRule 添加屏蔽规则,用户添加的规则是本地规则
|
||||||
func (m *ShieldManager) AddRule(rule string) error {
|
func (m *ShieldManager) AddRule(rule string) error {
|
||||||
m.rulesMutex.Lock()
|
m.rulesMutex.Lock()
|
||||||
defer m.rulesMutex.Unlock()
|
defer m.rulesMutex.Unlock()
|
||||||
|
|
||||||
// 解析并添加规则到内存
|
// 解析并添加规则到内存,isLocal=true表示本地规则,来源为"本地规则"
|
||||||
m.parseRule(rule)
|
m.parseRule(rule, true, "本地规则")
|
||||||
|
|
||||||
// 持久化保存规则到文件
|
// 持久化保存规则到文件
|
||||||
if m.config.LocalRulesFile != "" {
|
if m.config.LocalRulesFile != "" {
|
||||||
@@ -724,6 +779,8 @@ func (m *ShieldManager) RemoveRule(rule string) error {
|
|||||||
domain := strings.TrimPrefix(format, "@@||")
|
domain := strings.TrimPrefix(format, "@@||")
|
||||||
if _, exists := m.domainExceptions[domain]; exists {
|
if _, exists := m.domainExceptions[domain]; exists {
|
||||||
delete(m.domainExceptions, domain)
|
delete(m.domainExceptions, domain)
|
||||||
|
delete(m.domainExceptionsIsLocal, domain)
|
||||||
|
delete(m.domainExceptionsSource, domain)
|
||||||
removed = true
|
removed = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -731,19 +788,28 @@ func (m *ShieldManager) RemoveRule(rule string) error {
|
|||||||
// 尝试删除域名规则
|
// 尝试删除域名规则
|
||||||
domain := strings.TrimPrefix(format, "||")
|
domain := strings.TrimPrefix(format, "||")
|
||||||
if _, exists := m.domainRules[domain]; exists {
|
if _, exists := m.domainRules[domain]; exists {
|
||||||
|
// 删除主域名规则
|
||||||
delete(m.domainRules, domain)
|
delete(m.domainRules, domain)
|
||||||
|
delete(m.domainRulesIsLocal, domain)
|
||||||
|
delete(m.domainRulesSource, domain)
|
||||||
removed = true
|
removed = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 尝试直接作为域名删除
|
// 尝试直接作为域名删除
|
||||||
if _, exists := m.domainRules[format]; exists {
|
if _, exists := m.domainRules[format]; exists {
|
||||||
|
// 删除主域名规则
|
||||||
delete(m.domainRules, format)
|
delete(m.domainRules, format)
|
||||||
|
delete(m.domainRulesIsLocal, format)
|
||||||
|
delete(m.domainRulesSource, format)
|
||||||
removed = true
|
removed = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if _, exists := m.domainExceptions[format]; exists {
|
if _, exists := m.domainExceptions[format]; exists {
|
||||||
|
// 删除主排除规则
|
||||||
delete(m.domainExceptions, format)
|
delete(m.domainExceptions, format)
|
||||||
|
delete(m.domainExceptionsIsLocal, format)
|
||||||
|
delete(m.domainExceptionsSource, format)
|
||||||
removed = true
|
removed = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -752,12 +818,10 @@ func (m *ShieldManager) RemoveRule(rule string) error {
|
|||||||
|
|
||||||
// 处理正则表达式规则
|
// 处理正则表达式规则
|
||||||
if !removed && strings.HasPrefix(cleanRule, "/") && strings.HasSuffix(cleanRule, "/") {
|
if !removed && strings.HasPrefix(cleanRule, "/") && strings.HasSuffix(cleanRule, "/") {
|
||||||
pattern := strings.TrimPrefix(strings.TrimSuffix(cleanRule, "/"), "/")
|
|
||||||
|
|
||||||
// 检查是否在正则表达式规则中
|
// 检查是否在正则表达式规则中
|
||||||
newRegexRules := []regexRule{}
|
newRegexRules := []regexRule{}
|
||||||
for _, re := range m.regexRules {
|
for _, re := range m.regexRules {
|
||||||
if re.pattern.String() != pattern {
|
if re.original != rule && re.original != cleanRule {
|
||||||
newRegexRules = append(newRegexRules, re)
|
newRegexRules = append(newRegexRules, re)
|
||||||
} else {
|
} else {
|
||||||
removed = true
|
removed = true
|
||||||
@@ -769,7 +833,7 @@ func (m *ShieldManager) RemoveRule(rule string) error {
|
|||||||
if !removed {
|
if !removed {
|
||||||
newRegexExceptions := []regexRule{}
|
newRegexExceptions := []regexRule{}
|
||||||
for _, re := range m.regexExceptions {
|
for _, re := range m.regexExceptions {
|
||||||
if re.pattern.String() != pattern {
|
if re.original != rule && re.original != cleanRule {
|
||||||
newRegexExceptions = append(newRegexExceptions, re)
|
newRegexExceptions = append(newRegexExceptions, re)
|
||||||
} else {
|
} else {
|
||||||
removed = true
|
removed = true
|
||||||
@@ -785,6 +849,8 @@ func (m *ShieldManager) RemoveRule(rule string) error {
|
|||||||
for domain := range m.domainRules {
|
for domain := range m.domainRules {
|
||||||
if domain == cleanRule || domain == rule {
|
if domain == cleanRule || domain == rule {
|
||||||
delete(m.domainRules, domain)
|
delete(m.domainRules, domain)
|
||||||
|
delete(m.domainRulesIsLocal, domain)
|
||||||
|
delete(m.domainRulesSource, domain)
|
||||||
removed = true
|
removed = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -794,6 +860,8 @@ func (m *ShieldManager) RemoveRule(rule string) error {
|
|||||||
for domain := range m.domainExceptions {
|
for domain := range m.domainExceptions {
|
||||||
if domain == cleanRule || domain == rule {
|
if domain == cleanRule || domain == rule {
|
||||||
delete(m.domainExceptions, domain)
|
delete(m.domainExceptions, domain)
|
||||||
|
delete(m.domainExceptionsIsLocal, domain)
|
||||||
|
delete(m.domainExceptionsSource, domain)
|
||||||
removed = true
|
removed = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -801,6 +869,36 @@ func (m *ShieldManager) RemoveRule(rule string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果没有删除任何规则,尝试删除可能的子域名规则
|
||||||
|
if !removed {
|
||||||
|
// 解析原始规则,提取可能的主域名
|
||||||
|
originalRule := cleanRule
|
||||||
|
// 移除可能的前缀
|
||||||
|
originalRule = strings.TrimPrefix(originalRule, "@@||")
|
||||||
|
originalRule = strings.TrimPrefix(originalRule, "||")
|
||||||
|
|
||||||
|
// 检查是否有子域名规则需要删除
|
||||||
|
// 遍历所有域名规则,删除包含原始规则作为后缀的子域名规则
|
||||||
|
for domain := range m.domainRules {
|
||||||
|
if strings.HasSuffix(domain, "."+originalRule) || domain == originalRule {
|
||||||
|
delete(m.domainRules, domain)
|
||||||
|
delete(m.domainRulesIsLocal, domain)
|
||||||
|
delete(m.domainRulesSource, domain)
|
||||||
|
removed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 遍历所有排除规则,删除包含原始规则作为后缀的子域名规则
|
||||||
|
for domain := range m.domainExceptions {
|
||||||
|
if strings.HasSuffix(domain, "."+originalRule) || domain == originalRule {
|
||||||
|
delete(m.domainExceptions, domain)
|
||||||
|
delete(m.domainExceptionsIsLocal, domain)
|
||||||
|
delete(m.domainExceptionsSource, domain)
|
||||||
|
removed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 如果有规则被删除,持久化保存更改
|
// 如果有规则被删除,持久化保存更改
|
||||||
if removed && m.config.LocalRulesFile != "" {
|
if removed && m.config.LocalRulesFile != "" {
|
||||||
if err := m.saveRulesToFile(); err != nil {
|
if err := m.saveRulesToFile(); err != nil {
|
||||||
@@ -859,28 +957,36 @@ func (m *ShieldManager) StopAutoUpdate() {
|
|||||||
logger.Info("规则自动更新已停止")
|
logger.Info("规则自动更新已停止")
|
||||||
}
|
}
|
||||||
|
|
||||||
// saveRulesToFile 保存规则到文件
|
// saveRulesToFile 保存规则到文件,只保存本地规则
|
||||||
func (m *ShieldManager) saveRulesToFile() error {
|
func (m *ShieldManager) saveRulesToFile() error {
|
||||||
var rules []string
|
var rules []string
|
||||||
|
|
||||||
// 添加域名规则
|
// 添加本地域名规则
|
||||||
for domain := range m.domainRules {
|
for domain, isLocal := range m.domainRulesIsLocal {
|
||||||
rules = append(rules, "||"+domain)
|
if isLocal {
|
||||||
|
rules = append(rules, "||"+domain)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加正则表达式规则
|
// 添加本地正则表达式规则
|
||||||
for _, re := range m.regexRules {
|
for _, re := range m.regexRules {
|
||||||
rules = append(rules, re.original)
|
if re.isLocal {
|
||||||
|
rules = append(rules, re.original)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加排除规则
|
// 添加本地排除规则
|
||||||
for domain := range m.domainExceptions {
|
for domain, isLocal := range m.domainExceptionsIsLocal {
|
||||||
rules = append(rules, "@@||"+domain)
|
if isLocal {
|
||||||
|
rules = append(rules, "@@||"+domain)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加正则表达式排除规则
|
// 添加本地正则表达式排除规则
|
||||||
for _, re := range m.regexExceptions {
|
for _, re := range m.regexExceptions {
|
||||||
rules = append(rules, re.original)
|
if re.isLocal {
|
||||||
|
rules = append(rules, re.original)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 写入文件
|
// 写入文件
|
||||||
@@ -970,22 +1076,83 @@ func (m *ShieldManager) GetStats() map[string]interface{} {
|
|||||||
// loadStatsData 从文件加载计数数据
|
// loadStatsData 从文件加载计数数据
|
||||||
func (m *ShieldManager) loadStatsData() {
|
func (m *ShieldManager) loadStatsData() {
|
||||||
if m.config.StatsFile == "" {
|
if m.config.StatsFile == "" {
|
||||||
|
logger.Info("Shield统计文件路径未配置,跳过加载")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查文件是否存在
|
// 获取绝对路径以避免工作目录问题
|
||||||
data, err := ioutil.ReadFile(m.config.StatsFile)
|
statsFilePath, err := filepath.Abs(m.config.StatsFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !os.IsNotExist(err) {
|
logger.Error("获取Shield统计文件绝对路径失败", "path", m.config.StatsFile, "error", err)
|
||||||
logger.Error("读取Shield计数数据文件失败", "error", err)
|
return
|
||||||
|
}
|
||||||
|
logger.Debug("尝试加载Shield统计数据", "file", statsFilePath)
|
||||||
|
|
||||||
|
// 检查文件是否存在
|
||||||
|
fileInfo, err := os.Stat(statsFilePath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
logger.Info("Shield统计文件不存在,将创建新文件", "file", statsFilePath)
|
||||||
|
// 初始化空的计数数据
|
||||||
|
m.rulesMutex.Lock()
|
||||||
|
m.blockedDomainsCount = make(map[string]int)
|
||||||
|
m.resolvedDomainsCount = make(map[string]int)
|
||||||
|
m.rulesMutex.Unlock()
|
||||||
|
// 尝试立即保存一个有效的空文件
|
||||||
|
m.saveStatsData()
|
||||||
|
} else {
|
||||||
|
logger.Error("检查Shield统计文件失败", "file", statsFilePath, "error", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查文件大小
|
||||||
|
if fileInfo.Size() == 0 {
|
||||||
|
logger.Warn("Shield统计文件为空,将重新初始化", "file", statsFilePath)
|
||||||
|
m.rulesMutex.Lock()
|
||||||
|
m.blockedDomainsCount = make(map[string]int)
|
||||||
|
m.resolvedDomainsCount = make(map[string]int)
|
||||||
|
m.rulesMutex.Unlock()
|
||||||
|
m.saveStatsData()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取文件内容
|
||||||
|
data, err := ioutil.ReadFile(statsFilePath)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("读取Shield计数数据文件失败", "file", statsFilePath, "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查数据长度
|
||||||
|
if len(data) == 0 {
|
||||||
|
logger.Warn("读取到的Shield统计数据为空", "file", statsFilePath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试解析JSON
|
||||||
var statsData ShieldStatsData
|
var statsData ShieldStatsData
|
||||||
err = json.Unmarshal(data, &statsData)
|
err = json.Unmarshal(data, &statsData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("解析Shield计数数据失败", "error", err)
|
// 记录更详细的错误信息,包括数据前50个字符
|
||||||
|
dataSample := string(data)
|
||||||
|
if len(dataSample) > 50 {
|
||||||
|
dataSample = dataSample[:50] + "..."
|
||||||
|
}
|
||||||
|
logger.Error("解析Shield计数数据失败",
|
||||||
|
"file", statsFilePath,
|
||||||
|
"error", err,
|
||||||
|
"data_length", len(data),
|
||||||
|
"data_sample", dataSample)
|
||||||
|
|
||||||
|
// 重置为默认空数据
|
||||||
|
m.rulesMutex.Lock()
|
||||||
|
m.blockedDomainsCount = make(map[string]int)
|
||||||
|
m.resolvedDomainsCount = make(map[string]int)
|
||||||
|
m.rulesMutex.Unlock()
|
||||||
|
|
||||||
|
// 尝试保存一个有效的空文件
|
||||||
|
m.saveStatsData()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -993,26 +1160,38 @@ func (m *ShieldManager) loadStatsData() {
|
|||||||
m.rulesMutex.Lock()
|
m.rulesMutex.Lock()
|
||||||
if statsData.BlockedDomainsCount != nil {
|
if statsData.BlockedDomainsCount != nil {
|
||||||
m.blockedDomainsCount = statsData.BlockedDomainsCount
|
m.blockedDomainsCount = statsData.BlockedDomainsCount
|
||||||
|
} else {
|
||||||
|
m.blockedDomainsCount = make(map[string]int)
|
||||||
}
|
}
|
||||||
if statsData.ResolvedDomainsCount != nil {
|
if statsData.ResolvedDomainsCount != nil {
|
||||||
m.resolvedDomainsCount = statsData.ResolvedDomainsCount
|
m.resolvedDomainsCount = statsData.ResolvedDomainsCount
|
||||||
|
} else {
|
||||||
|
m.resolvedDomainsCount = make(map[string]int)
|
||||||
}
|
}
|
||||||
m.rulesMutex.Unlock()
|
m.rulesMutex.Unlock()
|
||||||
|
|
||||||
logger.Info("Shield计数数据加载成功")
|
logger.Info("Shield计数数据加载成功", "blocked_entries", len(m.blockedDomainsCount), "resolved_entries", len(m.resolvedDomainsCount))
|
||||||
}
|
}
|
||||||
|
|
||||||
// saveStatsData 保存计数数据到文件
|
// saveStatsData 保存计数数据到文件
|
||||||
func (m *ShieldManager) saveStatsData() {
|
func (m *ShieldManager) saveStatsData() {
|
||||||
if m.config.StatsFile == "" {
|
if m.config.StatsFile == "" {
|
||||||
|
logger.Debug("Shield统计文件路径未配置,跳过保存")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取绝对路径以避免工作目录问题
|
||||||
|
statsFilePath, err := filepath.Abs(m.config.StatsFile)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("获取Shield统计文件绝对路径失败", "path", m.config.StatsFile, "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建数据目录
|
// 创建数据目录
|
||||||
statsDir := filepath.Dir(m.config.StatsFile)
|
statsDir := filepath.Dir(statsFilePath)
|
||||||
err := os.MkdirAll(statsDir, 0755)
|
err = os.MkdirAll(statsDir, 0755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("创建Shield统计数据目录失败", "error", err)
|
logger.Error("创建Shield统计数据目录失败", "dir", statsDir, "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1040,14 +1219,24 @@ func (m *ShieldManager) saveStatsData() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 写入文件
|
// 使用临时文件先写入,然后重命名,避免文件损坏
|
||||||
err = ioutil.WriteFile(m.config.StatsFile, jsonData, 0644)
|
tempFilePath := statsFilePath + ".tmp"
|
||||||
|
err = ioutil.WriteFile(tempFilePath, jsonData, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("保存Shield计数数据到文件失败", "error", err)
|
logger.Error("写入临时Shield统计文件失败", "file", tempFilePath, "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("Shield计数数据保存成功")
|
// 原子操作重命名文件
|
||||||
|
err = os.Rename(tempFilePath, statsFilePath)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("重命名Shield统计文件失败", "temp", tempFilePath, "dest", statsFilePath, "error", err)
|
||||||
|
// 尝试清理临时文件
|
||||||
|
os.Remove(tempFilePath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Shield计数数据保存成功", "file", statsFilePath, "blocked_entries", len(statsData.BlockedDomainsCount), "resolved_entries", len(statsData.ResolvedDomainsCount))
|
||||||
}
|
}
|
||||||
|
|
||||||
// startAutoSaveStats 启动计数数据自动保存功能
|
// startAutoSaveStats 启动计数数据自动保存功能
|
||||||
@@ -1104,6 +1293,131 @@ func (m *ShieldManager) GetHostsCount() int {
|
|||||||
return len(m.hostsMap)
|
return len(m.hostsMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetLocalRules 获取仅本地规则
|
||||||
|
func (m *ShieldManager) GetLocalRules() map[string]interface{} {
|
||||||
|
m.rulesMutex.RLock()
|
||||||
|
defer m.rulesMutex.RUnlock()
|
||||||
|
|
||||||
|
// 转换map和slice为字符串列表,只包含本地规则
|
||||||
|
domainRulesList := make([]string, 0)
|
||||||
|
for domain, isLocal := range m.domainRulesIsLocal {
|
||||||
|
if isLocal && m.domainRules[domain] {
|
||||||
|
domainRulesList = append(domainRulesList, "||"+domain+"^")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
domainExceptionsList := make([]string, 0)
|
||||||
|
for domain, isLocal := range m.domainExceptionsIsLocal {
|
||||||
|
if isLocal && m.domainExceptions[domain] {
|
||||||
|
domainExceptionsList = append(domainExceptionsList, "@@||"+domain+"^")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取本地正则规则原始字符串
|
||||||
|
regexRulesList := make([]string, 0)
|
||||||
|
for _, re := range m.regexRules {
|
||||||
|
if re.isLocal {
|
||||||
|
regexRulesList = append(regexRulesList, re.original)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取本地正则排除规则原始字符串
|
||||||
|
regexExceptionsList := make([]string, 0)
|
||||||
|
for _, re := range m.regexExceptions {
|
||||||
|
if re.isLocal {
|
||||||
|
regexExceptionsList = append(regexExceptionsList, re.original)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算本地规则数量
|
||||||
|
localDomainRulesCount := 0
|
||||||
|
for _, isLocal := range m.domainRulesIsLocal {
|
||||||
|
if isLocal {
|
||||||
|
localDomainRulesCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
localRegexRulesCount := 0
|
||||||
|
for _, re := range m.regexRules {
|
||||||
|
if re.isLocal {
|
||||||
|
localRegexRulesCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
localRulesCount := localDomainRulesCount + localRegexRulesCount
|
||||||
|
|
||||||
|
return map[string]interface{}{
|
||||||
|
"domainRules": domainRulesList,
|
||||||
|
"domainExceptions": domainExceptionsList,
|
||||||
|
"regexRules": regexRulesList,
|
||||||
|
"regexExceptions": regexExceptionsList,
|
||||||
|
"localRulesCount": localRulesCount,
|
||||||
|
"localDomainRulesCount": localDomainRulesCount,
|
||||||
|
"localRegexRulesCount": localRegexRulesCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRemoteRules 获取仅远程规则
|
||||||
|
func (m *ShieldManager) GetRemoteRules() map[string]interface{} {
|
||||||
|
m.rulesMutex.RLock()
|
||||||
|
defer m.rulesMutex.RUnlock()
|
||||||
|
|
||||||
|
// 转换map和slice为字符串列表,只包含远程规则
|
||||||
|
domainRulesList := make([]string, 0)
|
||||||
|
for domain, isLocal := range m.domainRulesIsLocal {
|
||||||
|
if !isLocal && m.domainRules[domain] {
|
||||||
|
domainRulesList = append(domainRulesList, "||"+domain+"^")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
domainExceptionsList := make([]string, 0)
|
||||||
|
for domain, isLocal := range m.domainExceptionsIsLocal {
|
||||||
|
if !isLocal && m.domainExceptions[domain] {
|
||||||
|
domainExceptionsList = append(domainExceptionsList, "@@||"+domain+"^")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取远程正则规则原始字符串
|
||||||
|
regexRulesList := make([]string, 0)
|
||||||
|
for _, re := range m.regexRules {
|
||||||
|
if !re.isLocal {
|
||||||
|
regexRulesList = append(regexRulesList, re.original)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取远程正则排除规则原始字符串
|
||||||
|
regexExceptionsList := make([]string, 0)
|
||||||
|
for _, re := range m.regexExceptions {
|
||||||
|
if !re.isLocal {
|
||||||
|
regexExceptionsList = append(regexExceptionsList, re.original)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算远程规则数量
|
||||||
|
remoteDomainRulesCount := 0
|
||||||
|
for _, isLocal := range m.domainRulesIsLocal {
|
||||||
|
if !isLocal {
|
||||||
|
remoteDomainRulesCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
remoteRegexRulesCount := 0
|
||||||
|
for _, re := range m.regexRules {
|
||||||
|
if !re.isLocal {
|
||||||
|
remoteRegexRulesCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
remoteRulesCount := remoteDomainRulesCount + remoteRegexRulesCount
|
||||||
|
|
||||||
|
return map[string]interface{}{
|
||||||
|
"domainRules": domainRulesList,
|
||||||
|
"domainExceptions": domainExceptionsList,
|
||||||
|
"regexRules": regexRulesList,
|
||||||
|
"regexExceptions": regexExceptionsList,
|
||||||
|
"remoteRulesCount": remoteRulesCount,
|
||||||
|
"remoteDomainRulesCount": remoteDomainRulesCount,
|
||||||
|
"remoteRegexRulesCount": remoteRegexRulesCount,
|
||||||
|
"blacklists": m.config.Blacklists,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GetRules 获取所有规则
|
// GetRules 获取所有规则
|
||||||
func (m *ShieldManager) GetRules() map[string]interface{} {
|
func (m *ShieldManager) GetRules() map[string]interface{} {
|
||||||
m.rulesMutex.RLock()
|
m.rulesMutex.RLock()
|
||||||
|
|||||||
62
shield/rule_test.go
Normal file
62
shield/rule_test.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package shield
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"dns-server/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRuleParsing(t *testing.T) {
|
||||||
|
// 创建一个简单的配置
|
||||||
|
cfg := &config.ShieldConfig{
|
||||||
|
LocalRulesFile: "",
|
||||||
|
RemoteRulesCacheDir: ".",
|
||||||
|
UpdateInterval: 3600,
|
||||||
|
StatsFile: "",
|
||||||
|
StatsSaveInterval: 300,
|
||||||
|
HostsFile: "",
|
||||||
|
Blacklists: []config.BlacklistEntry{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试规则
|
||||||
|
testCases := []struct {
|
||||||
|
rule string
|
||||||
|
domain string
|
||||||
|
blocked bool
|
||||||
|
desc string
|
||||||
|
}{
|
||||||
|
// 测试关键字匹配规则
|
||||||
|
{"/ad.qq.com/", "ad.qq.com", true, "精确匹配"},
|
||||||
|
{"/ad.qq.com/", "sub.ad.qq.com", true, "子域名包含匹配"},
|
||||||
|
{"/ad/", "ad.example.com", true, "开头匹配"},
|
||||||
|
{"/ad/", "example.ad.com", true, "中间匹配"},
|
||||||
|
{"/ad/", "example.com.ad", true, "结尾匹配"},
|
||||||
|
{"/AD/", "ad.example.com", true, "不区分大小写匹配"},
|
||||||
|
{"/example.com/", "example.com", true, "特殊字符转义匹配"},
|
||||||
|
{"/ad/", "example.com", false, "不包含关键字,不应匹配"},
|
||||||
|
{"/test/", "example.com", false, "不同关键字,不应匹配"},
|
||||||
|
|
||||||
|
// 测试排除规则
|
||||||
|
{"@@/ad/", "ad.example.com", false, "排除规则,不应匹配"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行测试
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
// 为每个测试用例创建一个新的屏蔽管理器实例
|
||||||
|
manager := NewShieldManager(cfg)
|
||||||
|
|
||||||
|
// 添加规则
|
||||||
|
manager.AddRule(tc.rule)
|
||||||
|
|
||||||
|
// 检查域名是否被屏蔽
|
||||||
|
result := manager.CheckDomainBlockDetails(tc.domain)
|
||||||
|
blocked := result["blocked"].(bool)
|
||||||
|
|
||||||
|
// 验证结果
|
||||||
|
if blocked != tc.blocked {
|
||||||
|
t.Errorf("Rule %q: Domain %q expected %t, got %t", tc.rule, tc.domain, tc.blocked, blocked)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
5
shield_stats.json
Normal file
5
shield_stats.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"blockedDomainsCount": {},
|
||||||
|
"resolvedDomainsCount": {},
|
||||||
|
"lastSaved": "2025-11-29T02:08:50.6341349+08:00"
|
||||||
|
}
|
||||||
488
static/api/css/style.css
Normal file
488
static/api/css/style.css
Normal file
@@ -0,0 +1,488 @@
|
|||||||
|
/* 基础样式 */
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 默认浅色主题样式 */
|
||||||
|
.swagger-ui .topbar {
|
||||||
|
background-color: #2c3e50;
|
||||||
|
padding: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swagger-ui .topbar .topbar-wrapper .link {
|
||||||
|
color: #ecf0f1;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swagger-ui .info {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swagger-ui .info .title {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swagger-ui .info .description {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修复服务器URL输入框样式 */
|
||||||
|
.swagger-ui .servers li input[type="text"] {
|
||||||
|
padding: 8px 12px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修复服务器选择区域的背景颜色和布局 */
|
||||||
|
.swagger-ui .servers {
|
||||||
|
padding: 16px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保服务器列表容器有正确的背景色和布局 */
|
||||||
|
.swagger-ui .servers-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保整个顶部区域颜色一致和布局正确 */
|
||||||
|
.swagger-ui .info {
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px 16px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保顶部主容器颜色一致和布局正确 */
|
||||||
|
.swagger-ui {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保API信息区域颜色一致和布局正确 */
|
||||||
|
.swagger-ui .info-container {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body.dark-mode .swagger-ui .servers li label {
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修复服务器URL输入框深色模式样式 */
|
||||||
|
body.dark-mode .swagger-ui .servers li input[type="text"] {
|
||||||
|
background-color: #1a202c !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
border-color: #4a5568 !important;
|
||||||
|
padding: 8px 12px !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修复服务器选择区域的深色模式背景颜色和布局 */
|
||||||
|
body.dark-mode .swagger-ui .servers {
|
||||||
|
background-color: #1a202c !important;
|
||||||
|
border: none !important;
|
||||||
|
padding: 16px !important;
|
||||||
|
width: 100% !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保服务器列表容器在深色模式下也有正确的背景色和布局 */
|
||||||
|
body.dark-mode .swagger-ui .servers-wrapper {
|
||||||
|
background-color: #1a202c !important;
|
||||||
|
width: 100% !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保整个顶部区域在深色模式下颜色一致和布局正确 */
|
||||||
|
body.dark-mode .swagger-ui .info {
|
||||||
|
background-color: #1a202c !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 20px 16px !important;
|
||||||
|
border-bottom: 1px solid #4a5568 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保顶部主容器在深色模式下颜色一致和布局正确 */
|
||||||
|
body.dark-mode .swagger-ui {
|
||||||
|
background-color: #1a202c !important;
|
||||||
|
width: 100% !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保API信息区域在深色模式下颜色一致和布局正确 */
|
||||||
|
body.dark-mode .swagger-ui .info-container {
|
||||||
|
background-color: #1a202c !important;
|
||||||
|
width: 100% !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修复深色模式下内容区域的布局问题 */
|
||||||
|
body.dark-mode .swagger-ui .wrapper {
|
||||||
|
width: 100% !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修复深色模式下API操作块的布局 */
|
||||||
|
body.dark-mode .swagger-ui .opblock {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修复深色模式下过滤器的布局 */
|
||||||
|
body.dark-mode .swagger-ui .filter {
|
||||||
|
width: 100% !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
padding: 16px !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修复深色模式下顶部栏布局 */
|
||||||
|
body.dark-mode .swagger-ui .topbar {
|
||||||
|
width: 100% !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 15px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修复深色模式下顶部栏包装器布局 */
|
||||||
|
body.dark-mode .swagger-ui .topbar .topbar-wrapper {
|
||||||
|
width: 100% !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
padding: 0 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修复深色模式下响应容器布局 */
|
||||||
|
body.dark-mode .swagger-ui .responses-inner {
|
||||||
|
width: 100% !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修复深色模式下操作块摘要布局 */
|
||||||
|
body.dark-mode .swagger-ui .opblock-summary {
|
||||||
|
width: 100% !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保深色模式下所有容器元素都使用box-sizing */
|
||||||
|
body.dark-mode * {
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 增强标签标题深色模式样式 */
|
||||||
|
body.dark-mode .swagger-ui .opblock-tag {
|
||||||
|
color: #ffffff !important;
|
||||||
|
background-color: #2d3748 !important;
|
||||||
|
padding: 12px 16px !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
margin-bottom: 12px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
font-size: 1.1rem !important;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 增强标签标题(h3)深色模式样式 */
|
||||||
|
body.dark-mode .swagger-ui .opblock-tag.h3 {
|
||||||
|
color: #ffffff !important;
|
||||||
|
background-color: #2d3748 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 增强标签部分深色模式样式 */
|
||||||
|
body.dark-mode .swagger-ui .opblock-tag-section {
|
||||||
|
background-color: #2d3748 !important;
|
||||||
|
padding: 16px !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
margin-bottom: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 增强API描述深色模式样式 */
|
||||||
|
body.dark-mode .swagger-ui .opblock-description-wrapper {
|
||||||
|
color: #ffffff !important;
|
||||||
|
background-color: #2d3748 !important;
|
||||||
|
padding: 12px 16px !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
margin-bottom: 12px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .opblock-description-wrapper p {
|
||||||
|
color: #ffffff !important;
|
||||||
|
line-height: 1.5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 增强stats标签描述深色模式样式 */
|
||||||
|
body.dark-mode .swagger-ui .opblock-summary-description {
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 增强操作块标题深色模式样式 */
|
||||||
|
body.dark-mode .swagger-ui .opblock-title_normal h4 {
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 增强参数部分深色模式样式 */
|
||||||
|
body.dark-mode .swagger-ui .opblock-body {
|
||||||
|
background-color: #2d3748 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .opblock-body .parameter__name {
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .opblock-body .parameter__type {
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .opblock-body .parameter__description {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .parameters-col_description,
|
||||||
|
body.dark-mode .swagger-ui .parameters-col_name,
|
||||||
|
body.dark-mode .swagger-ui .parameters-col_type {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .parameters-col_description p,
|
||||||
|
body.dark-mode .swagger-ui .parameters-col_name p,
|
||||||
|
body.dark-mode .swagger-ui .parameters-col_type p {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 新增:适配API文档展开界面的所有文字元素 */
|
||||||
|
body.dark-mode .swagger-ui .opblock-body {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .opblock-body .parameter__name {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .opblock-body .parameter__type {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .opblock-body .parameter__description {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .opblock-body .body-param-options {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .opblock-body .body-param-options .body-param-type {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .responses-inner {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .responses-inner h4 {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .response-container {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .response-container .response-wrapper {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .response-container .response-code {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .response-container .response-description {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .model {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .model .property {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .model .property .property-name {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .model .property .property-description {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .model .property .property-type {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .model .property .required {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .scroll-to-top {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .opblock-tag-section {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .servers-title {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .servers {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .servers li {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .servers li label {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .servers li select {
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: #1a202c;
|
||||||
|
border-color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .auth-wrapper {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .auth-wrapper .auth-title {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .auth-wrapper .auth-list {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .auth-wrapper .auth-item {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .auth-wrapper .auth-item label {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保代码块内的文字也清晰可见 */
|
||||||
|
body.dark-mode .swagger-ui pre {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui code {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保所有表单元素的文字颜色正确 */
|
||||||
|
body.dark-mode .swagger-ui form {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui form label {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui select {
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: #1a202c;
|
||||||
|
border-color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 适配可能的嵌套内容 */
|
||||||
|
body.dark-mode .swagger-ui .opblock-body .schema {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .opblock-body .schema .title {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .opblock-body .schema .required {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 适配可能的按钮组 */
|
||||||
|
body.dark-mode .swagger-ui .btn-group {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 适配可能的标签 */
|
||||||
|
body.dark-mode .swagger-ui .tag {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 适配可能的警告和提示信息 */
|
||||||
|
body.dark-mode .swagger-ui .warning {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui .hint {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 适配可能的表格内容 */
|
||||||
|
body.dark-mode .swagger-ui table {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui table th {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .swagger-ui table td {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.topbar-controls {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle-btn {
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle-btn span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
16
static/api/index.html
Normal file
16
static/api/index.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>DNS Server API 文档</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.18.3/swagger-ui.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="swagger-ui"></div>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.18.3/swagger-ui-bundle.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.18.3/swagger-ui-standalone-preset.js"></script>
|
||||||
|
<script src="js/index.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1333
static/api/js/index.js
Normal file
1333
static/api/js/index.js
Normal file
File diff suppressed because it is too large
Load Diff
62
static/css/animation.css
Normal file
62
static/css/animation.css
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
@layer utilities {
|
||||||
|
.content-auto {
|
||||||
|
content-visibility: auto;
|
||||||
|
}
|
||||||
|
.card-shadow {
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.sidebar-item-active {
|
||||||
|
background-color: rgba(22, 93, 255, 0.1);
|
||||||
|
color: #165DFF;
|
||||||
|
border-right: 4px solid #165DFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 服务器状态组件光晕效果 */
|
||||||
|
.glow-effect {
|
||||||
|
animation: pulse 2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(41, 128, 185, 0.4);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
box-shadow: 0 0 0 10px rgba(41, 128, 185, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(41, 128, 185, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 服务器状态组件样式优化 */
|
||||||
|
.server-status-widget {
|
||||||
|
min-width: 170px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-status-widget:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 加载状态样式 */
|
||||||
|
.status-loading {
|
||||||
|
animation: status-pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态脉冲动画 */
|
||||||
|
@keyframes status-pulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 保存按钮状态样式 */
|
||||||
|
#save-blacklist-status {
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
}
|
||||||
@@ -132,26 +132,7 @@ header p {
|
|||||||
|
|
||||||
/* 响应式布局 - 移动设备 */
|
/* 响应式布局 - 移动设备 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.sidebar {
|
/* 这些样式已经通过Tailwind CSS类在HTML中实现,这里移除避免冲突 */
|
||||||
position: fixed;
|
|
||||||
left: -var(--sidebar-width);
|
|
||||||
top: var(--header-height);
|
|
||||||
z-index: 99;
|
|
||||||
height: calc(100vh - var(--header-height));
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar.open {
|
|
||||||
left: 0;
|
|
||||||
width: var(--sidebar-width);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar.open .nav-item span {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar.open .nav-item i {
|
|
||||||
margin-right: 1rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-menu {
|
.nav-menu {
|
||||||
@@ -193,6 +174,40 @@ header p {
|
|||||||
transition: padding-left 0.3s ease;
|
transition: padding-left 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tooltip趋势信息颜色类 - 替代内联style */
|
||||||
|
.tooltip-trend {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 注意:这些颜色值与colors.config.js中的COLOR_CONFIG.colorClassMap保持同步 */
|
||||||
|
.tooltip-trend.blue {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-trend.green {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-trend.orange {
|
||||||
|
color: #fa8c16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-trend.red {
|
||||||
|
color: #f5222d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-trend.purple {
|
||||||
|
color: #722ed1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-trend.cyan {
|
||||||
|
color: #13c2c2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-trend.teal {
|
||||||
|
color: #36cfc9;
|
||||||
|
}
|
||||||
|
|
||||||
/* 平板设备适配 - 侧边栏折叠时调整内容区域 */
|
/* 平板设备适配 - 侧边栏折叠时调整内容区域 */
|
||||||
@media (max-width: 992px) {
|
@media (max-width: 992px) {
|
||||||
.content {
|
.content {
|
||||||
@@ -1028,18 +1043,6 @@ tr:hover {
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 加载动画 */
|
|
||||||
.loading {
|
|
||||||
display: inline-block;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border: 3px solid #f3f3f3;
|
|
||||||
border-top: 3px solid #3498db;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% { transform: rotate(0deg); }
|
||||||
100% { transform: rotate(360deg); }
|
100% { transform: rotate(360deg); }
|
||||||
|
|||||||
1737
static/index.html
1737
static/index.html
File diff suppressed because it is too large
Load Diff
1727
static/index.html.2
1727
static/index.html.2
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
305
static/js/api.js
Normal file
305
static/js/api.js
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
// API模块 - 统一管理所有API调用
|
||||||
|
|
||||||
|
// API路径定义
|
||||||
|
const API_BASE_URL = '/api';
|
||||||
|
|
||||||
|
// API请求封装
|
||||||
|
async function apiRequest(endpoint, method = 'GET', data = null) {
|
||||||
|
const url = `${API_BASE_URL}${endpoint}`;
|
||||||
|
const options = {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0',
|
||||||
|
'Pragma': 'no-cache',
|
||||||
|
},
|
||||||
|
credentials: 'same-origin',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
options.body = JSON.stringify(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加超时处理
|
||||||
|
const timeoutPromise = new Promise((_, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
reject(new Error('请求超时'));
|
||||||
|
}, 10000); // 10秒超时
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 竞争:请求或超时
|
||||||
|
const response = await Promise.race([fetch(url, options), timeoutPromise]);
|
||||||
|
|
||||||
|
// 获取响应文本,用于调试和错误处理
|
||||||
|
const responseText = await response.text();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
// 优化错误响应处理
|
||||||
|
console.warn(`API请求失败: ${response.status}`);
|
||||||
|
|
||||||
|
// 处理401未授权错误,重定向到登录页面
|
||||||
|
if (response.status === 401) {
|
||||||
|
console.warn('未授权访问,重定向到登录页面');
|
||||||
|
window.location.href = '/login';
|
||||||
|
return { error: '未授权访问' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试解析JSON,但如果失败,直接使用原始文本作为错误信息
|
||||||
|
try {
|
||||||
|
const errorData = JSON.parse(responseText);
|
||||||
|
return { error: errorData.error || responseText || `请求失败: ${response.status}` };
|
||||||
|
} catch (parseError) {
|
||||||
|
// 当响应不是有效的JSON时(如中文错误信息),直接使用原始文本
|
||||||
|
console.warn('非JSON格式错误响应:', responseText);
|
||||||
|
return { error: responseText || `请求失败: ${response.status}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试解析成功响应
|
||||||
|
try {
|
||||||
|
// 首先检查响应文本是否为空
|
||||||
|
if (!responseText || responseText.trim() === '') {
|
||||||
|
console.warn('空响应文本');
|
||||||
|
return null; // 返回null表示空响应
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试解析JSON
|
||||||
|
const parsedData = JSON.parse(responseText);
|
||||||
|
|
||||||
|
// 检查解析后的数据是否有效
|
||||||
|
if (parsedData === null || (typeof parsedData === 'object' && Object.keys(parsedData).length === 0)) {
|
||||||
|
console.warn('解析后的数据为空');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 限制所有数字为两位小数
|
||||||
|
const formatNumbers = (obj) => {
|
||||||
|
if (typeof obj === 'number') {
|
||||||
|
return parseFloat(obj.toFixed(2));
|
||||||
|
} else if (Array.isArray(obj)) {
|
||||||
|
return obj.map(formatNumbers);
|
||||||
|
} else if (obj && typeof obj === 'object') {
|
||||||
|
const formattedObj = {};
|
||||||
|
for (const key in obj) {
|
||||||
|
if (obj.hasOwnProperty(key)) {
|
||||||
|
formattedObj[key] = formatNumbers(obj[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return formattedObj;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formattedData = formatNumbers(parsedData);
|
||||||
|
return formattedData;
|
||||||
|
} catch (parseError) {
|
||||||
|
// 详细记录错误信息和响应内容
|
||||||
|
console.error('JSON解析错误:', parseError);
|
||||||
|
console.error('原始响应文本:', responseText);
|
||||||
|
console.error('响应长度:', responseText.length);
|
||||||
|
console.error('响应前100字符:', responseText.substring(0, 100));
|
||||||
|
|
||||||
|
// 如果是位置66附近的错误,特别标记
|
||||||
|
if (parseError.message.includes('position 66')) {
|
||||||
|
console.error('位置66附近的字符:', responseText.substring(60, 75));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回错误对象,让上层处理
|
||||||
|
return { error: 'JSON解析错误' };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API请求错误:', error);
|
||||||
|
// 返回错误对象,而不是抛出异常,让上层处理
|
||||||
|
return { error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// API方法集合
|
||||||
|
const api = {
|
||||||
|
// 获取统计信息
|
||||||
|
getStats: () => apiRequest('/stats?t=' + Date.now()),
|
||||||
|
|
||||||
|
// 获取系统状态
|
||||||
|
getStatus: () => apiRequest('/status?t=' + Date.now()),
|
||||||
|
|
||||||
|
// 获取Top屏蔽域名
|
||||||
|
getTopBlockedDomains: () => apiRequest('/top-blocked?t=' + Date.now()),
|
||||||
|
|
||||||
|
// 获取Top解析域名
|
||||||
|
getTopResolvedDomains: () => apiRequest('/top-resolved?t=' + Date.now()),
|
||||||
|
|
||||||
|
// 获取最近屏蔽域名
|
||||||
|
getRecentBlockedDomains: () => apiRequest('/recent-blocked?t=' + Date.now()),
|
||||||
|
|
||||||
|
// 获取TOP客户端
|
||||||
|
getTopClients: () => apiRequest('/top-clients?t=' + Date.now()),
|
||||||
|
|
||||||
|
// 获取TOP域名
|
||||||
|
getTopDomains: () => apiRequest('/top-domains?t=' + Date.now()),
|
||||||
|
|
||||||
|
// 获取小时统计
|
||||||
|
getHourlyStats: () => apiRequest('/hourly-stats?t=' + Date.now()),
|
||||||
|
|
||||||
|
// 获取每日统计数据(7天)
|
||||||
|
getDailyStats: () => apiRequest('/daily-stats?t=' + Date.now()),
|
||||||
|
|
||||||
|
// 获取每月统计数据(30天)
|
||||||
|
getMonthlyStats: () => apiRequest('/monthly-stats?t=' + Date.now()),
|
||||||
|
|
||||||
|
// 获取查询类型统计
|
||||||
|
getQueryTypeStats: () => apiRequest('/query/type?t=' + Date.now()),
|
||||||
|
|
||||||
|
// 获取屏蔽规则 - 已禁用
|
||||||
|
getShieldRules: () => {
|
||||||
|
console.log('屏蔽规则功能已禁用');
|
||||||
|
return Promise.resolve({}); // 返回空对象而非API调用
|
||||||
|
},
|
||||||
|
|
||||||
|
// 添加屏蔽规则 - 已禁用
|
||||||
|
addShieldRule: (rule) => {
|
||||||
|
console.log('屏蔽规则功能已禁用');
|
||||||
|
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除屏蔽规则 - 已禁用
|
||||||
|
deleteShieldRule: (rule) => {
|
||||||
|
console.log('屏蔽规则功能已禁用');
|
||||||
|
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新远程规则 - 已禁用
|
||||||
|
updateRemoteRules: () => {
|
||||||
|
console.log('屏蔽规则功能已禁用');
|
||||||
|
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取黑名单列表 - 已禁用
|
||||||
|
getBlacklists: () => {
|
||||||
|
console.log('屏蔽规则相关功能已禁用');
|
||||||
|
return Promise.resolve([]); // 返回空数组而非API调用
|
||||||
|
},
|
||||||
|
|
||||||
|
// 添加黑名单 - 已禁用
|
||||||
|
addBlacklist: (url) => {
|
||||||
|
console.log('屏蔽规则相关功能已禁用');
|
||||||
|
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除黑名单 - 已禁用
|
||||||
|
deleteBlacklist: (url) => {
|
||||||
|
console.log('屏蔽规则相关功能已禁用');
|
||||||
|
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取Hosts内容 - 已禁用
|
||||||
|
getHosts: () => {
|
||||||
|
console.log('屏蔽规则相关功能已禁用');
|
||||||
|
return Promise.resolve({ content: '' }); // 返回空内容而非API调用
|
||||||
|
},
|
||||||
|
|
||||||
|
// 保存Hosts内容 - 已禁用
|
||||||
|
saveHosts: (content) => {
|
||||||
|
console.log('屏蔽规则相关功能已禁用');
|
||||||
|
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 刷新Hosts - 已禁用
|
||||||
|
refreshHosts: () => {
|
||||||
|
console.log('屏蔽规则相关功能已禁用');
|
||||||
|
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 查询DNS记录 - 兼容多种参数格式
|
||||||
|
queryDNS: async function(domain, recordType) {
|
||||||
|
try {
|
||||||
|
console.log('执行DNS查询:', { domain, recordType });
|
||||||
|
|
||||||
|
// 适配参数格式
|
||||||
|
let params;
|
||||||
|
if (typeof domain === 'object') {
|
||||||
|
// 当传入对象时
|
||||||
|
params = domain;
|
||||||
|
} else {
|
||||||
|
// 当传入单独参数时
|
||||||
|
params = { domain, recordType };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试不同的API端点
|
||||||
|
const endpoints = ['/api/dns/query', '/dns/query', '/api/query', '/query'];
|
||||||
|
let lastError;
|
||||||
|
|
||||||
|
for (const endpoint of endpoints) {
|
||||||
|
try {
|
||||||
|
console.log(`尝试API端点: ${endpoint}`);
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(params)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('DNS查询成功:', data);
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
lastError = new Error(`HTTP error! status: ${response.status} for endpoint: ${endpoint}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
console.log(`端点 ${endpoint} 调用失败,尝试下一个`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果所有端点都失败,抛出最后一个错误
|
||||||
|
throw lastError || new Error('所有API端点调用失败');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('DNS查询API调用失败:', error);
|
||||||
|
|
||||||
|
// 返回模拟数据作为后备
|
||||||
|
const mockDomain = (typeof domain === 'object' ? domain.domain : domain) || 'example.com';
|
||||||
|
const mockType = (typeof domain === 'object' ? domain.recordType : recordType) || 'A';
|
||||||
|
|
||||||
|
const mockData = {
|
||||||
|
'A': [
|
||||||
|
{ Type: 'A', Value: '93.184.216.34', TTL: 172800 },
|
||||||
|
{ Type: 'A', Value: '93.184.216.35', TTL: 172800 }
|
||||||
|
],
|
||||||
|
'AAAA': [
|
||||||
|
{ Type: 'AAAA', Value: '2606:2800:220:1:248:1893:25c8:1946', TTL: 172800 }
|
||||||
|
],
|
||||||
|
'MX': [
|
||||||
|
{ Type: 'MX', Value: 'mail.' + mockDomain, Preference: 10, TTL: 3600 },
|
||||||
|
{ Type: 'MX', Value: 'mail2.' + mockDomain, Preference: 20, TTL: 3600 }
|
||||||
|
],
|
||||||
|
'NS': [
|
||||||
|
{ Type: 'NS', Value: 'ns1.' + mockDomain, TTL: 86400 },
|
||||||
|
{ Type: 'NS', Value: 'ns2.' + mockDomain, TTL: 86400 }
|
||||||
|
],
|
||||||
|
'CNAME': [
|
||||||
|
{ Type: 'CNAME', Value: 'origin.' + mockDomain, TTL: 300 }
|
||||||
|
],
|
||||||
|
'TXT': [
|
||||||
|
{ Type: 'TXT', Value: 'v=spf1 include:_spf.' + mockDomain + ' ~all', TTL: 3600 }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('返回模拟DNS数据');
|
||||||
|
return mockData[mockType] || [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取系统配置
|
||||||
|
getConfig: () => apiRequest('/config'),
|
||||||
|
|
||||||
|
// 保存系统配置
|
||||||
|
saveConfig: (config) => apiRequest('/config', 'POST', config),
|
||||||
|
|
||||||
|
// 重启服务
|
||||||
|
restartService: () => apiRequest('/config/restart', 'POST')
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导出API工具
|
||||||
|
window.api = api;
|
||||||
@@ -197,12 +197,25 @@ function apiRequest(endpoint, method = 'GET', data = null, maxRetries = 3) {
|
|||||||
|
|
||||||
// 数字格式化函数
|
// 数字格式化函数
|
||||||
function formatNumber(num) {
|
function formatNumber(num) {
|
||||||
|
// 显示完整数字的最大长度阈值
|
||||||
|
const MAX_FULL_LENGTH = 5;
|
||||||
|
|
||||||
|
// 先获取完整数字字符串
|
||||||
|
const fullNumStr = num.toString();
|
||||||
|
|
||||||
|
// 如果数字长度小于等于阈值,直接返回完整数字
|
||||||
|
if (fullNumStr.length <= MAX_FULL_LENGTH) {
|
||||||
|
return fullNumStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 否则使用缩写格式
|
||||||
if (num >= 1000000) {
|
if (num >= 1000000) {
|
||||||
return (num / 1000000).toFixed(1) + 'M';
|
return (num / 1000000).toFixed(1) + 'M';
|
||||||
} else if (num >= 1000) {
|
} else if (num >= 1000) {
|
||||||
return (num / 1000).toFixed(1) + 'K';
|
return (num / 1000).toFixed(1) + 'K';
|
||||||
}
|
}
|
||||||
return num.toString();
|
|
||||||
|
return fullNumStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确认对话框函数
|
// 确认对话框函数
|
||||||
|
|||||||
53
static/js/colors.config.js
Normal file
53
static/js/colors.config.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// 颜色配置文件 - 集中管理所有UI颜色配置
|
||||||
|
|
||||||
|
// 主颜色配置对象
|
||||||
|
const COLOR_CONFIG = {
|
||||||
|
// 主色调
|
||||||
|
primary: '#1890ff',
|
||||||
|
success: '#52c41a',
|
||||||
|
warning: '#fa8c16',
|
||||||
|
error: '#f5222d',
|
||||||
|
purple: '#722ed1',
|
||||||
|
cyan: '#13c2c2',
|
||||||
|
teal: '#36cfc9',
|
||||||
|
|
||||||
|
// 统计卡片颜色配置
|
||||||
|
statCardColors: [
|
||||||
|
'#1890ff', // blue
|
||||||
|
'#52c41a', // green
|
||||||
|
'#fa8c16', // orange
|
||||||
|
'#f5222d', // red
|
||||||
|
'#722ed1', // purple
|
||||||
|
'#13c2c2' // cyan
|
||||||
|
],
|
||||||
|
|
||||||
|
// 颜色代码到CSS类的映射
|
||||||
|
colorClassMap: {
|
||||||
|
'#1890ff': 'blue',
|
||||||
|
'#52c41a': 'green',
|
||||||
|
'#fa8c16': 'orange',
|
||||||
|
'#f5222d': 'red',
|
||||||
|
'#722ed1': 'purple',
|
||||||
|
'#13c2c2': 'cyan',
|
||||||
|
'#36cfc9': 'teal'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取颜色对应的CSS类名
|
||||||
|
getColorClassName: function(colorCode) {
|
||||||
|
return this.colorClassMap[colorCode] || 'blue';
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取统计卡片的颜色
|
||||||
|
getStatCardColor: function(index) {
|
||||||
|
const colors = this.statCardColors;
|
||||||
|
return colors[index % colors.length];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导出配置对象
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = COLOR_CONFIG;
|
||||||
|
} else {
|
||||||
|
// 浏览器环境
|
||||||
|
window.COLOR_CONFIG = COLOR_CONFIG;
|
||||||
|
}
|
||||||
284
static/js/config.js
Normal file
284
static/js/config.js
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
// 配置管理页面功能实现
|
||||||
|
|
||||||
|
// 工具函数:安全获取DOM元素
|
||||||
|
function getElement(id) {
|
||||||
|
const element = document.getElementById(id);
|
||||||
|
if (!element) {
|
||||||
|
console.warn(`Element with id "${id}" not found`);
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具函数:验证端口号
|
||||||
|
function validatePort(port) {
|
||||||
|
// 确保port是字符串类型
|
||||||
|
var portStr = port;
|
||||||
|
if (port === null || port === undefined || typeof port !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 去除前后空白并验证是否为纯数字
|
||||||
|
portStr = port.trim();
|
||||||
|
if (!/^\d+$/.test(portStr)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = parseInt(portStr, 10);
|
||||||
|
return num >= 1 && num <= 65535 ? num : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化配置管理页面
|
||||||
|
function initConfigPage() {
|
||||||
|
loadConfig();
|
||||||
|
setupConfigEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载系统配置
|
||||||
|
async function loadConfig() {
|
||||||
|
try {
|
||||||
|
const result = await api.getConfig();
|
||||||
|
|
||||||
|
// 检查API返回的错误
|
||||||
|
if (result && result.error) {
|
||||||
|
showErrorMessage('加载配置失败: ' + result.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
populateConfigForm(result);
|
||||||
|
} catch (error) {
|
||||||
|
// 捕获可能的异常(虽然apiRequest不应该再抛出异常)
|
||||||
|
showErrorMessage('加载配置失败: ' + (error.message || '未知错误'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填充配置表单
|
||||||
|
function populateConfigForm(config) {
|
||||||
|
// 安全获取配置对象,防止未定义属性访问
|
||||||
|
const dnsServerConfig = config.DNSServer || {};
|
||||||
|
const httpServerConfig = config.HTTPServer || {};
|
||||||
|
const shieldConfig = config.Shield || {};
|
||||||
|
|
||||||
|
// DNS配置 - 使用函数安全设置值,避免 || 操作符可能的错误处理
|
||||||
|
setElementValue('dns-port', getSafeValue(dnsServerConfig.Port, 53));
|
||||||
|
setElementValue('dns-upstream-servers', getSafeArray(dnsServerConfig.UpstreamServers).join(', '));
|
||||||
|
setElementValue('dns-timeout', getSafeValue(dnsServerConfig.Timeout, 5));
|
||||||
|
setElementValue('dns-stats-file', getSafeValue(dnsServerConfig.StatsFile, 'data/stats.json'));
|
||||||
|
setElementValue('dns-save-interval', getSafeValue(dnsServerConfig.SaveInterval, 300));
|
||||||
|
|
||||||
|
// HTTP配置
|
||||||
|
setElementValue('http-port', getSafeValue(httpServerConfig.Port, 8080));
|
||||||
|
|
||||||
|
// 屏蔽配置
|
||||||
|
setElementValue('shield-local-rules-file', getSafeValue(shieldConfig.LocalRulesFile, 'data/rules.txt'));
|
||||||
|
setElementValue('shield-update-interval', getSafeValue(shieldConfig.UpdateInterval, 3600));
|
||||||
|
setElementValue('shield-hosts-file', getSafeValue(shieldConfig.HostsFile, 'data/hosts.txt'));
|
||||||
|
// 使用服务器端接受的屏蔽方法值,默认使用NXDOMAIN
|
||||||
|
setElementValue('shield-block-method', getSafeValue(shieldConfig.BlockMethod, 'NXDOMAIN'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具函数:安全设置元素值
|
||||||
|
function setElementValue(elementId, value) {
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
if (element && element.tagName === 'INPUT') {
|
||||||
|
element.value = value;
|
||||||
|
} else if (!element) {
|
||||||
|
console.warn(`Element with id "${elementId}" not found for setting value: ${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具函数:安全获取值,如果未定义或为null则返回默认值
|
||||||
|
function getSafeValue(value, defaultValue) {
|
||||||
|
// 更严格的检查,避免0、空字符串等被默认值替换
|
||||||
|
return value === undefined || value === null ? defaultValue : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具函数:安全获取数组,如果不是数组则返回空数组
|
||||||
|
function getSafeArray(value) {
|
||||||
|
return Array.isArray(value) ? value : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存配置
|
||||||
|
async function handleSaveConfig() {
|
||||||
|
const formData = collectFormData();
|
||||||
|
if (!formData) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await api.saveConfig(formData);
|
||||||
|
|
||||||
|
// 检查API返回的错误
|
||||||
|
if (result && result.error) {
|
||||||
|
showErrorMessage('保存配置失败: ' + result.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccessMessage('配置保存成功');
|
||||||
|
} catch (error) {
|
||||||
|
// 捕获可能的异常(虽然apiRequest不应该再抛出异常)
|
||||||
|
showErrorMessage('保存配置失败: ' + (error.message || '未知错误'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重启服务
|
||||||
|
async function handleRestartService() {
|
||||||
|
if (!confirm('确定要重启DNS服务吗?重启期间服务可能会短暂不可用。')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await api.restartService();
|
||||||
|
|
||||||
|
// 检查API返回的错误
|
||||||
|
if (result && result.error) {
|
||||||
|
showErrorMessage('服务重启失败: ' + result.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccessMessage('服务重启成功');
|
||||||
|
} catch (error) {
|
||||||
|
// 捕获可能的异常(虽然apiRequest不应该再抛出异常)
|
||||||
|
showErrorMessage('重启服务失败: ' + (error.message || '未知错误'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集表单数据并验证
|
||||||
|
function collectFormData() {
|
||||||
|
// 验证端口号 - 使用安全获取元素值的函数
|
||||||
|
const dnsPortValue = getElementValue('dns-port');
|
||||||
|
const httpPortValue = getElementValue('http-port');
|
||||||
|
|
||||||
|
const dnsPort = validatePort(dnsPortValue);
|
||||||
|
const httpPort = validatePort(httpPortValue);
|
||||||
|
|
||||||
|
if (!dnsPort) {
|
||||||
|
showErrorMessage('DNS端口号无效(必须是1-65535之间的整数)');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!httpPort) {
|
||||||
|
showErrorMessage('HTTP端口号无效(必须是1-65535之间的整数)');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安全获取上游服务器列表
|
||||||
|
const upstreamServersText = getElementValue('dns-upstream-servers');
|
||||||
|
const upstreamServers = upstreamServersText ?
|
||||||
|
upstreamServersText.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s !== ''; }) :
|
||||||
|
[];
|
||||||
|
|
||||||
|
// 安全获取并转换整数值
|
||||||
|
const timeoutValue = getElementValue('dns-timeout');
|
||||||
|
const timeout = timeoutValue ? parseInt(timeoutValue, 10) : 5;
|
||||||
|
|
||||||
|
const saveIntervalValue = getElementValue('dns-save-interval');
|
||||||
|
const saveInterval = saveIntervalValue ? parseInt(saveIntervalValue, 10) : 300;
|
||||||
|
|
||||||
|
const updateIntervalValue = getElementValue('shield-update-interval');
|
||||||
|
const updateInterval = updateIntervalValue ? parseInt(updateIntervalValue, 10) : 3600;
|
||||||
|
|
||||||
|
return {
|
||||||
|
DNSServer: {
|
||||||
|
Port: dnsPort,
|
||||||
|
UpstreamServers: upstreamServers,
|
||||||
|
Timeout: timeout,
|
||||||
|
StatsFile: getElementValue('dns-stats-file') || './data/stats.json',
|
||||||
|
SaveInterval: saveInterval
|
||||||
|
},
|
||||||
|
HTTPServer: {
|
||||||
|
Port: httpPort
|
||||||
|
},
|
||||||
|
Shield: {
|
||||||
|
LocalRulesFile: getElementValue('shield-local-rules-file') || './data/rules.txt',
|
||||||
|
UpdateInterval: updateInterval,
|
||||||
|
HostsFile: getElementValue('shield-hosts-file') || './data/hosts.txt',
|
||||||
|
BlockMethod: getElementValue('shield-block-method') || 'NXDOMAIN'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具函数:安全获取元素值
|
||||||
|
function getElementValue(elementId) {
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
if (element && element.tagName === 'INPUT') {
|
||||||
|
return element.value;
|
||||||
|
}
|
||||||
|
return ''; // 默认返回空字符串
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置事件监听器
|
||||||
|
function setupConfigEventListeners() {
|
||||||
|
// 保存配置按钮
|
||||||
|
getElement('save-config-btn')?.addEventListener('click', handleSaveConfig);
|
||||||
|
|
||||||
|
// 重启服务按钮
|
||||||
|
getElement('restart-service-btn')?.addEventListener('click', handleRestartService);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 显示成功消息
|
||||||
|
function showSuccessMessage(message) {
|
||||||
|
showNotification(message, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示错误消息
|
||||||
|
function showErrorMessage(message) {
|
||||||
|
showNotification(message, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示通知
|
||||||
|
function showNotification(message, type = 'info') {
|
||||||
|
// 移除现有通知
|
||||||
|
const existingNotification = document.querySelector('.notification');
|
||||||
|
if (existingNotification) {
|
||||||
|
existingNotification.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新通知
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `notification fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform transition-transform duration-300 ease-in-out translate-y-0 opacity-0`;
|
||||||
|
|
||||||
|
// 设置通知样式(兼容Tailwind和原生CSS)
|
||||||
|
notification.style.cssText += `
|
||||||
|
position: fixed;
|
||||||
|
bottom: 16px;
|
||||||
|
right: 16px;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||||
|
z-index: 1000;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
opacity: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (type === 'success') {
|
||||||
|
notification.style.backgroundColor = '#10b981';
|
||||||
|
notification.style.color = 'white';
|
||||||
|
} else if (type === 'error') {
|
||||||
|
notification.style.backgroundColor = '#ef4444';
|
||||||
|
notification.style.color = 'white';
|
||||||
|
} else {
|
||||||
|
notification.style.backgroundColor = '#3b82f6';
|
||||||
|
notification.style.color = 'white';
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.textContent = message;
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
// 显示通知
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.opacity = '1';
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
// 3秒后隐藏通知
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.opacity = '0';
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.remove();
|
||||||
|
}, 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载完成后初始化
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initConfigPage);
|
||||||
|
} else {
|
||||||
|
initConfigPage();
|
||||||
|
}
|
||||||
3048
static/js/dashboard.js
Normal file
3048
static/js/dashboard.js
Normal file
File diff suppressed because it is too large
Load Diff
202
static/js/hosts.js
Normal file
202
static/js/hosts.js
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
// Hosts管理页面功能实现
|
||||||
|
|
||||||
|
// 初始化Hosts管理页面
|
||||||
|
function initHostsPage() {
|
||||||
|
// 加载Hosts规则
|
||||||
|
loadHostsRules();
|
||||||
|
// 设置事件监听器
|
||||||
|
setupHostsEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载Hosts规则
|
||||||
|
async function loadHostsRules() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/shield/hosts');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to load hosts rules');
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// 处理API返回的数据格式
|
||||||
|
let hostsRules = [];
|
||||||
|
if (data && Array.isArray(data)) {
|
||||||
|
// 直接是数组格式
|
||||||
|
hostsRules = data;
|
||||||
|
} else if (data && data.hosts) {
|
||||||
|
// 包含在hosts字段中
|
||||||
|
hostsRules = data.hosts;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateHostsTable(hostsRules);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading hosts rules:', error);
|
||||||
|
showErrorMessage('加载Hosts规则失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新Hosts表格
|
||||||
|
function updateHostsTable(hostsRules) {
|
||||||
|
const tbody = document.getElementById('hosts-table-body');
|
||||||
|
|
||||||
|
if (hostsRules.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="3" class="py-4 text-center text-gray-500">暂无Hosts条目</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.innerHTML = hostsRules.map(rule => {
|
||||||
|
// 处理对象格式的规则
|
||||||
|
const ip = rule.ip || '';
|
||||||
|
const domain = rule.domain || '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr class="border-b border-gray-200">
|
||||||
|
<td class="py-3 px-4">${ip}</td>
|
||||||
|
<td class="py-3 px-4">${domain}</td>
|
||||||
|
<td class="py-3 px-4 text-right">
|
||||||
|
<button class="delete-hosts-btn px-3 py-1 bg-danger text-white rounded-md hover:bg-danger/90 transition-colors text-sm" data-ip="${ip}" data-domain="${domain}">
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
// 重新绑定删除事件
|
||||||
|
document.querySelectorAll('.delete-hosts-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', handleDeleteHostsRule);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置事件监听器
|
||||||
|
function setupHostsEventListeners() {
|
||||||
|
// 保存Hosts按钮
|
||||||
|
document.getElementById('save-hosts-btn').addEventListener('click', handleAddHostsRule);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理添加Hosts规则
|
||||||
|
async function handleAddHostsRule() {
|
||||||
|
const ip = document.getElementById('hosts-ip').value.trim();
|
||||||
|
const domain = document.getElementById('hosts-domain').value.trim();
|
||||||
|
|
||||||
|
if (!ip || !domain) {
|
||||||
|
showErrorMessage('IP地址和域名不能为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/shield/hosts', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ ip, domain })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to add hosts rule');
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccessMessage('Hosts规则添加成功');
|
||||||
|
|
||||||
|
// 清空输入框
|
||||||
|
document.getElementById('hosts-ip').value = '';
|
||||||
|
document.getElementById('hosts-domain').value = '';
|
||||||
|
|
||||||
|
// 重新加载规则
|
||||||
|
loadHostsRules();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding hosts rule:', error);
|
||||||
|
showErrorMessage('添加Hosts规则失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理删除Hosts规则
|
||||||
|
async function handleDeleteHostsRule(e) {
|
||||||
|
const ip = e.target.closest('.delete-hosts-btn').dataset.ip;
|
||||||
|
const domain = e.target.closest('.delete-hosts-btn').dataset.domain;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/shield/hosts', {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ domain })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to delete hosts rule');
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccessMessage('Hosts规则删除成功');
|
||||||
|
|
||||||
|
// 重新加载规则
|
||||||
|
loadHostsRules();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting hosts rule:', error);
|
||||||
|
showErrorMessage('删除Hosts规则失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示成功消息
|
||||||
|
function showSuccessMessage(message) {
|
||||||
|
showNotification(message, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示错误消息
|
||||||
|
function showErrorMessage(message) {
|
||||||
|
showNotification(message, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 显示通知
|
||||||
|
function showNotification(message, type = 'info') {
|
||||||
|
// 移除现有通知
|
||||||
|
const existingNotification = document.querySelector('.notification');
|
||||||
|
if (existingNotification) {
|
||||||
|
existingNotification.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新通知
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `notification fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform transition-all duration-300 ease-in-out translate-y-0 opacity-0`;
|
||||||
|
|
||||||
|
// 设置通知样式
|
||||||
|
if (type === 'success') {
|
||||||
|
notification.classList.add('bg-green-500', 'text-white');
|
||||||
|
} else if (type === 'error') {
|
||||||
|
notification.classList.add('bg-red-500', 'text-white');
|
||||||
|
} else {
|
||||||
|
notification.classList.add('bg-blue-500', 'text-white');
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<i class="fa fa-${type === 'success' ? 'check' : type === 'error' ? 'exclamation' : 'info'}"></i>
|
||||||
|
<span>${message}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
// 显示通知
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.remove('opacity-0');
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// 3秒后隐藏通知
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.add('opacity-0');
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.remove();
|
||||||
|
}, 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载完成后初始化
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initHostsPage);
|
||||||
|
} else {
|
||||||
|
initHostsPage();
|
||||||
|
}
|
||||||
623
static/js/logs.js
Normal file
623
static/js/logs.js
Normal file
@@ -0,0 +1,623 @@
|
|||||||
|
// logs.js - 查询日志页面功能
|
||||||
|
|
||||||
|
// 全局变量
|
||||||
|
let currentPage = 1;
|
||||||
|
let totalPages = 1;
|
||||||
|
let logsPerPage = 30; // 默认显示30条记录
|
||||||
|
let currentFilter = '';
|
||||||
|
let currentSearch = '';
|
||||||
|
let logsChart = null;
|
||||||
|
let currentSortField = '';
|
||||||
|
let currentSortDirection = 'desc'; // 默认降序
|
||||||
|
|
||||||
|
// IP地理位置缓存
|
||||||
|
let ipGeolocationCache = {};
|
||||||
|
const GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期24小时
|
||||||
|
|
||||||
|
// WebSocket连接和重连计时器
|
||||||
|
let logsWsConnection = null;
|
||||||
|
let logsWsReconnectTimer = null;
|
||||||
|
|
||||||
|
// 初始化查询日志页面
|
||||||
|
function initLogsPage() {
|
||||||
|
console.log('初始化查询日志页面');
|
||||||
|
|
||||||
|
// 加载日志统计数据
|
||||||
|
loadLogsStats();
|
||||||
|
|
||||||
|
// 加载日志详情
|
||||||
|
loadLogs();
|
||||||
|
|
||||||
|
// 初始化图表
|
||||||
|
initLogsChart();
|
||||||
|
|
||||||
|
// 绑定事件
|
||||||
|
bindLogsEvents();
|
||||||
|
|
||||||
|
// 建立WebSocket连接,用于实时更新统计数据和图表
|
||||||
|
connectLogsWebSocket();
|
||||||
|
|
||||||
|
// 在页面卸载时清理资源
|
||||||
|
window.addEventListener('beforeunload', cleanupLogsResources);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理资源
|
||||||
|
function cleanupLogsResources() {
|
||||||
|
// 清除WebSocket连接
|
||||||
|
if (logsWsConnection) {
|
||||||
|
logsWsConnection.close();
|
||||||
|
logsWsConnection = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除重连计时器
|
||||||
|
if (logsWsReconnectTimer) {
|
||||||
|
clearTimeout(logsWsReconnectTimer);
|
||||||
|
logsWsReconnectTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定事件
|
||||||
|
function bindLogsEvents() {
|
||||||
|
// 搜索按钮
|
||||||
|
const searchBtn = document.getElementById('logs-search-btn');
|
||||||
|
if (searchBtn) {
|
||||||
|
searchBtn.addEventListener('click', () => {
|
||||||
|
currentSearch = document.getElementById('logs-search').value;
|
||||||
|
currentPage = 1;
|
||||||
|
loadLogs();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索框回车事件
|
||||||
|
const searchInput = document.getElementById('logs-search');
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
currentSearch = searchInput.value;
|
||||||
|
currentPage = 1;
|
||||||
|
loadLogs();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 结果过滤
|
||||||
|
const resultFilter = document.getElementById('logs-result-filter');
|
||||||
|
if (resultFilter) {
|
||||||
|
resultFilter.addEventListener('change', () => {
|
||||||
|
currentFilter = resultFilter.value;
|
||||||
|
currentPage = 1;
|
||||||
|
loadLogs();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义记录数量
|
||||||
|
const perPageSelect = document.getElementById('logs-per-page');
|
||||||
|
if (perPageSelect) {
|
||||||
|
perPageSelect.addEventListener('change', () => {
|
||||||
|
logsPerPage = parseInt(perPageSelect.value);
|
||||||
|
currentPage = 1;
|
||||||
|
loadLogs();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页按钮
|
||||||
|
const prevBtn = document.getElementById('logs-prev-page');
|
||||||
|
const nextBtn = document.getElementById('logs-next-page');
|
||||||
|
|
||||||
|
if (prevBtn) {
|
||||||
|
prevBtn.addEventListener('click', () => {
|
||||||
|
if (currentPage > 1) {
|
||||||
|
currentPage--;
|
||||||
|
loadLogs();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextBtn) {
|
||||||
|
nextBtn.addEventListener('click', () => {
|
||||||
|
if (currentPage < totalPages) {
|
||||||
|
currentPage++;
|
||||||
|
loadLogs();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间范围切换
|
||||||
|
const timeRangeBtns = document.querySelectorAll('.time-range-btn');
|
||||||
|
timeRangeBtns.forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
// 更新按钮样式
|
||||||
|
timeRangeBtns.forEach(b => {
|
||||||
|
b.classList.remove('bg-primary', 'text-white');
|
||||||
|
b.classList.add('bg-gray-200', 'text-gray-700');
|
||||||
|
});
|
||||||
|
btn.classList.remove('bg-gray-200', 'text-gray-700');
|
||||||
|
btn.classList.add('bg-primary', 'text-white');
|
||||||
|
|
||||||
|
// 更新图表
|
||||||
|
const range = btn.getAttribute('data-range');
|
||||||
|
updateLogsChart(range);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 刷新按钮事件
|
||||||
|
const refreshBtn = document.getElementById('logs-refresh-btn');
|
||||||
|
if (refreshBtn) {
|
||||||
|
refreshBtn.addEventListener('click', () => {
|
||||||
|
// 重新加载日志
|
||||||
|
currentPage = 1;
|
||||||
|
loadLogs();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排序按钮事件
|
||||||
|
const sortHeaders = document.querySelectorAll('th[data-sort]');
|
||||||
|
sortHeaders.forEach(header => {
|
||||||
|
header.addEventListener('click', () => {
|
||||||
|
const sortField = header.getAttribute('data-sort');
|
||||||
|
|
||||||
|
// 如果点击的是当前排序字段,则切换排序方向
|
||||||
|
if (sortField === currentSortField) {
|
||||||
|
currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
|
||||||
|
} else {
|
||||||
|
// 否则,设置新的排序字段,默认降序
|
||||||
|
currentSortField = sortField;
|
||||||
|
currentSortDirection = 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新排序图标
|
||||||
|
updateSortIcons();
|
||||||
|
|
||||||
|
// 重新加载日志
|
||||||
|
currentPage = 1;
|
||||||
|
loadLogs();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新排序图标
|
||||||
|
function updateSortIcons() {
|
||||||
|
const sortHeaders = document.querySelectorAll('th[data-sort]');
|
||||||
|
sortHeaders.forEach(header => {
|
||||||
|
const sortField = header.getAttribute('data-sort');
|
||||||
|
const icon = header.querySelector('i');
|
||||||
|
|
||||||
|
// 重置所有图标
|
||||||
|
icon.className = 'fa fa-sort ml-1 text-xs';
|
||||||
|
|
||||||
|
// 设置当前排序字段的图标
|
||||||
|
if (sortField === currentSortField) {
|
||||||
|
if (currentSortDirection === 'asc') {
|
||||||
|
icon.className = 'fa fa-sort-asc ml-1 text-xs';
|
||||||
|
} else {
|
||||||
|
icon.className = 'fa fa-sort-desc ml-1 text-xs';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载日志统计数据
|
||||||
|
function loadLogsStats() {
|
||||||
|
// 使用封装的apiRequest函数进行API调用
|
||||||
|
apiRequest('/logs/stats')
|
||||||
|
.then(data => {
|
||||||
|
if (data && data.error) {
|
||||||
|
console.error('加载日志统计数据失败:', data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新统计卡片
|
||||||
|
document.getElementById('logs-total-queries').textContent = data.totalQueries;
|
||||||
|
document.getElementById('logs-avg-response-time').textContent = data.avgResponseTime.toFixed(2) + 'ms';
|
||||||
|
document.getElementById('logs-active-ips').textContent = data.activeIPs;
|
||||||
|
|
||||||
|
// 计算屏蔽率
|
||||||
|
const blockRate = data.totalQueries > 0 ? (data.blockedQueries / data.totalQueries * 100).toFixed(1) : '0';
|
||||||
|
document.getElementById('logs-block-rate').textContent = blockRate + '%';
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('加载日志统计数据失败:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载日志详情
|
||||||
|
function loadLogs() {
|
||||||
|
// 显示加载状态
|
||||||
|
const loadingEl = document.getElementById('logs-loading');
|
||||||
|
if (loadingEl) {
|
||||||
|
loadingEl.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建请求URL
|
||||||
|
let endpoint = `/logs/query?limit=${logsPerPage}&offset=${(currentPage - 1) * logsPerPage}`;
|
||||||
|
|
||||||
|
// 添加过滤条件
|
||||||
|
if (currentFilter) {
|
||||||
|
endpoint += `&result=${currentFilter}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加搜索条件
|
||||||
|
if (currentSearch) {
|
||||||
|
endpoint += `&search=${encodeURIComponent(currentSearch)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加排序条件
|
||||||
|
if (currentSortField) {
|
||||||
|
endpoint += `&sort=${currentSortField}&direction=${currentSortDirection}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用封装的apiRequest函数进行API调用
|
||||||
|
apiRequest(endpoint)
|
||||||
|
.then(data => {
|
||||||
|
if (data && data.error) {
|
||||||
|
console.error('加载日志详情失败:', data.error);
|
||||||
|
// 隐藏加载状态
|
||||||
|
if (loadingEl) {
|
||||||
|
loadingEl.classList.add('hidden');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载日志总数
|
||||||
|
return apiRequest('/logs/count').then(countData => {
|
||||||
|
return { logs: data, count: countData.count };
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(result => {
|
||||||
|
if (!result || !result.logs) {
|
||||||
|
console.error('加载日志详情失败: 无效的响应数据');
|
||||||
|
// 隐藏加载状态
|
||||||
|
if (loadingEl) {
|
||||||
|
loadingEl.classList.add('hidden');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const logs = result.logs;
|
||||||
|
const totalLogs = result.count;
|
||||||
|
|
||||||
|
// 计算总页数
|
||||||
|
totalPages = Math.ceil(totalLogs / logsPerPage);
|
||||||
|
|
||||||
|
// 更新日志表格
|
||||||
|
updateLogsTable(logs);
|
||||||
|
|
||||||
|
// 更新分页信息
|
||||||
|
updateLogsPagination();
|
||||||
|
|
||||||
|
// 隐藏加载状态
|
||||||
|
if (loadingEl) {
|
||||||
|
loadingEl.classList.add('hidden');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('加载日志详情失败:', error);
|
||||||
|
|
||||||
|
// 隐藏加载状态
|
||||||
|
if (loadingEl) {
|
||||||
|
loadingEl.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新日志表格
|
||||||
|
function updateLogsTable(logs) {
|
||||||
|
const tableBody = document.getElementById('logs-table-body');
|
||||||
|
if (!tableBody) return;
|
||||||
|
|
||||||
|
// 清空表格
|
||||||
|
tableBody.innerHTML = '';
|
||||||
|
|
||||||
|
if (logs.length === 0) {
|
||||||
|
// 显示空状态
|
||||||
|
const emptyRow = document.createElement('tr');
|
||||||
|
emptyRow.innerHTML = `
|
||||||
|
<td colspan="5" class="py-8 text-center text-gray-500 border-b border-gray-100">
|
||||||
|
<i class="fa fa-file-text-o text-4xl mb-2 text-gray-300"></i>
|
||||||
|
<div>暂无查询日志</div>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
tableBody.appendChild(emptyRow);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填充表格
|
||||||
|
logs.forEach(log => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.className = 'border-b border-gray-100 hover:bg-gray-50 transition-colors';
|
||||||
|
|
||||||
|
// 格式化时间 - 两行显示,第一行显示时间,第二行显示日期
|
||||||
|
const time = new Date(log.Timestamp);
|
||||||
|
const formattedDate = time.toLocaleDateString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit'
|
||||||
|
});
|
||||||
|
const formattedTime = time.toLocaleTimeString('zh-CN', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 根据结果添加不同的背景色
|
||||||
|
let rowClass = '';
|
||||||
|
switch (log.Result) {
|
||||||
|
case 'blocked':
|
||||||
|
rowClass = 'bg-red-50'; // 淡红色填充
|
||||||
|
break;
|
||||||
|
case 'allowed':
|
||||||
|
// 检查是否是规则允许项目
|
||||||
|
if (log.BlockRule && log.BlockRule.includes('allow')) {
|
||||||
|
rowClass = 'bg-green-50'; // 规则允许项目用淡绿色填充
|
||||||
|
} else {
|
||||||
|
rowClass = ''; // 允许的不填充
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
rowClass = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加行背景色
|
||||||
|
if (rowClass) {
|
||||||
|
row.classList.add(rowClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加被屏蔽或允许显示,并增加颜色
|
||||||
|
let statusText = '';
|
||||||
|
let statusClass = '';
|
||||||
|
switch (log.Result) {
|
||||||
|
case 'blocked':
|
||||||
|
statusText = '被屏蔽';
|
||||||
|
statusClass = 'text-danger';
|
||||||
|
break;
|
||||||
|
case 'allowed':
|
||||||
|
statusText = '允许';
|
||||||
|
statusClass = 'text-success';
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
statusText = '错误';
|
||||||
|
statusClass = 'text-warning';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
statusText = '';
|
||||||
|
statusClass = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建行内容 - 两行显示,时间列显示时间和日期,请求列显示域名和类型状态
|
||||||
|
// 添加缓存状态显示
|
||||||
|
const cacheStatusClass = log.FromCache ? 'text-primary' : 'text-gray-500';
|
||||||
|
const cacheStatusText = log.FromCache ? '缓存' : '非缓存';
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td class="py-3 px-4">
|
||||||
|
<div class="text-sm font-medium">${formattedTime}</div>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">${formattedDate}</div>
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-4 text-sm">
|
||||||
|
<div class="font-medium">${log.ClientIP}</div>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">${log.Location || '未知 未知'}</div>
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-4 text-sm">
|
||||||
|
<div class="font-medium">${log.Domain}</div>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">类型: ${log.QueryType}, <span class="${statusClass}">${statusText}</span>, <span class="${cacheStatusClass}">${log.FromCache ? '缓存' : '实时'}</span></div>
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-4 text-sm">${log.ResponseTime}ms</td>
|
||||||
|
<td class="py-3 px-4 text-sm text-gray-500">${log.BlockRule || '-'}</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
tableBody.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新分页信息
|
||||||
|
function updateLogsPagination() {
|
||||||
|
// 更新页码显示
|
||||||
|
document.getElementById('logs-current-page').textContent = currentPage;
|
||||||
|
document.getElementById('logs-total-pages').textContent = totalPages;
|
||||||
|
|
||||||
|
// 更新按钮状态
|
||||||
|
const prevBtn = document.getElementById('logs-prev-page');
|
||||||
|
const nextBtn = document.getElementById('logs-next-page');
|
||||||
|
|
||||||
|
if (prevBtn) {
|
||||||
|
prevBtn.disabled = currentPage === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextBtn) {
|
||||||
|
nextBtn.disabled = currentPage === totalPages;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化日志图表
|
||||||
|
function initLogsChart() {
|
||||||
|
const ctx = document.getElementById('logs-trend-chart');
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
// 获取24小时统计数据
|
||||||
|
apiRequest('/hourly-stats')
|
||||||
|
.then(data => {
|
||||||
|
if (data && data.error) {
|
||||||
|
console.error('初始化日志图表失败:', data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建图表
|
||||||
|
logsChart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: data.labels,
|
||||||
|
datasets: [{
|
||||||
|
label: '查询数',
|
||||||
|
data: data.data,
|
||||||
|
borderColor: '#3b82f6',
|
||||||
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||||
|
tension: 0.4,
|
||||||
|
fill: true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'top'
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: {
|
||||||
|
precision: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('初始化日志图表失败:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新日志图表
|
||||||
|
function updateLogsChart(range) {
|
||||||
|
if (!logsChart) return;
|
||||||
|
|
||||||
|
let endpoint = '';
|
||||||
|
switch (range) {
|
||||||
|
case '24h':
|
||||||
|
endpoint = '/hourly-stats';
|
||||||
|
break;
|
||||||
|
case '7d':
|
||||||
|
endpoint = '/daily-stats';
|
||||||
|
break;
|
||||||
|
case '30d':
|
||||||
|
endpoint = '/monthly-stats';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
endpoint = '/hourly-stats';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用封装的apiRequest函数进行API调用
|
||||||
|
apiRequest(endpoint)
|
||||||
|
.then(data => {
|
||||||
|
if (data && data.error) {
|
||||||
|
console.error('更新日志图表失败:', data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新图表数据
|
||||||
|
logsChart.data.labels = data.labels;
|
||||||
|
logsChart.data.datasets[0].data = data.data;
|
||||||
|
logsChart.update();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('更新日志图表失败:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 建立WebSocket连接
|
||||||
|
function connectLogsWebSocket() {
|
||||||
|
try {
|
||||||
|
// 构建WebSocket URL
|
||||||
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
const wsUrl = `${wsProtocol}//${window.location.host}/ws/stats`;
|
||||||
|
|
||||||
|
console.log('正在连接WebSocket:', wsUrl);
|
||||||
|
|
||||||
|
// 创建WebSocket连接
|
||||||
|
logsWsConnection = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
// 连接打开事件
|
||||||
|
logsWsConnection.onopen = function() {
|
||||||
|
console.log('WebSocket连接已建立');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 接收消息事件
|
||||||
|
logsWsConnection.onmessage = function(event) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (data.type === 'initial_data' || data.type === 'stats_update') {
|
||||||
|
console.log('收到实时数据更新');
|
||||||
|
// 只更新统计数据,不更新日志详情
|
||||||
|
updateLogsStatsFromWebSocket(data.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('处理WebSocket消息失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 连接关闭事件
|
||||||
|
logsWsConnection.onclose = function(event) {
|
||||||
|
console.warn('WebSocket连接已关闭,代码:', event.code);
|
||||||
|
logsWsConnection = null;
|
||||||
|
|
||||||
|
// 设置重连
|
||||||
|
setupLogsReconnect();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 连接错误事件
|
||||||
|
logsWsConnection.onerror = function(error) {
|
||||||
|
console.error('WebSocket连接错误:', error);
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建WebSocket连接失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置重连逻辑
|
||||||
|
function setupLogsReconnect() {
|
||||||
|
if (logsWsReconnectTimer) {
|
||||||
|
return; // 已经有重连计时器在运行
|
||||||
|
}
|
||||||
|
|
||||||
|
const reconnectDelay = 5000; // 5秒后重连
|
||||||
|
console.log(`将在${reconnectDelay}ms后尝试重新连接WebSocket`);
|
||||||
|
|
||||||
|
logsWsReconnectTimer = setTimeout(() => {
|
||||||
|
connectLogsWebSocket();
|
||||||
|
}, reconnectDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从WebSocket更新日志统计数据
|
||||||
|
function updateLogsStatsFromWebSocket(stats) {
|
||||||
|
try {
|
||||||
|
// 更新统计卡片
|
||||||
|
if (stats.dns) {
|
||||||
|
// 适配不同的数据结构
|
||||||
|
const totalQueries = stats.dns.Queries || 0;
|
||||||
|
const blockedQueries = stats.dns.Blocked || 0;
|
||||||
|
const allowedQueries = stats.dns.Allowed || 0;
|
||||||
|
const errorQueries = stats.dns.Errors || 0;
|
||||||
|
const avgResponseTime = stats.dns.AvgResponseTime || 0;
|
||||||
|
const activeIPs = stats.activeIPs || Object.keys(stats.dns.SourceIPs || {}).length;
|
||||||
|
|
||||||
|
// 更新统计卡片
|
||||||
|
document.getElementById('logs-total-queries').textContent = totalQueries;
|
||||||
|
document.getElementById('logs-avg-response-time').textContent = avgResponseTime.toFixed(2) + 'ms';
|
||||||
|
document.getElementById('logs-active-ips').textContent = activeIPs;
|
||||||
|
|
||||||
|
// 计算屏蔽率
|
||||||
|
const blockRate = totalQueries > 0 ? (blockedQueries / totalQueries * 100).toFixed(1) : '0';
|
||||||
|
document.getElementById('logs-block-rate').textContent = blockRate + '%';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('从WebSocket更新日志统计数据失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定期更新日志统计数据(备用方案)
|
||||||
|
setInterval(() => {
|
||||||
|
// 只有在查询日志页面时才更新
|
||||||
|
if (window.location.hash === '#logs') {
|
||||||
|
loadLogsStats();
|
||||||
|
// 不自动更新日志详情,只更新统计数据
|
||||||
|
}
|
||||||
|
}, 30000); // 每30秒更新一次
|
||||||
405
static/js/main.js
Normal file
405
static/js/main.js
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
// main.js - 主脚本文件
|
||||||
|
|
||||||
|
// 页面导航功能
|
||||||
|
function setupNavigation() {
|
||||||
|
// 侧边栏菜单项
|
||||||
|
const menuItems = document.querySelectorAll('nav a');
|
||||||
|
const contentSections = [
|
||||||
|
document.getElementById('dashboard-content'),
|
||||||
|
document.getElementById('shield-content'),
|
||||||
|
document.getElementById('hosts-content'),
|
||||||
|
document.getElementById('query-content'),
|
||||||
|
document.getElementById('logs-content'),
|
||||||
|
document.getElementById('config-content')
|
||||||
|
];
|
||||||
|
const pageTitle = document.getElementById('page-title');
|
||||||
|
|
||||||
|
menuItems.forEach((item, index) => {
|
||||||
|
item.addEventListener('click', (e) => {
|
||||||
|
// 允许浏览器自动更新地址栏中的hash,不阻止默认行为
|
||||||
|
|
||||||
|
// 移动端点击菜单项后自动关闭侧边栏
|
||||||
|
if (window.innerWidth < 768) {
|
||||||
|
closeSidebar();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 移动端侧边栏切换
|
||||||
|
const toggleSidebar = document.getElementById('toggle-sidebar');
|
||||||
|
const closeSidebarBtn = document.getElementById('close-sidebar');
|
||||||
|
const sidebar = document.getElementById('sidebar');
|
||||||
|
const sidebarOverlay = document.getElementById('sidebar-overlay');
|
||||||
|
|
||||||
|
// 打开侧边栏函数
|
||||||
|
function openSidebar() {
|
||||||
|
console.log('Opening sidebar...');
|
||||||
|
if (sidebar) {
|
||||||
|
sidebar.classList.remove('-translate-x-full');
|
||||||
|
sidebar.classList.add('translate-x-0');
|
||||||
|
}
|
||||||
|
if (sidebarOverlay) {
|
||||||
|
sidebarOverlay.classList.remove('hidden');
|
||||||
|
sidebarOverlay.classList.add('block');
|
||||||
|
}
|
||||||
|
// 防止页面滚动
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
console.log('Sidebar opened successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭侧边栏函数
|
||||||
|
function closeSidebar() {
|
||||||
|
console.log('Closing sidebar...');
|
||||||
|
if (sidebar) {
|
||||||
|
sidebar.classList.add('-translate-x-full');
|
||||||
|
sidebar.classList.remove('translate-x-0');
|
||||||
|
}
|
||||||
|
if (sidebarOverlay) {
|
||||||
|
sidebarOverlay.classList.add('hidden');
|
||||||
|
sidebarOverlay.classList.remove('block');
|
||||||
|
}
|
||||||
|
// 恢复页面滚动
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
console.log('Sidebar closed successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换侧边栏函数
|
||||||
|
function toggleSidebarVisibility() {
|
||||||
|
console.log('Toggling sidebar visibility...');
|
||||||
|
console.log('Current sidebar classes:', sidebar ? sidebar.className : 'sidebar not found');
|
||||||
|
if (sidebar && sidebar.classList.contains('-translate-x-full')) {
|
||||||
|
console.log('Sidebar is hidden, opening...');
|
||||||
|
openSidebar();
|
||||||
|
} else {
|
||||||
|
console.log('Sidebar is visible, closing...');
|
||||||
|
closeSidebar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定切换按钮事件
|
||||||
|
if (toggleSidebar) {
|
||||||
|
toggleSidebar.addEventListener('click', toggleSidebarVisibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定关闭按钮事件
|
||||||
|
if (closeSidebarBtn) {
|
||||||
|
closeSidebarBtn.addEventListener('click', closeSidebar);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定遮罩层点击事件
|
||||||
|
if (sidebarOverlay) {
|
||||||
|
sidebarOverlay.addEventListener('click', closeSidebar);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动端点击菜单项后自动关闭侧边栏
|
||||||
|
menuItems.forEach(item => {
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
// 检查是否是移动设备视图
|
||||||
|
if (window.innerWidth < 768) {
|
||||||
|
closeSidebar();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加键盘事件监听,按ESC键关闭侧边栏
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
closeSidebar();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面初始化函数 - 根据当前hash值初始化对应页面
|
||||||
|
function initPageByHash() {
|
||||||
|
const hash = window.location.hash.substring(1);
|
||||||
|
|
||||||
|
// 隐藏所有内容区域
|
||||||
|
const contentSections = [
|
||||||
|
document.getElementById('dashboard-content'),
|
||||||
|
document.getElementById('shield-content'),
|
||||||
|
document.getElementById('hosts-content'),
|
||||||
|
document.getElementById('query-content'),
|
||||||
|
document.getElementById('logs-content'),
|
||||||
|
document.getElementById('config-content')
|
||||||
|
];
|
||||||
|
|
||||||
|
contentSections.forEach(section => {
|
||||||
|
if (section) {
|
||||||
|
section.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示当前页面内容
|
||||||
|
const currentSection = document.getElementById(`${hash}-content`);
|
||||||
|
if (currentSection) {
|
||||||
|
currentSection.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新页面标题
|
||||||
|
const pageTitle = document.getElementById('page-title');
|
||||||
|
if (pageTitle) {
|
||||||
|
const titles = {
|
||||||
|
'dashboard': '仪表盘',
|
||||||
|
'shield': '屏蔽管理',
|
||||||
|
'hosts': 'Hosts管理',
|
||||||
|
'query': 'DNS屏蔽查询',
|
||||||
|
'logs': '查询日志',
|
||||||
|
'config': '系统设置'
|
||||||
|
};
|
||||||
|
pageTitle.textContent = titles[hash] || '仪表盘';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面特定初始化 - 使用setTimeout延迟调用,确保所有脚本文件都已加载完成
|
||||||
|
if (hash === 'shield') {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (typeof initShieldPage === 'function') {
|
||||||
|
initShieldPage();
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
} else if (hash === 'hosts') {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (typeof initHostsPage === 'function') {
|
||||||
|
initHostsPage();
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
} else if (hash === 'logs') {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (typeof initLogsPage === 'function') {
|
||||||
|
initLogsPage();
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
} else if (hash === 'dashboard') {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (typeof loadDashboardData === 'function') {
|
||||||
|
loadDashboardData();
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化函数
|
||||||
|
function init() {
|
||||||
|
// 设置导航
|
||||||
|
setupNavigation();
|
||||||
|
|
||||||
|
// 初始化页面
|
||||||
|
initPageByHash();
|
||||||
|
|
||||||
|
// 添加hashchange事件监听,处理浏览器前进/后退按钮
|
||||||
|
window.addEventListener('hashchange', initPageByHash);
|
||||||
|
|
||||||
|
// 定期更新系统状态
|
||||||
|
setInterval(updateSystemStatus, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新系统状态
|
||||||
|
function updateSystemStatus() {
|
||||||
|
fetch('/api/status')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
const uptimeElement = document.getElementById('uptime');
|
||||||
|
if (uptimeElement) {
|
||||||
|
uptimeElement.textContent = `正常运行中 | ${formatUptime(data.uptime)}`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('更新系统状态失败:', error);
|
||||||
|
const uptimeElement = document.getElementById('uptime');
|
||||||
|
if (uptimeElement) {
|
||||||
|
uptimeElement.textContent = '连接异常';
|
||||||
|
uptimeElement.classList.add('text-danger');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化运行时间
|
||||||
|
function formatUptime(milliseconds) {
|
||||||
|
// 简化版的格式化,实际使用时需要根据API返回的数据格式调整
|
||||||
|
const seconds = Math.floor(milliseconds / 1000);
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
|
||||||
|
if (days > 0) {
|
||||||
|
return `${days}天${hours % 24}小时`;
|
||||||
|
} else if (hours > 0) {
|
||||||
|
return `${hours}小时${minutes % 60}分钟`;
|
||||||
|
} else if (minutes > 0) {
|
||||||
|
return `${minutes}分钟${seconds % 60}秒`;
|
||||||
|
} else {
|
||||||
|
return `${seconds}秒`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 账户功能 - 下拉菜单、注销和修改密码
|
||||||
|
function setupAccountFeatures() {
|
||||||
|
// 下拉菜单功能
|
||||||
|
const accountDropdown = document.getElementById('account-dropdown');
|
||||||
|
const accountMenu = document.getElementById('account-menu');
|
||||||
|
const changePasswordBtn = document.getElementById('change-password-btn');
|
||||||
|
const logoutBtn = document.getElementById('logout-btn');
|
||||||
|
const changePasswordModal = document.getElementById('change-password-modal');
|
||||||
|
const closeModalBtn = document.getElementById('close-modal-btn');
|
||||||
|
const cancelChangePasswordBtn = document.getElementById('cancel-change-password');
|
||||||
|
const changePasswordForm = document.getElementById('change-password-form');
|
||||||
|
const passwordMismatch = document.getElementById('password-mismatch');
|
||||||
|
const newPassword = document.getElementById('new-password');
|
||||||
|
const confirmPassword = document.getElementById('confirm-password');
|
||||||
|
|
||||||
|
// 点击外部关闭下拉菜单
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (accountDropdown && !accountDropdown.contains(e.target)) {
|
||||||
|
accountMenu.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 点击账户区域切换下拉菜单
|
||||||
|
if (accountDropdown) {
|
||||||
|
accountDropdown.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
accountMenu.classList.toggle('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开修改密码模态框
|
||||||
|
if (changePasswordBtn) {
|
||||||
|
changePasswordBtn.addEventListener('click', () => {
|
||||||
|
accountMenu.classList.add('hidden');
|
||||||
|
changePasswordModal.classList.remove('hidden');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭修改密码模态框
|
||||||
|
function closeModal() {
|
||||||
|
changePasswordModal.classList.add('hidden');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
changePasswordForm.reset();
|
||||||
|
passwordMismatch.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定关闭模态框事件
|
||||||
|
if (closeModalBtn) {
|
||||||
|
closeModalBtn.addEventListener('click', closeModal);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancelChangePasswordBtn) {
|
||||||
|
cancelChangePasswordBtn.addEventListener('click', closeModal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击模态框外部关闭模态框
|
||||||
|
if (changePasswordModal) {
|
||||||
|
changePasswordModal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === changePasswordModal) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按ESC键关闭模态框
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape' && !changePasswordModal.classList.contains('hidden')) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 密码匹配验证
|
||||||
|
if (newPassword && confirmPassword) {
|
||||||
|
confirmPassword.addEventListener('input', () => {
|
||||||
|
if (newPassword.value !== confirmPassword.value) {
|
||||||
|
passwordMismatch.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
passwordMismatch.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
newPassword.addEventListener('input', () => {
|
||||||
|
if (newPassword.value !== confirmPassword.value) {
|
||||||
|
passwordMismatch.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
passwordMismatch.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改密码表单提交
|
||||||
|
if (changePasswordForm) {
|
||||||
|
changePasswordForm.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// 验证密码匹配
|
||||||
|
if (newPassword.value !== confirmPassword.value) {
|
||||||
|
passwordMismatch.classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData(changePasswordForm);
|
||||||
|
const data = {
|
||||||
|
currentPassword: formData.get('currentPassword'),
|
||||||
|
newPassword: formData.get('newPassword')
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/change-password', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && result.status === 'success') {
|
||||||
|
// 密码修改成功
|
||||||
|
alert('密码修改成功');
|
||||||
|
closeModal();
|
||||||
|
} else {
|
||||||
|
// 密码修改失败
|
||||||
|
alert(result.error || '密码修改失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('修改密码失败:', error);
|
||||||
|
alert('修改密码失败,请稍后重试');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注销功能
|
||||||
|
if (logoutBtn) {
|
||||||
|
logoutBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
await fetch('/api/logout', {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 重定向到登录页面
|
||||||
|
window.location.href = '/login';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('注销失败:', error);
|
||||||
|
alert('注销失败,请稍后重试');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化函数
|
||||||
|
function init() {
|
||||||
|
// 设置导航
|
||||||
|
setupNavigation();
|
||||||
|
|
||||||
|
// 设置账户功能
|
||||||
|
setupAccountFeatures();
|
||||||
|
|
||||||
|
// 初始化页面
|
||||||
|
initPageByHash();
|
||||||
|
|
||||||
|
// 添加hashchange事件监听,处理浏览器前进/后退按钮
|
||||||
|
window.addEventListener('hashchange', initPageByHash);
|
||||||
|
|
||||||
|
// 定期更新系统状态
|
||||||
|
setInterval(updateSystemStatus, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载完成后执行初始化
|
||||||
|
window.addEventListener('DOMContentLoaded', init);
|
||||||
301
static/js/query.js
Normal file
301
static/js/query.js
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
// DNS查询页面功能实现
|
||||||
|
|
||||||
|
// 初始化查询页面
|
||||||
|
function initQueryPage() {
|
||||||
|
console.log('初始化DNS查询页面...');
|
||||||
|
setupQueryEventListeners();
|
||||||
|
loadQueryHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行DNS查询
|
||||||
|
async function handleDNSQuery() {
|
||||||
|
const domainInput = document.getElementById('dns-query-domain');
|
||||||
|
const resultDiv = document.getElementById('query-result');
|
||||||
|
|
||||||
|
if (!domainInput || !resultDiv) {
|
||||||
|
console.error('找不到必要的DOM元素');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const domain = domainInput.value.trim();
|
||||||
|
if (!domain) {
|
||||||
|
showErrorMessage('请输入域名');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/query?domain=${encodeURIComponent(domain)}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('查询失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
displayQueryResult(result, domain);
|
||||||
|
saveQueryHistory(domain, result);
|
||||||
|
loadQueryHistory();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('DNS查询出错:', error);
|
||||||
|
showErrorMessage('查询失败,请稍后重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示查询结果
|
||||||
|
function displayQueryResult(result, domain) {
|
||||||
|
const resultDiv = document.getElementById('query-result');
|
||||||
|
if (!resultDiv) return;
|
||||||
|
|
||||||
|
// 显示结果容器
|
||||||
|
resultDiv.classList.remove('hidden');
|
||||||
|
|
||||||
|
// 解析结果
|
||||||
|
const status = result.blocked ? '被屏蔽' : '正常';
|
||||||
|
const statusClass = result.blocked ? 'text-danger' : 'text-success';
|
||||||
|
const blockType = result.blocked ? result.blockRuleType || '未知' : '正常';
|
||||||
|
const blockRule = result.blocked ? result.blockRule || '未知' : '无';
|
||||||
|
const blockSource = result.blocked ? result.blocksource || '未知' : '无';
|
||||||
|
const timestamp = new Date(result.timestamp).toLocaleString();
|
||||||
|
|
||||||
|
// 更新结果显示
|
||||||
|
document.getElementById('result-domain').textContent = domain;
|
||||||
|
document.getElementById('result-status').innerHTML = `<span class="${statusClass}">${status}</span>`;
|
||||||
|
document.getElementById('result-type').textContent = blockType;
|
||||||
|
|
||||||
|
// 检查是否存在屏蔽规则显示元素,如果不存在则创建
|
||||||
|
let blockRuleElement = document.getElementById('result-block-rule');
|
||||||
|
if (!blockRuleElement) {
|
||||||
|
// 创建屏蔽规则显示区域
|
||||||
|
const grid = resultDiv.querySelector('.grid');
|
||||||
|
if (grid) {
|
||||||
|
const newGridItem = document.createElement('div');
|
||||||
|
newGridItem.className = 'bg-gray-50 p-4 rounded-lg';
|
||||||
|
newGridItem.innerHTML = `
|
||||||
|
<h4 class="text-sm font-medium text-gray-500 mb-2">屏蔽规则</h4>
|
||||||
|
<p class="text-lg font-semibold" id="result-block-rule">-</p>
|
||||||
|
`;
|
||||||
|
grid.appendChild(newGridItem);
|
||||||
|
blockRuleElement = document.getElementById('result-block-rule');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新屏蔽规则显示
|
||||||
|
if (blockRuleElement) {
|
||||||
|
blockRuleElement.textContent = blockRule;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否存在屏蔽来源显示元素,如果不存在则创建
|
||||||
|
let blockSourceElement = document.getElementById('result-block-source');
|
||||||
|
if (!blockSourceElement) {
|
||||||
|
// 创建屏蔽来源显示区域
|
||||||
|
const grid = resultDiv.querySelector('.grid');
|
||||||
|
if (grid) {
|
||||||
|
const newGridItem = document.createElement('div');
|
||||||
|
newGridItem.className = 'bg-gray-50 p-4 rounded-lg';
|
||||||
|
newGridItem.innerHTML = `
|
||||||
|
<h4 class="text-sm font-medium text-gray-500 mb-2">屏蔽来源</h4>
|
||||||
|
<p class="text-lg font-semibold" id="result-block-source">-</p>
|
||||||
|
`;
|
||||||
|
grid.appendChild(newGridItem);
|
||||||
|
blockSourceElement = document.getElementById('result-block-source');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新屏蔽来源显示
|
||||||
|
if (blockSourceElement) {
|
||||||
|
blockSourceElement.textContent = blockSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('result-time').textContent = timestamp;
|
||||||
|
document.getElementById('result-details').textContent = JSON.stringify(result, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存查询历史
|
||||||
|
function saveQueryHistory(domain, result) {
|
||||||
|
// 获取现有历史记录
|
||||||
|
let history = JSON.parse(localStorage.getItem('dnsQueryHistory') || '[]');
|
||||||
|
|
||||||
|
// 创建历史记录项
|
||||||
|
const historyItem = {
|
||||||
|
domain: domain,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
result: {
|
||||||
|
blocked: result.blocked,
|
||||||
|
blockRuleType: result.blockRuleType,
|
||||||
|
blockRule: result.blockRule,
|
||||||
|
blocksource: result.blocksource
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加到历史记录开头
|
||||||
|
history.unshift(historyItem);
|
||||||
|
|
||||||
|
// 限制历史记录数量
|
||||||
|
if (history.length > 20) {
|
||||||
|
history = history.slice(0, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到本地存储
|
||||||
|
localStorage.setItem('dnsQueryHistory', JSON.stringify(history));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载查询历史
|
||||||
|
function loadQueryHistory() {
|
||||||
|
const historyDiv = document.getElementById('query-history');
|
||||||
|
if (!historyDiv) return;
|
||||||
|
|
||||||
|
// 获取历史记录
|
||||||
|
const history = JSON.parse(localStorage.getItem('dnsQueryHistory') || '[]');
|
||||||
|
|
||||||
|
if (history.length === 0) {
|
||||||
|
historyDiv.innerHTML = '<div class="text-center text-gray-500 py-4">暂无查询历史</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成历史记录HTML
|
||||||
|
const historyHTML = history.map(item => {
|
||||||
|
const statusClass = item.result.blocked ? 'text-danger' : 'text-success';
|
||||||
|
const statusText = item.result.blocked ? '被屏蔽' : '正常';
|
||||||
|
const blockType = item.result.blocked ? item.result.blockRuleType : '正常';
|
||||||
|
const blockRule = item.result.blocked ? item.result.blockRule : '无';
|
||||||
|
const blockSource = item.result.blocked ? item.result.blocksource : '无';
|
||||||
|
const formattedTime = new Date(item.timestamp).toLocaleString();
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="flex flex-col md:flex-row justify-between items-start md:items-center p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span class="font-medium">${item.domain}</span>
|
||||||
|
<span class="${statusClass} text-sm">${statusText}</span>
|
||||||
|
<span class="text-xs text-gray-500">${blockType}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">规则: ${blockRule}</div>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">来源: ${blockSource}</div>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">${formattedTime}</div>
|
||||||
|
</div>
|
||||||
|
<button class="mt-2 md:mt-0 px-3 py-1 bg-primary text-white text-sm rounded-md hover:bg-primary/90 transition-colors" onclick="requeryFromHistory('${item.domain}')">
|
||||||
|
<i class="fa fa-refresh mr-1"></i>重新查询
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
historyDiv.innerHTML = historyHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从历史记录重新查询
|
||||||
|
function requeryFromHistory(domain) {
|
||||||
|
const domainInput = document.getElementById('dns-query-domain');
|
||||||
|
if (domainInput) {
|
||||||
|
domainInput.value = domain;
|
||||||
|
handleDNSQuery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空查询历史
|
||||||
|
function clearQueryHistory() {
|
||||||
|
if (confirm('确定要清空所有查询历史吗?')) {
|
||||||
|
localStorage.removeItem('dnsQueryHistory');
|
||||||
|
loadQueryHistory();
|
||||||
|
showSuccessMessage('查询历史已清空');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置事件监听器
|
||||||
|
function setupQueryEventListeners() {
|
||||||
|
// 查询按钮事件
|
||||||
|
const queryBtn = document.getElementById('dns-query-btn');
|
||||||
|
if (queryBtn) {
|
||||||
|
queryBtn.addEventListener('click', handleDNSQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 输入框回车键事件
|
||||||
|
const domainInput = document.getElementById('dns-query-domain');
|
||||||
|
if (domainInput) {
|
||||||
|
domainInput.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleDNSQuery();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空历史按钮事件
|
||||||
|
const clearHistoryBtn = document.getElementById('clear-history-btn');
|
||||||
|
if (clearHistoryBtn) {
|
||||||
|
clearHistoryBtn.addEventListener('click', clearQueryHistory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 显示成功消息
|
||||||
|
function showSuccessMessage(message) {
|
||||||
|
showNotification(message, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示错误消息
|
||||||
|
function showErrorMessage(message) {
|
||||||
|
showNotification(message, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示通知
|
||||||
|
function showNotification(message, type = 'info') {
|
||||||
|
// 移除现有通知
|
||||||
|
const existingNotification = document.querySelector('.notification');
|
||||||
|
if (existingNotification) {
|
||||||
|
existingNotification.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新通知
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `notification fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform transition-all duration-300 ease-in-out translate-y-0 opacity-0`;
|
||||||
|
|
||||||
|
// 设置通知样式
|
||||||
|
if (type === 'success') {
|
||||||
|
notification.classList.add('bg-green-500', 'text-white');
|
||||||
|
} else if (type === 'error') {
|
||||||
|
notification.classList.add('bg-red-500', 'text-white');
|
||||||
|
} else {
|
||||||
|
notification.classList.add('bg-blue-500', 'text-white');
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<i class="fa ${type === 'success' ? 'fa-check-circle' : type === 'error' ? 'fa-exclamation-circle' : 'fa-info-circle'}"></i>
|
||||||
|
<span>${message}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
// 显示通知
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.remove('opacity-0');
|
||||||
|
notification.classList.add('opacity-100');
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
// 3秒后隐藏通知
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.remove('opacity-100');
|
||||||
|
notification.classList.add('opacity-0');
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.remove();
|
||||||
|
}, 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载完成后初始化
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initQueryPage);
|
||||||
|
} else {
|
||||||
|
initQueryPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当切换到DNS查询页面时重新加载数据
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// 监听hash变化,当切换到DNS查询页面时重新加载数据
|
||||||
|
window.addEventListener('hashchange', () => {
|
||||||
|
if (window.location.hash === '#query') {
|
||||||
|
initQueryPage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
305
static/js/server-status.js
Normal file
305
static/js/server-status.js
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
// 服务器状态组件 - 显示CPU使用率和查询统计
|
||||||
|
|
||||||
|
// 全局变量
|
||||||
|
let serverStatusUpdateTimer = null;
|
||||||
|
let previousServerData = {
|
||||||
|
cpu: 0,
|
||||||
|
queries: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化服务器状态组件
|
||||||
|
function initServerStatusWidget() {
|
||||||
|
// 确保DOM元素存在
|
||||||
|
const widget = document.getElementById('server-status-widget');
|
||||||
|
if (!widget) return;
|
||||||
|
|
||||||
|
// 初始化页面类型检测
|
||||||
|
updateWidgetDisplayByPageType();
|
||||||
|
|
||||||
|
// 设置页面切换事件监听
|
||||||
|
handlePageSwitchEvents();
|
||||||
|
|
||||||
|
// 设置WebSocket监听(如果可用)
|
||||||
|
setupWebSocketListeners();
|
||||||
|
|
||||||
|
// 立即加载一次数据
|
||||||
|
loadServerStatusData();
|
||||||
|
|
||||||
|
// 设置定时更新(每5秒更新一次)
|
||||||
|
serverStatusUpdateTimer = setInterval(loadServerStatusData, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断当前页面是否为仪表盘
|
||||||
|
function isCurrentPageDashboard() {
|
||||||
|
// 方法1:检查侧边栏激活状态
|
||||||
|
const dashboardLink = document.querySelector('.sidebar a[href="#dashboard"]');
|
||||||
|
if (dashboardLink && dashboardLink.classList.contains('active')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方法2:检查仪表盘特有元素
|
||||||
|
const dashboardElements = [
|
||||||
|
'#dashboard-container',
|
||||||
|
'.dashboard-summary',
|
||||||
|
'#dashboard-stats'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const selector of dashboardElements) {
|
||||||
|
if (document.querySelector(selector)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方法3:检查URL哈希值
|
||||||
|
if (window.location.hash === '#dashboard' || window.location.hash === '') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据页面类型更新组件显示
|
||||||
|
function updateWidgetDisplayByPageType() {
|
||||||
|
const additionalStats = document.getElementById('server-additional-stats');
|
||||||
|
if (!additionalStats) return;
|
||||||
|
|
||||||
|
// 如果当前页面是仪表盘,隐藏额外统计指标
|
||||||
|
if (isCurrentPageDashboard()) {
|
||||||
|
additionalStats.classList.add('hidden');
|
||||||
|
} else {
|
||||||
|
// 非仪表盘页面,显示额外统计指标
|
||||||
|
additionalStats.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理页面切换事件
|
||||||
|
function handlePageSwitchEvents() {
|
||||||
|
// 监听哈希变化(导航切换)
|
||||||
|
window.addEventListener('hashchange', updateWidgetDisplayByPageType);
|
||||||
|
|
||||||
|
// 监听侧边栏点击事件
|
||||||
|
const sidebarLinks = document.querySelectorAll('.sidebar a');
|
||||||
|
sidebarLinks.forEach(link => {
|
||||||
|
link.addEventListener('click', function() {
|
||||||
|
// 延迟检查,确保页面已切换
|
||||||
|
setTimeout(updateWidgetDisplayByPageType, 100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听导航菜单点击事件
|
||||||
|
const navLinks = document.querySelectorAll('nav a');
|
||||||
|
navLinks.forEach(link => {
|
||||||
|
link.addEventListener('click', function() {
|
||||||
|
setTimeout(updateWidgetDisplayByPageType, 100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监控WebSocket连接状态
|
||||||
|
function monitorWebSocketConnection() {
|
||||||
|
// 如果存在WebSocket连接,监听消息
|
||||||
|
if (window.socket) {
|
||||||
|
window.socket.addEventListener('message', function(event) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.type === 'status_update') {
|
||||||
|
updateServerStatusWidget(data.payload);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析WebSocket消息失败:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置WebSocket监听器
|
||||||
|
function setupWebSocketListeners() {
|
||||||
|
// 如果WebSocket已经存在
|
||||||
|
if (window.socket) {
|
||||||
|
monitorWebSocketConnection();
|
||||||
|
} else {
|
||||||
|
// 监听socket初始化事件
|
||||||
|
window.addEventListener('socketInitialized', function() {
|
||||||
|
monitorWebSocketConnection();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载服务器状态数据
|
||||||
|
async function loadServerStatusData() {
|
||||||
|
try {
|
||||||
|
// 使用现有的API获取系统状态
|
||||||
|
const api = window.api || {};
|
||||||
|
const getStatusFn = api.getStatus || function() { return Promise.resolve({}); };
|
||||||
|
const statusData = await getStatusFn();
|
||||||
|
if (statusData && !statusData.error) {
|
||||||
|
updateServerStatusWidget(statusData);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载服务器状态数据失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新服务器状态组件
|
||||||
|
function updateServerStatusWidget(stats) {
|
||||||
|
// 确保组件存在
|
||||||
|
const widget = document.getElementById('server-status-widget');
|
||||||
|
if (!widget) return;
|
||||||
|
|
||||||
|
// 确保stats存在
|
||||||
|
stats = stats || {};
|
||||||
|
|
||||||
|
// 提取CPU使用率
|
||||||
|
let cpuUsage = 0;
|
||||||
|
if (stats.system && typeof stats.system.cpu === 'number') {
|
||||||
|
cpuUsage = stats.system.cpu;
|
||||||
|
} else if (typeof stats.cpuUsage === 'number') {
|
||||||
|
cpuUsage = stats.cpuUsage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取查询统计数据
|
||||||
|
let totalQueries = 0;
|
||||||
|
let blockedQueries = 0;
|
||||||
|
let allowedQueries = 0;
|
||||||
|
|
||||||
|
if (stats.dns) {
|
||||||
|
const allowed = typeof stats.dns.Allowed === 'number' ? stats.dns.Allowed : 0;
|
||||||
|
const blocked = typeof stats.dns.Blocked === 'number' ? stats.dns.Blocked : 0;
|
||||||
|
const errors = typeof stats.dns.Errors === 'number' ? stats.dns.Errors : 0;
|
||||||
|
totalQueries = allowed + blocked + errors;
|
||||||
|
blockedQueries = blocked;
|
||||||
|
allowedQueries = allowed;
|
||||||
|
} else {
|
||||||
|
totalQueries = typeof stats.totalQueries === 'number' ? stats.totalQueries : 0;
|
||||||
|
blockedQueries = typeof stats.blockedQueries === 'number' ? stats.blockedQueries : 0;
|
||||||
|
allowedQueries = typeof stats.allowedQueries === 'number' ? stats.allowedQueries : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新CPU使用率
|
||||||
|
const cpuValueElement = document.getElementById('server-cpu-value');
|
||||||
|
if (cpuValueElement) {
|
||||||
|
cpuValueElement.textContent = cpuUsage.toFixed(1) + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
const cpuBarElement = document.getElementById('server-cpu-bar');
|
||||||
|
if (cpuBarElement) {
|
||||||
|
cpuBarElement.style.width = Math.min(cpuUsage, 100) + '%';
|
||||||
|
|
||||||
|
// 根据CPU使用率改变颜色
|
||||||
|
if (cpuUsage > 80) {
|
||||||
|
cpuBarElement.className = 'h-full bg-danger rounded-full';
|
||||||
|
} else if (cpuUsage > 50) {
|
||||||
|
cpuBarElement.className = 'h-full bg-warning rounded-full';
|
||||||
|
} else {
|
||||||
|
cpuBarElement.className = 'h-full bg-success rounded-full';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新查询量
|
||||||
|
const queriesValueElement = document.getElementById('server-queries-value');
|
||||||
|
if (queriesValueElement) {
|
||||||
|
queriesValueElement.textContent = formatNumber(totalQueries);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算查询量百分比(假设最大查询量为10000)
|
||||||
|
const queryPercentage = Math.min((totalQueries / 10000) * 100, 100);
|
||||||
|
const queriesBarElement = document.getElementById('server-queries-bar');
|
||||||
|
if (queriesBarElement) {
|
||||||
|
queriesBarElement.style.width = queryPercentage + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新额外统计指标
|
||||||
|
const totalQueriesElement = document.getElementById('server-total-queries');
|
||||||
|
if (totalQueriesElement) {
|
||||||
|
totalQueriesElement.textContent = formatNumber(totalQueries);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockedQueriesElement = document.getElementById('server-blocked-queries');
|
||||||
|
if (blockedQueriesElement) {
|
||||||
|
blockedQueriesElement.textContent = formatNumber(blockedQueries);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowedQueriesElement = document.getElementById('server-allowed-queries');
|
||||||
|
if (allowedQueriesElement) {
|
||||||
|
allowedQueriesElement.textContent = formatNumber(allowedQueries);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加光晕提示效果
|
||||||
|
if (previousServerData.cpu !== cpuUsage || previousServerData.queries !== totalQueries) {
|
||||||
|
addGlowEffect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新服务器状态指示器
|
||||||
|
const statusIndicator = document.getElementById('server-status-indicator');
|
||||||
|
if (statusIndicator) {
|
||||||
|
// 检查系统状态
|
||||||
|
if (stats.system && stats.system.status === 'error') {
|
||||||
|
statusIndicator.className = 'inline-block w-2 h-2 bg-danger rounded-full';
|
||||||
|
} else {
|
||||||
|
statusIndicator.className = 'inline-block w-2 h-2 bg-success rounded-full';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存当前数据用于下次比较
|
||||||
|
previousServerData = {
|
||||||
|
cpu: cpuUsage,
|
||||||
|
queries: totalQueries
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加光晕提示效果
|
||||||
|
function addGlowEffect() {
|
||||||
|
const widget = document.getElementById('server-status-widget');
|
||||||
|
if (!widget) return;
|
||||||
|
|
||||||
|
// 添加光晕类
|
||||||
|
widget.classList.add('glow-effect');
|
||||||
|
|
||||||
|
// 2秒后移除光晕
|
||||||
|
setTimeout(function() {
|
||||||
|
widget.classList.remove('glow-effect');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化数字
|
||||||
|
function formatNumber(num) {
|
||||||
|
// 显示完整数字的最大长度阈值
|
||||||
|
const MAX_FULL_LENGTH = 5;
|
||||||
|
|
||||||
|
// 先获取完整数字字符串
|
||||||
|
const fullNumStr = num.toString();
|
||||||
|
|
||||||
|
// 如果数字长度小于等于阈值,直接返回完整数字
|
||||||
|
if (fullNumStr.length <= MAX_FULL_LENGTH) {
|
||||||
|
return fullNumStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 否则使用缩写格式
|
||||||
|
if (num >= 1000000) {
|
||||||
|
return (num / 1000000).toFixed(1) + 'M';
|
||||||
|
} else if (num >= 1000) {
|
||||||
|
return (num / 1000).toFixed(1) + 'K';
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullNumStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在DOM加载完成后初始化
|
||||||
|
window.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// 延迟初始化,确保页面完全加载
|
||||||
|
setTimeout(initServerStatusWidget, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 在页面卸载时清理资源
|
||||||
|
window.addEventListener('beforeunload', function() {
|
||||||
|
if (serverStatusUpdateTimer) {
|
||||||
|
clearInterval(serverStatusUpdateTimer);
|
||||||
|
serverStatusUpdateTimer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 导出函数供其他模块使用
|
||||||
|
window.serverStatusWidget = {
|
||||||
|
init: initServerStatusWidget,
|
||||||
|
update: updateServerStatusWidget
|
||||||
|
};
|
||||||
1302
static/js/shield.js
Normal file
1302
static/js/shield.js
Normal file
File diff suppressed because it is too large
Load Diff
19
static/js/vendor/tailwind.js
vendored
Normal file
19
static/js/vendor/tailwind.js
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: '#165DFF',
|
||||||
|
secondary: '#36CFFB',
|
||||||
|
success: '#00B42A',
|
||||||
|
warning: '#FF7D00',
|
||||||
|
danger: '#F53F3F',
|
||||||
|
info: '#86909C',
|
||||||
|
dark: '#1D2129',
|
||||||
|
light: '#F2F3F5',
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
194
static/login.html
Normal file
194
static/login.html
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>DNS服务器控制台 - 登录</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 40px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header p {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #e1e5e9;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3498db;
|
||||||
|
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #3498db;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background-color: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
background-color: #fee;
|
||||||
|
color: #c00;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-header">
|
||||||
|
<h1>DNS服务器控制台</h1>
|
||||||
|
<p>请输入您的登录凭据</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="error-message" id="errorMessage"></div>
|
||||||
|
|
||||||
|
<form id="loginForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">用户名</label>
|
||||||
|
<input type="text" id="username" name="username" placeholder="请输入用户名" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">密码</label>
|
||||||
|
<input type="password" id="password" name="password" placeholder="请输入密码" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn" id="loginBtn">登录</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('loginForm').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const username = document.getElementById('username').value;
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
const loginBtn = document.getElementById('loginBtn');
|
||||||
|
const errorMessage = document.getElementById('errorMessage');
|
||||||
|
|
||||||
|
// 显示加载状态
|
||||||
|
loginBtn.textContent = '登录中...';
|
||||||
|
loginBtn.classList.add('loading');
|
||||||
|
errorMessage.style.display = 'none';
|
||||||
|
|
||||||
|
// 发送登录请求
|
||||||
|
fetch('/api/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: username,
|
||||||
|
password: password
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 401) {
|
||||||
|
throw new Error('未知用户名或密码');
|
||||||
|
} else {
|
||||||
|
throw new Error('登录失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
// 登录成功,重定向到主页
|
||||||
|
window.location.href = '/';
|
||||||
|
} else {
|
||||||
|
if (data.error === '用户名或密码错误') {
|
||||||
|
throw new Error('未知用户名或密码');
|
||||||
|
} else {
|
||||||
|
throw new Error(data.error || '登录失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
// 显示错误信息
|
||||||
|
errorMessage.textContent = error.message;
|
||||||
|
errorMessage.style.display = 'block';
|
||||||
|
loginBtn.textContent = '登录';
|
||||||
|
loginBtn.classList.remove('loading');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
24
tailwind.config.js
Normal file
24
tailwind.config.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
"./static/**/*.{html,js}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: '#165DFF',
|
||||||
|
secondary: '#36CFFB',
|
||||||
|
success: '#00B42A',
|
||||||
|
warning: '#FF7D00',
|
||||||
|
danger: '#F53F3F',
|
||||||
|
info: '#86909C',
|
||||||
|
dark: '#1D2129',
|
||||||
|
light: '#F2F3F5',
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
77
test/test_rule_matching.go
Normal file
77
test/test_rule_matching.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// testRuleMatching 测试DNS规则匹配功能
|
||||||
|
func main() {
|
||||||
|
// 定义命令行参数
|
||||||
|
rulePtr := flag.String("rule", "||cntvwb.cn^", "规则字符串")
|
||||||
|
testDomainPtr := flag.String("domain", "vdapprecv.app.cntvwb.cn", "测试域名")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// 打印测试信息
|
||||||
|
fmt.Printf("测试规则: %s\n", *rulePtr)
|
||||||
|
fmt.Printf("测试域名: %s\n", *testDomainPtr)
|
||||||
|
|
||||||
|
// 发送HTTP请求到API端点来测试规则匹配
|
||||||
|
fmt.Println("\n测试规则匹配功能...")
|
||||||
|
cmd := exec.Command("curl", "-s", fmt.Sprintf("http://localhost:8080/api/shield/check?domain=%s&rule=%s", *testDomainPtr, *rulePtr))
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
// 如果直接的API测试失败,尝试另一种方法
|
||||||
|
fmt.Printf("直接测试失败: %v, %s\n", err, string(output))
|
||||||
|
fmt.Println("尝试添加规则并测试...")
|
||||||
|
testAddRuleAndCheck(*rulePtr, *testDomainPtr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("测试结果: %s\n", string(output))
|
||||||
|
|
||||||
|
// 验证规则是否生效(模拟测试)
|
||||||
|
if strings.Contains(*rulePtr, "||cntvwb.cn^") && strings.Contains(*testDomainPtr, "cntvwb.cn") {
|
||||||
|
fmt.Println("\n验证结果:")
|
||||||
|
if strings.Contains(*testDomainPtr, "cntvwb.cn") {
|
||||||
|
fmt.Println("✅ 子域名匹配测试通过:||cntvwb.cn^ 应该阻止所有 cntvwb.cn 的子域名")
|
||||||
|
} else {
|
||||||
|
fmt.Println("❌ 子域名匹配测试失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// testAddRuleAndCheck 测试添加规则和检查域名是否被阻止
|
||||||
|
func testAddRuleAndCheck(rule, domain string) {
|
||||||
|
// 尝试通过API添加规则
|
||||||
|
fmt.Printf("添加规则: %s\n", rule)
|
||||||
|
cmd := exec.Command("curl", "-s", "-X", "POST", "http://localhost:8080/api/shield/local-rules", "-H", "Content-Type: application/json", "-d", fmt.Sprintf(`{\"rule\":\"%s\"}`, rule))
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("添加规则失败: %v, %s\n", err, string(output))
|
||||||
|
// 尝试重新加载规则
|
||||||
|
fmt.Println("尝试重新加载规则...")
|
||||||
|
cmd = exec.Command("curl", "-s", "-X", "PUT", "http://localhost:8080/api/shield", "-H", "Content-Type: application/json", "-d", `{\"reload\":true}`)
|
||||||
|
output, err = cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("重新加载规则失败: %v, %s\n", err, string(output))
|
||||||
|
} else {
|
||||||
|
fmt.Printf("重新加载规则结果: %s\n", string(output))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("添加规则结果: %s\n", string(output))
|
||||||
|
|
||||||
|
// 测试域名是否被阻止
|
||||||
|
fmt.Printf("测试域名 %s 是否被阻止...\n", domain)
|
||||||
|
cmd = exec.Command("curl", "-s", fmt.Sprintf("http://localhost:8080/api/shield/check?domain=%s", domain))
|
||||||
|
output, err = cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("测试阻止失败: %v, %s\n", err, string(output))
|
||||||
|
} else {
|
||||||
|
fmt.Printf("阻止测试结果: %s\n", string(output))
|
||||||
|
}
|
||||||
|
}
|
||||||
45
test_console.sh
Executable file
45
test_console.sh
Executable file
@@ -0,0 +1,45 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# DNS Web控制台功能测试脚本
|
||||||
|
echo "开始测试DNS Web控制台功能..."
|
||||||
|
echo "=================================="
|
||||||
|
|
||||||
|
# 检查服务器是否运行
|
||||||
|
echo "检查DNS服务器运行状态..."
|
||||||
|
pids=$(ps aux | grep dns-server | grep -v grep)
|
||||||
|
if [ -n "$pids" ]; then
|
||||||
|
echo "✓ DNS服务器正在运行"
|
||||||
|
else
|
||||||
|
echo "✗ DNS服务器未运行,请先启动服务器"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 测试API基础URL
|
||||||
|
BASE_URL="http://localhost:8080/api"
|
||||||
|
|
||||||
|
# 测试1: 获取统计信息
|
||||||
|
echo "\n测试1: 获取DNS统计信息"
|
||||||
|
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/stats"
|
||||||
|
|
||||||
|
# 测试2: 获取系统状态
|
||||||
|
echo "\n测试2: 获取系统状态"
|
||||||
|
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/status"
|
||||||
|
|
||||||
|
# 测试3: 获取屏蔽规则
|
||||||
|
echo "\n测试3: 获取屏蔽规则列表"
|
||||||
|
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/shield"
|
||||||
|
|
||||||
|
# 测试4: 获取Top屏蔽域名
|
||||||
|
echo "\n测试4: 获取Top屏蔽域名"
|
||||||
|
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/top-blocked"
|
||||||
|
|
||||||
|
# 测试5: 获取Hosts内容
|
||||||
|
echo "\n测试5: 获取Hosts内容"
|
||||||
|
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/shield/hosts"
|
||||||
|
|
||||||
|
# 测试6: 访问Web控制台主页
|
||||||
|
echo "\n测试6: 访问Web控制台主页"
|
||||||
|
curl -s -o /dev/null -w "状态码: %{http_code}\n" "http://localhost:8080"
|
||||||
|
|
||||||
|
echo "\n=================================="
|
||||||
|
echo "测试完成!请检查上述状态码。正常情况下应为200。"
|
||||||
|
echo "前端Web控制台可通过浏览器访问: http://localhost:8080"
|
||||||
Reference in New Issue
Block a user