81 Commits
beta1 ... beta2

Author SHA1 Message Date
Alex Yang
bcf3e865b6 修复响应时间异常 2025-11-30 21:48:07 +08:00
Alex Yang
c16e147931 实现缓存功能 2025-11-30 20:20:59 +08:00
Alex Yang
f623654151 修复IP位置显示 2025-11-30 13:31:16 +08:00
Alex Yang
9311124602 修复IP位置显示 2025-11-30 13:00:47 +08:00
Alex Yang
ed719255b7 增加显示IP地理位置的功能 2025-11-30 12:46:22 +08:00
Alex Yang
dadfd4c78d 实现修改密码和注销功能 2025-11-30 11:52:41 +08:00
Alex Yang
72aa2846e5 实现登录功能 2025-11-30 11:44:26 +08:00
Alex Yang
4f0815a5f9 Web页面优化和修复 2025-11-30 03:46:47 +08:00
Alex Yang
b4c37f33b0 web日志查询增加修复以及日志数据持久化 2025-11-30 03:39:16 +08:00
Alex Yang
9fffac0fb7 优化日志查询页面 2025-11-30 03:24:20 +08:00
Alex Yang
fb9a62f2a7 实现了日志查询功能 2025-11-30 03:02:12 +08:00
Alex Yang
843350300f 实现解析日志查询功能 2025-11-30 02:29:21 +08:00
Alex Yang
0f0039f2d4 update 2025-11-30 02:27:10 +08:00
Alex Yang
9781dddfaf Merge branch 'beta2' of https://gitea.amazehome.xyz/AMAZEHOME/dns-server into beta2
pull
2025-11-30 02:26:28 +08:00
Alex Yang
9ec2d6598e 实现日志功能 2025-11-30 02:25:36 +08:00
Alex Yang
dfcfc7634a update 2025-11-30 01:58:13 +08:00
Alex Yang
48b2f27090 修复统计数据异常的问题 2025-11-30 01:25:45 +08:00
Alex Yang
e800ad1774 修改Swagger API文档 2025-11-30 00:52:10 +08:00
Alex Yang
c34f1ed682 修复服务器对 2025-11-29 23:52:19 +08:00
Alex Yang
70cf1a7306 更新 2025-11-29 23:40:01 +08:00
Alex Yang
26889f5b38 更新Swagger API文档 2025-11-29 19:32:55 +08:00
Alex Yang
ca876a8951 更新Swagger API文档 2025-11-29 19:30:47 +08:00
Alex Yang
11e52e0ffc 修复移动设备视图 2025-11-29 02:24:56 +08:00
Alex Yang
11bf99355d update 2025-11-29 02:15:12 +08:00
Alex Yang
0468f52050 修复正则匹配字符串不生效的问题 2025-11-29 00:23:20 +08:00
Alex Yang
3207510c91 Web优化 2025-11-28 23:59:58 +08:00
Alex Yang
8e2ea02a62 屏蔽规则页面丰富显示 2025-11-28 22:55:43 +08:00
Alex Yang
24b8cf19aa 修复服务器重启时端口被占用和数据保存问题 2025-11-28 19:38:21 +08:00
Alex Yang
16bc615a52 修复了规则更新后没有生效的问题 2025-11-28 18:52:22 +08:00
Alex Yang
ee148fe6c3 修复本地规则管理不工作的问题 2025-11-28 18:41:55 +08:00
Alex Yang
ca4a32422c 解决请求DNS解析的客户端数据未持久化的问题 2025-11-28 18:02:36 +08:00
Alex Yang
ec1e051252 修复数据显示问题 2025-11-28 17:59:13 +08:00
Alex Yang
bc912056cd 移除废案只修复请求域名排行显示 2025-11-28 17:08:34 +08:00
Alex Yang
25a21e284b 解决了config.js浏览器控制台报错的问题 2025-11-28 02:25:43 +08:00
Alex Yang
2e7d5fb1ce 添加了Swagger API文档以及诸多优化 2025-11-28 02:15:42 +08:00
Alex Yang
67c651c804 修复配置更新未写入配置文件的问题 2025-11-28 01:10:43 +08:00
Alex Yang
0e0ac8b016 修复本地屏蔽规则管理 2025-11-28 00:46:36 +08:00
Alex Yang
45ed4d0d6b 增加了API断电 2025-11-28 00:42:11 +08:00
Alex Yang
b1c63f6713 增加web功能 2025-11-28 00:17:56 +08:00
Alex Yang
7dd31c8f5a web增加屏蔽管理和DNS查询功能 2025-11-27 19:58:16 +08:00
Alex Yang
79eddf7fb2 优化web逻辑 2025-11-27 19:13:09 +08:00
Alex Yang
46acf4123a 优化web逻辑 2025-11-27 18:51:47 +08:00
Alex Yang
65ff630868 服务器启动自动创建默认配置文件等 2025-11-27 18:40:11 +08:00
Alex Yang
2541996b18 添加重启endpoint 2025-11-27 18:31:32 +08:00
Alex Yang
82f17ad875 添加重启endpoint 2025-11-27 18:26:00 +08:00
Alex Yang
8ee1d94471 web优化 2025-11-27 15:51:55 +08:00
Alex Yang
7970a4f093 update 2025-11-27 01:49:49 +08:00
Alex Yang
acf0ff6d96 设置界面更新 2025-11-27 01:37:53 +08:00
Alex Yang
6fc1283519 更新 2025-11-27 01:04:58 +08:00
Alex Yang
de1055b959 设置卡片数据占满时以K方式显示数据 2025-11-27 01:02:02 +08:00
Alex Yang
f89522cb88 设置卡片数据占满时以K方式显示数据 2025-11-27 00:40:50 +08:00
Alex Yang
91241d08da 数据更新时,卡片文字滚动翻页显示 2025-11-26 16:55:52 +08:00
Alex Yang
3ee31047e9 点击展开按钮悬浮窗显示详细24小时/7天/30天详细信息 2025-11-26 15:16:55 +08:00
Alex Yang
30ddd53f19 增加websocket,数据实时显示 2025-11-26 01:42:14 +08:00
Alex Yang
d293831bf9 增加websocket,数据实时显示 2025-11-26 01:37:47 +08:00
Alex Yang
3d9990ddea 增加websocket,数据实时显示 2025-11-26 01:17:25 +08:00
Alex Yang
54dbb024e1 增加websocket,数据实时显示 2025-11-26 01:11:37 +08:00
Alex Yang
63154085f7 页面数据更新优化 2025-11-26 00:58:43 +08:00
Alex Yang
b73f1be3dd 页面数据更新优化 2025-11-26 00:48:04 +08:00
Alex Yang
5ad91654cc upddate 2025-11-26 00:41:06 +08:00
Alex Yang
76139350a9 web请求趋势视图优化 2025-11-26 00:40:13 +08:00
Alex Yang
3494ce88a1 web添加解析类型显示 2025-11-26 00:06:14 +08:00
Alex Yang
d6e9cc990b 修复更多内容 2025-11-25 23:37:22 +08:00
Alex Yang
4d53b13220 将模拟数据修改为真实服务器统计数据 2025-11-25 18:14:53 +08:00
Alex Yang
397181429e 修复CPU使用率为随机模拟数据 2025-11-25 18:02:13 +08:00
Alex Yang
e21e02a233 增加API 2025-11-25 17:07:15 +08:00
Alex Yang
cd816ae065 优化调整 2025-11-25 16:55:27 +08:00
Alex Yang
e7b1a74a8d 移除多余文件 2025-11-25 16:53:41 +08:00
Alex Yang
5b5c805768 web增加恢复解析统计图表 2025-11-25 16:51:27 +08:00
Alex Yang
1a205a8e8a web增加恢复解析统计图表 2025-11-25 16:47:29 +08:00
Alex Yang
39f33e99a7 web增加恢复解析统计图表 2025-11-25 16:46:49 +08:00
Alex Yang
722961ac93 Merge branch 'beta2' of https://gitea.amazehome.xyz/AMAZEHOME/dns-server into beta2
web渲染
2025-11-25 16:30:38 +08:00
Alex Yang
f346b55688 web增加恢复解析统计图表 2025-11-25 16:30:28 +08:00
Alex Yang
6154764091 web增加恢复解析统计图表 2025-11-25 16:28:24 +08:00
Alex Yang
2fd2c65d64 web增加恢复解析统计图表 2025-11-25 15:35:53 +08:00
Alex Yang
e86c3db45f web异常修复 2025-11-25 15:17:45 +08:00
Alex Yang
aea162a616 更新使可以显示正常数据 2025-11-25 01:57:26 +08:00
Alex Yang
c701603bb1 更新beta2 2025-11-25 01:42:09 +08:00
Alex Yang
cb428300cd 更新beta2 2025-11-25 01:35:18 +08:00
Alex Yang
747f53b997 更新beta2 2025-11-25 01:34:50 +08:00
Alex Yang
d9a8462cf6 更新beta2 2025-11-25 01:34:20 +08:00
44 changed files with 13236 additions and 5668 deletions

234
README.md Normal file
View 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)
- 修复搜索和过滤功能
- 优化查询日志显示
- 修复样式间隔问题
- 添加查询日志刷新按钮
## 致谢
感谢所有为该项目做出贡献的开源项目和开发者!

View File

@@ -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查询结果的各种网络环境。

View File

@@ -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查询结果的各种网络环境。

View File

@@ -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,

View File

@@ -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

Binary file not shown.

127
dns/cache.go Normal file
View 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)
}
}
}

File diff suppressed because it is too large Load Diff

1
go.mod
View File

@@ -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
View File

@@ -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=

View File

@@ -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

File diff suppressed because it is too large Load Diff

1574
index.html

File diff suppressed because it is too large Load Diff

View File

@@ -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
View File

@@ -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
View 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"
}

View File

@@ -1,5 +0,0 @@
||hm.baidu.com
||baidu.com
/.*tracking.*/
/adjust.net/
/ad./

View File

@@ -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,45 +26,65 @@ 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,
} }
// 加载已保存的计数数据 // 加载已保存的计数数据
manager.loadStatsData() manager.loadStatsData()
return manager return manager
} }
@@ -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来源为"本地规则"
} }
// 更新本地规则计数 // 更新本地规则计数
@@ -187,11 +207,11 @@ func (m *ShieldManager) shouldUpdateCache(cacheFile string) bool {
func (m *ShieldManager) fetchRemoteRules(url string) error { func (m *ShieldManager) fetchRemoteRules(url string) error {
// 获取缓存文件路径 // 获取缓存文件路径
cacheFile := m.getCacheFilePath(url) cacheFile := m.getCacheFilePath(url)
// 尝试从缓存加载 // 尝试从缓存加载
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 {
@@ -843,7 +941,7 @@ func (m *ShieldManager) StartAutoUpdate() {
} }
} }
}() }()
logger.Info("规则自动更新已启动", "interval", m.config.UpdateInterval) logger.Info("规则自动更新已启动", "interval", m.config.UpdateInterval)
// 如果是首次启动,先保存一次数据确保目录存在 // 如果是首次启动,先保存一次数据确保目录存在
@@ -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
View 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
View File

@@ -0,0 +1,5 @@
{
"blockedDomainsCount": {},
"resolvedDomainsCount": {},
"lastSaved": "2025-11-29T02:08:50.6341349+08:00"
}

488
static/api/css/style.css Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

62
static/css/animation.css Normal file
View 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;
}

View File

@@ -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); }

File diff suppressed because it is too large Load Diff

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
View 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;

View File

@@ -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;
} }
// 确认对话框函数 // 确认对话框函数

View 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
View 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

File diff suppressed because it is too large Load Diff

202
static/js/hosts.js Normal file
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

19
static/js/vendor/tailwind.js vendored Normal file
View 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
View 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
View 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: [],
}

View 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
View 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"