Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ce6e4224f2 | |||
| d34ba6cfff | |||
| c470b97001 | |||
| 332eeb1fed |
234
README.md
234
README.md
@@ -1,234 +0,0 @@
|
||||
# DNS服务器项目
|
||||
|
||||
## 项目简介
|
||||
|
||||
这是一个基于Go语言开发的DNS服务器,具有屏蔽规则管理、查询日志记录和统计、Web控制台等功能。该服务器可以拦截特定域名的DNS查询,提供实时的查询统计和日志记录,并通过Web控制台进行管理。
|
||||
|
||||
### 技术栈
|
||||
|
||||
- Go语言
|
||||
- Gorilla Mux (HTTP路由)
|
||||
- Gorilla WebSocket (实时通信)
|
||||
- Chart.js (数据可视化)
|
||||
- Tailwind CSS (样式框架)
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 1. DNS查询处理
|
||||
- 支持UDP和TCP协议
|
||||
- 支持常见DNS查询类型(A, AAAA, CNAME, MX, NS, TXT等)
|
||||
- 高性能查询处理
|
||||
|
||||
### 2. 屏蔽规则管理
|
||||
- 支持域名规则和正则表达式规则
|
||||
- 支持规则例外
|
||||
- 支持远程规则列表
|
||||
- 支持本地规则管理
|
||||
|
||||
### 3. 查询日志记录和统计
|
||||
- 实时记录DNS查询日志
|
||||
- 支持日志持久化到文件
|
||||
- 提供查询统计和趋势分析
|
||||
- 支持日志搜索和过滤
|
||||
- 支持日志排序
|
||||
|
||||
### 4. Web控制台
|
||||
- 直观的仪表盘
|
||||
- 实时统计数据
|
||||
- 图表可视化
|
||||
- 规则管理界面
|
||||
- 查询日志详情页面
|
||||
- 支持分页和自定义记录数量
|
||||
|
||||
### 5. WebSocket实时更新
|
||||
- 实时更新统计数据
|
||||
- 实时更新图表
|
||||
- 支持连接状态管理
|
||||
|
||||
### 6. 查询日志持久化
|
||||
- 将查询日志保存到`querylog.json`文件
|
||||
- 定期自动保存
|
||||
- 服务器重启后自动加载
|
||||
|
||||
## 安装步骤
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Go 1.18或更高版本
|
||||
- Linux或Windows操作系统
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
go mod download
|
||||
```
|
||||
|
||||
### 编译和运行
|
||||
|
||||
```bash
|
||||
# 编译
|
||||
go build -o dns-server main.go
|
||||
|
||||
# 运行
|
||||
./dns-server
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 主要配置项
|
||||
|
||||
- `ListenPort`: DNS服务器监听端口,默认53
|
||||
- `HTTPPort`: HTTP控制台监听端口,默认8080
|
||||
- `StatsFile`: 统计数据保存文件,默认`data/stats.json`
|
||||
- `SaveInterval`: 自动保存间隔(秒),默认300
|
||||
- `MaxQueryLogs`: 最大保存日志数量,默认1000
|
||||
|
||||
### 配置文件格式
|
||||
|
||||
配置文件使用JSON格式,位于`config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"ListenPort": 53,
|
||||
"HTTPPort": 8080,
|
||||
"StatsFile": "data/stats.json",
|
||||
"SaveInterval": 300,
|
||||
"MaxQueryLogs": 1000
|
||||
}
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 启动服务器
|
||||
|
||||
```bash
|
||||
./dns-server
|
||||
```
|
||||
|
||||
### 访问Web控制台
|
||||
|
||||
在浏览器中访问:
|
||||
```
|
||||
http://localhost:8080
|
||||
```
|
||||
|
||||
### 管理屏蔽规则
|
||||
|
||||
1. 登录Web控制台
|
||||
2. 点击左侧菜单中的"屏蔽管理"
|
||||
3. 在"本地规则管理"中添加或删除规则
|
||||
4. 在"远程黑名单管理"中添加或删除远程规则列表
|
||||
|
||||
### 查看查询日志
|
||||
|
||||
1. 登录Web控制台
|
||||
2. 点击左侧菜单中的"查询日志"
|
||||
3. 查看日志统计和趋势
|
||||
4. 使用搜索和过滤功能查找特定日志
|
||||
5. 点击列头进行排序
|
||||
6. 使用刷新按钮手动刷新日志
|
||||
|
||||
## API文档
|
||||
|
||||
### 主要API端点
|
||||
|
||||
#### 1. DNS查询
|
||||
```
|
||||
GET /api/query?domain=example.com
|
||||
```
|
||||
|
||||
#### 2. 屏蔽规则管理
|
||||
```
|
||||
GET /api/shield/rules
|
||||
POST /api/shield/rules
|
||||
DELETE /api/shield/rules/:id
|
||||
```
|
||||
|
||||
#### 3. Hosts管理
|
||||
```
|
||||
GET /api/hosts
|
||||
POST /api/hosts
|
||||
DELETE /api/hosts/:id
|
||||
```
|
||||
|
||||
#### 4. 查询日志
|
||||
```
|
||||
GET /api/logs/stats
|
||||
GET /api/logs/query
|
||||
GET /api/logs/count
|
||||
```
|
||||
|
||||
#### 5. WebSocket
|
||||
```
|
||||
ws://localhost:8080/ws/stats
|
||||
```
|
||||
|
||||
## 开发说明
|
||||
|
||||
### 项目结构
|
||||
|
||||
```
|
||||
/root/dnsbak/
|
||||
├── config/ # 配置文件
|
||||
├── data/ # 数据文件
|
||||
├── dns/ # DNS服务器相关代码
|
||||
├── http/ # HTTP服务器相关代码
|
||||
├── shield/ # 屏蔽规则管理
|
||||
├── static/ # 静态资源
|
||||
│ ├── css/ # CSS文件
|
||||
│ ├── js/ # JavaScript文件
|
||||
│ └── index.html # 主页面
|
||||
├── main.go # 入口文件
|
||||
├── go.mod # Go模块文件
|
||||
└── go.sum # Go依赖校验文件
|
||||
```
|
||||
|
||||
### 开发流程
|
||||
|
||||
1. 克隆仓库
|
||||
2. 安装依赖
|
||||
3. 开发功能
|
||||
4. 编译和测试
|
||||
5. 提交代码
|
||||
|
||||
### 测试
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎提交Issue和Pull Request!
|
||||
|
||||
## 联系方式
|
||||
|
||||
如有问题或建议,请通过以下方式联系:
|
||||
|
||||
- Email: wxf26054@live.cn
|
||||
- Git: https://gitea.amazehome.xyz/AMAZEHOME/dns-server
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.0.0 (2025-11-30)
|
||||
- 初始版本
|
||||
- 实现基本DNS服务器功能
|
||||
- 实现屏蔽规则管理
|
||||
- 实现查询日志记录和统计
|
||||
- 实现Web控制台
|
||||
- 实现WebSocket实时更新
|
||||
- 实现查询日志持久化
|
||||
|
||||
### v1.0.1 (2025-11-30)
|
||||
- 修复搜索和过滤功能
|
||||
- 优化查询日志显示
|
||||
- 修复样式间隔问题
|
||||
- 添加查询日志刷新按钮
|
||||
|
||||
## 致谢
|
||||
|
||||
感谢所有为该项目做出贡献的开源项目和开发者!
|
||||
81
Readme.md
Normal file
81
Readme.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# 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查询结果的各种网络环境。
|
||||
105
config.json
105
config.json
@@ -1,115 +1,24 @@
|
||||
{
|
||||
"dns": {
|
||||
"port": 53,
|
||||
"upstreamDNS": [
|
||||
"223.5.5.5:53",
|
||||
"223.6.6.6:53"
|
||||
],
|
||||
"timeout": 5000,
|
||||
"statsFile": "data/stats.json",
|
||||
"saveInterval": 300
|
||||
"upstreamDNS": ["223.5.5.5:53", "223.6.6.6:53"],
|
||||
"timeout": 5000
|
||||
},
|
||||
"http": {
|
||||
"port": 8080,
|
||||
"host": "0.0.0.0",
|
||||
"enableAPI": true,
|
||||
"username": "admin",
|
||||
"password": "admin"
|
||||
"enableAPI": true
|
||||
},
|
||||
"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,
|
||||
"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
|
||||
}
|
||||
"remoteRules": [
|
||||
"https://example.com/rules.txt"
|
||||
],
|
||||
"updateInterval": 3600,
|
||||
"hostsFile": "data/hosts.txt",
|
||||
"blockMethod": "NXDOMAIN",
|
||||
"customBlockIP": "",
|
||||
"statsFile": "./data/shield_stats.json",
|
||||
"statsSaveInterval": 60,
|
||||
"remoteRulesCacheDir": "data/remote_rules"
|
||||
"hostsFile": "data/hosts.txt"
|
||||
},
|
||||
"log": {
|
||||
"file": "logs/dns-server.log",
|
||||
"file": "dns-server.log",
|
||||
"level": "debug",
|
||||
"maxSize": 100,
|
||||
"maxBackups": 10,
|
||||
|
||||
@@ -12,7 +12,6 @@ type DNSConfig struct {
|
||||
Timeout int `json:"timeout"`
|
||||
StatsFile string `json:"statsFile"` // 统计数据持久化文件
|
||||
SaveInterval int `json:"saveInterval"` // 数据保存间隔(秒)
|
||||
CacheTTL int `json:"cacheTTL"` // DNS缓存过期时间(分钟)
|
||||
}
|
||||
|
||||
// HTTPConfig HTTP控制台配置
|
||||
@@ -20,30 +19,18 @@ type HTTPConfig struct {
|
||||
Port int `json:"port"`
|
||||
Host string `json:"host"`
|
||||
EnableAPI bool `json:"enableAPI"`
|
||||
Username string `json:"username"` // 登录用户名
|
||||
Password string `json:"password"` // 登录密码
|
||||
}
|
||||
|
||||
// BlacklistEntry 黑名单条目
|
||||
type BlacklistEntry struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Enabled bool `json:"enabled"`
|
||||
RuleCount int `json:"ruleCount,omitempty"`
|
||||
LastUpdateTime string `json:"lastUpdateTime,omitempty"`
|
||||
}
|
||||
|
||||
// ShieldConfig 屏蔽规则配置
|
||||
type ShieldConfig struct {
|
||||
LocalRulesFile string `json:"localRulesFile"`
|
||||
Blacklists []BlacklistEntry `json:"blacklists"`
|
||||
UpdateInterval int `json:"updateInterval"`
|
||||
HostsFile string `json:"hostsFile"`
|
||||
BlockMethod string `json:"blockMethod"` // 屏蔽方法: "NXDOMAIN", "refused", "emptyIP", "customIP"
|
||||
CustomBlockIP string `json:"customBlockIP"` // 自定义屏蔽IP,当BlockMethod为"customIP"时使用
|
||||
StatsFile string `json:"statsFile"` // 计数数据持久化文件
|
||||
StatsSaveInterval int `json:"statsSaveInterval"` // 计数数据保存间隔(秒)
|
||||
RemoteRulesCacheDir string `json:"remoteRulesCacheDir"` // 远程规则缓存目录
|
||||
LocalRulesFile string `json:"localRulesFile"`
|
||||
RemoteRules []string `json:"remoteRules"`
|
||||
UpdateInterval int `json:"updateInterval"`
|
||||
HostsFile string `json:"hostsFile"`
|
||||
BlockMethod string `json:"blockMethod"` // 屏蔽方法: "NXDOMAIN", "refused", "emptyIP", "customIP"
|
||||
CustomBlockIP string `json:"customBlockIP"` // 自定义屏蔽IP,当BlockMethod为"customIP"时使用
|
||||
StatsFile string `json:"statsFile"` // 计数数据持久化文件
|
||||
StatsSaveInterval int `json:"statsSaveInterval"` // 计数数据保存间隔(秒)
|
||||
}
|
||||
|
||||
// LogConfig 日志配置
|
||||
@@ -89,23 +76,12 @@ func LoadConfig(path string) (*Config, error) {
|
||||
if config.DNS.SaveInterval == 0 {
|
||||
config.DNS.SaveInterval = 300 // 默认5分钟保存一次
|
||||
}
|
||||
// 默认DNS缓存TTL为30分钟
|
||||
if config.DNS.CacheTTL == 0 {
|
||||
config.DNS.CacheTTL = 30 // 默认30分钟
|
||||
}
|
||||
if config.HTTP.Port == 0 {
|
||||
config.HTTP.Port = 8080
|
||||
}
|
||||
if config.HTTP.Host == "" {
|
||||
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 {
|
||||
config.Shield.UpdateInterval = 3600
|
||||
}
|
||||
@@ -118,19 +94,6 @@ func LoadConfig(path string) (*Config, error) {
|
||||
if config.Shield.StatsSaveInterval == 0 {
|
||||
config.Shield.StatsSaveInterval = 300 // 默认5分钟保存一次
|
||||
}
|
||||
if config.Shield.RemoteRulesCacheDir == "" {
|
||||
config.Shield.RemoteRulesCacheDir = "./data/remote_rules" // 默认远程规则缓存目录
|
||||
}
|
||||
|
||||
// 如果黑名单列表为空,添加一些默认的黑名单
|
||||
if len(config.Shield.Blacklists) == 0 {
|
||||
config.Shield.Blacklists = []BlacklistEntry{
|
||||
{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},
|
||||
}
|
||||
}
|
||||
if config.Log.Level == "" {
|
||||
config.Log.Level = "info"
|
||||
}
|
||||
|
||||
6
data/hosts.txt
Normal file
6
data/hosts.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
# DNS Server Hosts File
|
||||
# Generated by DNS Server
|
||||
|
||||
::1 localhost
|
||||
ad.qq.com 127.0.0.1
|
||||
ad.qq.com 0.0.0.0
|
||||
5
data/rules.txt
Normal file
5
data/rules.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
||hm.baidu.com
|
||||
||baidu.com
|
||||
/.*tracking.*/
|
||||
/adjust.net/
|
||||
/ad./
|
||||
0
data/shield_stats.json
Normal file
0
data/shield_stats.json
Normal file
42
data/stats.json
Normal file
42
data/stats.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"stats": {
|
||||
"Queries": 33,
|
||||
"Blocked": 24,
|
||||
"Allowed": 19,
|
||||
"Errors": 2,
|
||||
"LastQuery": "2025-11-23T19:06:10.694259822+08:00"
|
||||
},
|
||||
"blockedDomains": {
|
||||
"ad.qq.com": {
|
||||
"Domain": "ad.qq.com",
|
||||
"Count": 6,
|
||||
"LastSeen": "2025-11-23T19:06:10.694532789+08:00"
|
||||
},
|
||||
"ad.qq.com.amazehome.xyz": {
|
||||
"Domain": "ad.qq.com.amazehome.xyz",
|
||||
"Count": 6,
|
||||
"LastSeen": "2025-11-23T19:06:10.691565251+08:00"
|
||||
}
|
||||
},
|
||||
"resolvedDomains": {
|
||||
"ad.qq.com": {
|
||||
"Domain": "ad.qq.com",
|
||||
"Count": 10,
|
||||
"LastSeen": "2025-11-23T19:05:54.429002187+08:00"
|
||||
},
|
||||
"ad.qq.com.amazehome.xyz": {
|
||||
"Domain": "ad.qq.com.amazehome.xyz",
|
||||
"Count": 8,
|
||||
"LastSeen": "2025-11-23T19:05:54.356216418+08:00"
|
||||
},
|
||||
"apd-pcdnwxstat.teg.tencent-cloud.net": {
|
||||
"Domain": "apd-pcdnwxstat.teg.tencent-cloud.net",
|
||||
"Count": 1,
|
||||
"LastSeen": "2025-11-23T19:05:56.842653777+08:00"
|
||||
}
|
||||
},
|
||||
"hourlyStats": {
|
||||
"2025-11-23-19": 12
|
||||
},
|
||||
"lastSaved": "2025-11-23T19:06:21.194988376+08:00"
|
||||
}
|
||||
BIN
dns-server
BIN
dns-server
Binary file not shown.
127
dns/cache.go
127
dns/cache.go
@@ -1,127 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
972
dns/server.go
972
dns/server.go
File diff suppressed because it is too large
Load Diff
13
go.mod
13
go.mod
@@ -5,7 +5,6 @@ go 1.23.0
|
||||
toolchain go1.24.10
|
||||
|
||||
require (
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/miekg/dns v1.1.68
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
)
|
||||
@@ -14,11 +13,9 @@ require (
|
||||
// 之前的go.sum可能包含lumberjack的记录,但现在已经不再使用
|
||||
|
||||
require (
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
)
|
||||
|
||||
32
go.sum
32
go.sum
@@ -1,10 +1,8 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -12,21 +10,19 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
1262
http/server.go
1262
http/server.go
File diff suppressed because it is too large
Load Diff
1574
index.html
Normal file
1574
index.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,8 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -17,7 +15,7 @@ var (
|
||||
)
|
||||
|
||||
// InitLogger 初始化日志系统
|
||||
func InitLogger(logFile, level string, maxSize, maxBackups, maxAge int, _ bool) error {
|
||||
func InitLogger(logFile, level string, maxSize, maxBackups, maxAge int) error {
|
||||
logMutex.Lock()
|
||||
defer logMutex.Unlock()
|
||||
|
||||
@@ -32,42 +30,21 @@ func InitLogger(logFile, level string, maxSize, maxBackups, maxAge int, _ bool)
|
||||
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 != "" {
|
||||
// 使用标准库打开文件,支持追加写入
|
||||
file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
// 无法打开日志文件时,回退到标准输出
|
||||
log.Println("无法打开日志文件,将使用标准输出:", err)
|
||||
logrus.Warn("无法打开日志文件,将使用标准输出", "error", err)
|
||||
log.SetOutput(os.Stdout)
|
||||
} else {
|
||||
outputTargets = append(outputTargets, file)
|
||||
defer file.Sync() // 确保日志内容被写入磁盘
|
||||
// 同时输出到文件和标准输出
|
||||
log.SetOutput(io.MultiWriter(file, os.Stdout))
|
||||
}
|
||||
}
|
||||
|
||||
// 无论是否指定日志文件,都同时输出到标准输出
|
||||
if len(outputTargets) > 0 {
|
||||
outputTargets = append(outputTargets, os.Stdout)
|
||||
} else {
|
||||
// 如果没有指定日志文件,仅使用标准输出
|
||||
outputTargets = append(outputTargets, os.Stdout)
|
||||
log.SetOutput(os.Stdout)
|
||||
}
|
||||
|
||||
// 设置日志输出
|
||||
log.SetOutput(io.MultiWriter(outputTargets...))
|
||||
|
||||
// 设置日志级别
|
||||
logLevel, err := logrus.ParseLevel(level)
|
||||
if err != nil {
|
||||
@@ -84,20 +61,13 @@ func Close() {
|
||||
logMutex.Lock()
|
||||
defer logMutex.Unlock()
|
||||
|
||||
if !initialized || log == nil {
|
||||
if !initialized {
|
||||
return
|
||||
}
|
||||
|
||||
// 执行日志刷新
|
||||
log.Warn("日志系统已关闭")
|
||||
|
||||
// 确保日志被写入磁盘
|
||||
if loggerOutput, ok := log.Out.(*os.File); ok {
|
||||
loggerOutput.Sync()
|
||||
}
|
||||
|
||||
initialized = false
|
||||
log = nil
|
||||
}
|
||||
|
||||
// Info 记录信息级别日志
|
||||
@@ -152,20 +122,6 @@ 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字段
|
||||
func toFields(keyValues []interface{}) logrus.Fields {
|
||||
fields := make(logrus.Fields)
|
||||
|
||||
169
main.go
169
main.go
@@ -1,14 +1,3 @@
|
||||
// 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
|
||||
|
||||
import (
|
||||
@@ -17,7 +6,6 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"dns-server/config"
|
||||
@@ -27,156 +15,19 @@ import (
|
||||
"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() {
|
||||
// 命令行参数解析
|
||||
var configFile string
|
||||
flag.StringVar(&configFile, "config", "config.json", "配置文件路径")
|
||||
// 解析命令行参数
|
||||
configPath := flag.String("config", "config.json", "配置文件路径")
|
||||
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)
|
||||
}
|
||||
|
||||
// 初始化配置
|
||||
var cfg *config.Config
|
||||
var err error
|
||||
cfg, err = config.LoadConfig(configFile)
|
||||
cfg, err := config.LoadConfig(*configPath)
|
||||
if err != nil {
|
||||
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, false); err != nil {
|
||||
if err := logger.InitLogger(cfg.Log.File, cfg.Log.Level, 0, 0, 0); err != nil {
|
||||
log.Fatalf("初始化日志系统失败: %v", err)
|
||||
}
|
||||
defer logger.Close()
|
||||
@@ -210,16 +61,14 @@ func main() {
|
||||
logger.Info(fmt.Sprintf("DNS服务器已启动,监听端口: %d", cfg.DNS.Port))
|
||||
logger.Info(fmt.Sprintf("HTTP控制台已启动,监听端口: %d", cfg.HTTP.Port))
|
||||
|
||||
// 监听信号
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigCh
|
||||
// 等待退出信号
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigChan
|
||||
|
||||
// 清理资源
|
||||
logger.Info("正在关闭服务...")
|
||||
dnsServer.Stop()
|
||||
httpServer.Stop()
|
||||
shieldManager.StopAutoUpdate()
|
||||
|
||||
logger.Info("服务已关闭")
|
||||
logger.Info("所有服务已关闭")
|
||||
}
|
||||
|
||||
18
package.json
18
package.json
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,62 +0,0 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"blockedDomainsCount": {},
|
||||
"resolvedDomainsCount": {},
|
||||
"lastSaved": "2025-11-29T02:08:50.6341349+08:00"
|
||||
}
|
||||
@@ -1,488 +0,0 @@
|
||||
/* 基础样式 */
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<!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>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,62 +0,0 @@
|
||||
@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;
|
||||
}
|
||||
1066
static/css/style.css
1066
static/css/style.css
File diff suppressed because it is too large
Load Diff
9
static/css/vendor/all.min.css
vendored
9
static/css/vendor/all.min.css
vendored
File diff suppressed because one or more lines are too long
Binary file not shown.
2856
static/index.html
2856
static/index.html
File diff suppressed because it is too large
Load Diff
305
static/js/api.js
305
static/js/api.js
@@ -1,305 +0,0 @@
|
||||
// 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;
|
||||
317
static/js/app.js
317
static/js/app.js
@@ -1,317 +0,0 @@
|
||||
// 全局配置
|
||||
const API_BASE_URL = '.';
|
||||
|
||||
// DOM 加载完成后执行
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 初始化面板切换
|
||||
initPanelNavigation();
|
||||
|
||||
// 加载初始数据
|
||||
loadInitialData();
|
||||
|
||||
// 直接调用dashboard面板初始化函数,确保数据正确加载
|
||||
if (typeof initDashboardPanel === 'function') {
|
||||
initDashboardPanel();
|
||||
}
|
||||
|
||||
// 注意:实时更新现在由index.html中的startRealTimeUpdate函数控制
|
||||
// 并根据面板状态自动启用/禁用
|
||||
});
|
||||
|
||||
// 初始化面板导航
|
||||
function initPanelNavigation() {
|
||||
const navItems = document.querySelectorAll('.nav-item');
|
||||
const panels = document.querySelectorAll('.panel');
|
||||
|
||||
navItems.forEach(item => {
|
||||
item.addEventListener('click', function() {
|
||||
// 移除所有活动类
|
||||
navItems.forEach(nav => nav.classList.remove('active'));
|
||||
panels.forEach(panel => panel.classList.remove('active'));
|
||||
|
||||
// 添加当前活动类
|
||||
this.classList.add('active');
|
||||
const target = this.getAttribute('data-target');
|
||||
document.getElementById(target).classList.add('active');
|
||||
|
||||
// 面板激活时执行相应的初始化函数
|
||||
if (window[`init${target.charAt(0).toUpperCase() + target.slice(1)}Panel`]) {
|
||||
window[`init${target.charAt(0).toUpperCase() + target.slice(1)}Panel`]();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 保留原有的通知函数作为兼容层
|
||||
// 现在主通知功能由index.html中的showNotification函数实现
|
||||
if (typeof window.showNotification === 'undefined') {
|
||||
window.showNotification = function(message, type = 'info') {
|
||||
// 创建临时通知元素
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification notification-${type} show`;
|
||||
notification.innerHTML = `
|
||||
<div class="notification-content">${message}</div>
|
||||
`;
|
||||
notification.style.cssText = 'position: fixed; top: 20px; right: 20px; background: #333; color: white; padding: 10px 15px; border-radius: 4px; z-index: 10000;';
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 3000);
|
||||
};
|
||||
}
|
||||
|
||||
// 加载初始数据(主要用于服务器状态)
|
||||
function loadInitialData() {
|
||||
// 加载服务器状态
|
||||
fetch(`${API_BASE_URL}/api/status`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// 更新服务器状态指示器
|
||||
const statusDot = document.querySelector('.status-dot');
|
||||
const serverStatus = document.getElementById('server-status');
|
||||
|
||||
if (data && data.status === 'running') {
|
||||
statusDot.classList.add('connected');
|
||||
serverStatus.textContent = '运行中';
|
||||
} else {
|
||||
statusDot.classList.remove('connected');
|
||||
serverStatus.textContent = '离线';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取服务器状态失败:', error);
|
||||
|
||||
// 更新状态为离线
|
||||
const statusDot = document.querySelector('.status-dot');
|
||||
const serverStatus = document.getElementById('server-status');
|
||||
statusDot.classList.remove('connected');
|
||||
serverStatus.textContent = '离线';
|
||||
|
||||
// 使用新的通知功能
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('获取服务器状态失败', 'danger');
|
||||
}
|
||||
});
|
||||
|
||||
// 注意:统计数据更新现在由dashboard.js中的updateStatCards函数处理
|
||||
}
|
||||
|
||||
// 注意:统计卡片数据更新现在由dashboard.js中的updateStatCards函数处理
|
||||
// 此函数保留作为兼容层,实际功能已迁移
|
||||
function updateStatCards(stats) {
|
||||
// 空实现,保留函数声明以避免引用错误
|
||||
console.log('更新统计卡片 - 此功能现在由dashboard.js处理');
|
||||
}
|
||||
|
||||
// 注意:获取规则数量功能现在由dashboard.js中的updateStatCards函数处理
|
||||
function fetchRulesCount() {
|
||||
// 空实现,保留函数声明以避免引用错误
|
||||
}
|
||||
|
||||
// 注意:获取hosts数量功能现在由dashboard.js中的updateStatCards函数处理
|
||||
function fetchHostsCount() {
|
||||
// 空实现,保留函数声明以避免引用错误
|
||||
}
|
||||
|
||||
// 通用API请求函数 - 添加错误处理和重试机制
|
||||
function apiRequest(endpoint, method = 'GET', data = null, maxRetries = 3) {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
const config = {
|
||||
method,
|
||||
headers,
|
||||
timeout: 10000, // 设置超时时间为10秒
|
||||
};
|
||||
|
||||
// 处理请求URL和参数
|
||||
let url = `${API_BASE_URL}${endpoint}`;
|
||||
|
||||
if (data) {
|
||||
if (method === 'GET') {
|
||||
// 为GET请求拼接查询参数
|
||||
const params = new URLSearchParams();
|
||||
Object.keys(data).forEach(key => {
|
||||
params.append(key, data[key]);
|
||||
});
|
||||
url += `?${params.toString()}`;
|
||||
} else if (method === 'POST' || method === 'PUT' || method === 'DELETE') {
|
||||
// 为其他方法设置body
|
||||
config.body = JSON.stringify(data);
|
||||
}
|
||||
}
|
||||
|
||||
let retries = 0;
|
||||
|
||||
function makeRequest() {
|
||||
return fetch(url, config)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
// 检查响应是否完整
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
// 使用.text()先获取响应文本,处理可能的JSON解析错误
|
||||
return response.text().then(text => {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (e) {
|
||||
console.error('JSON解析错误:', e, '响应文本:', text);
|
||||
// 针对ERR_INCOMPLETE_CHUNKED_ENCODING错误进行重试
|
||||
if (retries < maxRetries) {
|
||||
retries++;
|
||||
console.warn(`请求失败,正在进行第${retries}次重试...`);
|
||||
return new Promise(resolve => setTimeout(() => resolve(makeRequest()), 1000 * retries));
|
||||
}
|
||||
throw new Error('JSON解析失败且重试次数已达上限');
|
||||
}
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('API请求错误:', error);
|
||||
|
||||
// 检查是否为网络错误或ERR_INCOMPLETE_CHUNKED_ENCODING相关错误
|
||||
if ((error.name === 'TypeError' && error.message.includes('Failed to fetch')) ||
|
||||
error.message.includes('incomplete chunked encoding')) {
|
||||
|
||||
if (retries < maxRetries) {
|
||||
retries++;
|
||||
console.warn(`网络错误,正在进行第${retries}次重试...`);
|
||||
return new Promise(resolve => setTimeout(() => resolve(makeRequest()), 1000 * retries));
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
return makeRequest();
|
||||
}
|
||||
|
||||
// 数字格式化函数
|
||||
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;
|
||||
}
|
||||
|
||||
// 确认对话框函数
|
||||
function confirmAction(message, onConfirm) {
|
||||
if (confirm(message)) {
|
||||
onConfirm();
|
||||
}
|
||||
}
|
||||
|
||||
// 加载状态函数
|
||||
function showLoading(element) {
|
||||
if (element) {
|
||||
element.innerHTML = '<td colspan="100%" class="loading">加载中...</td>';
|
||||
}
|
||||
}
|
||||
|
||||
// 错误状态函数
|
||||
function showError(element, message) {
|
||||
if (element) {
|
||||
element.innerHTML = `<td colspan="100%" style="color: #e74c3c;">${message}</td>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 空状态函数
|
||||
function showEmpty(element, message) {
|
||||
if (element) {
|
||||
element.innerHTML = `<td colspan="100%" style="color: #7f8c8d; font-style: italic;">${message}</td>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 表格排序功能
|
||||
function initTableSort(tableId) {
|
||||
const table = document.getElementById(tableId);
|
||||
if (!table) return;
|
||||
|
||||
const headers = table.querySelectorAll('thead th');
|
||||
headers.forEach(header => {
|
||||
header.addEventListener('click', function() {
|
||||
const columnIndex = Array.from(headers).indexOf(this);
|
||||
const isAscending = this.getAttribute('data-sort') !== 'asc';
|
||||
|
||||
// 重置所有标题
|
||||
headers.forEach(h => h.setAttribute('data-sort', ''));
|
||||
this.setAttribute('data-sort', isAscending ? 'asc' : 'desc');
|
||||
|
||||
// 排序行
|
||||
sortTable(table, columnIndex, isAscending);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 表格排序实现
|
||||
function sortTable(table, columnIndex, isAscending) {
|
||||
const tbody = table.querySelector('tbody');
|
||||
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||||
|
||||
// 排序行
|
||||
rows.sort((a, b) => {
|
||||
const aValue = a.cells[columnIndex].textContent.trim();
|
||||
const bValue = b.cells[columnIndex].textContent.trim();
|
||||
|
||||
// 尝试数字排序
|
||||
const aNum = parseFloat(aValue);
|
||||
const bNum = parseFloat(bValue);
|
||||
|
||||
if (!isNaN(aNum) && !isNaN(bNum)) {
|
||||
return isAscending ? aNum - bNum : bNum - aNum;
|
||||
}
|
||||
|
||||
// 字符串排序
|
||||
return isAscending
|
||||
? aValue.localeCompare(bValue)
|
||||
: bValue.localeCompare(aValue);
|
||||
});
|
||||
|
||||
// 重新添加行
|
||||
rows.forEach(row => tbody.appendChild(row));
|
||||
}
|
||||
|
||||
// 搜索过滤功能
|
||||
function initSearchFilter(inputId, tableId, columnIndex) {
|
||||
const input = document.getElementById(inputId);
|
||||
const table = document.getElementById(tableId);
|
||||
|
||||
if (!input || !table) return;
|
||||
|
||||
input.addEventListener('input', function() {
|
||||
const filter = this.value.toLowerCase();
|
||||
const rows = table.querySelectorAll('tbody tr');
|
||||
|
||||
rows.forEach(row => {
|
||||
const cell = row.cells[columnIndex];
|
||||
if (cell) {
|
||||
const text = cell.textContent.toLowerCase();
|
||||
row.style.display = text.includes(filter) ? '' : 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
// 颜色配置文件 - 集中管理所有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;
|
||||
}
|
||||
@@ -1,284 +0,0 @@
|
||||
// 配置管理页面功能实现
|
||||
|
||||
// 工具函数:安全获取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();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,202 +0,0 @@
|
||||
// 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();
|
||||
}
|
||||
@@ -1,623 +0,0 @@
|
||||
// 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秒更新一次
|
||||
@@ -1,405 +0,0 @@
|
||||
// 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);
|
||||
@@ -1,255 +0,0 @@
|
||||
// 初始化远程黑名单面板
|
||||
function initBlacklistsPanel() {
|
||||
// 加载远程黑名单列表
|
||||
loadBlacklists();
|
||||
|
||||
// 初始化事件监听器
|
||||
initBlacklistsEventListeners();
|
||||
}
|
||||
|
||||
// 初始化事件监听器
|
||||
function initBlacklistsEventListeners() {
|
||||
// 添加黑名单按钮
|
||||
document.getElementById('add-blacklist').addEventListener('click', addBlacklist);
|
||||
|
||||
// 更新所有黑名单按钮
|
||||
document.getElementById('update-all-blacklists').addEventListener('click', updateAllBlacklists);
|
||||
|
||||
// 按Enter键添加黑名单
|
||||
document.getElementById('blacklist-url').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
addBlacklist();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 加载远程黑名单列表
|
||||
function loadBlacklists() {
|
||||
const tbody = document.getElementById('blacklists-table').querySelector('tbody');
|
||||
showLoading(tbody);
|
||||
|
||||
apiRequest('/api/shield/blacklists')
|
||||
.then(data => {
|
||||
// 直接渲染返回的blacklists数组
|
||||
renderBlacklists(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取远程黑名单列表失败:', error);
|
||||
showError(tbody, '获取远程黑名单列表失败');
|
||||
window.showNotification('获取远程黑名单列表失败', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染远程黑名单表格
|
||||
function renderBlacklists(blacklists) {
|
||||
const tbody = document.getElementById('blacklists-table').querySelector('tbody');
|
||||
if (!tbody) return;
|
||||
|
||||
if (!blacklists || blacklists.length === 0) {
|
||||
showEmpty(tbody, '暂无远程黑名单');
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = '';
|
||||
|
||||
blacklists.forEach(list => {
|
||||
addBlacklistToTable(list);
|
||||
});
|
||||
|
||||
// 初始化表格排序
|
||||
initTableSort('blacklists-table');
|
||||
|
||||
// 初始化操作按钮监听器
|
||||
initBlacklistsActionListeners();
|
||||
}
|
||||
|
||||
// 添加黑名单到表格
|
||||
function addBlacklistToTable(list) {
|
||||
const tbody = document.getElementById('blacklists-table').querySelector('tbody');
|
||||
const row = document.createElement('tr');
|
||||
|
||||
const statusClass = list.status === 'success' ? 'status-success' :
|
||||
list.status === 'error' ? 'status-error' : 'status-pending';
|
||||
|
||||
const statusText = list.status === 'success' ? '正常' :
|
||||
list.status === 'error' ? '错误' : '等待中';
|
||||
|
||||
const lastUpdate = list.lastUpdate ? new Date(list.lastUpdate).toLocaleString() : '从未';
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${list.name}</td>
|
||||
<td>${list.url}</td>
|
||||
<td>
|
||||
<span class="status-badge ${statusClass}">${statusText}</span>
|
||||
</td>
|
||||
<td>${list.rulesCount || 0}</td>
|
||||
<td>${lastUpdate}</td>
|
||||
<td class="actions-cell">
|
||||
<button class="btn btn-primary btn-sm update-blacklist" data-id="${list.id}">
|
||||
<i class="fas fa-sync-alt"></i> 更新
|
||||
</button>
|
||||
<button class="btn btn-danger btn-sm delete-blacklist" data-id="${list.id}">
|
||||
<i class="fas fa-trash-alt"></i> 删除
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(row);
|
||||
}
|
||||
|
||||
// 添加远程黑名单
|
||||
function addBlacklist() {
|
||||
const nameInput = document.getElementById('blacklist-name');
|
||||
const urlInput = document.getElementById('blacklist-url');
|
||||
|
||||
const name = nameInput.value.trim();
|
||||
const url = urlInput.value.trim();
|
||||
|
||||
if (!name) {
|
||||
window.showNotification('请输入黑名单名称', 'warning');
|
||||
nameInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
window.showNotification('请输入黑名单URL', 'warning');
|
||||
urlInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// 简单的URL格式验证
|
||||
if (!isValidUrl(url)) {
|
||||
window.showNotification('请输入有效的URL', 'warning');
|
||||
urlInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
apiRequest('/api/shield/blacklists', 'POST', { name: name, url: url })
|
||||
.then(data => {
|
||||
// 检查响应中是否有status字段
|
||||
if (!data || typeof data === 'undefined') {
|
||||
window.showNotification('远程黑名单添加失败: 无效的响应', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.status === 'success') {
|
||||
window.showNotification('远程黑名单添加成功', 'success');
|
||||
nameInput.value = '';
|
||||
urlInput.value = '';
|
||||
loadBlacklists();
|
||||
} else {
|
||||
window.showNotification(`添加失败: ${data.message || '未知错误'}`, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('添加远程黑名单失败:', error);
|
||||
window.showNotification('添加远程黑名单失败', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// 更新远程黑名单
|
||||
function updateBlacklist(id) {
|
||||
apiRequest(`/api/shield/blacklists/${id}/update`, 'POST')
|
||||
.then(data => {
|
||||
// 检查响应中是否有status字段
|
||||
if (!data || typeof data === 'undefined') {
|
||||
window.showNotification('远程黑名单更新失败: 无效的响应', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.status === 'success') {
|
||||
window.showNotification('远程黑名单更新成功', 'success');
|
||||
loadBlacklists();
|
||||
} else {
|
||||
window.showNotification(`更新失败: ${data.message || '未知错误'}`, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('更新远程黑名单失败:', error);
|
||||
window.showNotification('更新远程黑名单失败', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// 更新所有远程黑名单
|
||||
function updateAllBlacklists() {
|
||||
confirmAction(
|
||||
'确定要更新所有远程黑名单吗?这可能需要一些时间。',
|
||||
() => {
|
||||
apiRequest('/api/shield/blacklists', 'PUT')
|
||||
.then(data => {
|
||||
// 检查响应中是否有status字段
|
||||
if (!data || typeof data === 'undefined') {
|
||||
window.showNotification('所有远程黑名单更新失败: 无效的响应', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.status === 'success') {
|
||||
window.showNotification('所有远程黑名单更新成功', 'success');
|
||||
loadBlacklists();
|
||||
} else {
|
||||
window.showNotification(`更新失败: ${data.message || '未知错误'}`, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('更新所有远程黑名单失败:', error);
|
||||
window.showNotification('更新所有远程黑名单失败', 'error');
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 删除远程黑名单
|
||||
function deleteBlacklist(id) {
|
||||
apiRequest(`/api/shield/blacklists/${id}`, 'DELETE')
|
||||
.then(data => {
|
||||
// 检查响应中是否有status字段
|
||||
if (!data || typeof data === 'undefined') {
|
||||
window.showNotification('远程黑名单删除失败: 无效的响应', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.status === 'success') {
|
||||
window.showNotification('远程黑名单删除成功', 'success');
|
||||
loadBlacklists();
|
||||
} else {
|
||||
window.showNotification(`删除失败: ${data.message || '未知错误'}`, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('删除远程黑名单失败:', error);
|
||||
window.showNotification('删除远程黑名单失败', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// 为操作按钮添加事件监听器
|
||||
function initBlacklistsActionListeners() {
|
||||
// 更新按钮
|
||||
document.querySelectorAll('.update-blacklist').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const id = this.getAttribute('data-id');
|
||||
updateBlacklist(id);
|
||||
});
|
||||
});
|
||||
|
||||
// 删除按钮
|
||||
document.querySelectorAll('.delete-blacklist').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const id = this.getAttribute('data-id');
|
||||
|
||||
confirmAction(
|
||||
'确定要删除这条远程黑名单吗?',
|
||||
() => deleteBlacklist(id)
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 验证URL格式
|
||||
function isValidUrl(url) {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
// 初始化配置管理面板
|
||||
function initConfigPanel() {
|
||||
// 加载当前配置
|
||||
loadConfig();
|
||||
|
||||
// 初始化事件监听器
|
||||
initConfigEventListeners();
|
||||
}
|
||||
|
||||
// 初始化事件监听器
|
||||
function initConfigEventListeners() {
|
||||
// 保存配置按钮
|
||||
document.getElementById('save-config').addEventListener('click', saveConfig);
|
||||
|
||||
// 屏蔽方法变更
|
||||
document.getElementById('block-method').addEventListener('change', updateCustomBlockIpVisibility);
|
||||
}
|
||||
|
||||
// 加载当前配置
|
||||
function loadConfig() {
|
||||
apiRequest('/config')
|
||||
.then(config => {
|
||||
renderConfig(config);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取配置失败:', error);
|
||||
window.showNotification('获取配置失败', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染配置表单
|
||||
function renderConfig(config) {
|
||||
if (!config) return;
|
||||
|
||||
// 设置屏蔽方法
|
||||
const blockMethodSelect = document.getElementById('block-method');
|
||||
if (config.shield && config.shield.blockMethod) {
|
||||
blockMethodSelect.value = config.shield.blockMethod;
|
||||
}
|
||||
|
||||
// 设置自定义屏蔽IP
|
||||
const customBlockIpInput = document.getElementById('custom-block-ip');
|
||||
if (config.shield && config.shield.customBlockIP) {
|
||||
customBlockIpInput.value = config.shield.customBlockIP;
|
||||
}
|
||||
|
||||
// 设置远程规则更新间隔
|
||||
const updateIntervalInput = document.getElementById('update-interval');
|
||||
if (config.shield && config.shield.updateInterval) {
|
||||
updateIntervalInput.value = config.shield.updateInterval;
|
||||
}
|
||||
|
||||
// 更新自定义屏蔽IP的可见性
|
||||
updateCustomBlockIpVisibility();
|
||||
}
|
||||
|
||||
// 更新自定义屏蔽IP输入框的可见性
|
||||
function updateCustomBlockIpVisibility() {
|
||||
const blockMethod = document.getElementById('block-method').value;
|
||||
const customBlockIpContainer = document.getElementById('custom-block-ip').closest('.form-group');
|
||||
|
||||
if (blockMethod === 'customIP') {
|
||||
customBlockIpContainer.style.display = 'block';
|
||||
} else {
|
||||
customBlockIpContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
function saveConfig() {
|
||||
// 收集表单数据
|
||||
const configData = {
|
||||
shield: {
|
||||
blockMethod: document.getElementById('block-method').value,
|
||||
updateInterval: parseInt(document.getElementById('update-interval').value)
|
||||
}
|
||||
};
|
||||
|
||||
// 如果选择了自定义IP,添加到配置中
|
||||
if (configData.shield.blockMethod === 'customIP') {
|
||||
const customBlockIp = document.getElementById('custom-block-ip').value.trim();
|
||||
|
||||
// 验证自定义IP格式
|
||||
if (!isValidIp(customBlockIp)) {
|
||||
window.showNotification('请输入有效的自定义屏蔽IP', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
configData.shield.customBlockIP = customBlockIp;
|
||||
}
|
||||
|
||||
// 验证更新间隔
|
||||
if (isNaN(configData.shield.updateInterval) || configData.shield.updateInterval < 60) {
|
||||
window.showNotification('更新间隔必须大于等于60秒', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
apiRequest('/config', 'PUT', configData)
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
window.showNotification('配置保存成功', 'success');
|
||||
|
||||
// 由于服务器没有提供重启API,移除重启提示
|
||||
// 直接提示用户配置已保存
|
||||
} else {
|
||||
window.showNotification(`保存失败: ${response.message || '未知错误'}`, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('保存配置失败:', error);
|
||||
window.showNotification('保存配置失败', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// 服务重启功能已移除,因为服务器没有提供对应的API端点
|
||||
|
||||
// 验证IP地址格式
|
||||
function isValidIp(ip) {
|
||||
// 支持IPv4和IPv6简单验证
|
||||
const ipv4Regex = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/;
|
||||
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}$/;
|
||||
|
||||
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,308 +0,0 @@
|
||||
// 初始化Hosts面板
|
||||
function initHostsPanel() {
|
||||
// 加载Hosts列表
|
||||
loadHosts();
|
||||
|
||||
// 初始化事件监听器
|
||||
initHostsEventListeners();
|
||||
}
|
||||
|
||||
// 初始化事件监听器
|
||||
function initHostsEventListeners() {
|
||||
// 添加Hosts按钮
|
||||
document.getElementById('add-hosts').addEventListener('click', addHostsEntry);
|
||||
|
||||
// Hosts过滤
|
||||
document.getElementById('hosts-filter').addEventListener('input', filterHosts);
|
||||
|
||||
// 按Enter键添加Hosts
|
||||
document.getElementById('hosts-domain').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
addHostsEntry();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 加载Hosts列表
|
||||
function loadHosts() {
|
||||
const tbody = document.getElementById('hosts-table').querySelector('tbody');
|
||||
showLoading(tbody);
|
||||
|
||||
// 更新API路径,使用完整路径
|
||||
apiRequest('/api/shield/hosts', 'GET')
|
||||
.then(data => {
|
||||
// 处理不同格式的响应数据
|
||||
let hostsData;
|
||||
if (Array.isArray(data)) {
|
||||
hostsData = data;
|
||||
} else if (data && data.hosts) {
|
||||
hostsData = data.hosts;
|
||||
} else {
|
||||
hostsData = [];
|
||||
}
|
||||
|
||||
renderHosts(hostsData);
|
||||
|
||||
// 更新Hosts数量统计
|
||||
if (window.updateHostsCount && typeof window.updateHostsCount === 'function') {
|
||||
window.updateHostsCount(hostsData.length);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取Hosts列表失败:', error);
|
||||
|
||||
if (tbody) {
|
||||
tbody.innerHTML = '<tr><td colspan="3" class="text-center py-4">' +
|
||||
'<div class="empty-state">' +
|
||||
'<div class="empty-icon"><i class="fas fa-server text-muted"></i></div>' +
|
||||
'<div class="empty-title text-muted">加载失败</div>' +
|
||||
'<div class="empty-description text-muted">无法获取Hosts列表,请稍后重试</div>' +
|
||||
'</div>' +
|
||||
'</td></tr>';
|
||||
}
|
||||
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('获取Hosts列表失败', 'danger');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染Hosts表格
|
||||
function renderHosts(hosts) {
|
||||
const tbody = document.getElementById('hosts-table').querySelector('tbody');
|
||||
if (!tbody) return;
|
||||
|
||||
if (!hosts || hosts.length === 0) {
|
||||
// 使用更友好的空状态显示
|
||||
tbody.innerHTML = '<tr><td colspan="3" class="text-center py-4">' +
|
||||
'<div class="empty-state">' +
|
||||
'<div class="empty-icon"><i class="fas fa-file-alt text-muted"></i></div>' +
|
||||
'<div class="empty-title text-muted">暂无Hosts条目</div>' +
|
||||
'<div class="empty-description text-muted">添加自定义Hosts条目以控制DNS解析</div>' +
|
||||
'</div>' +
|
||||
'</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = '';
|
||||
|
||||
hosts.forEach(entry => {
|
||||
addHostsToTable(entry.ip, entry.domain);
|
||||
});
|
||||
|
||||
// 初始化删除按钮监听器
|
||||
initDeleteHostsListeners();
|
||||
}
|
||||
|
||||
// 添加Hosts到表格
|
||||
function addHostsToTable(ip, domain) {
|
||||
const tbody = document.getElementById('hosts-table').querySelector('tbody');
|
||||
const row = document.createElement('tr');
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${ip}</td>
|
||||
<td>${domain}</td>
|
||||
<td class="actions-cell">
|
||||
<button class="btn btn-danger btn-sm delete-hosts" data-ip="${ip}" data-domain="${domain}">
|
||||
<i class="fas fa-trash-alt"></i> 删除
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
// 添加行动画效果
|
||||
row.style.opacity = '0';
|
||||
row.style.transform = 'translateY(10px)';
|
||||
tbody.appendChild(row);
|
||||
|
||||
// 使用requestAnimationFrame确保动画平滑
|
||||
requestAnimationFrame(() => {
|
||||
row.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
||||
row.style.opacity = '1';
|
||||
row.style.transform = 'translateY(0)';
|
||||
});
|
||||
}
|
||||
|
||||
// 添加Hosts条目
|
||||
function addHostsEntry() {
|
||||
const ipInput = document.getElementById('hosts-ip');
|
||||
const domainInput = document.getElementById('hosts-domain');
|
||||
|
||||
const ip = ipInput.value.trim();
|
||||
const domain = domainInput.value.trim();
|
||||
|
||||
if (!ip) {
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('请输入IP地址', 'warning');
|
||||
}
|
||||
ipInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!domain) {
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('请输入域名', 'warning');
|
||||
}
|
||||
domainInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// 简单的IP地址格式验证
|
||||
if (!isValidIp(ip)) {
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('请输入有效的IP地址', 'warning');
|
||||
}
|
||||
ipInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// 修复重复API调用问题,只调用一次
|
||||
apiRequest('/api/shield/hosts', 'POST', { ip: ip, domain: domain })
|
||||
.then(data => {
|
||||
// 处理不同的响应格式
|
||||
if (data.success || data.status === 'success') {
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('Hosts条目添加成功', 'success');
|
||||
}
|
||||
|
||||
// 清空输入框并聚焦到域名输入
|
||||
ipInput.value = '';
|
||||
domainInput.value = '';
|
||||
domainInput.focus();
|
||||
|
||||
// 重新加载Hosts列表
|
||||
loadHosts();
|
||||
|
||||
// 触发数据刷新事件
|
||||
if (typeof window.triggerDataRefresh === 'function') {
|
||||
window.triggerDataRefresh('hosts');
|
||||
}
|
||||
} else {
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification(`添加失败: ${data.message || '未知错误'}`, 'danger');
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('添加Hosts条目失败:', error);
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('添加Hosts条目失败', 'danger');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 删除Hosts条目
|
||||
function deleteHostsEntry(ip, domain) {
|
||||
// 找到要删除的行并添加删除动画
|
||||
const rows = document.querySelectorAll('#hosts-table tbody tr');
|
||||
let targetRow = null;
|
||||
|
||||
rows.forEach(row => {
|
||||
if (row.cells[0].textContent === ip && row.cells[1].textContent === domain) {
|
||||
targetRow = row;
|
||||
}
|
||||
});
|
||||
|
||||
if (targetRow) {
|
||||
targetRow.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
||||
targetRow.style.opacity = '0';
|
||||
targetRow.style.transform = 'translateX(-20px)';
|
||||
}
|
||||
|
||||
// 更新API路径
|
||||
apiRequest('/api/shield/hosts', 'DELETE', { ip: ip, domain: domain })
|
||||
.then(data => {
|
||||
// 处理不同的响应格式
|
||||
if (data.success || data.status === 'success') {
|
||||
// 等待动画完成后重新加载列表
|
||||
setTimeout(() => {
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('Hosts条目删除成功', 'success');
|
||||
}
|
||||
loadHosts();
|
||||
|
||||
// 触发数据刷新事件
|
||||
if (typeof window.triggerDataRefresh === 'function') {
|
||||
window.triggerDataRefresh('hosts');
|
||||
}
|
||||
}, 300);
|
||||
} else {
|
||||
// 恢复行样式
|
||||
if (targetRow) {
|
||||
targetRow.style.opacity = '1';
|
||||
targetRow.style.transform = 'translateX(0)';
|
||||
}
|
||||
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification(`删除失败: ${data.message || '未知错误'}`, 'danger');
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// 恢复行样式
|
||||
if (targetRow) {
|
||||
targetRow.style.opacity = '1';
|
||||
targetRow.style.transform = 'translateX(0)';
|
||||
}
|
||||
|
||||
console.error('删除Hosts条目失败:', error);
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('删除Hosts条目失败', 'danger');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 过滤Hosts
|
||||
function filterHosts() {
|
||||
const filterText = document.getElementById('hosts-filter').value.toLowerCase();
|
||||
const rows = document.querySelectorAll('#hosts-table tbody tr');
|
||||
|
||||
rows.forEach(row => {
|
||||
const ip = row.cells[0].textContent.toLowerCase();
|
||||
const domain = row.cells[1].textContent.toLowerCase();
|
||||
|
||||
row.style.display = (ip.includes(filterText) || domain.includes(filterText)) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// 为删除按钮添加事件监听器
|
||||
function initDeleteHostsListeners() {
|
||||
document.querySelectorAll('.delete-hosts').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const ip = this.getAttribute('data-ip');
|
||||
const domain = this.getAttribute('data-domain');
|
||||
|
||||
// 使用标准confirm对话框
|
||||
if (confirm(`确定要删除这条Hosts条目吗?\n${ip} ${domain}`)) {
|
||||
deleteHostsEntry(ip, domain);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 验证IP地址格式
|
||||
function isValidIp(ip) {
|
||||
// 支持IPv4和IPv6简单验证
|
||||
const ipv4Regex = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/;
|
||||
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}$/;
|
||||
|
||||
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
|
||||
}
|
||||
|
||||
// 导出函数,供其他模块调用
|
||||
window.updateHostsCount = function(count) {
|
||||
const hostsCountElement = document.getElementById('hosts-count');
|
||||
if (hostsCountElement) {
|
||||
hostsCountElement.textContent = count;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出初始化函数
|
||||
window.initHostsPanel = initHostsPanel;
|
||||
|
||||
// 注册到面板导航系统
|
||||
if (window.registerPanelModule) {
|
||||
window.registerPanelModule('hosts-panel', {
|
||||
init: initHostsPanel,
|
||||
refresh: loadHosts
|
||||
});
|
||||
}
|
||||
@@ -1,294 +0,0 @@
|
||||
// 初始化DNS查询面板
|
||||
function initQueryPanel() {
|
||||
// 初始化事件监听器
|
||||
initQueryEventListeners();
|
||||
|
||||
// 确保结果容器默认隐藏
|
||||
const resultContainer = document.getElementById('query-result-container');
|
||||
if (resultContainer) {
|
||||
resultContainer.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化事件监听器
|
||||
function initQueryEventListeners() {
|
||||
// 查询按钮
|
||||
document.getElementById('run-query').addEventListener('click', runDnsQuery);
|
||||
|
||||
// 按Enter键执行查询
|
||||
document.getElementById('query-domain').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
runDnsQuery();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 执行DNS查询
|
||||
function runDnsQuery() {
|
||||
const domainInput = document.getElementById('query-domain');
|
||||
const domain = domainInput.value.trim();
|
||||
|
||||
if (!domain) {
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('请输入要查询的域名', 'warning');
|
||||
}
|
||||
domainInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示查询中状态
|
||||
showQueryLoading();
|
||||
|
||||
// 更新API路径,使用完整路径
|
||||
apiRequest('/api/query', 'GET', { domain: domain })
|
||||
.then(data => {
|
||||
// 处理可能的不同响应格式
|
||||
renderQueryResult(data);
|
||||
|
||||
// 触发数据刷新事件
|
||||
if (typeof window.triggerDataRefresh === 'function') {
|
||||
window.triggerDataRefresh('query');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('DNS查询失败:', error);
|
||||
showQueryError('查询失败,请稍后重试');
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('DNS查询失败', 'danger');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 显示查询加载状态
|
||||
function showQueryLoading() {
|
||||
const resultContainer = document.getElementById('query-result-container');
|
||||
if (!resultContainer) return;
|
||||
|
||||
// 添加加载动画类
|
||||
resultContainer.classList.add('loading-animation');
|
||||
resultContainer.classList.remove('hidden', 'error-animation', 'success-animation');
|
||||
|
||||
// 清空之前的结果
|
||||
const resultHeader = resultContainer.querySelector('.result-header h3');
|
||||
const resultContent = resultContainer.querySelector('.result-content');
|
||||
|
||||
if (resultHeader) resultHeader.textContent = '查询中...';
|
||||
if (resultContent) {
|
||||
resultContent.innerHTML = '<div class="loading">' +
|
||||
'<div class="spinner"></div><span>正在查询...</span>' +
|
||||
'</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// 显示查询错误
|
||||
function showQueryError(message) {
|
||||
const resultContainer = document.getElementById('query-result-container');
|
||||
if (!resultContainer) return;
|
||||
|
||||
// 添加错误动画类
|
||||
resultContainer.classList.add('error-animation');
|
||||
resultContainer.classList.remove('hidden', 'loading-animation', 'success-animation');
|
||||
|
||||
const resultHeader = resultContainer.querySelector('.result-header h3');
|
||||
const resultContent = resultContainer.querySelector('.result-content');
|
||||
|
||||
if (resultHeader) resultHeader.textContent = '查询错误';
|
||||
if (resultContent) {
|
||||
resultContent.innerHTML = `<div class="result-item error-message">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<span>${message}</span>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染查询结果
|
||||
function renderQueryResult(result) {
|
||||
const resultContainer = document.getElementById('query-result-container');
|
||||
if (!resultContainer) return;
|
||||
|
||||
// 添加成功动画类
|
||||
resultContainer.classList.add('success-animation');
|
||||
resultContainer.classList.remove('hidden', 'loading-animation', 'error-animation');
|
||||
|
||||
const resultHeader = resultContainer.querySelector('.result-header h3');
|
||||
const resultContent = resultContainer.querySelector('.result-content');
|
||||
|
||||
if (resultHeader) resultHeader.textContent = '查询结果';
|
||||
if (!resultContent) return;
|
||||
|
||||
// 安全的HTML转义函数
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text || '';
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// 根据查询结果构建内容
|
||||
let content = '<div class="result-grid">';
|
||||
|
||||
// 域名
|
||||
const safeDomain = escapeHtml(result.domain || '');
|
||||
content += `<div class="result-item domain-item">
|
||||
<div class="result-label"><i class="fas fa-globe"></i> 域名</div>
|
||||
<div class="result-value" id="result-domain">${safeDomain}</div>
|
||||
</div>`;
|
||||
|
||||
// 状态 - 映射API字段
|
||||
const isBlocked = result.blocked || false;
|
||||
const isExcluded = result.excluded || false;
|
||||
const isAllowed = !isBlocked || isExcluded;
|
||||
|
||||
const statusText = isBlocked ? '被屏蔽' : isAllowed ? '允许访问' : '未知';
|
||||
const statusClass = isBlocked ? 'status-error' : isAllowed ? 'status-success' : '';
|
||||
const statusIcon = isBlocked ? 'fa-ban' : isAllowed ? 'fa-check-circle' : 'fa-question-circle';
|
||||
content += `<div class="result-item status-item">
|
||||
<div class="result-label"><i class="fas fa-shield-alt"></i> 状态</div>
|
||||
<div class="result-value" id="result-status" class="${statusClass}">
|
||||
<i class="fas ${statusIcon}"></i> ${statusText}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// 规则类型 - 映射API字段
|
||||
let ruleType = '';
|
||||
if (isBlocked) {
|
||||
if (result.blockRuleType && result.blockRuleType.toLowerCase().includes('regex')) {
|
||||
ruleType = '正则表达式规则';
|
||||
} else {
|
||||
ruleType = result.blockRuleType || '域名规则';
|
||||
}
|
||||
} else {
|
||||
if (isExcluded) {
|
||||
ruleType = '白名单规则';
|
||||
} else if (result.hasHosts) {
|
||||
ruleType = 'Hosts记录';
|
||||
} else {
|
||||
ruleType = '未匹配任何规则';
|
||||
}
|
||||
}
|
||||
content += `<div class="result-item rule-type-item">
|
||||
<div class="result-label"><i class="fas fa-list-alt"></i> 规则类型</div>
|
||||
<div class="result-value" id="result-rule-type">${escapeHtml(ruleType)}</div>
|
||||
</div>`;
|
||||
|
||||
// 匹配规则 - 映射API字段
|
||||
let matchedRule = '';
|
||||
if (isBlocked) {
|
||||
matchedRule = result.blockRule || '无';
|
||||
} else if (isExcluded) {
|
||||
matchedRule = result.excludeRule || '无';
|
||||
} else {
|
||||
matchedRule = '无';
|
||||
}
|
||||
content += `<div class="result-item matched-rule-item">
|
||||
<div class="result-label"><i class="fas fa-sitemap"></i> 匹配规则</div>
|
||||
<div class="result-value rule-code" id="result-rule">${escapeHtml(matchedRule)}</div>
|
||||
</div>`;
|
||||
|
||||
// Hosts记录 - 映射API字段
|
||||
const hostsRecord = result.hasHosts && result.hostsIP ?
|
||||
escapeHtml(`${result.hostsIP} ${result.domain}`) : '无';
|
||||
content += `<div class="result-item hosts-item">
|
||||
<div class="result-label"><i class="fas fa-file-alt"></i> Hosts记录</div>
|
||||
<div class="result-value" id="result-hosts">${hostsRecord}</div>
|
||||
</div>`;
|
||||
|
||||
// 查询时间 - API没有提供,计算当前时间
|
||||
const queryTime = `${Date.now() % 100} ms`;
|
||||
content += `<div class="result-item time-item">
|
||||
<div class="result-label"><i class="fas fa-clock"></i> 查询时间</div>
|
||||
<div class="result-value" id="result-time">${queryTime}</div>
|
||||
</div>`;
|
||||
|
||||
content += '</div>'; // 结束result-grid
|
||||
|
||||
// DNS响应(如果有)
|
||||
if (result.dnsResponse) {
|
||||
content += '<div class="dns-response-section">';
|
||||
content += '<h4><i class="fas fa-exchange-alt"></i> DNS响应</h4>';
|
||||
|
||||
if (result.dnsResponse.answers && result.dnsResponse.answers.length > 0) {
|
||||
content += '<div class="dns-answers">';
|
||||
result.dnsResponse.answers.forEach((answer, index) => {
|
||||
content += `<div class="dns-answer-item">
|
||||
<span class="answer-index">#${index + 1}</span>
|
||||
<span class="answer-name">${escapeHtml(answer.name)}</span>
|
||||
<span class="answer-type">${escapeHtml(answer.type)}</span>
|
||||
<span class="answer-value">${escapeHtml(answer.value)}</span>
|
||||
</div>`;
|
||||
});
|
||||
content += '</div>';
|
||||
} else {
|
||||
content += '<div class="empty-dns"><i class="fas fa-info-circle"></i> 无DNS响应记录</div>';
|
||||
}
|
||||
content += '</div>';
|
||||
}
|
||||
|
||||
// 添加复制功能
|
||||
content += `<div class="result-actions">
|
||||
<button class="btn btn-sm btn-secondary" onclick="copyQueryResult()">
|
||||
<i class="fas fa-copy"></i> 复制结果
|
||||
</button>
|
||||
</div>`;
|
||||
|
||||
resultContent.innerHTML = content;
|
||||
|
||||
// 通知用户查询成功
|
||||
if (typeof window.showNotification === 'function') {
|
||||
const statusMsg = isBlocked ? '查询完成,该域名被屏蔽' :
|
||||
isAllowed ? '查询完成,该域名允许访问' : '查询完成';
|
||||
window.showNotification(statusMsg, 'info');
|
||||
}
|
||||
}
|
||||
|
||||
// 复制查询结果到剪贴板
|
||||
function copyQueryResult() {
|
||||
const resultContainer = document.getElementById('query-result-container');
|
||||
if (!resultContainer) return;
|
||||
|
||||
// 收集关键信息
|
||||
const domain = document.getElementById('result-domain')?.textContent || '未知域名';
|
||||
const status = document.getElementById('result-status')?.textContent || '未知状态';
|
||||
const ruleType = document.getElementById('result-rule-type')?.textContent || '无规则类型';
|
||||
const matchedRule = document.getElementById('result-rule')?.textContent || '无匹配规则';
|
||||
const queryTime = document.getElementById('result-time')?.textContent || '未知时间';
|
||||
|
||||
// 构建要复制的文本
|
||||
const textToCopy = `DNS查询结果:\n` +
|
||||
`域名: ${domain}\n` +
|
||||
`状态: ${status}\n` +
|
||||
`规则类型: ${ruleType}\n` +
|
||||
`匹配规则: ${matchedRule}\n` +
|
||||
`查询时间: ${queryTime}`;
|
||||
|
||||
// 复制到剪贴板
|
||||
navigator.clipboard.writeText(textToCopy)
|
||||
.then(() => {
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('查询结果已复制到剪贴板', 'success');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('复制失败,请手动复制', 'warning');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 导出函数,供其他模块调用
|
||||
window.initQueryPanel = initQueryPanel;
|
||||
window.runDnsQuery = runDnsQuery;
|
||||
|
||||
// 注册到面板导航系统
|
||||
if (window.registerPanelModule) {
|
||||
window.registerPanelModule('query-panel', {
|
||||
init: initQueryPanel,
|
||||
refresh: function() {
|
||||
// 清除当前查询结果
|
||||
const resultContainer = document.getElementById('query-result-container');
|
||||
if (resultContainer) {
|
||||
resultContainer.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,422 +0,0 @@
|
||||
// 屏蔽规则管理模块
|
||||
|
||||
// 全局变量
|
||||
let rules = [];
|
||||
let currentPage = 1;
|
||||
let itemsPerPage = 50; // 默认每页显示50条规则
|
||||
let filteredRules = [];
|
||||
|
||||
// 初始化屏蔽规则面板
|
||||
function initRulesPanel() {
|
||||
// 加载规则列表
|
||||
loadRules();
|
||||
|
||||
// 绑定添加规则按钮事件
|
||||
document.getElementById('add-rule-btn').addEventListener('click', addNewRule);
|
||||
|
||||
// 绑定刷新规则按钮事件
|
||||
document.getElementById('reload-rules-btn').addEventListener('click', reloadRules);
|
||||
|
||||
// 绑定搜索框事件
|
||||
document.getElementById('rule-search').addEventListener('input', filterRules);
|
||||
|
||||
// 绑定每页显示数量变更事件
|
||||
document.getElementById('items-per-page').addEventListener('change', () => {
|
||||
itemsPerPage = parseInt(document.getElementById('items-per-page').value);
|
||||
currentPage = 1; // 重置为第一页
|
||||
renderRulesList();
|
||||
});
|
||||
|
||||
// 绑定分页按钮事件
|
||||
document.getElementById('prev-page-btn').addEventListener('click', goToPreviousPage);
|
||||
document.getElementById('next-page-btn').addEventListener('click', goToNextPage);
|
||||
document.getElementById('first-page-btn').addEventListener('click', goToFirstPage);
|
||||
document.getElementById('last-page-btn').addEventListener('click', goToLastPage);
|
||||
}
|
||||
|
||||
// 加载规则列表
|
||||
async function loadRules() {
|
||||
try {
|
||||
const rulesPanel = document.getElementById('rules-panel');
|
||||
showLoading(rulesPanel);
|
||||
|
||||
// 更新API路径,使用正确的API路径
|
||||
const data = await apiRequest('/api/shield', 'GET');
|
||||
|
||||
// 处理后端返回的复杂对象数据格式
|
||||
let allRules = [];
|
||||
if (data && typeof data === 'object') {
|
||||
// 合并所有类型的规则到一个数组
|
||||
if (Array.isArray(data.domainRules)) allRules = allRules.concat(data.domainRules);
|
||||
if (Array.isArray(data.domainExceptions)) allRules = allRules.concat(data.domainExceptions);
|
||||
if (Array.isArray(data.regexRules)) allRules = allRules.concat(data.regexRules);
|
||||
if (Array.isArray(data.regexExceptions)) allRules = allRules.concat(data.regexExceptions);
|
||||
}
|
||||
|
||||
rules = allRules;
|
||||
filteredRules = [...rules];
|
||||
currentPage = 1; // 重置为第一页
|
||||
renderRulesList();
|
||||
|
||||
// 更新规则数量统计卡片
|
||||
if (window.updateRulesCount && typeof window.updateRulesCount === 'function') {
|
||||
window.updateRulesCount(rules.length);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载规则失败:', error);
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('加载规则失败', 'danger');
|
||||
}
|
||||
} finally {
|
||||
const rulesPanel = document.getElementById('rules-panel');
|
||||
hideLoading(rulesPanel);
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染规则列表
|
||||
function renderRulesList() {
|
||||
const rulesList = document.getElementById('rules-list');
|
||||
const paginationInfo = document.getElementById('pagination-info');
|
||||
|
||||
// 清空列表
|
||||
rulesList.innerHTML = '';
|
||||
|
||||
if (filteredRules.length === 0) {
|
||||
// 使用更友好的空状态显示
|
||||
rulesList.innerHTML = '<tr><td colspan="4" class="text-center py-4">' +
|
||||
'<div class="empty-state">' +
|
||||
'<div class="empty-icon"><i class="fas fa-shield-alt text-muted"></i></div>' +
|
||||
'<div class="empty-title text-muted">暂无规则</div>' +
|
||||
'<div class="empty-description text-muted">点击添加按钮或刷新规则来获取规则列表</div>' +
|
||||
'</div>' +
|
||||
'</td></tr>';
|
||||
paginationInfo.textContent = '共0条规则';
|
||||
updatePaginationButtons();
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算分页数据
|
||||
const totalPages = Math.ceil(filteredRules.length / itemsPerPage);
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = Math.min(startIndex + itemsPerPage, filteredRules.length);
|
||||
const currentRules = filteredRules.slice(startIndex, endIndex);
|
||||
|
||||
// 渲染当前页的规则
|
||||
currentRules.forEach((rule, index) => {
|
||||
const row = document.createElement('tr');
|
||||
const globalIndex = startIndex + index;
|
||||
|
||||
// 根据规则类型添加不同的样式
|
||||
const ruleTypeClass = getRuleTypeClass(rule);
|
||||
|
||||
row.innerHTML = `
|
||||
<td class="rule-id">${globalIndex + 1}</td>
|
||||
<td class="rule-content ${ruleTypeClass}"><pre>${escapeHtml(rule)}</pre></td>
|
||||
<td class="rule-actions">
|
||||
<button class="btn btn-danger btn-sm delete-rule" data-index="${globalIndex}">
|
||||
<i class="fas fa-trash"></i> 删除
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
// 添加行动画效果
|
||||
row.style.opacity = '0';
|
||||
row.style.transform = 'translateY(10px)';
|
||||
rulesList.appendChild(row);
|
||||
|
||||
// 使用requestAnimationFrame确保动画平滑
|
||||
requestAnimationFrame(() => {
|
||||
row.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
||||
row.style.opacity = '1';
|
||||
row.style.transform = 'translateY(0)';
|
||||
});
|
||||
});
|
||||
|
||||
// 绑定删除按钮事件
|
||||
document.querySelectorAll('.delete-rule').forEach(button => {
|
||||
button.addEventListener('click', (e) => {
|
||||
const index = parseInt(e.currentTarget.dataset.index);
|
||||
deleteRule(index);
|
||||
});
|
||||
});
|
||||
|
||||
// 更新分页信息
|
||||
paginationInfo.textContent = `显示 ${startIndex + 1}-${endIndex} 条,共 ${filteredRules.length} 条规则,第 ${currentPage}/${totalPages} 页`;
|
||||
|
||||
// 更新分页按钮状态
|
||||
updatePaginationButtons();
|
||||
}
|
||||
|
||||
// 更新分页按钮状态
|
||||
function updatePaginationButtons() {
|
||||
const totalPages = Math.ceil(filteredRules.length / itemsPerPage);
|
||||
const prevBtn = document.getElementById('prev-page-btn');
|
||||
const nextBtn = document.getElementById('next-page-btn');
|
||||
const firstBtn = document.getElementById('first-page-btn');
|
||||
const lastBtn = document.getElementById('last-page-btn');
|
||||
|
||||
prevBtn.disabled = currentPage === 1;
|
||||
nextBtn.disabled = currentPage === totalPages || totalPages === 0;
|
||||
firstBtn.disabled = currentPage === 1;
|
||||
lastBtn.disabled = currentPage === totalPages || totalPages === 0;
|
||||
}
|
||||
|
||||
// 上一页
|
||||
function goToPreviousPage() {
|
||||
if (currentPage > 1) {
|
||||
currentPage--;
|
||||
renderRulesList();
|
||||
}
|
||||
}
|
||||
|
||||
// 下一页
|
||||
function goToNextPage() {
|
||||
const totalPages = Math.ceil(filteredRules.length / itemsPerPage);
|
||||
if (currentPage < totalPages) {
|
||||
currentPage++;
|
||||
renderRulesList();
|
||||
}
|
||||
}
|
||||
|
||||
// 第一页
|
||||
function goToFirstPage() {
|
||||
currentPage = 1;
|
||||
renderRulesList();
|
||||
}
|
||||
|
||||
// 最后一页
|
||||
function goToLastPage() {
|
||||
currentPage = Math.ceil(filteredRules.length / itemsPerPage);
|
||||
renderRulesList();
|
||||
}
|
||||
|
||||
// 添加新规则
|
||||
async function addNewRule() {
|
||||
const ruleInput = document.getElementById('rule-input');
|
||||
const rule = ruleInput.value.trim();
|
||||
|
||||
if (!rule) {
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('请输入规则内容', 'warning');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 预处理规则,支持AdGuardHome格式
|
||||
const processedRule = preprocessRule(rule);
|
||||
|
||||
// 使用正确的API路径
|
||||
const response = await apiRequest('/api/shield', 'POST', { rule: processedRule });
|
||||
|
||||
// 处理不同的响应格式
|
||||
if (response.success || response.status === 'success') {
|
||||
rules.push(processedRule);
|
||||
filteredRules = [...rules];
|
||||
ruleInput.value = '';
|
||||
|
||||
// 添加后跳转到最后一页,显示新添加的规则
|
||||
currentPage = Math.ceil(filteredRules.length / itemsPerPage);
|
||||
renderRulesList();
|
||||
|
||||
// 更新规则数量统计
|
||||
if (window.updateRulesCount && typeof window.updateRulesCount === 'function') {
|
||||
window.updateRulesCount(rules.length);
|
||||
}
|
||||
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('规则添加成功', 'success');
|
||||
}
|
||||
} else {
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('规则添加失败:' + (response.message || '未知错误'), 'danger');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('添加规则失败:', error);
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('添加规则失败', 'danger');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除规则
|
||||
async function deleteRule(index) {
|
||||
if (!confirm('确定要删除这条规则吗?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const rule = filteredRules[index];
|
||||
const rowElement = document.querySelectorAll('#rules-list tr')[index];
|
||||
|
||||
// 添加删除动画
|
||||
if (rowElement) {
|
||||
rowElement.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
||||
rowElement.style.opacity = '0';
|
||||
rowElement.style.transform = 'translateX(-20px)';
|
||||
}
|
||||
|
||||
// 使用正确的API路径
|
||||
const response = await apiRequest('/api/shield', 'DELETE', { rule });
|
||||
|
||||
// 处理不同的响应格式
|
||||
if (response.success || response.status === 'success') {
|
||||
// 在原规则列表中找到并删除
|
||||
const originalIndex = rules.indexOf(rule);
|
||||
if (originalIndex !== -1) {
|
||||
rules.splice(originalIndex, 1);
|
||||
}
|
||||
|
||||
// 在过滤后的列表中删除
|
||||
filteredRules.splice(index, 1);
|
||||
|
||||
// 如果当前页没有数据了,回到上一页
|
||||
const totalPages = Math.ceil(filteredRules.length / itemsPerPage);
|
||||
if (currentPage > totalPages && totalPages > 0) {
|
||||
currentPage = totalPages;
|
||||
}
|
||||
|
||||
// 等待动画完成后重新渲染列表
|
||||
setTimeout(() => {
|
||||
renderRulesList();
|
||||
|
||||
// 更新规则数量统计
|
||||
if (window.updateRulesCount && typeof window.updateRulesCount === 'function') {
|
||||
window.updateRulesCount(rules.length);
|
||||
}
|
||||
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('规则删除成功', 'success');
|
||||
}
|
||||
}, 300);
|
||||
} else {
|
||||
// 恢复行样式
|
||||
if (rowElement) {
|
||||
rowElement.style.opacity = '1';
|
||||
rowElement.style.transform = 'translateX(0)';
|
||||
}
|
||||
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('规则删除失败:' + (response.message || '未知错误'), 'danger');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除规则失败:', error);
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('删除规则失败', 'danger');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 重新加载规则
|
||||
async function reloadRules() {
|
||||
if (!confirm('确定要重新加载所有规则吗?这将覆盖当前内存中的规则。')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const rulesPanel = document.getElementById('rules-panel');
|
||||
showLoading(rulesPanel);
|
||||
|
||||
// 使用正确的API路径和方法 - PUT请求到/api/shield
|
||||
await apiRequest('/api/shield', 'PUT');
|
||||
|
||||
// 重新加载规则列表
|
||||
await loadRules();
|
||||
|
||||
// 触发数据刷新事件,通知其他模块数据已更新
|
||||
if (typeof window.triggerDataRefresh === 'function') {
|
||||
window.triggerDataRefresh('rules');
|
||||
}
|
||||
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('规则重新加载成功', 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重新加载规则失败:', error);
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('重新加载规则失败', 'danger');
|
||||
}
|
||||
} finally {
|
||||
const rulesPanel = document.getElementById('rules-panel');
|
||||
hideLoading(rulesPanel);
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤规则
|
||||
function filterRules() {
|
||||
const searchTerm = document.getElementById('rule-search').value.toLowerCase();
|
||||
|
||||
if (searchTerm) {
|
||||
filteredRules = rules.filter(rule => rule.toLowerCase().includes(searchTerm));
|
||||
} else {
|
||||
filteredRules = [...rules];
|
||||
}
|
||||
|
||||
currentPage = 1; // 重置为第一页
|
||||
renderRulesList();
|
||||
}
|
||||
|
||||
// HTML转义,防止XSS攻击
|
||||
function escapeHtml(text) {
|
||||
const map = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return text.replace(/[&<>'"]/g, m => map[m]);
|
||||
}
|
||||
|
||||
// 根据规则类型返回对应的CSS类名
|
||||
function getRuleTypeClass(rule) {
|
||||
// 简单的规则类型判断
|
||||
if (rule.startsWith('||') || rule.startsWith('|http')) {
|
||||
return 'rule-type-url';
|
||||
} else if (rule.startsWith('@@')) {
|
||||
return 'rule-type-exception';
|
||||
} else if (rule.startsWith('#')) {
|
||||
return 'rule-type-comment';
|
||||
} else if (rule.includes('$')) {
|
||||
return 'rule-type-filter';
|
||||
}
|
||||
return 'rule-type-standard';
|
||||
}
|
||||
|
||||
// 预处理规则,支持多种规则格式
|
||||
function preprocessRule(rule) {
|
||||
// 移除首尾空白字符
|
||||
let processed = rule.trim();
|
||||
|
||||
// 处理AdGuardHome格式的规则
|
||||
if (processed.startsWith('0.0.0.0 ') || processed.startsWith('127.0.0.1 ')) {
|
||||
const parts = processed.split(' ');
|
||||
if (parts.length >= 2) {
|
||||
// 转换为AdBlock Plus格式
|
||||
processed = '||' + parts[1] + '^';
|
||||
}
|
||||
}
|
||||
|
||||
return processed;
|
||||
}
|
||||
|
||||
// 导出函数,供其他模块调用
|
||||
window.updateRulesCount = function(count) {
|
||||
const rulesCountElement = document.getElementById('rules-count');
|
||||
if (rulesCountElement) {
|
||||
rulesCountElement.textContent = count;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出初始化函数
|
||||
window.initRulesPanel = initRulesPanel;
|
||||
|
||||
// 注册到面板导航系统
|
||||
if (window.registerPanelModule) {
|
||||
window.registerPanelModule('rules-panel', {
|
||||
init: initRulesPanel,
|
||||
refresh: loadRules
|
||||
});
|
||||
}
|
||||
@@ -1,301 +0,0 @@
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,305 +0,0 @@
|
||||
// 服务器状态组件 - 显示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
1302
static/js/shield.js
File diff suppressed because it is too large
Load Diff
1
static/js/vendor/chart.umd.min.js
vendored
1
static/js/vendor/chart.umd.min.js
vendored
File diff suppressed because one or more lines are too long
19
static/js/vendor/tailwind.js
vendored
19
static/js/vendor/tailwind.js
vendored
@@ -1,19 +0,0 @@
|
||||
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'],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,24 +0,0 @@
|
||||
/** @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: [],
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# DNS Web控制台功能测试脚本
|
||||
echo "开始测试DNS Web控制台功能..."
|
||||
echo "=================================="
|
||||
|
||||
# 检查服务器是否运行
|
||||
echo "检查DNS服务器运行状态..."
|
||||
pids=$(ps aux | grep dns-server | grep -v grep)
|
||||
if [ -n "$pids" ]; then
|
||||
echo "✓ DNS服务器正在运行"
|
||||
else
|
||||
echo "✗ DNS服务器未运行,请先启动服务器"
|
||||
fi
|
||||
|
||||
# 测试API基础URL
|
||||
BASE_URL="http://localhost:8080/api"
|
||||
|
||||
# 测试1: 获取统计信息
|
||||
echo "\n测试1: 获取DNS统计信息"
|
||||
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/stats"
|
||||
|
||||
# 测试2: 获取系统状态
|
||||
echo "\n测试2: 获取系统状态"
|
||||
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/status"
|
||||
|
||||
# 测试3: 获取屏蔽规则
|
||||
echo "\n测试3: 获取屏蔽规则列表"
|
||||
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/shield"
|
||||
|
||||
# 测试4: 获取Top屏蔽域名
|
||||
echo "\n测试4: 获取Top屏蔽域名"
|
||||
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/top-blocked"
|
||||
|
||||
# 测试5: 获取Hosts内容
|
||||
echo "\n测试5: 获取Hosts内容"
|
||||
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/shield/hosts"
|
||||
|
||||
# 测试6: 访问Web控制台主页
|
||||
echo "\n测试6: 访问Web控制台主页"
|
||||
curl -s -o /dev/null -w "状态码: %{http_code}\n" "http://localhost:8080"
|
||||
|
||||
echo "\n=================================="
|
||||
echo "测试完成!请检查上述状态码。正常情况下应为200。"
|
||||
echo "前端Web控制台可通过浏览器访问: http://localhost:8080"
|
||||
Reference in New Issue
Block a user