修复规则问题
This commit is contained in:
@@ -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查询结果的各种网络环境。
|
||||||
@@ -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查询结果的各种网络环境。
|
||||||
+3
-56
@@ -12,9 +12,7 @@
|
|||||||
"http": {
|
"http": {
|
||||||
"port": 8080,
|
"port": 8080,
|
||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
"enableAPI": true,
|
"enableAPI": true
|
||||||
"username": "admin",
|
|
||||||
"password": "admin"
|
|
||||||
},
|
},
|
||||||
"shield": {
|
"shield": {
|
||||||
"localRulesFile": "data/rules.txt",
|
"localRulesFile": "data/rules.txt",
|
||||||
@@ -40,64 +38,13 @@
|
|||||||
{
|
{
|
||||||
"name": "My GitHub Rules",
|
"name": "My GitHub Rules",
|
||||||
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/costomize.txt",
|
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/costomize.txt",
|
||||||
"enabled": true,
|
"enabled": false,
|
||||||
"lastUpdateTime": "2025-11-29T17:05:40.283Z"
|
"lastUpdateTime": "2025-11-28T16:13:05.960Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "CNList",
|
"name": "CNList",
|
||||||
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/list/china.list",
|
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/list/china.list",
|
||||||
"enabled": false
|
"enabled": false
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "大圣净化",
|
|
||||||
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/dsjh.txt",
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Hate \u0026 Junk",
|
|
||||||
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hate-and-junk-extended.txt",
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "My Gitlab Hosts",
|
|
||||||
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hosts/costomize.txt",
|
|
||||||
"enabled": true,
|
|
||||||
"lastUpdateTime": "2025-11-29T17:11:28.130Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Anti Remote Requests",
|
|
||||||
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hosts/anti-remoterequests.txt",
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "URL-Based.txt",
|
|
||||||
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/url-based-adguard.txt",
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "My Gitlab A/T Rules",
|
|
||||||
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/ads-and-trackers.txt",
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "My Gitlab Malware List",
|
|
||||||
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/malware.txt",
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "hosts",
|
|
||||||
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/costomize.txt",
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "AWAvenue-Ads-Rule",
|
|
||||||
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/rules/AWAvenue-Ads-Rule.txt",
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "诈骗域名",
|
|
||||||
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/cheat.txt",
|
|
||||||
"enabled": true
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updateInterval": 3600,
|
"updateInterval": 3600,
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ type DNSConfig struct {
|
|||||||
Timeout int `json:"timeout"`
|
Timeout int `json:"timeout"`
|
||||||
StatsFile string `json:"statsFile"` // 统计数据持久化文件
|
StatsFile string `json:"statsFile"` // 统计数据持久化文件
|
||||||
SaveInterval int `json:"saveInterval"` // 数据保存间隔(秒)
|
SaveInterval int `json:"saveInterval"` // 数据保存间隔(秒)
|
||||||
CacheTTL int `json:"cacheTTL"` // DNS缓存过期时间(分钟)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPConfig HTTP控制台配置
|
// HTTPConfig HTTP控制台配置
|
||||||
@@ -20,8 +19,6 @@ type HTTPConfig struct {
|
|||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
EnableAPI bool `json:"enableAPI"`
|
EnableAPI bool `json:"enableAPI"`
|
||||||
Username string `json:"username"` // 登录用户名
|
|
||||||
Password string `json:"password"` // 登录密码
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BlacklistEntry 黑名单条目
|
// BlacklistEntry 黑名单条目
|
||||||
@@ -89,23 +86,12 @@ func LoadConfig(path string) (*Config, error) {
|
|||||||
if config.DNS.SaveInterval == 0 {
|
if config.DNS.SaveInterval == 0 {
|
||||||
config.DNS.SaveInterval = 300 // 默认5分钟保存一次
|
config.DNS.SaveInterval = 300 // 默认5分钟保存一次
|
||||||
}
|
}
|
||||||
// 默认DNS缓存TTL为30分钟
|
|
||||||
if config.DNS.CacheTTL == 0 {
|
|
||||||
config.DNS.CacheTTL = 30 // 默认30分钟
|
|
||||||
}
|
|
||||||
if config.HTTP.Port == 0 {
|
if config.HTTP.Port == 0 {
|
||||||
config.HTTP.Port = 8080
|
config.HTTP.Port = 8080
|
||||||
}
|
}
|
||||||
if config.HTTP.Host == "" {
|
if config.HTTP.Host == "" {
|
||||||
config.HTTP.Host = "0.0.0.0"
|
config.HTTP.Host = "0.0.0.0"
|
||||||
}
|
}
|
||||||
// 默认用户名和密码,如果未配置则使用admin/admin
|
|
||||||
if config.HTTP.Username == "" {
|
|
||||||
config.HTTP.Username = "admin"
|
|
||||||
}
|
|
||||||
if config.HTTP.Password == "" {
|
|
||||||
config.HTTP.Password = "admin"
|
|
||||||
}
|
|
||||||
if config.Shield.UpdateInterval == 0 {
|
if config.Shield.UpdateInterval == 0 {
|
||||||
config.Shield.UpdateInterval = 3600
|
config.Shield.UpdateInterval = 3600
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# DNS Server Hosts File
|
||||||
|
# Generated by DNS Server
|
||||||
|
|
||||||
|
127.0.0.1 localhost
|
||||||
|
::1 localhost
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang="en"><head><title>Example Domain</title><meta name="viewport" content="width=device-width, initial-scale=1"><style>body{background:#eee;width:60vw;margin:15vh auto;font-family:system-ui,sans-serif}h1{font-size:1.5em}div{opacity:0.8}a:link,a:visited{color:#348}</style><body><div><h1>Example Domain</h1><p>This domain is for use in documentation examples without needing permission. Avoid use in operations.<p><a href="https://iana.org/domains/example">Learn more</a></div></body></html>
|
||||||
+10607
File diff suppressed because it is too large
Load Diff
+82585
File diff suppressed because it is too large
Load Diff
+53291
File diff suppressed because it is too large
Load Diff
+68590
File diff suppressed because it is too large
Load Diff
+1176
File diff suppressed because it is too large
Load Diff
+549303
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
|||||||
|
@@||so.com
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"blockedDomainsCount": {},
|
||||||
|
"resolvedDomainsCount": {},
|
||||||
|
"lastSaved": "2025-12-15T16:03:57.683197046+08:00"
|
||||||
|
}
|
||||||
+5514
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
+34
-529
@@ -6,7 +6,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -38,27 +37,6 @@ type ClientStats struct {
|
|||||||
LastSeen time.Time
|
LastSeen time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// IPGeolocation IP地理位置信息
|
|
||||||
type IPGeolocation struct {
|
|
||||||
Country string `json:"country"` // 国家
|
|
||||||
City string `json:"city"` // 城市
|
|
||||||
Expiry time.Time `json:"expiry"` // 缓存过期时间
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryLog 查询日志记录
|
|
||||||
type QueryLog struct {
|
|
||||||
Timestamp time.Time // 查询时间
|
|
||||||
ClientIP string // 客户端IP
|
|
||||||
Location string // IP地理位置(国家 城市)
|
|
||||||
Domain string // 查询域名
|
|
||||||
QueryType string // 查询类型
|
|
||||||
ResponseTime int64 // 响应时间(ms)
|
|
||||||
Result string // 查询结果(allowed, blocked, error)
|
|
||||||
BlockRule string // 屏蔽规则(如果被屏蔽)
|
|
||||||
BlockType string // 屏蔽类型(如果被屏蔽)
|
|
||||||
FromCache bool // 是否来自缓存
|
|
||||||
}
|
|
||||||
|
|
||||||
// StatsData 用于持久化的统计数据结构
|
// StatsData 用于持久化的统计数据结构
|
||||||
type StatsData struct {
|
type StatsData struct {
|
||||||
Stats *Stats `json:"stats"`
|
Stats *Stats `json:"stats"`
|
||||||
@@ -95,22 +73,11 @@ type Server struct {
|
|||||||
dailyStats map[string]int64 // 按天统计屏蔽数量
|
dailyStats map[string]int64 // 按天统计屏蔽数量
|
||||||
monthlyStatsMutex sync.RWMutex
|
monthlyStatsMutex sync.RWMutex
|
||||||
monthlyStats map[string]int64 // 按月统计屏蔽数量
|
monthlyStats map[string]int64 // 按月统计屏蔽数量
|
||||||
queryLogsMutex sync.RWMutex
|
|
||||||
queryLogs []QueryLog // 查询日志列表
|
|
||||||
maxQueryLogs int // 最大保存日志数量
|
|
||||||
saveTicker *time.Ticker // 用于定时保存数据
|
saveTicker *time.Ticker // 用于定时保存数据
|
||||||
startTime time.Time // 服务器启动时间
|
startTime time.Time // 服务器启动时间
|
||||||
saveDone chan struct{} // 用于通知保存协程停止
|
saveDone chan struct{} // 用于通知保存协程停止
|
||||||
stopped bool // 服务器是否已经停止
|
stopped bool // 服务器是否已经停止
|
||||||
stoppedMutex sync.Mutex // 保护stopped标志的互斥锁
|
stoppedMutex sync.Mutex // 保护stopped标志的互斥锁
|
||||||
|
|
||||||
// IP地理位置缓存
|
|
||||||
ipGeolocationCache map[string]*IPGeolocation // IP地址到地理位置的映射
|
|
||||||
ipGeolocationCacheMutex sync.RWMutex // 保护IP地理位置缓存的互斥锁
|
|
||||||
ipGeolocationCacheTTL time.Duration // 缓存有效期
|
|
||||||
|
|
||||||
// DNS查询缓存
|
|
||||||
dnsCache *DNSCache // DNS响应缓存
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stats DNS服务器统计信息
|
// Stats DNS服务器统计信息
|
||||||
@@ -130,10 +97,6 @@ type Stats struct {
|
|||||||
// NewServer 创建DNS服务器实例
|
// NewServer 创建DNS服务器实例
|
||||||
func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shieldManager *shield.ShieldManager) *Server {
|
func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shieldManager *shield.ShieldManager) *Server {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
// 从配置中读取DNS缓存TTL值(分钟)
|
|
||||||
cacheTTL := time.Duration(config.CacheTTL) * time.Minute
|
|
||||||
|
|
||||||
server := &Server{
|
server := &Server{
|
||||||
config: config,
|
config: config,
|
||||||
shieldConfig: shieldConfig,
|
shieldConfig: shieldConfig,
|
||||||
@@ -162,15 +125,8 @@ func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shie
|
|||||||
hourlyStats: make(map[string]int64),
|
hourlyStats: make(map[string]int64),
|
||||||
dailyStats: make(map[string]int64),
|
dailyStats: make(map[string]int64),
|
||||||
monthlyStats: make(map[string]int64),
|
monthlyStats: make(map[string]int64),
|
||||||
queryLogs: make([]QueryLog, 0, 1000), // 初始化查询日志切片,容量1000
|
|
||||||
maxQueryLogs: 10000, // 最大保存10000条日志
|
|
||||||
saveDone: make(chan struct{}),
|
saveDone: make(chan struct{}),
|
||||||
stopped: false, // 初始化为未停止状态
|
stopped: false, // 初始化为未停止状态
|
||||||
// IP地理位置缓存初始化
|
|
||||||
ipGeolocationCache: make(map[string]*IPGeolocation),
|
|
||||||
ipGeolocationCacheTTL: 24 * time.Hour, // 缓存有效期24小时
|
|
||||||
// DNS查询缓存初始化
|
|
||||||
dnsCache: NewDNSCache(cacheTTL),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载已保存的统计数据
|
// 加载已保存的统计数据
|
||||||
@@ -276,17 +232,9 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
// 获取来源IP
|
// 获取来源IP
|
||||||
sourceIP := w.RemoteAddr().String()
|
sourceIP := w.RemoteAddr().String()
|
||||||
// 提取IP地址部分,去掉端口
|
// 提取IP地址部分,去掉端口
|
||||||
if strings.HasPrefix(sourceIP, "[") {
|
|
||||||
// IPv6地址格式: [::1]:53
|
|
||||||
if idx := strings.Index(sourceIP, "]"); idx >= 0 {
|
|
||||||
sourceIP = sourceIP[1:idx] // 去掉方括号
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// IPv4地址格式: 127.0.0.1:53
|
|
||||||
if idx := strings.LastIndex(sourceIP, ":"); idx >= 0 {
|
if idx := strings.LastIndex(sourceIP, ":"); idx >= 0 {
|
||||||
sourceIP = sourceIP[:idx]
|
sourceIP = sourceIP[:idx]
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 更新来源IP统计
|
// 更新来源IP统计
|
||||||
s.updateStats(func(stats *Stats) {
|
s.updateStats(func(stats *Stats) {
|
||||||
@@ -298,27 +246,6 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
// 更新客户端统计
|
// 更新客户端统计
|
||||||
s.updateClientStats(sourceIP)
|
s.updateClientStats(sourceIP)
|
||||||
|
|
||||||
// 获取查询域名和类型
|
|
||||||
var domain string
|
|
||||||
var queryType string
|
|
||||||
var qType uint16
|
|
||||||
if len(r.Question) > 0 {
|
|
||||||
domain = r.Question[0].Name
|
|
||||||
// 移除末尾的点
|
|
||||||
if len(domain) > 0 && domain[len(domain)-1] == '.' {
|
|
||||||
domain = domain[:len(domain)-1]
|
|
||||||
}
|
|
||||||
// 获取查询类型
|
|
||||||
queryType = dns.TypeToString[r.Question[0].Qtype]
|
|
||||||
qType = r.Question[0].Qtype
|
|
||||||
// 更新查询类型统计
|
|
||||||
s.updateStats(func(stats *Stats) {
|
|
||||||
stats.QueryTypes[queryType]++
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debug("接收到DNS查询", "domain", domain, "type", queryType, "client", w.RemoteAddr())
|
|
||||||
|
|
||||||
// 只处理递归查询
|
// 只处理递归查询
|
||||||
if r.RecursionDesired == false {
|
if r.RecursionDesired == false {
|
||||||
response := new(dns.Msg)
|
response := new(dns.Msg)
|
||||||
@@ -327,44 +254,52 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
response.SetRcode(r, dns.RcodeRefused)
|
response.SetRcode(r, dns.RcodeRefused)
|
||||||
w.WriteMsg(response)
|
w.WriteMsg(response)
|
||||||
|
|
||||||
// 缓存命中,响应时间设为0ms
|
// 计算响应时间
|
||||||
responseTime := int64(0)
|
responseTime := time.Since(startTime).Milliseconds()
|
||||||
s.updateStats(func(stats *Stats) {
|
s.updateStats(func(stats *Stats) {
|
||||||
stats.TotalResponseTime += responseTime
|
stats.TotalResponseTime += responseTime
|
||||||
if stats.Queries > 0 {
|
if stats.Queries > 0 {
|
||||||
stats.AvgResponseTime = float64(stats.TotalResponseTime) / float64(stats.Queries)
|
stats.AvgResponseTime = float64(stats.TotalResponseTime) / float64(stats.Queries)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 添加查询日志
|
|
||||||
s.addQueryLog(sourceIP, domain, queryType, responseTime, "error", "", "", false)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取查询域名和类型
|
||||||
|
var domain string
|
||||||
|
var queryType string
|
||||||
|
if len(r.Question) > 0 {
|
||||||
|
domain = r.Question[0].Name
|
||||||
|
// 移除末尾的点
|
||||||
|
if len(domain) > 0 && domain[len(domain)-1] == '.' {
|
||||||
|
domain = domain[:len(domain)-1]
|
||||||
|
}
|
||||||
|
// 获取查询类型
|
||||||
|
queryType = dns.TypeToString[r.Question[0].Qtype]
|
||||||
|
// 更新查询类型统计
|
||||||
|
s.updateStats(func(stats *Stats) {
|
||||||
|
stats.QueryTypes[queryType]++
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("接收到DNS查询", "domain", domain, "type", queryType, "client", w.RemoteAddr())
|
||||||
|
|
||||||
// 检查hosts文件是否有匹配
|
// 检查hosts文件是否有匹配
|
||||||
if ip, exists := s.shieldManager.GetHostsIP(domain); exists {
|
if ip, exists := s.shieldManager.GetHostsIP(domain); exists {
|
||||||
s.handleHostsResponse(w, r, ip)
|
s.handleHostsResponse(w, r, ip)
|
||||||
// 缓存命中,响应时间设为0ms
|
// 计算响应时间
|
||||||
responseTime := int64(0)
|
responseTime := time.Since(startTime).Milliseconds()
|
||||||
s.updateStats(func(stats *Stats) {
|
s.updateStats(func(stats *Stats) {
|
||||||
stats.TotalResponseTime += responseTime
|
stats.TotalResponseTime += responseTime
|
||||||
if stats.Queries > 0 {
|
if stats.Queries > 0 {
|
||||||
stats.AvgResponseTime = float64(stats.TotalResponseTime) / float64(stats.Queries)
|
stats.AvgResponseTime = float64(stats.TotalResponseTime) / float64(stats.Queries)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 添加查询日志
|
|
||||||
s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", false)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否被屏蔽
|
// 检查是否被屏蔽
|
||||||
if s.shieldManager.IsBlocked(domain) {
|
if s.shieldManager.IsBlocked(domain) {
|
||||||
// 获取屏蔽详情
|
|
||||||
blockDetails := s.shieldManager.CheckDomainBlockDetails(domain)
|
|
||||||
blockRule, _ := blockDetails["blockRule"].(string)
|
|
||||||
blockType, _ := blockDetails["blockRuleType"].(string)
|
|
||||||
|
|
||||||
s.handleBlockedResponse(w, r, domain)
|
s.handleBlockedResponse(w, r, domain)
|
||||||
// 计算响应时间
|
// 计算响应时间
|
||||||
responseTime := time.Since(startTime).Milliseconds()
|
responseTime := time.Since(startTime).Milliseconds()
|
||||||
@@ -374,20 +309,11 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
stats.AvgResponseTime = float64(stats.TotalResponseTime) / float64(stats.Queries)
|
stats.AvgResponseTime = float64(stats.TotalResponseTime) / float64(stats.Queries)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 添加查询日志
|
|
||||||
s.addQueryLog(sourceIP, domain, queryType, responseTime, "blocked", blockRule, blockType, false)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查缓存中是否有响应(增强版缓存查询)
|
// 转发到上游DNS服务器
|
||||||
if cachedResponse, found := s.dnsCache.Get(r.Question[0].Name, qType); found {
|
s.forwardDNSRequest(w, r, domain)
|
||||||
// 缓存命中,直接返回缓存的响应
|
|
||||||
cachedResponseCopy := cachedResponse.Copy() // 创建响应副本避免并发修改问题
|
|
||||||
cachedResponseCopy.Id = r.Id // 更新ID以匹配请求
|
|
||||||
cachedResponseCopy.Compress = true
|
|
||||||
w.WriteMsg(cachedResponseCopy)
|
|
||||||
|
|
||||||
// 计算响应时间
|
// 计算响应时间
|
||||||
responseTime := time.Since(startTime).Milliseconds()
|
responseTime := time.Since(startTime).Milliseconds()
|
||||||
s.updateStats(func(stats *Stats) {
|
s.updateStats(func(stats *Stats) {
|
||||||
@@ -396,46 +322,6 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
stats.AvgResponseTime = float64(stats.TotalResponseTime) / float64(stats.Queries)
|
stats.AvgResponseTime = float64(stats.TotalResponseTime) / float64(stats.Queries)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 添加查询日志 - 标记为缓存
|
|
||||||
s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", true)
|
|
||||||
logger.Debug("从缓存返回DNS响应", "domain", domain, "type", queryType)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 缓存未命中,转发到上游DNS服务器
|
|
||||||
response, rtt := s.forwardDNSRequestWithCache(r, domain)
|
|
||||||
if response != nil {
|
|
||||||
// 写入响应给客户端
|
|
||||||
w.WriteMsg(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用上游服务器的实际响应时间(转换为毫秒)
|
|
||||||
responseTime := int64(rtt.Milliseconds())
|
|
||||||
// 如果rtt为0(查询失败),则使用本地计算的时间
|
|
||||||
if responseTime == 0 {
|
|
||||||
responseTime = time.Since(startTime).Milliseconds()
|
|
||||||
}
|
|
||||||
|
|
||||||
s.updateStats(func(stats *Stats) {
|
|
||||||
stats.TotalResponseTime += responseTime
|
|
||||||
if stats.Queries > 0 {
|
|
||||||
stats.AvgResponseTime = float64(stats.TotalResponseTime) / float64(stats.Queries)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 如果响应成功,缓存结果(增强版缓存存储)
|
|
||||||
if response != nil && response.Rcode == dns.RcodeSuccess {
|
|
||||||
// 创建响应副本以避免后续修改影响缓存
|
|
||||||
responseCopy := response.Copy()
|
|
||||||
// 设置合理的TTL,不超过默认的30分钟
|
|
||||||
defaultCacheTTL := 30 * time.Minute
|
|
||||||
s.dnsCache.Set(r.Question[0].Name, qType, responseCopy, defaultCacheTTL)
|
|
||||||
logger.Debug("DNS响应已缓存", "domain", domain, "type", queryType, "ttl", defaultCacheTTL)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加查询日志 - 标记为实时
|
|
||||||
s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleHostsResponse 处理hosts文件匹配的响应
|
// handleHostsResponse 处理hosts文件匹配的响应
|
||||||
@@ -480,6 +366,11 @@ func (s *Server) handleBlockedResponse(w dns.ResponseWriter, r *dns.Msg, domain
|
|||||||
// 更新被屏蔽域名统计
|
// 更新被屏蔽域名统计
|
||||||
s.updateBlockedDomainStats(domain)
|
s.updateBlockedDomainStats(domain)
|
||||||
|
|
||||||
|
// 更新总体统计
|
||||||
|
s.updateStats(func(stats *Stats) {
|
||||||
|
stats.Blocked++
|
||||||
|
})
|
||||||
|
|
||||||
response := new(dns.Msg)
|
response := new(dns.Msg)
|
||||||
response.SetReply(r)
|
response.SetReply(r)
|
||||||
response.RecursionAvailable = true
|
response.RecursionAvailable = true
|
||||||
@@ -539,8 +430,7 @@ func (s *Server) handleBlockedResponse(w dns.ResponseWriter, r *dns.Msg, domain
|
|||||||
}
|
}
|
||||||
|
|
||||||
// forwardDNSRequest 转发DNS请求到上游服务器
|
// forwardDNSRequest 转发DNS请求到上游服务器
|
||||||
// forwardDNSRequestWithCache 转发DNS请求到上游服务器并返回响应
|
func (s *Server) forwardDNSRequest(w dns.ResponseWriter, r *dns.Msg, domain string) {
|
||||||
func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg, time.Duration) {
|
|
||||||
// 尝试所有上游DNS服务器
|
// 尝试所有上游DNS服务器
|
||||||
for _, upstream := range s.config.UpstreamDNS {
|
for _, upstream := range s.config.UpstreamDNS {
|
||||||
response, rtt, err := s.resolver.Exchange(r, upstream)
|
response, rtt, err := s.resolver.Exchange(r, upstream)
|
||||||
@@ -548,6 +438,7 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
|
|||||||
// 设置递归可用标志
|
// 设置递归可用标志
|
||||||
response.RecursionAvailable = true
|
response.RecursionAvailable = true
|
||||||
|
|
||||||
|
w.WriteMsg(response)
|
||||||
logger.Debug("DNS查询成功", "domain", domain, "rtt", rtt, "server", upstream)
|
logger.Debug("DNS查询成功", "domain", domain, "rtt", rtt, "server", upstream)
|
||||||
|
|
||||||
// 记录解析域名统计
|
// 记录解析域名统计
|
||||||
@@ -556,7 +447,7 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
|
|||||||
s.updateStats(func(stats *Stats) {
|
s.updateStats(func(stats *Stats) {
|
||||||
stats.Allowed++
|
stats.Allowed++
|
||||||
})
|
})
|
||||||
return response, rtt
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,18 +456,12 @@ func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg
|
|||||||
response.SetReply(r)
|
response.SetReply(r)
|
||||||
response.RecursionAvailable = true
|
response.RecursionAvailable = true
|
||||||
response.SetRcode(r, dns.RcodeServerFailure)
|
response.SetRcode(r, dns.RcodeServerFailure)
|
||||||
|
w.WriteMsg(response)
|
||||||
|
|
||||||
logger.Error("DNS查询失败", "domain", domain)
|
logger.Error("DNS查询失败", "domain", domain)
|
||||||
s.updateStats(func(stats *Stats) {
|
s.updateStats(func(stats *Stats) {
|
||||||
stats.Errors++
|
stats.Errors++
|
||||||
})
|
})
|
||||||
return response, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// forwardDNSRequest 转发DNS请求到上游服务器
|
|
||||||
func (s *Server) forwardDNSRequest(w dns.ResponseWriter, r *dns.Msg, domain string) {
|
|
||||||
response, _ := s.forwardDNSRequestWithCache(r, domain)
|
|
||||||
w.WriteMsg(response)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateBlockedDomainStats 更新被屏蔽域名统计
|
// updateBlockedDomainStats 更新被屏蔽域名统计
|
||||||
@@ -659,38 +544,6 @@ func (s *Server) updateStats(update func(*Stats)) {
|
|||||||
update(s.stats)
|
update(s.stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
// addQueryLog 添加查询日志
|
|
||||||
func (s *Server) addQueryLog(clientIP, domain, queryType string, responseTime int64, result, blockRule, blockType string, fromCache bool) {
|
|
||||||
// 获取IP地理位置
|
|
||||||
location := s.getIpGeolocation(clientIP)
|
|
||||||
|
|
||||||
// 创建日志记录
|
|
||||||
log := QueryLog{
|
|
||||||
Timestamp: time.Now(),
|
|
||||||
ClientIP: clientIP,
|
|
||||||
Location: location,
|
|
||||||
Domain: domain,
|
|
||||||
QueryType: queryType,
|
|
||||||
ResponseTime: responseTime,
|
|
||||||
Result: result,
|
|
||||||
BlockRule: blockRule,
|
|
||||||
BlockType: blockType,
|
|
||||||
FromCache: fromCache,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加到日志列表
|
|
||||||
s.queryLogsMutex.Lock()
|
|
||||||
defer s.queryLogsMutex.Unlock()
|
|
||||||
|
|
||||||
// 插入到列表开头
|
|
||||||
s.queryLogs = append([]QueryLog{log}, s.queryLogs...)
|
|
||||||
|
|
||||||
// 限制日志数量
|
|
||||||
if len(s.queryLogs) > s.maxQueryLogs {
|
|
||||||
s.queryLogs = s.queryLogs[:s.maxQueryLogs]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStartTime 获取服务器启动时间
|
// GetStartTime 获取服务器启动时间
|
||||||
func (s *Server) GetStartTime() time.Time {
|
func (s *Server) GetStartTime() time.Time {
|
||||||
return s.startTime
|
return s.startTime
|
||||||
@@ -728,145 +581,6 @@ func (s *Server) GetStats() *Stats {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetQueryLogs 获取查询日志
|
|
||||||
func (s *Server) GetQueryLogs(limit, offset int, sortField, sortDirection, resultFilter, searchTerm string) []QueryLog {
|
|
||||||
s.queryLogsMutex.RLock()
|
|
||||||
defer s.queryLogsMutex.RUnlock()
|
|
||||||
|
|
||||||
// 确保偏移量和限制值合理
|
|
||||||
if offset < 0 {
|
|
||||||
offset = 0
|
|
||||||
}
|
|
||||||
if limit <= 0 {
|
|
||||||
limit = 100 // 默认返回100条日志
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建日志副本用于过滤和排序
|
|
||||||
var logsCopy []QueryLog
|
|
||||||
|
|
||||||
// 先过滤日志
|
|
||||||
for _, log := range s.queryLogs {
|
|
||||||
// 应用结果过滤
|
|
||||||
if resultFilter != "" && log.Result != resultFilter {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 应用搜索过滤
|
|
||||||
if searchTerm != "" {
|
|
||||||
// 搜索域名或客户端IP
|
|
||||||
if !strings.Contains(log.Domain, searchTerm) && !strings.Contains(log.ClientIP, searchTerm) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logsCopy = append(logsCopy, log)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 排序日志
|
|
||||||
if sortField != "" {
|
|
||||||
sort.Slice(logsCopy, func(i, j int) bool {
|
|
||||||
var a, b interface{}
|
|
||||||
switch sortField {
|
|
||||||
case "time":
|
|
||||||
a = logsCopy[i].Timestamp
|
|
||||||
b = logsCopy[j].Timestamp
|
|
||||||
case "clientIp":
|
|
||||||
a = logsCopy[i].ClientIP
|
|
||||||
b = logsCopy[j].ClientIP
|
|
||||||
case "domain":
|
|
||||||
a = logsCopy[i].Domain
|
|
||||||
b = logsCopy[j].Domain
|
|
||||||
case "responseTime":
|
|
||||||
a = logsCopy[i].ResponseTime
|
|
||||||
b = logsCopy[j].ResponseTime
|
|
||||||
case "blockRule":
|
|
||||||
a = logsCopy[i].BlockRule
|
|
||||||
b = logsCopy[j].BlockRule
|
|
||||||
default:
|
|
||||||
// 默认按时间排序
|
|
||||||
a = logsCopy[i].Timestamp
|
|
||||||
b = logsCopy[j].Timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据排序方向比较
|
|
||||||
if sortDirection == "asc" {
|
|
||||||
return compareValues(a, b) < 0
|
|
||||||
}
|
|
||||||
return compareValues(a, b) > 0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算返回范围
|
|
||||||
start := offset
|
|
||||||
end := offset + limit
|
|
||||||
if end > len(logsCopy) {
|
|
||||||
end = len(logsCopy)
|
|
||||||
}
|
|
||||||
if start >= len(logsCopy) {
|
|
||||||
return []QueryLog{} // 没有数据,返回空切片
|
|
||||||
}
|
|
||||||
|
|
||||||
return logsCopy[start:end]
|
|
||||||
}
|
|
||||||
|
|
||||||
// compareValues 比较两个值
|
|
||||||
func compareValues(a, b interface{}) int {
|
|
||||||
switch v1 := a.(type) {
|
|
||||||
case time.Time:
|
|
||||||
v2 := b.(time.Time)
|
|
||||||
if v1.Before(v2) {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if v1.After(v2) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
case string:
|
|
||||||
v2 := b.(string)
|
|
||||||
if v1 < v2 {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if v1 > v2 {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
case int64:
|
|
||||||
v2 := b.(int64)
|
|
||||||
if v1 < v2 {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if v1 > v2 {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetQueryLogsCount 获取查询日志总数
|
|
||||||
func (s *Server) GetQueryLogsCount() int {
|
|
||||||
s.queryLogsMutex.RLock()
|
|
||||||
defer s.queryLogsMutex.RUnlock()
|
|
||||||
return len(s.queryLogs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetQueryStats 获取查询统计信息
|
|
||||||
func (s *Server) GetQueryStats() map[string]interface{} {
|
|
||||||
s.statsMutex.Lock()
|
|
||||||
defer s.statsMutex.Unlock()
|
|
||||||
|
|
||||||
// 计算统计数据
|
|
||||||
return map[string]interface{}{
|
|
||||||
"totalQueries": s.stats.Queries,
|
|
||||||
"blockedQueries": s.stats.Blocked,
|
|
||||||
"allowedQueries": s.stats.Allowed,
|
|
||||||
"errorQueries": s.stats.Errors,
|
|
||||||
"avgResponseTime": s.stats.AvgResponseTime,
|
|
||||||
"activeIPs": len(s.stats.SourceIPs),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTopBlockedDomains 获取TOP屏蔽域名列表
|
// GetTopBlockedDomains 获取TOP屏蔽域名列表
|
||||||
func (s *Server) GetTopBlockedDomains(limit int) []BlockedDomain {
|
func (s *Server) GetTopBlockedDomains(limit int) []BlockedDomain {
|
||||||
s.blockedDomainsMutex.RLock()
|
s.blockedDomainsMutex.RLock()
|
||||||
@@ -998,132 +712,6 @@ func (s *Server) GetMonthlyStats() map[string]int64 {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// isPrivateIP 检测IP地址是否为内网IP
|
|
||||||
func isPrivateIP(ip string) bool {
|
|
||||||
// 解析IP地址
|
|
||||||
parsedIP := net.ParseIP(ip)
|
|
||||||
if parsedIP == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查IPv4内网地址
|
|
||||||
if ipv4 := parsedIP.To4(); ipv4 != nil {
|
|
||||||
// 10.0.0.0/8
|
|
||||||
if ipv4[0] == 10 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// 172.16.0.0/12
|
|
||||||
if ipv4[0] == 172 && (ipv4[1] >= 16 && ipv4[1] <= 31) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// 192.168.0.0/16
|
|
||||||
if ipv4[0] == 192 && ipv4[1] == 168 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// 127.0.0.0/8 (localhost)
|
|
||||||
if ipv4[0] == 127 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// 169.254.0.0/16 (链路本地地址)
|
|
||||||
if ipv4[0] == 169 && ipv4[1] == 254 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查IPv6内网地址
|
|
||||||
// ::1/128 (localhost)
|
|
||||||
if parsedIP.IsLoopback() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// fc00::/7 (唯一本地地址)
|
|
||||||
if parsedIP[0]&0xfc == 0xfc {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// fe80::/10 (链路本地地址)
|
|
||||||
if parsedIP[0]&0xfe == 0xfe && parsedIP[1]&0xc0 == 0x80 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// getIpGeolocation 获取IP地址的地理位置信息
|
|
||||||
func (s *Server) getIpGeolocation(ip string) string {
|
|
||||||
// 检查IP是否为本地或内网地址
|
|
||||||
if isPrivateIP(ip) {
|
|
||||||
return "内网 内网"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 先检查缓存
|
|
||||||
s.ipGeolocationCacheMutex.RLock()
|
|
||||||
geo, exists := s.ipGeolocationCache[ip]
|
|
||||||
s.ipGeolocationCacheMutex.RUnlock()
|
|
||||||
|
|
||||||
// 如果缓存存在且未过期,直接返回
|
|
||||||
if exists && time.Now().Before(geo.Expiry) {
|
|
||||||
return fmt.Sprintf("%s %s", geo.Country, geo.City)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 缓存不存在或已过期,从API获取
|
|
||||||
geoInfo, err := s.fetchIpGeolocationFromAPI(ip)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("获取IP地理位置失败", "ip", ip, "error", err)
|
|
||||||
return "未知 未知"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存到缓存
|
|
||||||
s.ipGeolocationCacheMutex.Lock()
|
|
||||||
s.ipGeolocationCache[ip] = &IPGeolocation{
|
|
||||||
Country: geoInfo["country"].(string),
|
|
||||||
City: geoInfo["city"].(string),
|
|
||||||
Expiry: time.Now().Add(s.ipGeolocationCacheTTL),
|
|
||||||
}
|
|
||||||
s.ipGeolocationCacheMutex.Unlock()
|
|
||||||
|
|
||||||
// 返回格式化的地理位置
|
|
||||||
return fmt.Sprintf("%s %s", geoInfo["country"].(string), geoInfo["city"].(string))
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetchIpGeolocationFromAPI 从第三方API获取IP地理位置信息
|
|
||||||
func (s *Server) fetchIpGeolocationFromAPI(ip string) (map[string]interface{}, error) {
|
|
||||||
// 使用ip-api.com获取IP地理位置信息
|
|
||||||
url := fmt.Sprintf("http://ip-api.com/json/%s?fields=country,city", ip)
|
|
||||||
resp, err := http.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
// 读取响应内容
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析JSON响应
|
|
||||||
var result map[string]interface{}
|
|
||||||
err = json.Unmarshal(body, &result)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查API返回状态
|
|
||||||
status, ok := result["status"].(string)
|
|
||||||
if !ok || status != "success" {
|
|
||||||
return nil, fmt.Errorf("API返回错误状态: %v", result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保国家和城市字段存在
|
|
||||||
if _, ok := result["country"]; !ok {
|
|
||||||
result["country"] = "未知"
|
|
||||||
}
|
|
||||||
if _, ok := result["city"]; !ok {
|
|
||||||
result["city"] = "未知"
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadStatsData 从文件加载统计数据
|
// loadStatsData 从文件加载统计数据
|
||||||
func (s *Server) loadStatsData() {
|
func (s *Server) loadStatsData() {
|
||||||
if s.config.StatsFile == "" {
|
if s.config.StatsFile == "" {
|
||||||
@@ -1191,58 +779,6 @@ func (s *Server) loadStatsData() {
|
|||||||
s.clientStatsMutex.Unlock()
|
s.clientStatsMutex.Unlock()
|
||||||
|
|
||||||
logger.Info("统计数据加载成功")
|
logger.Info("统计数据加载成功")
|
||||||
|
|
||||||
// 加载查询日志
|
|
||||||
s.loadQueryLogs()
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadQueryLogs 从文件加载查询日志
|
|
||||||
func (s *Server) loadQueryLogs() {
|
|
||||||
if s.config.StatsFile == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取绝对路径
|
|
||||||
statsFilePath, err := filepath.Abs(s.config.StatsFile)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("获取统计文件绝对路径失败", "path", s.config.StatsFile, "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建查询日志文件路径
|
|
||||||
queryLogPath := filepath.Join(filepath.Dir(statsFilePath), "querylog.json")
|
|
||||||
|
|
||||||
// 检查文件是否存在
|
|
||||||
if _, err := os.Stat(queryLogPath); os.IsNotExist(err) {
|
|
||||||
logger.Info("查询日志文件不存在,将使用空列表", "file", queryLogPath)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取文件内容
|
|
||||||
data, err := ioutil.ReadFile(queryLogPath)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("读取查询日志文件失败", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析数据
|
|
||||||
var logs []QueryLog
|
|
||||||
err = json.Unmarshal(data, &logs)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("解析查询日志失败", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新查询日志
|
|
||||||
s.queryLogsMutex.Lock()
|
|
||||||
s.queryLogs = logs
|
|
||||||
// 确保日志数量不超过限制
|
|
||||||
if len(s.queryLogs) > s.maxQueryLogs {
|
|
||||||
s.queryLogs = s.queryLogs[:s.maxQueryLogs]
|
|
||||||
}
|
|
||||||
s.queryLogsMutex.Unlock()
|
|
||||||
|
|
||||||
logger.Info("查询日志加载成功", "count", len(logs))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// saveStatsData 保存统计数据到文件
|
// saveStatsData 保存统计数据到文件
|
||||||
@@ -1331,37 +867,6 @@ func (s *Server) saveStatsData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("统计数据保存成功", "file", statsFilePath)
|
logger.Info("统计数据保存成功", "file", statsFilePath)
|
||||||
|
|
||||||
// 保存查询日志到文件
|
|
||||||
s.saveQueryLogs(statsDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveQueryLogs 保存查询日志到文件
|
|
||||||
func (s *Server) saveQueryLogs(dataDir string) {
|
|
||||||
// 构建查询日志文件路径
|
|
||||||
queryLogPath := filepath.Join(dataDir, "querylog.json")
|
|
||||||
|
|
||||||
// 获取查询日志数据
|
|
||||||
s.queryLogsMutex.RLock()
|
|
||||||
logsCopy := make([]QueryLog, len(s.queryLogs))
|
|
||||||
copy(logsCopy, s.queryLogs)
|
|
||||||
s.queryLogsMutex.RUnlock()
|
|
||||||
|
|
||||||
// 序列化数据
|
|
||||||
jsonData, err := json.MarshalIndent(logsCopy, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("序列化查询日志失败", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 写入文件
|
|
||||||
err = os.WriteFile(queryLogPath, jsonData, 0644)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("保存查询日志到文件失败", "file", queryLogPath, "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("查询日志保存成功", "file", queryLogPath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// startCpuUsageMonitor 启动CPU使用率监控
|
// startCpuUsageMonitor 启动CPU使用率监控
|
||||||
|
|||||||
+28
-329
@@ -26,11 +26,6 @@ type Server struct {
|
|||||||
shieldManager *shield.ShieldManager
|
shieldManager *shield.ShieldManager
|
||||||
server *http.Server
|
server *http.Server
|
||||||
|
|
||||||
// 会话管理相关字段
|
|
||||||
sessions map[string]time.Time // 会话ID到过期时间的映射
|
|
||||||
sessionsMutex sync.Mutex // 会话映射的互斥锁
|
|
||||||
sessionTTL time.Duration // 会话过期时间
|
|
||||||
|
|
||||||
// WebSocket相关字段
|
// WebSocket相关字段
|
||||||
upgrader websocket.Upgrader
|
upgrader websocket.Upgrader
|
||||||
clients map[*websocket.Conn]bool
|
clients map[*websocket.Conn]bool
|
||||||
@@ -55,15 +50,10 @@ func NewServer(globalConfig *config.Config, dnsServer *dns.Server, shieldManager
|
|||||||
},
|
},
|
||||||
clients: make(map[*websocket.Conn]bool),
|
clients: make(map[*websocket.Conn]bool),
|
||||||
broadcastChan: make(chan []byte, 100),
|
broadcastChan: make(chan []byte, 100),
|
||||||
// 会话管理初始化
|
|
||||||
sessions: make(map[string]time.Time),
|
|
||||||
sessionTTL: 24 * time.Hour, // 会话有效期24小时
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 启动广播协程
|
// 启动广播协程
|
||||||
go server.startBroadcastLoop()
|
go server.startBroadcastLoop()
|
||||||
// 启动会话清理协程
|
|
||||||
go server.cleanupSessionsLoop()
|
|
||||||
|
|
||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
@@ -72,30 +62,17 @@ func NewServer(globalConfig *config.Config, dnsServer *dns.Server, shieldManager
|
|||||||
func (s *Server) Start() error {
|
func (s *Server) Start() error {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
// 登录路由,不需要认证
|
|
||||||
mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// 重定向到登录页面HTML
|
|
||||||
http.Redirect(w, r, "/login.html", http.StatusFound)
|
|
||||||
})
|
|
||||||
|
|
||||||
// API路由
|
// API路由
|
||||||
if s.config.EnableAPI {
|
if s.config.EnableAPI {
|
||||||
// 登录API端点,不需要认证
|
|
||||||
mux.HandleFunc("/api/login", s.handleLogin)
|
|
||||||
// 注销API端点,不需要认证
|
|
||||||
mux.HandleFunc("/api/logout", s.handleLogout)
|
|
||||||
// 修改密码API端点,需要认证
|
|
||||||
mux.HandleFunc("/api/change-password", s.loginRequired(s.handleChangePassword))
|
|
||||||
|
|
||||||
// 重定向/api到Swagger UI页面
|
// 重定向/api到Swagger UI页面
|
||||||
mux.HandleFunc("/api", s.loginRequired(func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, "/api/index.html", http.StatusMovedPermanently)
|
http.Redirect(w, r, "/api/index.html", http.StatusMovedPermanently)
|
||||||
}))
|
})
|
||||||
|
|
||||||
// 注册所有API端点,应用登录中间件
|
// 注册所有API端点
|
||||||
mux.HandleFunc("/api/stats", s.loginRequired(s.handleStats))
|
mux.HandleFunc("/api/stats", s.handleStats)
|
||||||
mux.HandleFunc("/api/shield", s.loginRequired(s.handleShield))
|
mux.HandleFunc("/api/shield", s.handleShield)
|
||||||
mux.HandleFunc("/api/shield/localrules", s.loginRequired(func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/api/shield/localrules", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
if r.Method == http.MethodGet {
|
if r.Method == http.MethodGet {
|
||||||
localRules := s.shieldManager.GetLocalRules()
|
localRules := s.shieldManager.GetLocalRules()
|
||||||
@@ -103,8 +80,8 @@ func (s *Server) Start() error {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
}))
|
})
|
||||||
mux.HandleFunc("/api/shield/remoterules", s.loginRequired(func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/api/shield/remoterules", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
if r.Method == http.MethodGet {
|
if r.Method == http.MethodGet {
|
||||||
remoteRules := s.shieldManager.GetRemoteRules()
|
remoteRules := s.shieldManager.GetRemoteRules()
|
||||||
@@ -112,57 +89,41 @@ func (s *Server) Start() error {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
}))
|
})
|
||||||
mux.HandleFunc("/api/shield/hosts", s.loginRequired(s.handleShieldHosts))
|
mux.HandleFunc("/api/shield/hosts", s.handleShieldHosts)
|
||||||
mux.HandleFunc("/api/shield/blacklists", s.loginRequired(s.handleShieldBlacklists))
|
mux.HandleFunc("/api/shield/blacklists", s.handleShieldBlacklists)
|
||||||
mux.HandleFunc("/api/query", s.loginRequired(s.handleQuery))
|
mux.HandleFunc("/api/query", s.handleQuery)
|
||||||
mux.HandleFunc("/api/status", s.loginRequired(s.handleStatus))
|
mux.HandleFunc("/api/status", s.handleStatus)
|
||||||
mux.HandleFunc("/api/config", s.loginRequired(s.handleConfig))
|
mux.HandleFunc("/api/config", s.handleConfig)
|
||||||
mux.HandleFunc("/api/config/restart", s.loginRequired(s.handleRestart))
|
mux.HandleFunc("/api/config/restart", s.handleRestart)
|
||||||
// 添加统计相关接口
|
// 添加统计相关接口
|
||||||
mux.HandleFunc("/api/top-blocked", s.loginRequired(s.handleTopBlockedDomains))
|
mux.HandleFunc("/api/top-blocked", s.handleTopBlockedDomains)
|
||||||
mux.HandleFunc("/api/top-resolved", s.loginRequired(s.handleTopResolvedDomains))
|
mux.HandleFunc("/api/top-resolved", s.handleTopResolvedDomains)
|
||||||
mux.HandleFunc("/api/top-clients", s.loginRequired(s.handleTopClients))
|
mux.HandleFunc("/api/top-clients", s.handleTopClients)
|
||||||
mux.HandleFunc("/api/top-domains", s.loginRequired(s.handleTopDomains))
|
mux.HandleFunc("/api/top-domains", s.handleTopDomains)
|
||||||
mux.HandleFunc("/api/recent-blocked", s.loginRequired(s.handleRecentBlockedDomains))
|
mux.HandleFunc("/api/recent-blocked", s.handleRecentBlockedDomains)
|
||||||
mux.HandleFunc("/api/hourly-stats", s.loginRequired(s.handleHourlyStats))
|
mux.HandleFunc("/api/hourly-stats", s.handleHourlyStats)
|
||||||
mux.HandleFunc("/api/daily-stats", s.loginRequired(s.handleDailyStats))
|
mux.HandleFunc("/api/daily-stats", s.handleDailyStats)
|
||||||
mux.HandleFunc("/api/monthly-stats", s.loginRequired(s.handleMonthlyStats))
|
mux.HandleFunc("/api/monthly-stats", s.handleMonthlyStats)
|
||||||
mux.HandleFunc("/api/query/type", s.loginRequired(s.handleQueryTypeStats))
|
mux.HandleFunc("/api/query/type", s.handleQueryTypeStats)
|
||||||
// 日志统计相关接口
|
|
||||||
mux.HandleFunc("/api/logs/stats", s.loginRequired(s.handleLogsStats))
|
|
||||||
mux.HandleFunc("/api/logs/query", s.loginRequired(s.handleLogsQuery))
|
|
||||||
mux.HandleFunc("/api/logs/count", s.loginRequired(s.handleLogsCount))
|
|
||||||
// WebSocket端点
|
// WebSocket端点
|
||||||
mux.HandleFunc("/ws/stats", s.loginRequired(s.handleWebSocketStats))
|
mux.HandleFunc("/ws/stats", s.handleWebSocketStats)
|
||||||
|
|
||||||
// 将/api/下的静态文件服务指向static/api目录,放在最后以避免覆盖API端点
|
// 将/api/下的静态文件服务指向static/api目录,放在最后以避免覆盖API端点
|
||||||
apiFileServer := http.FileServer(http.Dir("./static/api"))
|
apiFileServer := http.FileServer(http.Dir("./static/api"))
|
||||||
mux.Handle("/api/", s.loginRequired(http.StripPrefix("/api", apiFileServer).ServeHTTP))
|
mux.Handle("/api/", http.StripPrefix("/api", apiFileServer))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自定义静态文件服务处理器,用于禁用浏览器缓存,放在API路由之后
|
// 自定义静态文件服务处理器,用于禁用浏览器缓存,放在API路由之后
|
||||||
fileServer := http.FileServer(http.Dir("./static"))
|
fileServer := http.FileServer(http.Dir("./static"))
|
||||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
// 单独处理login.html,不需要登录
|
|
||||||
mux.HandleFunc("/login.html", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// 添加Cache-Control头,禁用浏览器缓存
|
|
||||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
||||||
w.Header().Set("Pragma", "no-cache")
|
|
||||||
w.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
|
|
||||||
// 直接提供login.html文件
|
|
||||||
http.ServeFile(w, r, "./static/login.html")
|
|
||||||
})
|
|
||||||
|
|
||||||
// 其他静态文件需要登录
|
|
||||||
mux.HandleFunc("/", s.loginRequired(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// 添加Cache-Control头,禁用浏览器缓存
|
// 添加Cache-Control头,禁用浏览器缓存
|
||||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
w.Header().Set("Pragma", "no-cache")
|
w.Header().Set("Pragma", "no-cache")
|
||||||
w.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
|
w.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
|
||||||
// 使用StripPrefix处理路径
|
// 使用StripPrefix处理路径
|
||||||
http.StripPrefix("/", fileServer).ServeHTTP(w, r)
|
http.StripPrefix("/", fileServer).ServeHTTP(w, r)
|
||||||
}))
|
})
|
||||||
|
|
||||||
s.server = &http.Server{
|
s.server = &http.Server{
|
||||||
Addr: fmt.Sprintf("%s:%d", s.config.Host, s.config.Port),
|
Addr: fmt.Sprintf("%s:%d", s.config.Host, s.config.Port),
|
||||||
@@ -413,78 +374,6 @@ func (s *Server) startBroadcastLoop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanupSessionsLoop 定期清理过期会话
|
|
||||||
func (s *Server) cleanupSessionsLoop() {
|
|
||||||
for {
|
|
||||||
time.Sleep(1 * time.Hour) // 每小时清理一次
|
|
||||||
s.sessionsMutex.Lock()
|
|
||||||
now := time.Now()
|
|
||||||
for sessionID, expiryTime := range s.sessions {
|
|
||||||
if now.After(expiryTime) {
|
|
||||||
delete(s.sessions, sessionID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.sessionsMutex.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// isAuthenticated 检查用户是否已认证
|
|
||||||
func (s *Server) isAuthenticated(r *http.Request) bool {
|
|
||||||
// 从Cookie中获取会话ID
|
|
||||||
cookie, err := r.Cookie("session_id")
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionID := cookie.Value
|
|
||||||
s.sessionsMutex.Lock()
|
|
||||||
defer s.sessionsMutex.Unlock()
|
|
||||||
|
|
||||||
// 检查会话是否存在且未过期
|
|
||||||
expiryTime, exists := s.sessions[sessionID]
|
|
||||||
if !exists {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if time.Now().After(expiryTime) {
|
|
||||||
// 会话已过期,删除它
|
|
||||||
delete(s.sessions, sessionID)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 延长会话有效期
|
|
||||||
s.sessions[sessionID] = time.Now().Add(s.sessionTTL)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// loginRequired 登录中间件
|
|
||||||
func (s *Server) loginRequired(next http.HandlerFunc) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// 检查是否为登录页面或登录API,允许直接访问
|
|
||||||
if r.URL.Path == "/login" || r.URL.Path == "/api/login" {
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否已认证
|
|
||||||
if !s.isAuthenticated(r) {
|
|
||||||
// 如果是API请求,返回401错误
|
|
||||||
if strings.HasPrefix(r.URL.Path, "/api/") || strings.HasPrefix(r.URL.Path, "/ws/") {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"error": "未授权访问"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 否则重定向到登录页面
|
|
||||||
http.Redirect(w, r, "/login", http.StatusFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 已认证,继续处理请求
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleTopBlockedDomains 处理TOP屏蔽域名请求
|
// handleTopBlockedDomains 处理TOP屏蔽域名请求
|
||||||
func (s *Server) handleTopBlockedDomains(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleTopBlockedDomains(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
@@ -1328,64 +1217,6 @@ func checkURLExists(url string) bool {
|
|||||||
return resp.StatusCode >= 200 && resp.StatusCode < 400
|
return resp.StatusCode >= 200 && resp.StatusCode < 400
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleLogsStats 处理日志统计请求
|
|
||||||
func (s *Server) handleLogsStats(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodGet {
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取日志统计数据
|
|
||||||
logStats := s.dnsServer.GetQueryStats()
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
json.NewEncoder(w).Encode(logStats)
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleLogsQuery 处理日志查询请求
|
|
||||||
func (s *Server) handleLogsQuery(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodGet {
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取查询参数
|
|
||||||
limit := 100 // 默认返回100条日志
|
|
||||||
offset := 0
|
|
||||||
sortField := r.URL.Query().Get("sort")
|
|
||||||
sortDirection := r.URL.Query().Get("direction")
|
|
||||||
resultFilter := r.URL.Query().Get("result")
|
|
||||||
searchTerm := r.URL.Query().Get("search")
|
|
||||||
|
|
||||||
if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
|
|
||||||
fmt.Sscanf(limitStr, "%d", &limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
if offsetStr := r.URL.Query().Get("offset"); offsetStr != "" {
|
|
||||||
fmt.Sscanf(offsetStr, "%d", &offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取日志数据
|
|
||||||
logs := s.dnsServer.GetQueryLogs(limit, offset, sortField, sortDirection, resultFilter, searchTerm)
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
json.NewEncoder(w).Encode(logs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleLogsCount 处理日志总数请求
|
|
||||||
func (s *Server) handleLogsCount(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodGet {
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取日志总数
|
|
||||||
count := s.dnsServer.GetQueryLogsCount()
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
json.NewEncoder(w).Encode(map[string]int{"count": count})
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleRestart 处理重启服务请求
|
// handleRestart 处理重启服务请求
|
||||||
func (s *Server) handleRestart(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleRestart(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
@@ -1419,135 +1250,3 @@ func (s *Server) handleRestart(w http.ResponseWriter, r *http.Request) {
|
|||||||
json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "服务已重启"})
|
json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "服务已重启"})
|
||||||
logger.Info("服务重启成功")
|
logger.Info("服务重启成功")
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleLogin 处理登录请求
|
|
||||||
func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析请求体
|
|
||||||
var loginData struct {
|
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&loginData); err != nil {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"error": "无效的请求体"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证用户名和密码
|
|
||||||
if loginData.Username != s.config.Username || loginData.Password != s.config.Password {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"error": "用户名或密码错误"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成会话ID
|
|
||||||
sessionID := fmt.Sprintf("%d_%d", time.Now().UnixNano(), len(s.sessions))
|
|
||||||
|
|
||||||
// 保存会话
|
|
||||||
s.sessionsMutex.Lock()
|
|
||||||
s.sessions[sessionID] = time.Now().Add(s.sessionTTL)
|
|
||||||
s.sessionsMutex.Unlock()
|
|
||||||
|
|
||||||
// 设置Cookie
|
|
||||||
cookie := &http.Cookie{
|
|
||||||
Name: "session_id",
|
|
||||||
Value: sessionID,
|
|
||||||
Path: "/",
|
|
||||||
Expires: time.Now().Add(s.sessionTTL),
|
|
||||||
HttpOnly: true,
|
|
||||||
Secure: false, // 开发环境下使用false,生产环境应使用true
|
|
||||||
}
|
|
||||||
http.SetCookie(w, cookie)
|
|
||||||
|
|
||||||
// 返回成功响应
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "登录成功"})
|
|
||||||
logger.Info(fmt.Sprintf("用户 %s 登录成功", loginData.Username))
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleLogout 处理注销请求
|
|
||||||
func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从Cookie中获取会话ID
|
|
||||||
cookie, err := r.Cookie("session_id")
|
|
||||||
if err == nil {
|
|
||||||
// 删除会话
|
|
||||||
s.sessionsMutex.Lock()
|
|
||||||
delete(s.sessions, cookie.Value)
|
|
||||||
s.sessionsMutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清除Cookie
|
|
||||||
clearCookie := &http.Cookie{
|
|
||||||
Name: "session_id",
|
|
||||||
Value: "",
|
|
||||||
Path: "/",
|
|
||||||
Expires: time.Unix(0, 0),
|
|
||||||
HttpOnly: true,
|
|
||||||
Secure: false,
|
|
||||||
}
|
|
||||||
http.SetCookie(w, clearCookie)
|
|
||||||
|
|
||||||
// 返回成功响应
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "注销成功"})
|
|
||||||
logger.Info("用户注销成功")
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleChangePassword 处理修改密码请求
|
|
||||||
func (s *Server) handleChangePassword(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析请求体
|
|
||||||
var changePasswordData struct {
|
|
||||||
CurrentPassword string `json:"currentPassword"`
|
|
||||||
NewPassword string `json:"newPassword"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&changePasswordData); err != nil {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"error": "无效的请求体"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证当前密码
|
|
||||||
if changePasswordData.CurrentPassword != s.config.Password {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"error": "当前密码错误"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新密码
|
|
||||||
s.config.Password = changePasswordData.NewPassword
|
|
||||||
|
|
||||||
// 保存配置到文件
|
|
||||||
if err := saveConfigToFile(s.globalConfig, "./config.json"); err != nil {
|
|
||||||
logger.Error("保存配置文件失败", "error", err)
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"error": "保存密码失败"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回成功响应
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "密码修改成功"})
|
|
||||||
logger.Info("密码修改成功")
|
|
||||||
}
|
|
||||||
|
|||||||
+7
-51
@@ -1,10 +1,8 @@
|
|||||||
package logger
|
package logger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@@ -17,7 +15,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// InitLogger 初始化日志系统
|
// InitLogger 初始化日志系统
|
||||||
func InitLogger(logFile, level string, maxSize, maxBackups, maxAge int, _ bool) error {
|
func InitLogger(logFile, level string, maxSize, maxBackups, maxAge int) error {
|
||||||
logMutex.Lock()
|
logMutex.Lock()
|
||||||
defer logMutex.Unlock()
|
defer logMutex.Unlock()
|
||||||
|
|
||||||
@@ -32,42 +30,21 @@ func InitLogger(logFile, level string, maxSize, maxBackups, maxAge int, _ bool)
|
|||||||
FullTimestamp: true,
|
FullTimestamp: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 创建日志目录
|
|
||||||
if logFile != "" {
|
|
||||||
logDir := filepath.Dir(logFile)
|
|
||||||
if logDir != "." {
|
|
||||||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
|
||||||
return fmt.Errorf("创建日志目录失败: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置输出目标
|
// 设置输出目标
|
||||||
outputTargets := []io.Writer{}
|
|
||||||
|
|
||||||
if logFile != "" {
|
if logFile != "" {
|
||||||
// 使用标准库打开文件,支持追加写入
|
// 使用标准库打开文件,支持追加写入
|
||||||
file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 无法打开日志文件时,回退到标准输出
|
logrus.Warn("无法打开日志文件,将使用标准输出", "error", err)
|
||||||
log.Println("无法打开日志文件,将使用标准输出:", err)
|
log.SetOutput(os.Stdout)
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
// 如果没有指定日志文件,仅使用标准输出
|
log.SetOutput(os.Stdout)
|
||||||
outputTargets = append(outputTargets, os.Stdout)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置日志输出
|
|
||||||
log.SetOutput(io.MultiWriter(outputTargets...))
|
|
||||||
|
|
||||||
// 设置日志级别
|
// 设置日志级别
|
||||||
logLevel, err := logrus.ParseLevel(level)
|
logLevel, err := logrus.ParseLevel(level)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -84,20 +61,13 @@ func Close() {
|
|||||||
logMutex.Lock()
|
logMutex.Lock()
|
||||||
defer logMutex.Unlock()
|
defer logMutex.Unlock()
|
||||||
|
|
||||||
if !initialized || log == nil {
|
if !initialized {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行日志刷新
|
// 执行日志刷新
|
||||||
log.Warn("日志系统已关闭")
|
log.Warn("日志系统已关闭")
|
||||||
|
|
||||||
// 确保日志被写入磁盘
|
|
||||||
if loggerOutput, ok := log.Out.(*os.File); ok {
|
|
||||||
loggerOutput.Sync()
|
|
||||||
}
|
|
||||||
|
|
||||||
initialized = false
|
initialized = false
|
||||||
log = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info 记录信息级别日志
|
// 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字段
|
// toFields 将键值对转换为logrus字段
|
||||||
func toFields(keyValues []interface{}) logrus.Fields {
|
func toFields(keyValues []interface{}) logrus.Fields {
|
||||||
fields := make(logrus.Fields)
|
fields := make(logrus.Fields)
|
||||||
|
|||||||
+157596
File diff suppressed because it is too large
Load Diff
@@ -42,11 +42,9 @@ func createDefaultConfig(configFile string) error {
|
|||||||
"saveInterval": 300
|
"saveInterval": 300
|
||||||
},
|
},
|
||||||
"http": {
|
"http": {
|
||||||
"port": 8080,
|
"port": 8081,
|
||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
"enableAPI": true,
|
"enableAPI": true
|
||||||
"username": "admin",
|
|
||||||
"password": "admin"
|
|
||||||
},
|
},
|
||||||
"shield": {
|
"shield": {
|
||||||
"localRulesFile": "data/rules.txt",
|
"localRulesFile": "data/rules.txt",
|
||||||
@@ -148,9 +146,20 @@ func createRequiredFiles(cfg *config.Config) error {
|
|||||||
func main() {
|
func main() {
|
||||||
// 命令行参数解析
|
// 命令行参数解析
|
||||||
var configFile string
|
var configFile string
|
||||||
|
var daemonMode bool
|
||||||
flag.StringVar(&configFile, "config", "config.json", "配置文件路径")
|
flag.StringVar(&configFile, "config", "config.json", "配置文件路径")
|
||||||
|
flag.BoolVar(&daemonMode, "daemon", false, "以守护进程模式运行")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
// 如果是守护进程模式,创建守护进程
|
||||||
|
if daemonMode {
|
||||||
|
if err := daemonize(); err != nil {
|
||||||
|
log.Fatalf("创建守护进程失败: %v", err)
|
||||||
|
}
|
||||||
|
// 父进程退出
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
// 检查配置文件是否存在,如果不存在则创建默认配置文件
|
// 检查配置文件是否存在,如果不存在则创建默认配置文件
|
||||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||||
log.Printf("配置文件 %s 不存在,正在创建默认配置文件...", configFile)
|
log.Printf("配置文件 %s 不存在,正在创建默认配置文件...", configFile)
|
||||||
@@ -176,7 +185,7 @@ func main() {
|
|||||||
log.Println("所需文件和文件夹创建成功")
|
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)
|
log.Fatalf("初始化日志系统失败: %v", err)
|
||||||
}
|
}
|
||||||
defer logger.Close()
|
defer logger.Close()
|
||||||
@@ -216,10 +225,45 @@ func main() {
|
|||||||
<-sigCh
|
<-sigCh
|
||||||
|
|
||||||
// 清理资源
|
// 清理资源
|
||||||
logger.Info("正在关闭服务...")
|
log.Println("正在关闭服务...")
|
||||||
dnsServer.Stop()
|
dnsServer.Stop()
|
||||||
httpServer.Stop()
|
httpServer.Stop()
|
||||||
shieldManager.StopAutoUpdate()
|
shieldManager.StopAutoUpdate()
|
||||||
|
// 守护进程模式下不需要删除PID文件
|
||||||
|
|
||||||
logger.Info("服务已关闭")
|
log.Println("服务已关闭")
|
||||||
|
}
|
||||||
|
|
||||||
|
// daemonize 创建守护进程
|
||||||
|
func daemonize() error {
|
||||||
|
// 使用更简单的方式创建守护进程:直接在当前进程中进行守护化处理
|
||||||
|
// 1. 重定向标准输入、输出、错误
|
||||||
|
nullFile, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("打开/dev/null失败: %w", err)
|
||||||
|
}
|
||||||
|
defer nullFile.Close()
|
||||||
|
|
||||||
|
// 重定向文件描述符
|
||||||
|
err = syscall.Dup2(int(nullFile.Fd()), int(os.Stdin.Fd()))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("重定向stdin失败: %w", err)
|
||||||
|
}
|
||||||
|
err = syscall.Dup2(int(nullFile.Fd()), int(os.Stdout.Fd()))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("重定向stdout失败: %w", err)
|
||||||
|
}
|
||||||
|
err = syscall.Dup2(int(nullFile.Fd()), int(os.Stderr.Fd()))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("重定向stderr失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 创建新的会话和进程组
|
||||||
|
_, err = syscall.Setsid()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("创建新会话失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("守护进程已启动")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Executable
BIN
Binary file not shown.
+41
-53
@@ -43,8 +43,6 @@ type ShieldManager struct {
|
|||||||
domainExceptionsIsLocal map[string]bool // 标记域名排除规则是否为本地规则
|
domainExceptionsIsLocal map[string]bool // 标记域名排除规则是否为本地规则
|
||||||
domainRulesSource map[string]string // 标记域名规则来源
|
domainRulesSource map[string]string // 标记域名规则来源
|
||||||
domainExceptionsSource map[string]string // 标记域名排除规则来源
|
domainExceptionsSource map[string]string // 标记域名排除规则来源
|
||||||
domainRulesOriginal map[string]string // 存储域名规则的原始字符串
|
|
||||||
domainExceptionsOriginal map[string]string // 存储域名排除规则的原始字符串
|
|
||||||
regexRules []regexRule
|
regexRules []regexRule
|
||||||
regexExceptions []regexRule
|
regexExceptions []regexRule
|
||||||
hostsMap map[string]string
|
hostsMap map[string]string
|
||||||
@@ -56,6 +54,7 @@ type ShieldManager struct {
|
|||||||
updateRunning bool
|
updateRunning bool
|
||||||
localRulesCount int // 本地规则数量
|
localRulesCount int // 本地规则数量
|
||||||
remoteRulesCount int // 远程规则数量
|
remoteRulesCount int // 远程规则数量
|
||||||
|
urlToBlacklistName map[string]string // URL到黑名单名称的映射
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewShieldManager 创建屏蔽管理器实例
|
// NewShieldManager 创建屏蔽管理器实例
|
||||||
@@ -69,8 +68,6 @@ func NewShieldManager(config *config.ShieldConfig) *ShieldManager {
|
|||||||
domainExceptionsIsLocal: make(map[string]bool),
|
domainExceptionsIsLocal: make(map[string]bool),
|
||||||
domainRulesSource: make(map[string]string),
|
domainRulesSource: make(map[string]string),
|
||||||
domainExceptionsSource: make(map[string]string),
|
domainExceptionsSource: make(map[string]string),
|
||||||
domainRulesOriginal: make(map[string]string),
|
|
||||||
domainExceptionsOriginal: make(map[string]string),
|
|
||||||
regexRules: []regexRule{},
|
regexRules: []regexRule{},
|
||||||
regexExceptions: []regexRule{},
|
regexExceptions: []regexRule{},
|
||||||
hostsMap: make(map[string]string),
|
hostsMap: make(map[string]string),
|
||||||
@@ -80,6 +77,7 @@ func NewShieldManager(config *config.ShieldConfig) *ShieldManager {
|
|||||||
updateCancel: cancel,
|
updateCancel: cancel,
|
||||||
localRulesCount: 0,
|
localRulesCount: 0,
|
||||||
remoteRulesCount: 0,
|
remoteRulesCount: 0,
|
||||||
|
urlToBlacklistName: make(map[string]string),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载已保存的计数数据
|
// 加载已保存的计数数据
|
||||||
@@ -100,8 +98,6 @@ func (m *ShieldManager) LoadRules() error {
|
|||||||
m.domainExceptionsIsLocal = make(map[string]bool)
|
m.domainExceptionsIsLocal = make(map[string]bool)
|
||||||
m.domainRulesSource = make(map[string]string)
|
m.domainRulesSource = make(map[string]string)
|
||||||
m.domainExceptionsSource = make(map[string]string)
|
m.domainExceptionsSource = make(map[string]string)
|
||||||
m.domainRulesOriginal = make(map[string]string)
|
|
||||||
m.domainExceptionsOriginal = make(map[string]string)
|
|
||||||
m.regexRules = []regexRule{}
|
m.regexRules = []regexRule{}
|
||||||
m.regexExceptions = []regexRule{}
|
m.regexExceptions = []regexRule{}
|
||||||
m.hostsMap = make(map[string]string)
|
m.hostsMap = make(map[string]string)
|
||||||
@@ -165,7 +161,13 @@ func (m *ShieldManager) loadLocalRules() error {
|
|||||||
|
|
||||||
// loadRemoteRules 加载远程规则
|
// loadRemoteRules 加载远程规则
|
||||||
func (m *ShieldManager) loadRemoteRules() error {
|
func (m *ShieldManager) loadRemoteRules() error {
|
||||||
|
// 清空URL到黑名单名称的映射
|
||||||
|
m.urlToBlacklistName = make(map[string]string)
|
||||||
|
|
||||||
|
// 构建URL到黑名单名称的映射
|
||||||
for _, blacklist := range m.config.Blacklists {
|
for _, blacklist := range m.config.Blacklists {
|
||||||
|
m.urlToBlacklistName[blacklist.URL] = blacklist.Name
|
||||||
|
|
||||||
if blacklist.Enabled {
|
if blacklist.Enabled {
|
||||||
if err := m.fetchRemoteRules(blacklist.URL); err != nil {
|
if err := m.fetchRemoteRules(blacklist.URL); err != nil {
|
||||||
logger.Error("获取远程规则失败", "url", blacklist.URL, "error", err)
|
logger.Error("获取远程规则失败", "url", blacklist.URL, "error", err)
|
||||||
@@ -339,9 +341,6 @@ func (m *ShieldManager) loadHosts() error {
|
|||||||
|
|
||||||
// parseRule 解析规则行
|
// parseRule 解析规则行
|
||||||
func (m *ShieldManager) parseRule(line string, isLocal bool, source string) {
|
func (m *ShieldManager) parseRule(line string, isLocal bool, source string) {
|
||||||
// 保存原始规则用于后续使用
|
|
||||||
originalLine := line
|
|
||||||
|
|
||||||
// 处理注释
|
// 处理注释
|
||||||
if strings.HasPrefix(line, "!") || strings.HasPrefix(line, "#") || line == "" {
|
if strings.HasPrefix(line, "!") || strings.HasPrefix(line, "#") || line == "" {
|
||||||
return
|
return
|
||||||
@@ -366,12 +365,12 @@ func (m *ShieldManager) parseRule(line string, isLocal bool, source string) {
|
|||||||
case strings.HasPrefix(line, "||") && strings.HasSuffix(line, "^"):
|
case strings.HasPrefix(line, "||") && strings.HasSuffix(line, "^"):
|
||||||
// AdGuardHome域名规则格式: ||example.com^
|
// AdGuardHome域名规则格式: ||example.com^
|
||||||
domain := strings.TrimSuffix(strings.TrimPrefix(line, "||"), "^")
|
domain := strings.TrimSuffix(strings.TrimPrefix(line, "||"), "^")
|
||||||
m.addDomainRule(domain, !isException, isLocal, source, originalLine)
|
m.addDomainRule(domain, !isException, isLocal, source)
|
||||||
|
|
||||||
case strings.HasPrefix(line, "||"):
|
case strings.HasPrefix(line, "||"):
|
||||||
// 精确域名匹配规则
|
// 精确域名匹配规则
|
||||||
domain := strings.TrimPrefix(line, "||")
|
domain := strings.TrimPrefix(line, "||")
|
||||||
m.addDomainRule(domain, !isException, isLocal, source, originalLine)
|
m.addDomainRule(domain, !isException, isLocal, source)
|
||||||
|
|
||||||
case strings.HasPrefix(line, "*"):
|
case strings.HasPrefix(line, "*"):
|
||||||
// 通配符规则,转换为正则表达式
|
// 通配符规则,转换为正则表达式
|
||||||
@@ -379,7 +378,7 @@ func (m *ShieldManager) parseRule(line string, isLocal bool, source string) {
|
|||||||
pattern = "^" + pattern + "$"
|
pattern = "^" + pattern + "$"
|
||||||
if re, err := regexp.Compile(pattern); err == nil {
|
if re, err := regexp.Compile(pattern); err == nil {
|
||||||
// 保存原始规则字符串
|
// 保存原始规则字符串
|
||||||
m.addRegexRule(re, originalLine, !isException, isLocal, source)
|
m.addRegexRule(re, line, !isException, isLocal, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasPrefix(line, "/") && strings.HasSuffix(line, "/"):
|
case strings.HasPrefix(line, "/") && strings.HasSuffix(line, "/"):
|
||||||
@@ -389,7 +388,7 @@ func (m *ShieldManager) parseRule(line string, isLocal bool, source string) {
|
|||||||
// 对于像 /domain/ 这样的规则,应该匹配包含 domain 字符串的任何域名
|
// 对于像 /domain/ 这样的规则,应该匹配包含 domain 字符串的任何域名
|
||||||
if re, err := regexp.Compile("(?i).*" + regexp.QuoteMeta(pattern) + ".*"); err == nil {
|
if re, err := regexp.Compile("(?i).*" + regexp.QuoteMeta(pattern) + ".*"); err == nil {
|
||||||
// 保存原始规则字符串
|
// 保存原始规则字符串
|
||||||
m.addRegexRule(re, originalLine, !isException, isLocal, source)
|
m.addRegexRule(re, line, !isException, isLocal, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasPrefix(line, "|") && strings.HasSuffix(line, "|"):
|
case strings.HasPrefix(line, "|") && strings.HasSuffix(line, "|"):
|
||||||
@@ -398,7 +397,7 @@ func (m *ShieldManager) parseRule(line string, isLocal bool, source string) {
|
|||||||
// 将URL模式转换为正则表达式
|
// 将URL模式转换为正则表达式
|
||||||
pattern := "^" + regexp.QuoteMeta(urlPattern) + "$"
|
pattern := "^" + regexp.QuoteMeta(urlPattern) + "$"
|
||||||
if re, err := regexp.Compile(pattern); err == nil {
|
if re, err := regexp.Compile(pattern); err == nil {
|
||||||
m.addRegexRule(re, originalLine, !isException, isLocal, source)
|
m.addRegexRule(re, line, !isException, isLocal, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasPrefix(line, "|"):
|
case strings.HasPrefix(line, "|"):
|
||||||
@@ -406,7 +405,7 @@ func (m *ShieldManager) parseRule(line string, isLocal bool, source string) {
|
|||||||
urlPattern := strings.TrimPrefix(line, "|")
|
urlPattern := strings.TrimPrefix(line, "|")
|
||||||
pattern := "^" + regexp.QuoteMeta(urlPattern)
|
pattern := "^" + regexp.QuoteMeta(urlPattern)
|
||||||
if re, err := regexp.Compile(pattern); err == nil {
|
if re, err := regexp.Compile(pattern); err == nil {
|
||||||
m.addRegexRule(re, originalLine, !isException, isLocal, source)
|
m.addRegexRule(re, line, !isException, isLocal, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasSuffix(line, "|"):
|
case strings.HasSuffix(line, "|"):
|
||||||
@@ -414,12 +413,12 @@ func (m *ShieldManager) parseRule(line string, isLocal bool, source string) {
|
|||||||
urlPattern := strings.TrimSuffix(line, "|")
|
urlPattern := strings.TrimSuffix(line, "|")
|
||||||
pattern := regexp.QuoteMeta(urlPattern) + "$"
|
pattern := regexp.QuoteMeta(urlPattern) + "$"
|
||||||
if re, err := regexp.Compile(pattern); err == nil {
|
if re, err := regexp.Compile(pattern); err == nil {
|
||||||
m.addRegexRule(re, originalLine, !isException, isLocal, source)
|
m.addRegexRule(re, line, !isException, isLocal, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// 默认作为普通域名规则
|
// 默认作为普通域名规则
|
||||||
m.addDomainRule(line, !isException, isLocal, source, originalLine)
|
m.addDomainRule(line, !isException, isLocal, source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,7 +442,7 @@ func (m *ShieldManager) parseRuleOptions(optionsStr string) map[string]string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// addDomainRule 添加域名规则,支持是否为阻止规则
|
// addDomainRule 添加域名规则,支持是否为阻止规则
|
||||||
func (m *ShieldManager) addDomainRule(domain string, block bool, isLocal bool, source string, original string) {
|
func (m *ShieldManager) addDomainRule(domain string, block bool, isLocal bool, source string) {
|
||||||
if block {
|
if block {
|
||||||
// 如果是远程规则,检查是否已经存在本地规则,如果存在则不覆盖
|
// 如果是远程规则,检查是否已经存在本地规则,如果存在则不覆盖
|
||||||
if !isLocal {
|
if !isLocal {
|
||||||
@@ -455,7 +454,6 @@ func (m *ShieldManager) addDomainRule(domain string, block bool, isLocal bool, s
|
|||||||
m.domainRules[domain] = true
|
m.domainRules[domain] = true
|
||||||
m.domainRulesIsLocal[domain] = isLocal
|
m.domainRulesIsLocal[domain] = isLocal
|
||||||
m.domainRulesSource[domain] = source
|
m.domainRulesSource[domain] = source
|
||||||
m.domainRulesOriginal[domain] = original
|
|
||||||
} else {
|
} else {
|
||||||
// 添加到排除规则
|
// 添加到排除规则
|
||||||
// 如果是远程规则,检查是否已经存在本地规则,如果存在则不覆盖
|
// 如果是远程规则,检查是否已经存在本地规则,如果存在则不覆盖
|
||||||
@@ -468,7 +466,6 @@ func (m *ShieldManager) addDomainRule(domain string, block bool, isLocal bool, s
|
|||||||
m.domainExceptions[domain] = true
|
m.domainExceptions[domain] = true
|
||||||
m.domainExceptionsIsLocal[domain] = isLocal
|
m.domainExceptionsIsLocal[domain] = isLocal
|
||||||
m.domainExceptionsSource[domain] = source
|
m.domainExceptionsSource[domain] = source
|
||||||
m.domainExceptionsOriginal[domain] = original
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,6 +521,7 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
|||||||
"blockRule": "",
|
"blockRule": "",
|
||||||
"blockRuleType": "",
|
"blockRuleType": "",
|
||||||
"blocksource": "",
|
"blocksource": "",
|
||||||
|
"blacklistName": "",
|
||||||
"excluded": false,
|
"excluded": false,
|
||||||
"excludeRule": "",
|
"excludeRule": "",
|
||||||
"excludeRuleType": "",
|
"excludeRuleType": "",
|
||||||
@@ -539,67 +537,49 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
|||||||
// 检查排除规则(优先级最高)
|
// 检查排除规则(优先级最高)
|
||||||
// 检查域名排除规则
|
// 检查域名排除规则
|
||||||
if m.domainExceptions[domain] {
|
if m.domainExceptions[domain] {
|
||||||
|
source := m.domainExceptionsSource[domain]
|
||||||
result["excluded"] = true
|
result["excluded"] = true
|
||||||
result["excludeRule"] = m.domainExceptionsOriginal[domain]
|
result["excludeRule"] = domain
|
||||||
result["excludeRuleType"] = "exact_domain"
|
result["excludeRuleType"] = "exact_domain"
|
||||||
result["blocksource"] = m.domainExceptionsSource[domain]
|
result["blocksource"] = source
|
||||||
|
result["blacklistName"] = m.getBlacklistNameByURL(source)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查子域名排除规则
|
|
||||||
parts := strings.Split(domain, ".")
|
|
||||||
for i := 0; i < len(parts)-1; i++ {
|
|
||||||
subdomain := strings.Join(parts[i:], ".")
|
|
||||||
if m.domainExceptions[subdomain] {
|
|
||||||
result["excluded"] = true
|
|
||||||
result["excludeRule"] = m.domainExceptionsOriginal[subdomain]
|
|
||||||
result["excludeRuleType"] = "subdomain"
|
|
||||||
result["blocksource"] = m.domainExceptionsSource[subdomain]
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查正则表达式排除规则
|
// 检查正则表达式排除规则
|
||||||
for _, re := range m.regexExceptions {
|
for _, re := range m.regexExceptions {
|
||||||
if re.pattern.MatchString(domain) {
|
if re.pattern.MatchString(domain) {
|
||||||
|
source := re.source
|
||||||
result["excluded"] = true
|
result["excluded"] = true
|
||||||
result["excludeRule"] = re.original
|
result["excludeRule"] = re.original
|
||||||
result["excludeRuleType"] = "regex"
|
result["excludeRuleType"] = "regex"
|
||||||
result["blocksource"] = re.source
|
result["blocksource"] = source
|
||||||
|
result["blacklistName"] = m.getBlacklistNameByURL(source)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查阻止规则 - 先检查精确域名匹配,再检查子域名匹配
|
// 检查阻止规则
|
||||||
// 检查精确域名匹配
|
// 检查精确域名匹配
|
||||||
if m.domainRules[domain] {
|
if m.domainRules[domain] {
|
||||||
|
source := m.domainRulesSource[domain]
|
||||||
result["blocked"] = true
|
result["blocked"] = true
|
||||||
result["blockRule"] = m.domainRulesOriginal[domain]
|
result["blockRule"] = domain
|
||||||
result["blockRuleType"] = "exact_domain"
|
result["blockRuleType"] = "exact_domain"
|
||||||
result["blocksource"] = m.domainRulesSource[domain]
|
result["blocksource"] = source
|
||||||
|
result["blacklistName"] = m.getBlacklistNameByURL(source)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查子域名匹配(AdGuardHome风格)
|
|
||||||
// 从最长的子域名开始匹配,确保优先级正确
|
|
||||||
for i := 0; i < len(parts)-1; i++ {
|
|
||||||
subdomain := strings.Join(parts[i:], ".")
|
|
||||||
if m.domainRules[subdomain] {
|
|
||||||
result["blocked"] = true
|
|
||||||
result["blockRule"] = m.domainRulesOriginal[subdomain]
|
|
||||||
result["blockRuleType"] = "subdomain"
|
|
||||||
result["blocksource"] = m.domainRulesSource[subdomain]
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查正则表达式匹配
|
// 检查正则表达式匹配
|
||||||
for _, re := range m.regexRules {
|
for _, re := range m.regexRules {
|
||||||
if re.pattern.MatchString(domain) {
|
if re.pattern.MatchString(domain) {
|
||||||
|
source := re.source
|
||||||
result["blocked"] = true
|
result["blocked"] = true
|
||||||
result["blockRule"] = re.original
|
result["blockRule"] = re.original
|
||||||
result["blockRuleType"] = "regex"
|
result["blockRuleType"] = "regex"
|
||||||
result["blocksource"] = re.source
|
result["blocksource"] = source
|
||||||
|
result["blacklistName"] = m.getBlacklistNameByURL(source)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1271,6 +1251,14 @@ func (m *ShieldManager) UpdateBlacklist(blacklists []config.BlacklistEntry) {
|
|||||||
m.config.Blacklists = blacklists
|
m.config.Blacklists = blacklists
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getBlacklistNameByURL 根据URL获取黑名单名称,如果没有找到则返回URL本身
|
||||||
|
func (m *ShieldManager) getBlacklistNameByURL(url string) string {
|
||||||
|
if name, exists := m.urlToBlacklistName[url]; exists {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
// GetAllHosts 获取所有hosts条目
|
// GetAllHosts 获取所有hosts条目
|
||||||
func (m *ShieldManager) GetAllHosts() map[string]string {
|
func (m *ShieldManager) GetAllHosts() map[string]string {
|
||||||
m.rulesMutex.RLock()
|
m.rulesMutex.RLock()
|
||||||
|
|||||||
+454
-6
@@ -3,14 +3,462 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>DNS Server API 文档</title>
|
<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/3.52.3/swagger-ui.css">
|
||||||
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.18.3/swagger-ui.css">
|
<style>
|
||||||
<link rel="stylesheet" type="text/css" href="css/style.css">
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
}
|
||||||
|
.swagger-ui .topbar {
|
||||||
|
background-color: #2c3e50;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
.swagger-ui .topbar .topbar-wrapper .link {
|
||||||
|
color: #ecf0f1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="swagger-ui"></div>
|
<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="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.52.3/swagger-ui-bundle.js"></script>
|
||||||
<script src="js/index.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.52.3/swagger-ui-standalone-preset.js"></script>
|
||||||
|
<script>
|
||||||
|
// 定义API文档的JSON
|
||||||
|
const swaggerDocument = {
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
"info": {
|
||||||
|
"title": "DNS Server API",
|
||||||
|
"description": "DNS服务器API文档",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"contact": {
|
||||||
|
"name": "API Support",
|
||||||
|
"email": "support@example.com"
|
||||||
|
},
|
||||||
|
"license": {
|
||||||
|
"name": "Apache 2.0",
|
||||||
|
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8080/api",
|
||||||
|
"description": "本地开发服务器"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"/stats": {
|
||||||
|
"get": {
|
||||||
|
"summary": "获取系统统计信息",
|
||||||
|
"description": "获取DNS服务器和Shield的统计信息",
|
||||||
|
"tags": ["stats"],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "成功获取统计信息",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"dns": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Queries": {"type": "integer"},
|
||||||
|
"Blocked": {"type": "integer"},
|
||||||
|
"Allowed": {"type": "integer"},
|
||||||
|
"Errors": {"type": "integer"},
|
||||||
|
"LastQuery": {"type": "string"},
|
||||||
|
"AvgResponseTime": {"type": "number"},
|
||||||
|
"TotalResponseTime": {"type": "number"},
|
||||||
|
"QueryTypes": {"type": "object"},
|
||||||
|
"SourceIPs": {"type": "object"},
|
||||||
|
"CpuUsage": {"type": "number"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shield": {"type": "object"},
|
||||||
|
"topQueryType": {"type": "string"},
|
||||||
|
"activeIPs": {"type": "integer"},
|
||||||
|
"avgResponseTime": {"type": "number"},
|
||||||
|
"cpuUsage": {"type": "number"},
|
||||||
|
"time": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "服务器内部错误",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/shield": {
|
||||||
|
"get": {
|
||||||
|
"summary": "获取Shield配置",
|
||||||
|
"description": "获取Shield的配置信息",
|
||||||
|
"tags": ["shield"],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "成功获取配置信息",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "服务器内部错误",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"summary": "更新Shield配置",
|
||||||
|
"description": "更新Shield的配置信息",
|
||||||
|
"tags": ["shield"],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "成功更新配置",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "请求参数错误",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "服务器内部错误",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/shield/blacklists": {
|
||||||
|
"get": {
|
||||||
|
"summary": "获取黑名单列表",
|
||||||
|
"description": "获取所有远程黑名单的列表",
|
||||||
|
"tags": ["shield"],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "成功获取黑名单列表",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"url": {"type": "string"},
|
||||||
|
"enabled": {"type": "boolean"},
|
||||||
|
"lastUpdate": {"type": "string"},
|
||||||
|
"status": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "服务器内部错误",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"summary": "添加黑名单",
|
||||||
|
"description": "添加新的远程黑名单",
|
||||||
|
"tags": ["shield"],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["name", "url"],
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"url": {"type": "string"},
|
||||||
|
"enabled": {"type": "boolean"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "成功添加黑名单",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "请求参数错误",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "服务器内部错误",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"summary": "更新黑名单",
|
||||||
|
"description": "更新黑名单的配置信息",
|
||||||
|
"tags": ["shield"],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["name"],
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"url": {"type": "string"},
|
||||||
|
"enabled": {"type": "boolean"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "成功更新黑名单",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "请求参数错误",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "黑名单不存在",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "服务器内部错误",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/shield/blacklists/{name}": {
|
||||||
|
"delete": {
|
||||||
|
"summary": "删除黑名单",
|
||||||
|
"description": "根据名称删除指定的远程黑名单",
|
||||||
|
"tags": ["shield"],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "黑名单名称"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "成功删除黑名单",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "黑名单不存在",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "服务器内部错误",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "stats",
|
||||||
|
"description": "统计信息相关API"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "shield",
|
||||||
|
"description": "Shield功能相关API"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化Swagger UI
|
||||||
|
window.onload = function() {
|
||||||
|
const ui = SwaggerUIBundle({
|
||||||
|
spec: swaggerDocument,
|
||||||
|
dom_id: '#swagger-ui',
|
||||||
|
deepLinking: true,
|
||||||
|
presets: [
|
||||||
|
SwaggerUIBundle.presets.apis,
|
||||||
|
SwaggerUIStandalonePreset
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
SwaggerUIBundle.plugins.DownloadUrl
|
||||||
|
],
|
||||||
|
layout: "StandaloneLayout"
|
||||||
|
});
|
||||||
|
window.ui = ui;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+32
-1
@@ -132,7 +132,26 @@ header p {
|
|||||||
|
|
||||||
/* 响应式布局 - 移动设备 */
|
/* 响应式布局 - 移动设备 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
/* 这些样式已经通过Tailwind CSS类在HTML中实现,这里移除避免冲突 */
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
left: -var(--sidebar-width);
|
||||||
|
top: var(--header-height);
|
||||||
|
z-index: 99;
|
||||||
|
height: calc(100vh - var(--header-height));
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.open {
|
||||||
|
left: 0;
|
||||||
|
width: var(--sidebar-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.open .nav-item span {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.open .nav-item i {
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-menu {
|
.nav-menu {
|
||||||
@@ -1043,6 +1062,18 @@ tr:hover {
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 加载动画 */
|
||||||
|
.loading {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 3px solid #f3f3f3;
|
||||||
|
border-top: 3px solid #3498db;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% { transform: rotate(0deg); }
|
||||||
100% { transform: rotate(360deg); }
|
100% { transform: rotate(360deg); }
|
||||||
|
|||||||
+103
-254
@@ -8,17 +8,110 @@
|
|||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<!-- Font Awesome -->
|
<!-- Font Awesome -->
|
||||||
<link href="https://cdn.staticfile.org/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
<link href="https://cdn.staticfile.org/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||||
|
<!-- Chart.js -->
|
||||||
<!-- Chart.js 本地备用 -->
|
<!-- Chart.js 本地备用 -->
|
||||||
<script src="js/vendor/chart.umd.min.js" onerror="this.onerror=null;this.src='js/chart.umd.min.js';"></script>
|
<script src="js/vendor/chart.umd.min.js" onerror="this.onerror=null;this.src='js/chart.umd.min.js';"></script>
|
||||||
|
|
||||||
<!-- Tailwind 配置 -->
|
<!-- Tailwind 配置 -->
|
||||||
<script src="js/vendor/tailwind.js"></script>
|
<script>
|
||||||
|
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'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- 自定义工具类 -->
|
<!-- 自定义工具类 -->
|
||||||
<style type="text/tailwindcss" src="css/index.css"></style>
|
<style type="text/tailwindcss">
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!-- 数字光晕效果样式 -->
|
||||||
|
<style>
|
||||||
|
/* 数字光晕效果基础样式
|
||||||
|
.number-glow {
|
||||||
|
animation: glow-pulse 2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 服务器状态组件光晕效果
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-50 text-dark font-sans">
|
<body class="bg-gray-50 text-dark font-sans">
|
||||||
<div class="flex h-screen overflow-hidden">
|
<div class="flex h-screen overflow-hidden">
|
||||||
<!-- 侧边栏 -->
|
<!-- 侧边栏 -->
|
||||||
<aside id="sidebar" class="fixed inset-y-0 left-0 w-64 bg-white border-r border-gray-200 flex flex-col transition-transform duration-300 z-50 md:relative md:translate-x-0 -translate-x-full shadow-lg">
|
<aside id="sidebar" class="fixed inset-y-0 left-0 w-64 bg-white border-r border-gray-200 flex flex-col transition-transform duration-300 z-50 md:relative md:translate-x-0 -translate-x-full shadow-lg transform-gpu overflow-hidden">
|
||||||
<!-- 移动端关闭按钮 -->
|
<!-- 移动端关闭按钮 -->
|
||||||
<div class="absolute top-4 right-4 md:hidden">
|
<div class="absolute top-4 right-4 md:hidden">
|
||||||
<button id="close-sidebar" class="p-2 text-gray-500 hover:text-gray-700 focus:outline-none">
|
<button id="close-sidebar" class="p-2 text-gray-500 hover:text-gray-700 focus:outline-none">
|
||||||
@@ -56,13 +149,7 @@
|
|||||||
<li>
|
<li>
|
||||||
<a href="#query" class="flex items-center px-4 py-3 text-gray-700 hover:bg-gray-100 rounded-md transition-all">
|
<a href="#query" class="flex items-center px-4 py-3 text-gray-700 hover:bg-gray-100 rounded-md transition-all">
|
||||||
<i class="fa fa-search mr-3 text-lg"></i>
|
<i class="fa fa-search mr-3 text-lg"></i>
|
||||||
<span>DNS屏蔽查询</span>
|
<span>DNS查询</span>
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#logs" class="flex items-center px-4 py-3 text-gray-700 hover:bg-gray-100 rounded-md transition-all">
|
|
||||||
<i class="fa fa-file-text-o mr-3 text-lg"></i>
|
|
||||||
<span>查询日志</span>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@@ -89,7 +176,7 @@
|
|||||||
<!-- 顶部导航栏 -->
|
<!-- 顶部导航栏 -->
|
||||||
<header class="bg-white border-b border-gray-200 h-16 flex items-center justify-between px-6">
|
<header class="bg-white border-b border-gray-200 h-16 flex items-center justify-between px-6">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<button id="toggle-sidebar" class="block md:hidden text-gray-500 hover:text-gray-700 focus:outline-none">
|
<button id="toggle-sidebar" class="md:hidden text-gray-500 hover:text-gray-700 focus:outline-none z-10">
|
||||||
<i class="fa fa-bars text-xl"></i>
|
<i class="fa fa-bars text-xl"></i>
|
||||||
</button>
|
</button>
|
||||||
<h2 class="ml-4 text-xl font-semibold" id="page-title">仪表盘</h2>
|
<h2 class="ml-4 text-xl font-semibold" id="page-title">仪表盘</h2>
|
||||||
@@ -149,22 +236,9 @@
|
|||||||
<button class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100">
|
<button class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100">
|
||||||
<i class="fa fa-bell text-lg"></i>
|
<i class="fa fa-bell text-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
<!-- 账户下拉菜单 -->
|
<div class="flex items-center">
|
||||||
<div class="relative group" id="account-dropdown">
|
|
||||||
<button class="flex items-center p-2 rounded-full hover:bg-gray-100 transition-colors focus:outline-none">
|
|
||||||
<img src="https://picsum.photos/id/1005/40/40" alt="用户头像" class="w-8 h-8 rounded-full">
|
<img src="https://picsum.photos/id/1005/40/40" alt="用户头像" class="w-8 h-8 rounded-full">
|
||||||
<span class="ml-2 hidden md:block">管理员</span>
|
<span class="ml-2 hidden md:block">管理员</span>
|
||||||
<i class="fa fa-caret-down ml-1 text-xs hidden md:block"></i>
|
|
||||||
</button>
|
|
||||||
<!-- 下拉菜单 -->
|
|
||||||
<div class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg py-2 z-50 hidden group-hover:block" id="account-menu">
|
|
||||||
<button id="change-password-btn" class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors">
|
|
||||||
<i class="fa fa-key mr-2"></i>修改密码
|
|
||||||
</button>
|
|
||||||
<button id="logout-btn" class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors">
|
|
||||||
<i class="fa fa-sign-out mr-2"></i>注销
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -402,7 +476,7 @@
|
|||||||
<h3 class="text-lg font-semibold mb-4">被拦截域名排行</h3>
|
<h3 class="text-lg font-semibold mb-4">被拦截域名排行</h3>
|
||||||
<div class="h-64 overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent">
|
<div class="h-64 overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent">
|
||||||
<div class="space-y-3" id="top-blocked-table">
|
<div class="space-y-3" id="top-blocked-table">
|
||||||
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-danger">
|
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 border-l-4 border-danger">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-danger/10 text-danger text-xs font-medium mr-3">1</span>
|
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-danger/10 text-danger text-xs font-medium mr-3">1</span>
|
||||||
@@ -456,7 +530,7 @@
|
|||||||
<h3 class="text-lg font-semibold mb-4">请求域名排行</h3>
|
<h3 class="text-lg font-semibold mb-4">请求域名排行</h3>
|
||||||
<div class="h-64 overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent">
|
<div class="h-64 overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent">
|
||||||
<div class="space-y-3" id="top-domains-table">
|
<div class="space-y-3" id="top-domains-table">
|
||||||
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-success">
|
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 border-l-4 border-success">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-success/10 text-success text-xs font-medium mr-3">1</span>
|
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-success/10 text-success text-xs font-medium mr-3">1</span>
|
||||||
@@ -488,7 +562,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="h-64 overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent">
|
<div class="h-64 overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent">
|
||||||
<div class="space-y-3" id="top-clients-table">
|
<div class="space-y-3" id="top-clients-table">
|
||||||
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-primary">
|
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 border-l-4 border-primary">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-medium mr-3">1</span>
|
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-medium mr-3">1</span>
|
||||||
@@ -662,7 +736,7 @@
|
|||||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">名称</th>
|
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">名称</th>
|
||||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">URL</th>
|
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">URL</th>
|
||||||
<th class="text-center py-3 px-4 text-sm font-medium text-gray-500">状态</th>
|
<th class="text-center py-3 px-4 text-sm font-medium text-gray-500">状态</th>
|
||||||
<th class="text-center py-3 px-4 text-sm font-medium text-gray-500"></th>
|
<th class="text-center py-3 px-4 text-sm font-medium text-gray-500">更新状态</th>
|
||||||
<th class="text-right py-3 px-4 text-sm font-medium text-gray-500">操作</th>
|
<th class="text-right py-3 px-4 text-sm font-medium text-gray-500">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -781,195 +855,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 查询日志页面 -->
|
|
||||||
<div id="logs-content" class="hidden space-y-6">
|
|
||||||
<!-- 日志统计概览 -->
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-4 gap-6">
|
|
||||||
<!-- 总查询数 -->
|
|
||||||
<div class="bg-blue-50 rounded-lg p-4 card-shadow relative overflow-hidden">
|
|
||||||
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-primary opacity-10"></div>
|
|
||||||
<div class="relative z-10">
|
|
||||||
<div class="flex items-center justify-between mb-4">
|
|
||||||
<h3 class="text-gray-500 font-medium">总查询数</h3>
|
|
||||||
<div class="p-2 rounded-full bg-primary/10 text-primary">
|
|
||||||
<i class="fa fa-refresh"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-2">
|
|
||||||
<div class="flex items-end justify-between">
|
|
||||||
<p class="text-3xl font-bold" id="logs-total-queries">0</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 平均响应时间 -->
|
|
||||||
<div class="bg-white rounded-lg p-4 card-shadow relative overflow-hidden">
|
|
||||||
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-info opacity-10"></div>
|
|
||||||
<div class="relative z-10">
|
|
||||||
<div class="flex items-center justify-between mb-4">
|
|
||||||
<h3 class="text-gray-500 font-medium">平均响应时间</h3>
|
|
||||||
<div class="p-2 rounded-full bg-info/10 text-info">
|
|
||||||
<i class="fa fa-clock-o"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-2">
|
|
||||||
<div class="flex items-end justify-between">
|
|
||||||
<p class="text-3xl font-bold" id="logs-avg-response-time">0ms</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 活跃来源IP -->
|
|
||||||
<div class="bg-white rounded-lg p-4 card-shadow relative overflow-hidden">
|
|
||||||
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-success opacity-10"></div>
|
|
||||||
<div class="relative z-10">
|
|
||||||
<div class="flex items-center justify-between mb-4">
|
|
||||||
<h3 class="text-gray-500 font-medium">活跃来源IP</h3>
|
|
||||||
<div class="p-2 rounded-full bg-success/10 text-success">
|
|
||||||
<i class="fa fa-globe"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-2">
|
|
||||||
<div class="flex items-end justify-between">
|
|
||||||
<p class="text-3xl font-bold" id="logs-active-ips">0</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 屏蔽率 -->
|
|
||||||
<div class="bg-red-50 rounded-lg p-4 card-shadow relative overflow-hidden">
|
|
||||||
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-danger opacity-10"></div>
|
|
||||||
<div class="relative z-10">
|
|
||||||
<div class="flex items-center justify-between mb-4">
|
|
||||||
<h3 class="text-gray-500 font-medium">屏蔽率</h3>
|
|
||||||
<div class="p-2 rounded-full bg-danger/10 text-danger">
|
|
||||||
<i class="fa fa-ban"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-2">
|
|
||||||
<div class="flex items-end justify-between">
|
|
||||||
<p class="text-3xl font-bold" id="logs-block-rate">0%</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 日志搜索和过滤 -->
|
|
||||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
|
||||||
<div class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4">
|
|
||||||
<div class="flex-1">
|
|
||||||
<input type="text" id="logs-search" placeholder="搜索域名或客户端IP" class="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
|
||||||
</div>
|
|
||||||
<div class="w-32">
|
|
||||||
<select id="logs-result-filter" class="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
|
||||||
<option value="">全部结果</option>
|
|
||||||
<option value="allowed">允许</option>
|
|
||||||
<option value="blocked">屏蔽</option>
|
|
||||||
<option value="error">错误</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="w-32">
|
|
||||||
<select id="logs-per-page" class="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
|
||||||
<option value="10">10条/页</option>
|
|
||||||
<option value="20">20条/页</option>
|
|
||||||
<option value="30" selected>30条/页</option>
|
|
||||||
<option value="50">50条/页</option>
|
|
||||||
<option value="100">100条/页</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button id="logs-search-btn" class="px-6 py-3 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors">
|
|
||||||
<i class="fa fa-search mr-2"></i>搜索
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 日志趋势图表 -->
|
|
||||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
|
||||||
<div class="flex items-center justify-between mb-6">
|
|
||||||
<h3 class="text-lg font-semibold">查询趋势</h3>
|
|
||||||
<div class="flex space-x-2">
|
|
||||||
<button class="time-range-btn px-4 py-2 rounded-md bg-primary text-white" data-range="24h">24小时</button>
|
|
||||||
<button class="time-range-btn px-4 py-2 rounded-md bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors" data-range="7d">7天</button>
|
|
||||||
<button class="time-range-btn px-4 py-2 rounded-md bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors" data-range="30d">30天</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="h-64">
|
|
||||||
<canvas id="logs-trend-chart"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 日志详情表格 -->
|
|
||||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
|
||||||
<div class="flex items-center justify-between mb-6">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<h3 class="text-lg font-semibold">查询日志详情</h3>
|
|
||||||
<button id="logs-refresh-btn" class="ml-3 p-2 text-gray-500 hover:text-primary hover:bg-gray-100 rounded-full transition-colors" title="刷新日志">
|
|
||||||
<i class="fa fa-refresh"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="logs-loading" class="flex items-center text-sm text-gray-500 hidden">
|
|
||||||
<i class="fa fa-spinner fa-spin mr-2"></i>
|
|
||||||
<span>加载中...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table class="min-w-full">
|
|
||||||
<thead>
|
|
||||||
<tr class="border-b border-gray-200">
|
|
||||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500 cursor-pointer hover:text-primary transition-colors" data-sort="time">
|
|
||||||
<div class="flex items-center">
|
|
||||||
时间
|
|
||||||
<i class="fa fa-sort ml-1 text-xs"></i>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500 cursor-pointer hover:text-primary transition-colors" data-sort="clientIp">
|
|
||||||
<div class="flex items-center">
|
|
||||||
客户端IP
|
|
||||||
<i class="fa fa-sort ml-1 text-xs"></i>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500 cursor-pointer hover:text-primary transition-colors" data-sort="domain">
|
|
||||||
<div class="flex items-center">
|
|
||||||
请求
|
|
||||||
<i class="fa fa-sort ml-1 text-xs"></i>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">响应时间</th>
|
|
||||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">屏蔽规则</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="logs-table-body">
|
|
||||||
<tr>
|
|
||||||
<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>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 分页 -->
|
|
||||||
<div class="flex items-center justify-between mt-6">
|
|
||||||
<div class="text-sm text-gray-500">
|
|
||||||
显示 <span id="logs-current-page">1</span> / <span id="logs-total-pages">1</span> 页
|
|
||||||
</div>
|
|
||||||
<div class="flex space-x-2">
|
|
||||||
<button id="logs-prev-page" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:border-transparent" disabled>
|
|
||||||
<i class="fa fa-chevron-left"></i>
|
|
||||||
</button>
|
|
||||||
<button id="logs-next-page" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:border-transparent" disabled>
|
|
||||||
<i class="fa fa-chevron-right"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="config-content" class="hidden">
|
<div id="config-content" class="hidden">
|
||||||
<!-- 系统设置页面内容 -->
|
<!-- 系统设置页面内容 -->
|
||||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
<div class="bg-white rounded-lg p-6 card-shadow">
|
||||||
@@ -1060,41 +945,6 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 修改密码模态框 -->
|
|
||||||
<div id="change-password-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex items-center justify-center">
|
|
||||||
<div class="bg-white rounded-lg shadow-xl w-full max-w-md p-6">
|
|
||||||
<div class="flex items-center justify-between mb-6">
|
|
||||||
<h3 class="text-xl font-semibold">修改密码</h3>
|
|
||||||
<button id="close-modal-btn" class="text-gray-500 hover:text-gray-700 focus:outline-none">
|
|
||||||
<i class="fa fa-times text-xl"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form id="change-password-form">
|
|
||||||
<div class="mb-4">
|
|
||||||
<label for="current-password" class="block text-sm font-medium text-gray-700 mb-1">当前密码</label>
|
|
||||||
<input type="password" id="current-password" name="currentPassword" placeholder="请输入当前密码" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<label for="new-password" class="block text-sm font-medium text-gray-700 mb-1">新密码</label>
|
|
||||||
<input type="password" id="new-password" name="newPassword" placeholder="请输入新密码" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-6">
|
|
||||||
<label for="confirm-password" class="block text-sm font-medium text-gray-700 mb-1">确认密码</label>
|
|
||||||
<input type="password" id="confirm-password" name="confirmPassword" placeholder="请再次输入新密码" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" required>
|
|
||||||
<div id="password-mismatch" class="text-danger text-sm mt-1 hidden">新密码和确认密码不匹配</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-end space-x-3">
|
|
||||||
<button type="button" id="cancel-change-password" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition-colors">取消</button>
|
|
||||||
<button type="submit" id="save-password-btn" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors">保存</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 脚本 -->
|
<!-- 脚本 -->
|
||||||
<script src="js/main.js"></script>
|
<script src="js/main.js"></script>
|
||||||
<script src="js/api.js"></script>
|
<script src="js/api.js"></script>
|
||||||
@@ -1103,7 +953,6 @@
|
|||||||
<script src="js/shield.js"></script>
|
<script src="js/shield.js"></script>
|
||||||
<script src="js/hosts.js"></script>
|
<script src="js/hosts.js"></script>
|
||||||
<script src="js/query.js"></script>
|
<script src="js/query.js"></script>
|
||||||
<script src="js/logs.js"></script>
|
|
||||||
<script src="js/config.js"></script>
|
<script src="js/config.js"></script>
|
||||||
|
|
||||||
<!-- 直接渲染滚动列表的静态HTML内容 -->
|
<!-- 直接渲染滚动列表的静态HTML内容 -->
|
||||||
|
|||||||
@@ -38,13 +38,6 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
|
|||||||
// 优化错误响应处理
|
// 优化错误响应处理
|
||||||
console.warn(`API请求失败: ${response.status}`);
|
console.warn(`API请求失败: ${response.status}`);
|
||||||
|
|
||||||
// 处理401未授权错误,重定向到登录页面
|
|
||||||
if (response.status === 401) {
|
|
||||||
console.warn('未授权访问,重定向到登录页面');
|
|
||||||
window.location.href = '/login';
|
|
||||||
return { error: '未授权访问' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试解析JSON,但如果失败,直接使用原始文本作为错误信息
|
// 尝试解析JSON,但如果失败,直接使用原始文本作为错误信息
|
||||||
try {
|
try {
|
||||||
const errorData = JSON.parse(responseText);
|
const errorData = JSON.parse(responseText);
|
||||||
|
|||||||
+196
-351
@@ -6,18 +6,12 @@ let dnsRequestsChart = null;
|
|||||||
let detailedDnsRequestsChart = null; // 详细DNS请求趋势图表(浮窗)
|
let detailedDnsRequestsChart = null; // 详细DNS请求趋势图表(浮窗)
|
||||||
let queryTypeChart = null; // 解析类型统计饼图
|
let queryTypeChart = null; // 解析类型统计饼图
|
||||||
let intervalId = null;
|
let intervalId = null;
|
||||||
let dashboardWsConnection = null;
|
let wsConnection = null;
|
||||||
let dashboardWsReconnectTimer = null;
|
let wsReconnectTimer = null;
|
||||||
// 存储统计卡片图表实例
|
// 存储统计卡片图表实例
|
||||||
let statCardCharts = {};
|
let statCardCharts = {};
|
||||||
// 存储统计卡片历史数据
|
// 存储统计卡片历史数据
|
||||||
let statCardHistoryData = {};
|
let statCardHistoryData = {};
|
||||||
// 存储仪表盘历史数据,用于计算趋势
|
|
||||||
window.dashboardHistoryData = window.dashboardHistoryData || {
|
|
||||||
prevResponseTime: null,
|
|
||||||
prevActiveIPs: null,
|
|
||||||
prevTopQueryTypeCount: null
|
|
||||||
};
|
|
||||||
|
|
||||||
// 引入颜色配置文件
|
// 引入颜色配置文件
|
||||||
const COLOR_CONFIG = window.COLOR_CONFIG || {};
|
const COLOR_CONFIG = window.COLOR_CONFIG || {};
|
||||||
@@ -33,6 +27,8 @@ async function initDashboard() {
|
|||||||
// 初始化图表
|
// 初始化图表
|
||||||
initCharts();
|
initCharts();
|
||||||
|
|
||||||
|
// 初始化统计卡片图表
|
||||||
|
initStatCardCharts();
|
||||||
|
|
||||||
|
|
||||||
// 初始化时间范围切换
|
// 初始化时间范围切换
|
||||||
@@ -59,22 +55,22 @@ function connectWebSocket() {
|
|||||||
console.log('正在连接WebSocket:', wsUrl);
|
console.log('正在连接WebSocket:', wsUrl);
|
||||||
|
|
||||||
// 创建WebSocket连接
|
// 创建WebSocket连接
|
||||||
dashboardWsConnection = new WebSocket(wsUrl);
|
wsConnection = new WebSocket(wsUrl);
|
||||||
|
|
||||||
// 连接打开事件
|
// 连接打开事件
|
||||||
dashboardWsConnection.onopen = function() {
|
wsConnection.onopen = function() {
|
||||||
console.log('WebSocket连接已建立');
|
console.log('WebSocket连接已建立');
|
||||||
showNotification('数据更新成功', 'success');
|
showNotification('数据更新成功', 'success');
|
||||||
|
|
||||||
// 清除重连计时器
|
// 清除重连计时器
|
||||||
if (dashboardWsReconnectTimer) {
|
if (wsReconnectTimer) {
|
||||||
clearTimeout(dashboardWsReconnectTimer);
|
clearTimeout(wsReconnectTimer);
|
||||||
dashboardWsReconnectTimer = null;
|
wsReconnectTimer = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 接收消息事件
|
// 接收消息事件
|
||||||
dashboardWsConnection.onmessage = function(event) {
|
wsConnection.onmessage = function(event) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
@@ -88,16 +84,16 @@ function connectWebSocket() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 连接关闭事件
|
// 连接关闭事件
|
||||||
dashboardWsConnection.onclose = function(event) {
|
wsConnection.onclose = function(event) {
|
||||||
console.warn('WebSocket连接已关闭,代码:', event.code);
|
console.warn('WebSocket连接已关闭,代码:', event.code);
|
||||||
dashboardWsConnection = null;
|
wsConnection = null;
|
||||||
|
|
||||||
// 设置重连
|
// 设置重连
|
||||||
setupReconnect();
|
setupReconnect();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 连接错误事件
|
// 连接错误事件
|
||||||
dashboardWsConnection.onerror = function(error) {
|
wsConnection.onerror = function(error) {
|
||||||
console.error('WebSocket连接错误:', error);
|
console.error('WebSocket连接错误:', error);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -110,14 +106,14 @@ function connectWebSocket() {
|
|||||||
|
|
||||||
// 设置重连逻辑
|
// 设置重连逻辑
|
||||||
function setupReconnect() {
|
function setupReconnect() {
|
||||||
if (dashboardWsReconnectTimer) {
|
if (wsReconnectTimer) {
|
||||||
return; // 已经有重连计时器在运行
|
return; // 已经有重连计时器在运行
|
||||||
}
|
}
|
||||||
|
|
||||||
const reconnectDelay = 5000; // 5秒后重连
|
const reconnectDelay = 5000; // 5秒后重连
|
||||||
console.log(`将在${reconnectDelay}ms后尝试重新连接WebSocket`);
|
console.log(`将在${reconnectDelay}ms后尝试重新连接WebSocket`);
|
||||||
|
|
||||||
dashboardWsReconnectTimer = setTimeout(() => {
|
wsReconnectTimer = setTimeout(() => {
|
||||||
connectWebSocket();
|
connectWebSocket();
|
||||||
}, reconnectDelay);
|
}, reconnectDelay);
|
||||||
}
|
}
|
||||||
@@ -128,6 +124,9 @@ function processRealTimeData(stats) {
|
|||||||
// 更新统计卡片 - 这会更新所有统计卡片,包括CPU使用率卡片
|
// 更新统计卡片 - 这会更新所有统计卡片,包括CPU使用率卡片
|
||||||
updateStatsCards(stats);
|
updateStatsCards(stats);
|
||||||
|
|
||||||
|
// 更新统计卡片图表
|
||||||
|
updateStatCardCharts(stats);
|
||||||
|
|
||||||
// 获取查询类型统计数据
|
// 获取查询类型统计数据
|
||||||
let queryTypeStats = null;
|
let queryTypeStats = null;
|
||||||
if (stats.dns && stats.dns.QueryTypes) {
|
if (stats.dns && stats.dns.QueryTypes) {
|
||||||
@@ -157,8 +156,6 @@ function processRealTimeData(stats) {
|
|||||||
|
|
||||||
// 更新新卡片数据
|
// 更新新卡片数据
|
||||||
if (document.getElementById('avg-response-time')) {
|
if (document.getElementById('avg-response-time')) {
|
||||||
const responseTime = stats.avgResponseTime ? stats.avgResponseTime.toFixed(2) + 'ms' : '---';
|
|
||||||
|
|
||||||
// 计算响应时间趋势
|
// 计算响应时间趋势
|
||||||
let responsePercent = '---';
|
let responsePercent = '---';
|
||||||
let trendClass = 'text-gray-400';
|
let trendClass = 'text-gray-400';
|
||||||
@@ -188,9 +185,16 @@ function processRealTimeData(stats) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('avg-response-time').textContent = responseTime;
|
// 使用滚轮效果更新响应时间
|
||||||
|
if (stats.avgResponseTime) {
|
||||||
|
animateValue('avg-response-time', stats.avgResponseTime + 'ms');
|
||||||
|
} else {
|
||||||
|
document.getElementById('avg-response-time').textContent = '---';
|
||||||
|
}
|
||||||
|
|
||||||
const responseTimePercentElem = document.getElementById('response-time-percent');
|
const responseTimePercentElem = document.getElementById('response-time-percent');
|
||||||
if (responseTimePercentElem) {
|
if (responseTimePercentElem) {
|
||||||
|
// 直接更新文本,移除动画效果
|
||||||
responseTimePercentElem.textContent = trendIcon + ' ' + responsePercent;
|
responseTimePercentElem.textContent = trendIcon + ' ' + responsePercent;
|
||||||
responseTimePercentElem.className = `text-sm flex items-center ${trendClass}`;
|
responseTimePercentElem.className = `text-sm flex items-center ${trendClass}`;
|
||||||
}
|
}
|
||||||
@@ -198,42 +202,15 @@ function processRealTimeData(stats) {
|
|||||||
|
|
||||||
if (document.getElementById('top-query-type')) {
|
if (document.getElementById('top-query-type')) {
|
||||||
const queryType = stats.topQueryType || '---';
|
const queryType = stats.topQueryType || '---';
|
||||||
document.getElementById('top-query-type').textContent = queryType;
|
|
||||||
|
|
||||||
const queryPercentElem = document.getElementById('query-type-percentage');
|
const queryPercentElem = document.getElementById('query-type-percentage');
|
||||||
if (queryPercentElem) {
|
if (queryPercentElem) {
|
||||||
// 计算查询类型趋势
|
queryPercentElem.textContent = '• ---';
|
||||||
let queryPercent = '---';
|
queryPercentElem.className = 'text-sm flex items-center text-gray-500';
|
||||||
let trendClass = 'text-gray-400';
|
|
||||||
let trendIcon = '---';
|
|
||||||
|
|
||||||
if (stats.topQueryTypeCount !== undefined && stats.topQueryTypeCount !== null) {
|
|
||||||
// 存储当前值用于下次计算趋势
|
|
||||||
const prevTopQueryTypeCount = window.dashboardHistoryData.prevTopQueryTypeCount || stats.topQueryTypeCount;
|
|
||||||
window.dashboardHistoryData.prevTopQueryTypeCount = stats.topQueryTypeCount;
|
|
||||||
|
|
||||||
// 计算变化百分比
|
|
||||||
if (prevTopQueryTypeCount > 0) {
|
|
||||||
const changePercent = ((stats.topQueryTypeCount - prevTopQueryTypeCount) / prevTopQueryTypeCount) * 100;
|
|
||||||
queryPercent = Math.abs(changePercent).toFixed(1) + '%';
|
|
||||||
|
|
||||||
// 设置趋势图标和颜色
|
|
||||||
if (changePercent > 0) {
|
|
||||||
trendIcon = '↑';
|
|
||||||
trendClass = 'text-primary';
|
|
||||||
} else if (changePercent < 0) {
|
|
||||||
trendIcon = '↓';
|
|
||||||
trendClass = 'text-secondary';
|
|
||||||
} else {
|
|
||||||
trendIcon = '•';
|
|
||||||
trendClass = 'text-gray-500';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
queryPercentElem.textContent = trendIcon + ' ' + queryPercent;
|
// 使用滚轮效果更新查询类型
|
||||||
queryPercentElem.className = `text-sm flex items-center ${trendClass}`;
|
animateValue('top-query-type', queryType);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (document.getElementById('active-ips')) {
|
if (document.getElementById('active-ips')) {
|
||||||
@@ -265,7 +242,8 @@ function processRealTimeData(stats) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('active-ips').textContent = activeIPs;
|
// 使用滚轮效果更新活跃IP数量
|
||||||
|
animateValue('active-ips', activeIPs);
|
||||||
const activeIpsPercentElem = document.getElementById('active-ips-percentage');
|
const activeIpsPercentElem = document.getElementById('active-ips-percentage');
|
||||||
if (activeIpsPercentElem) {
|
if (activeIpsPercentElem) {
|
||||||
activeIpsPercentElem.textContent = trendIcon + ' ' + ipsPercent;
|
activeIpsPercentElem.textContent = trendIcon + ' ' + ipsPercent;
|
||||||
@@ -274,7 +252,7 @@ function processRealTimeData(stats) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 实时更新TOP客户端和TOP域名数据
|
// 实时更新TOP客户端和TOP域名数据
|
||||||
updateTopData();
|
updateTopData(stats);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('处理实时数据失败:', error);
|
console.error('处理实时数据失败:', error);
|
||||||
@@ -282,9 +260,16 @@ function processRealTimeData(stats) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 实时更新TOP客户端和TOP域名数据
|
// 实时更新TOP客户端和TOP域名数据
|
||||||
async function updateTopData() {
|
async function updateTopData(stats = null) {
|
||||||
try {
|
try {
|
||||||
// 获取最新的TOP客户端数据
|
// 如果提供了WebSocket数据,直接使用
|
||||||
|
if (stats && stats.topClients) {
|
||||||
|
updateTopClientsTable(stats.topClients);
|
||||||
|
// 隐藏错误信息
|
||||||
|
const errorElement = document.getElementById('top-clients-error');
|
||||||
|
if (errorElement) errorElement.classList.add('hidden');
|
||||||
|
} else {
|
||||||
|
// 否则从API获取最新的TOP客户端数据
|
||||||
let clientsData = [];
|
let clientsData = [];
|
||||||
try {
|
try {
|
||||||
clientsData = await api.getTopClients();
|
clientsData = await api.getTopClients();
|
||||||
@@ -321,8 +306,16 @@ async function updateTopData() {
|
|||||||
];
|
];
|
||||||
updateTopClientsTable(mockClients);
|
updateTopClientsTable(mockClients);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 获取最新的TOP域名数据
|
// 如果提供了WebSocket数据,直接使用
|
||||||
|
if (stats && stats.topDomains) {
|
||||||
|
updateTopDomainsTable(stats.topDomains);
|
||||||
|
// 隐藏错误信息
|
||||||
|
const errorElement = document.getElementById('top-domains-error');
|
||||||
|
if (errorElement) errorElement.classList.add('hidden');
|
||||||
|
} else {
|
||||||
|
// 否则从API获取最新的TOP域名数据
|
||||||
let domainsData = [];
|
let domainsData = [];
|
||||||
try {
|
try {
|
||||||
domainsData = await api.getTopDomains();
|
domainsData = await api.getTopDomains();
|
||||||
@@ -359,6 +352,7 @@ async function updateTopData() {
|
|||||||
];
|
];
|
||||||
updateTopDomainsTable(mockDomains);
|
updateTopDomainsTable(mockDomains);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('更新TOP数据失败:', error);
|
console.error('更新TOP数据失败:', error);
|
||||||
// 出错时使用模拟数据
|
// 出错时使用模拟数据
|
||||||
@@ -396,15 +390,15 @@ function fallbackToIntervalRefresh() {
|
|||||||
// 清理资源
|
// 清理资源
|
||||||
function cleanupResources() {
|
function cleanupResources() {
|
||||||
// 清除WebSocket连接
|
// 清除WebSocket连接
|
||||||
if (dashboardWsConnection) {
|
if (wsConnection) {
|
||||||
dashboardWsConnection.close();
|
wsConnection.close();
|
||||||
dashboardWsConnection = null;
|
wsConnection = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除重连计时器
|
// 清除重连计时器
|
||||||
if (dashboardWsReconnectTimer) {
|
if (wsReconnectTimer) {
|
||||||
clearTimeout(dashboardWsReconnectTimer);
|
clearTimeout(wsReconnectTimer);
|
||||||
dashboardWsReconnectTimer = null;
|
wsReconnectTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除定时刷新
|
// 清除定时刷新
|
||||||
@@ -743,6 +737,20 @@ async function loadDashboardData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 更新统计卡片
|
// 更新统计卡片
|
||||||
|
// 格式化数字,添加千位分隔符
|
||||||
|
function formatNumber(num, element) {
|
||||||
|
// 如果是数字类型,转换为字符串
|
||||||
|
if (typeof num === 'number') {
|
||||||
|
// 处理浮点数(例如响应时间)
|
||||||
|
if (num % 1 !== 0 && element && element.id.includes('response-time')) {
|
||||||
|
return num.toFixed(2);
|
||||||
|
}
|
||||||
|
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||||
|
}
|
||||||
|
// 如果已经是字符串,直接返回
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
|
||||||
function updateStatsCards(stats) {
|
function updateStatsCards(stats) {
|
||||||
console.log('更新统计卡片,收到数据:', stats);
|
console.log('更新统计卡片,收到数据:', stats);
|
||||||
|
|
||||||
@@ -792,184 +800,22 @@ function updateStatsCards(stats) {
|
|||||||
queryTypePercentage = stats[0].queryTypePercentage || 0;
|
queryTypePercentage = stats[0].queryTypePercentage || 0;
|
||||||
activeIPs = stats[0].activeIPs || 0;
|
activeIPs = stats[0].activeIPs || 0;
|
||||||
activeIPsPercentage = stats[0].activeIPsPercentage || 0;
|
activeIPsPercentage = stats[0].activeIPsPercentage || 0;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 存储正在进行的动画状态,避免动画重叠
|
// 为数字元素添加滚轮式滚动特效
|
||||||
const animationInProgress = {};
|
// 直接更新数字元素,移除滚动动画
|
||||||
|
|
||||||
// 为数字元素添加翻页滚动特效
|
|
||||||
function animateValue(elementId, newValue) {
|
function animateValue(elementId, newValue) {
|
||||||
const element = document.getElementById(elementId);
|
const element = document.getElementById(elementId);
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
|
|
||||||
// 如果该元素正在进行动画,取消当前动画并立即更新值
|
// 先调用formatNumber获取格式化后的值
|
||||||
if (animationInProgress[elementId]) {
|
const formattedNewValue = formatNumber(newValue, element);
|
||||||
// 清除之前可能设置的定时器
|
const currentValue = element.textContent;
|
||||||
clearTimeout(animationInProgress[elementId].timeout1);
|
|
||||||
clearTimeout(animationInProgress[elementId].timeout2);
|
|
||||||
clearTimeout(animationInProgress[elementId].timeout3);
|
|
||||||
|
|
||||||
// 立即设置新值,避免显示错乱
|
// 如果值没有变化,不执行更新
|
||||||
const formattedNewValue = formatNumber(newValue);
|
if (currentValue !== formattedNewValue) {
|
||||||
element.innerHTML = formattedNewValue;
|
element.textContent = formattedNewValue;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldValue = parseInt(element.textContent.replace(/,/g, '')) || 0;
|
|
||||||
const formattedNewValue = formatNumber(newValue);
|
|
||||||
|
|
||||||
// 如果值没有变化,不执行动画
|
|
||||||
if (oldValue === newValue && element.textContent === formattedNewValue) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 先移除可能存在的光晕效果类
|
|
||||||
element.classList.remove('number-glow', 'number-glow-blue', 'number-glow-red', 'number-glow-green', 'number-glow-yellow');
|
|
||||||
element.classList.remove('number-glow-dark-blue', 'number-glow-dark-red', 'number-glow-dark-green', 'number-glow-dark-yellow');
|
|
||||||
|
|
||||||
// 保存原始样式
|
|
||||||
const originalStyle = element.getAttribute('style') || '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 复制原始元素的样式到新元素,确保大小完全一致
|
|
||||||
const computedStyle = getComputedStyle(element);
|
|
||||||
|
|
||||||
// 配置翻页容器样式,确保与原始元素大小完全一致
|
|
||||||
const containerStyle =
|
|
||||||
'position: relative; ' +
|
|
||||||
'display: ' + computedStyle.display + '; ' +
|
|
||||||
'overflow: hidden; ' +
|
|
||||||
'height: ' + element.offsetHeight + 'px; ' +
|
|
||||||
'width: ' + element.offsetWidth + 'px; ' +
|
|
||||||
'margin: ' + computedStyle.margin + '; ' +
|
|
||||||
'padding: ' + computedStyle.padding + '; ' +
|
|
||||||
'box-sizing: ' + computedStyle.boxSizing + '; ' +
|
|
||||||
'line-height: ' + computedStyle.lineHeight + ';';
|
|
||||||
|
|
||||||
// 创建翻页容器
|
|
||||||
const flipContainer = document.createElement('div');
|
|
||||||
flipContainer.style.cssText = containerStyle;
|
|
||||||
flipContainer.className = 'number-flip-container';
|
|
||||||
|
|
||||||
// 创建旧值元素
|
|
||||||
const oldValueElement = document.createElement('div');
|
|
||||||
oldValueElement.textContent = element.textContent;
|
|
||||||
oldValueElement.style.cssText =
|
|
||||||
'position: absolute; ' +
|
|
||||||
'top: 0; ' +
|
|
||||||
'left: 0; ' +
|
|
||||||
'width: 100%; ' +
|
|
||||||
'height: 100%; ' +
|
|
||||||
'display: flex; ' +
|
|
||||||
'align-items: center; ' +
|
|
||||||
'justify-content: center; ' +
|
|
||||||
'transition: transform 400ms ease-in-out; ' +
|
|
||||||
'transform-origin: center;';
|
|
||||||
|
|
||||||
// 创建新值元素
|
|
||||||
const newValueElement = document.createElement('div');
|
|
||||||
newValueElement.textContent = formattedNewValue;
|
|
||||||
newValueElement.style.cssText =
|
|
||||||
'position: absolute; ' +
|
|
||||||
'top: 0; ' +
|
|
||||||
'left: 0; ' +
|
|
||||||
'width: 100%; ' +
|
|
||||||
'height: 100%; ' +
|
|
||||||
'display: flex; ' +
|
|
||||||
'align-items: center; ' +
|
|
||||||
'justify-content: center; ' +
|
|
||||||
'transition: transform 400ms ease-in-out; ' +
|
|
||||||
'transform-origin: center; ' +
|
|
||||||
'transform: translateY(100%);';
|
|
||||||
[oldValueElement, newValueElement].forEach(el => {
|
|
||||||
el.style.fontSize = computedStyle.fontSize;
|
|
||||||
el.style.fontWeight = computedStyle.fontWeight;
|
|
||||||
el.style.color = computedStyle.color;
|
|
||||||
el.style.fontFamily = computedStyle.fontFamily;
|
|
||||||
el.style.textAlign = computedStyle.textAlign;
|
|
||||||
el.style.lineHeight = computedStyle.lineHeight;
|
|
||||||
el.style.width = '100%';
|
|
||||||
el.style.height = '100%';
|
|
||||||
el.style.margin = '0';
|
|
||||||
el.style.padding = '0';
|
|
||||||
el.style.boxSizing = 'border-box';
|
|
||||||
el.style.whiteSpace = computedStyle.whiteSpace;
|
|
||||||
el.style.overflow = 'hidden';
|
|
||||||
el.style.textOverflow = 'ellipsis';
|
|
||||||
// 确保垂直对齐正确
|
|
||||||
el.style.verticalAlign = 'middle';
|
|
||||||
});
|
|
||||||
|
|
||||||
// 替换原始元素的内容
|
|
||||||
element.textContent = '';
|
|
||||||
flipContainer.appendChild(oldValueElement);
|
|
||||||
flipContainer.appendChild(newValueElement);
|
|
||||||
element.appendChild(flipContainer);
|
|
||||||
|
|
||||||
// 标记该元素正在进行动画
|
|
||||||
animationInProgress[elementId] = {};
|
|
||||||
|
|
||||||
// 启动翻页动画
|
|
||||||
animationInProgress[elementId].timeout1 = setTimeout(() => {
|
|
||||||
if (oldValueElement && newValueElement) {
|
|
||||||
oldValueElement.style.transform = 'translateY(-100%)';
|
|
||||||
newValueElement.style.transform = 'translateY(0)';
|
|
||||||
}
|
|
||||||
}, 50);
|
|
||||||
|
|
||||||
// 动画结束后,恢复原始元素
|
|
||||||
animationInProgress[elementId].timeout2 = setTimeout(() => {
|
|
||||||
try {
|
|
||||||
// 清理并设置最终值
|
|
||||||
element.innerHTML = formattedNewValue;
|
|
||||||
if (originalStyle) {
|
|
||||||
element.setAttribute('style', originalStyle);
|
|
||||||
} else {
|
|
||||||
element.removeAttribute('style');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加当前卡片颜色的深色光晕效果
|
|
||||||
const card = element.closest('.stat-card, .bg-blue-50, .bg-red-50, .bg-green-50, .bg-yellow-50');
|
|
||||||
let glowColorClass = '';
|
|
||||||
|
|
||||||
if (card) {
|
|
||||||
if (card.classList.contains('bg-blue-50') || card.id.includes('total') || card.id.includes('response')) {
|
|
||||||
glowColorClass = 'number-glow-dark-blue';
|
|
||||||
} else if (card.classList.contains('bg-red-50') || card.id.includes('blocked')) {
|
|
||||||
glowColorClass = 'number-glow-dark-red';
|
|
||||||
} else if (card.classList.contains('bg-green-50') || card.id.includes('allowed') || card.id.includes('active')) {
|
|
||||||
glowColorClass = 'number-glow-dark-green';
|
|
||||||
} else if (card.classList.contains('bg-yellow-50') || card.id.includes('error') || card.id.includes('cpu')) {
|
|
||||||
glowColorClass = 'number-glow-dark-yellow';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (glowColorClass) {
|
|
||||||
element.classList.add(glowColorClass);
|
|
||||||
|
|
||||||
// 2秒后移除光晕效果
|
|
||||||
animationInProgress[elementId].timeout3 = setTimeout(() => {
|
|
||||||
element.classList.remove('number-glow-dark-blue', 'number-glow-dark-red', 'number-glow-dark-green', 'number-glow-dark-yellow');
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('更新元素失败:', e);
|
|
||||||
} finally {
|
|
||||||
// 清除动画状态标记
|
|
||||||
delete animationInProgress[elementId];
|
|
||||||
}
|
|
||||||
}, 450);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('创建动画失败:', e);
|
|
||||||
// 出错时直接设置值
|
|
||||||
element.innerHTML = formattedNewValue;
|
|
||||||
if (originalStyle) {
|
|
||||||
element.setAttribute('style', originalStyle);
|
|
||||||
} else {
|
|
||||||
element.removeAttribute('style');
|
|
||||||
}
|
|
||||||
// 清除动画状态标记
|
|
||||||
delete animationInProgress[elementId];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -978,37 +824,8 @@ function updateStatsCards(stats) {
|
|||||||
const element = document.getElementById(elementId);
|
const element = document.getElementById(elementId);
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
|
|
||||||
// 检查是否有正在进行的动画
|
// 直接更新文本,移除所有动画效果
|
||||||
if (animationInProgress[elementId + '_percent']) {
|
|
||||||
clearTimeout(animationInProgress[elementId + '_percent']);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
element.style.opacity = '0';
|
|
||||||
element.style.transition = 'opacity 200ms ease-out';
|
|
||||||
|
|
||||||
// 保存定时器ID,便于后续可能的取消
|
|
||||||
animationInProgress[elementId + '_percent'] = setTimeout(() => {
|
|
||||||
try {
|
|
||||||
element.textContent = value;
|
element.textContent = value;
|
||||||
element.style.opacity = '1';
|
|
||||||
} catch (e) {
|
|
||||||
console.error('更新百分比元素失败:', e);
|
|
||||||
} finally {
|
|
||||||
// 清除动画状态标记
|
|
||||||
delete animationInProgress[elementId + '_percent'];
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('设置百分比动画失败:', e);
|
|
||||||
// 出错时直接设置值
|
|
||||||
try {
|
|
||||||
element.textContent = value;
|
|
||||||
element.style.opacity = '1';
|
|
||||||
} catch (e2) {
|
|
||||||
console.error('直接更新百分比元素也失败:', e2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 平滑更新数量显示
|
// 平滑更新数量显示
|
||||||
@@ -1018,14 +835,10 @@ function updateStatsCards(stats) {
|
|||||||
animateValue('error-queries', errorQueries);
|
animateValue('error-queries', errorQueries);
|
||||||
animateValue('active-ips', activeIPs);
|
animateValue('active-ips', activeIPs);
|
||||||
|
|
||||||
// 直接更新文本和百分比,移除动画效果
|
// 平滑更新文本和百分比
|
||||||
const topQueryTypeElement = document.getElementById('top-query-type');
|
updatePercentage('top-query-type', topQueryType);
|
||||||
const queryTypePercentageElement = document.getElementById('query-type-percentage');
|
updatePercentage('query-type-percentage', `${Math.round(queryTypePercentage)}%`);
|
||||||
const activeIpsPercentElement = document.getElementById('active-ips-percent');
|
updatePercentage('active-ips-percent', `${Math.round(activeIPsPercentage)}%`);
|
||||||
|
|
||||||
if (topQueryTypeElement) topQueryTypeElement.textContent = topQueryType;
|
|
||||||
if (queryTypePercentageElement) queryTypePercentageElement.textContent = `${Math.round(queryTypePercentage)}%`;
|
|
||||||
if (activeIpsPercentElement) activeIpsPercentElement.textContent = `${Math.round(activeIPsPercentage)}%`;
|
|
||||||
|
|
||||||
// 计算并平滑更新百分比
|
// 计算并平滑更新百分比
|
||||||
if (totalQueries > 0) {
|
if (totalQueries > 0) {
|
||||||
@@ -1067,9 +880,11 @@ function updateTopBlockedTable(domains) {
|
|||||||
// 如果没有有效数据,提供示例数据
|
// 如果没有有效数据,提供示例数据
|
||||||
if (tableData.length === 0) {
|
if (tableData.length === 0) {
|
||||||
tableData = [
|
tableData = [
|
||||||
{ name: '---.---.---', count: '---' },
|
{ name: 'example1.com', count: 150 },
|
||||||
{ name: '---.---.---', count: '---' },
|
{ name: 'example2.com', count: 130 },
|
||||||
{ name: '---.---.---', count: '---' }
|
{ name: 'example3.com', count: 120 },
|
||||||
|
{ name: 'example4.com', count: 110 },
|
||||||
|
{ name: 'example5.com', count: 100 }
|
||||||
];
|
];
|
||||||
console.log('使用示例数据填充Top屏蔽域名表格');
|
console.log('使用示例数据填充Top屏蔽域名表格');
|
||||||
}
|
}
|
||||||
@@ -1078,7 +893,7 @@ function updateTopBlockedTable(domains) {
|
|||||||
for (let i = 0; i < tableData.length && i < 5; i++) {
|
for (let i = 0; i < tableData.length && i < 5; i++) {
|
||||||
const domain = tableData[i];
|
const domain = tableData[i];
|
||||||
html += `
|
html += `
|
||||||
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-danger">
|
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 border-l-4 border-danger">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-danger/10 text-danger text-xs font-medium mr-3">${i + 1}</span>
|
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-danger/10 text-danger text-xs font-medium mr-3">${i + 1}</span>
|
||||||
@@ -1119,11 +934,11 @@ function updateRecentBlockedTable(domains) {
|
|||||||
if (tableData.length === 0) {
|
if (tableData.length === 0) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
tableData = [
|
tableData = [
|
||||||
{ name: '---.---.---', timestamp: now - 5 * 60 * 1000, type: '广告' },
|
{ name: 'recent1.com', timestamp: now - 5 * 60 * 1000, type: '广告' },
|
||||||
{ name: '---.---.---', timestamp: now - 15 * 60 * 1000, type: '恶意' },
|
{ name: 'recent2.com', timestamp: now - 15 * 60 * 1000, type: '恶意' },
|
||||||
{ name: '---.---.---', timestamp: now - 30 * 60 * 1000, type: '广告' },
|
{ name: 'recent3.com', timestamp: now - 30 * 60 * 1000, type: '广告' },
|
||||||
{ name: '---.---.---', timestamp: now - 45 * 60 * 1000, type: '追踪' },
|
{ name: 'recent4.com', timestamp: now - 45 * 60 * 1000, type: '追踪' },
|
||||||
{ name: '---.---.---', timestamp: now - 60 * 60 * 1000, type: '恶意' }
|
{ name: 'recent5.com', timestamp: now - 60 * 60 * 1000, type: '恶意' }
|
||||||
];
|
];
|
||||||
console.log('使用示例数据填充最近屏蔽域名表格');
|
console.log('使用示例数据填充最近屏蔽域名表格');
|
||||||
}
|
}
|
||||||
@@ -1133,7 +948,7 @@ function updateRecentBlockedTable(domains) {
|
|||||||
const domain = tableData[i];
|
const domain = tableData[i];
|
||||||
const time = formatTime(domain.timestamp);
|
const time = formatTime(domain.timestamp);
|
||||||
html += `
|
html += `
|
||||||
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-warning">
|
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 border-l-4 border-warning">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="font-medium truncate">${domain.name}</div>
|
<div class="font-medium truncate">${domain.name}</div>
|
||||||
<div class="text-sm text-gray-500 mt-1">${time}</div>
|
<div class="text-sm text-gray-500 mt-1">${time}</div>
|
||||||
@@ -1176,11 +991,11 @@ function updateTopClientsTable(clients) {
|
|||||||
// 如果没有有效数据,提供示例数据
|
// 如果没有有效数据,提供示例数据
|
||||||
if (tableData.length === 0) {
|
if (tableData.length === 0) {
|
||||||
tableData = [
|
tableData = [
|
||||||
{ ip: '---.---.---', count: '---' },
|
{ ip: '192.168.1.100', count: 120 },
|
||||||
{ ip: '---.---.---', count: '---' },
|
{ ip: '192.168.1.101', count: 95 },
|
||||||
{ ip: '---.---.---', count: '---' },
|
{ ip: '192.168.1.102', count: 80 },
|
||||||
{ ip: '---.---.---', count: '---' },
|
{ ip: '192.168.1.103', count: 65 },
|
||||||
{ ip: '---.---.---', count: '---' }
|
{ ip: '192.168.1.104', count: 50 }
|
||||||
];
|
];
|
||||||
console.log('使用示例数据填充TOP客户端表格');
|
console.log('使用示例数据填充TOP客户端表格');
|
||||||
}
|
}
|
||||||
@@ -1192,7 +1007,7 @@ function updateTopClientsTable(clients) {
|
|||||||
for (let i = 0; i < tableData.length; i++) {
|
for (let i = 0; i < tableData.length; i++) {
|
||||||
const client = tableData[i];
|
const client = tableData[i];
|
||||||
html += `
|
html += `
|
||||||
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-primary">
|
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 border-l-4 border-primary">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-medium mr-3">${i + 1}</span>
|
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-medium mr-3">${i + 1}</span>
|
||||||
@@ -1253,7 +1068,7 @@ function updateTopDomainsTable(domains) {
|
|||||||
for (let i = 0; i < tableData.length; i++) {
|
for (let i = 0; i < tableData.length; i++) {
|
||||||
const domain = tableData[i];
|
const domain = tableData[i];
|
||||||
html += `
|
html += `
|
||||||
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-success">
|
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 border-l-4 border-success">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-success/10 text-success text-xs font-medium mr-3">${i + 1}</span>
|
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-success/10 text-success text-xs font-medium mr-3">${i + 1}</span>
|
||||||
@@ -1326,8 +1141,7 @@ function initTimeRangeToggle() {
|
|||||||
button.classList.remove('active', 'bg-blue-500', 'text-white', 'bg-gray-200', 'text-gray-700',
|
button.classList.remove('active', 'bg-blue-500', 'text-white', 'bg-gray-200', 'text-gray-700',
|
||||||
'bg-green-500', 'bg-purple-500', 'bg-gray-100');
|
'bg-green-500', 'bg-purple-500', 'bg-gray-100');
|
||||||
|
|
||||||
// 设置非选中状态样式
|
// 设置非选中状态样式,移除过渡动画
|
||||||
button.classList.add('transition-colors', 'duration-200');
|
|
||||||
button.classList.add(...styleConfig.normal);
|
button.classList.add(...styleConfig.normal);
|
||||||
button.classList.add(...styleConfig.hover);
|
button.classList.add(...styleConfig.hover);
|
||||||
|
|
||||||
@@ -1467,11 +1281,8 @@ function initCharts() {
|
|||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
// 添加全局动画配置,确保图表创建和更新时都平滑过渡
|
// 禁用图表动画
|
||||||
animation: {
|
animation: false,
|
||||||
duration: 500, // 延长动画时间,使过渡更平滑
|
|
||||||
easing: 'easeInOutQuart'
|
|
||||||
},
|
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
position: 'bottom',
|
position: 'bottom',
|
||||||
@@ -1539,11 +1350,8 @@ function initCharts() {
|
|||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
// 添加全局动画配置,确保图表创建和更新时都平滑过渡
|
// 禁用图表动画
|
||||||
animation: {
|
animation: false,
|
||||||
duration: 300,
|
|
||||||
easing: 'easeInOutQuart'
|
|
||||||
},
|
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
position: 'bottom',
|
position: 'bottom',
|
||||||
@@ -1704,7 +1512,7 @@ function initDetailedTimeRangeToggle() {
|
|||||||
'bg-green-500', 'bg-purple-500', 'bg-gray-100', 'mixed-view-active');
|
'bg-green-500', 'bg-purple-500', 'bg-gray-100', 'mixed-view-active');
|
||||||
|
|
||||||
// 设置非选中状态样式
|
// 设置非选中状态样式
|
||||||
button.classList.add('transition-colors', 'duration-200');
|
// 移除过渡动画类
|
||||||
button.classList.add(...styleConfig.normal);
|
button.classList.add(...styleConfig.normal);
|
||||||
button.classList.add(...styleConfig.hover);
|
button.classList.add(...styleConfig.hover);
|
||||||
|
|
||||||
@@ -1842,11 +1650,8 @@ function drawDetailedDNSRequestsChart() {
|
|||||||
detailedDnsRequestsChart.data.labels = results[0].labels;
|
detailedDnsRequestsChart.data.labels = results[0].labels;
|
||||||
detailedDnsRequestsChart.data.datasets = datasets;
|
detailedDnsRequestsChart.data.datasets = datasets;
|
||||||
detailedDnsRequestsChart.options.plugins.legend.display = showLegend;
|
detailedDnsRequestsChart.options.plugins.legend.display = showLegend;
|
||||||
// 使用平滑过渡动画更新图表
|
// 更新图表,不使用动画
|
||||||
detailedDnsRequestsChart.update({
|
detailedDnsRequestsChart.update();
|
||||||
duration: 800,
|
|
||||||
easing: 'easeInOutQuart'
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
detailedDnsRequestsChart = new Chart(chartContext, {
|
detailedDnsRequestsChart = new Chart(chartContext, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
@@ -1857,10 +1662,8 @@ function drawDetailedDNSRequestsChart() {
|
|||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
animation: {
|
// 禁用图表动画
|
||||||
duration: 800,
|
animation: false,
|
||||||
easing: 'easeInOutQuart'
|
|
||||||
},
|
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
display: showLegend,
|
display: showLegend,
|
||||||
@@ -2413,15 +2216,8 @@ function updateChartData(chartId, newValue) {
|
|||||||
chart.data.datasets[0].data = historyData;
|
chart.data.datasets[0].data = historyData;
|
||||||
chart.data.labels = generateTimeLabels(historyData.length);
|
chart.data.labels = generateTimeLabels(historyData.length);
|
||||||
|
|
||||||
// 使用自定义动画配置更新图表,确保平滑过渡,避免空白区域
|
// 更新图表,不使用动画
|
||||||
chart.update({
|
chart.update();
|
||||||
duration: 300, // 增加动画持续时间
|
|
||||||
easing: 'easeInOutQuart', // 使用平滑的缓动函数
|
|
||||||
transition: {
|
|
||||||
duration: 300,
|
|
||||||
easing: 'easeInOutQuart'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从统计数据中获取规则数
|
// 从统计数据中获取规则数
|
||||||
@@ -2527,11 +2323,8 @@ function initStatCardCharts() {
|
|||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
// 添加动画配置,确保平滑过渡
|
// 禁用图表动画
|
||||||
animation: {
|
animation: false,
|
||||||
duration: 800,
|
|
||||||
easing: 'easeInOutQuart'
|
|
||||||
},
|
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
display: false
|
display: false
|
||||||
@@ -2633,32 +2426,85 @@ function generateTimeLabels(count) {
|
|||||||
return labels;
|
return labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查元素内容是否溢出
|
||||||
|
function isContentOverflow(element) {
|
||||||
|
if (!element) return false;
|
||||||
|
return element.scrollWidth > element.clientWidth;
|
||||||
|
}
|
||||||
|
|
||||||
// 格式化数字显示(使用K/M后缀)
|
// 格式化数字显示(使用K/M后缀)
|
||||||
function formatNumber(num) {
|
function formatNumber(num, element = null) {
|
||||||
// 如果不是数字,直接返回
|
// 如果不是数字,直接返回
|
||||||
if (isNaN(num) || num === '---') {
|
if (isNaN(num) || num === '---') {
|
||||||
return num;
|
return num;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示完整数字的最大长度阈值
|
// 转换为数字类型
|
||||||
const MAX_FULL_LENGTH = 5;
|
const numericValue = Number(num);
|
||||||
|
// 获取数字的字符串表示形式
|
||||||
|
const numStr = numericValue.toString();
|
||||||
|
|
||||||
// 先获取完整数字字符串
|
// 检查是否需要使用K/M格式
|
||||||
const fullNumStr = num.toString();
|
let useCompactFormat = false;
|
||||||
|
|
||||||
// 如果数字长度小于等于阈值,直接返回完整数字
|
// 方法1: 基于元素内容是否溢出判断
|
||||||
if (fullNumStr.length <= MAX_FULL_LENGTH) {
|
if (element) {
|
||||||
return fullNumStr;
|
// 临时设置元素内容为完整数字
|
||||||
|
const originalContent = element.textContent;
|
||||||
|
element.textContent = numStr;
|
||||||
|
// 检查是否溢出
|
||||||
|
useCompactFormat = isContentOverflow(element);
|
||||||
|
// 恢复原始内容
|
||||||
|
element.textContent = originalContent;
|
||||||
|
}
|
||||||
|
// 方法2: 基于窗口宽度和数字长度的自适应判断
|
||||||
|
else {
|
||||||
|
// 根据窗口宽度动态调整阈值
|
||||||
|
let maxFullLength = 5;
|
||||||
|
if (window.innerWidth < 768) {
|
||||||
|
maxFullLength = 4; // 小屏幕更严格
|
||||||
|
} else if (window.innerWidth < 1024) {
|
||||||
|
maxFullLength = 5; // 中等屏幕
|
||||||
}
|
}
|
||||||
|
|
||||||
// 否则使用缩写格式
|
// 如果数字长度超过阈值,则使用K/M格式
|
||||||
if (num >= 1000000) {
|
useCompactFormat = numStr.length > maxFullLength;
|
||||||
return (num / 1000000).toFixed(1) + 'M';
|
|
||||||
} else if (num >= 1000) {
|
|
||||||
return (num / 1000).toFixed(1) + 'K';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fullNumStr;
|
// 如果需要使用紧凑格式
|
||||||
|
if (useCompactFormat) {
|
||||||
|
if (numericValue >= 1000000) {
|
||||||
|
return (numericValue / 1000000).toFixed(1) + 'M';
|
||||||
|
} else if (numericValue >= 1000) {
|
||||||
|
return (numericValue / 1000).toFixed(1) + 'K';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return numStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新计算所有统计卡片的数字显示格式
|
||||||
|
function updateStatsCardsFormat() {
|
||||||
|
const statCardElements = document.querySelectorAll('.stat-card .stat-value');
|
||||||
|
statCardElements.forEach(element => {
|
||||||
|
// 获取原始数值(可能已经是K/M格式)
|
||||||
|
const text = element.textContent;
|
||||||
|
let originalNum;
|
||||||
|
|
||||||
|
// 解析K/M格式的数字
|
||||||
|
if (text.includes('M')) {
|
||||||
|
originalNum = parseFloat(text) * 1000000;
|
||||||
|
} else if (text.includes('K')) {
|
||||||
|
originalNum = parseFloat(text) * 1000;
|
||||||
|
} else {
|
||||||
|
originalNum = parseFloat(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新计算显示格式
|
||||||
|
if (!isNaN(originalNum)) {
|
||||||
|
element.textContent = formatNumber(originalNum, element);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新运行状态
|
// 更新运行状态
|
||||||
@@ -2730,7 +2576,7 @@ function showNotification(message, type = 'info') {
|
|||||||
// 创建通知元素
|
// 创建通知元素
|
||||||
const notification = document.createElement('div');
|
const notification = document.createElement('div');
|
||||||
notification.id = 'notification';
|
notification.id = 'notification';
|
||||||
notification.className = `fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform transition-all duration-300 translate-y-0 opacity-0`;
|
notification.className = `fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform translate-y-0 opacity-100`;
|
||||||
|
|
||||||
// 设置样式和内容
|
// 设置样式和内容
|
||||||
let bgColor, textColor, icon;
|
let bgColor, textColor, icon;
|
||||||
@@ -2767,18 +2613,9 @@ function showNotification(message, type = 'info') {
|
|||||||
// 添加到页面
|
// 添加到页面
|
||||||
document.body.appendChild(notification);
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
// 显示通知
|
// 自动关闭,直接移除元素,无动画效果
|
||||||
setTimeout(() => {
|
|
||||||
notification.classList.remove('translate-y-0', 'opacity-0');
|
|
||||||
notification.classList.add('-translate-y-2', 'opacity-100');
|
|
||||||
}, 10);
|
|
||||||
|
|
||||||
// 自动关闭
|
|
||||||
setTimeout(() => {
|
|
||||||
notification.classList.add('translate-y-0', 'opacity-0');
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
notification.remove();
|
notification.remove();
|
||||||
}, 300);
|
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2957,6 +2794,9 @@ function handleResponsive() {
|
|||||||
chart.update();
|
chart.update();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 更新统计卡片数字格式,确保在窗口缩小时内容不溢出
|
||||||
|
updateStatsCardsFormat();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 添加触摸事件支持,用于移动端
|
// 添加触摸事件支持,用于移动端
|
||||||
@@ -3045,4 +2885,9 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 页面加载完成后,初始化统计卡片的数字格式,确保内容不会溢出
|
||||||
|
setTimeout(() => {
|
||||||
|
updateStatsCardsFormat();
|
||||||
|
}, 500);
|
||||||
});
|
});
|
||||||
+29
-256
@@ -9,7 +9,6 @@ function setupNavigation() {
|
|||||||
document.getElementById('shield-content'),
|
document.getElementById('shield-content'),
|
||||||
document.getElementById('hosts-content'),
|
document.getElementById('hosts-content'),
|
||||||
document.getElementById('query-content'),
|
document.getElementById('query-content'),
|
||||||
document.getElementById('logs-content'),
|
|
||||||
document.getElementById('config-content')
|
document.getElementById('config-content')
|
||||||
];
|
];
|
||||||
const pageTitle = document.getElementById('page-title');
|
const pageTitle = document.getElementById('page-title');
|
||||||
@@ -22,6 +21,14 @@ function setupNavigation() {
|
|||||||
if (window.innerWidth < 768) {
|
if (window.innerWidth < 768) {
|
||||||
closeSidebar();
|
closeSidebar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 页面特定初始化 - 保留这部分逻辑,因为它不会与hashchange事件处理逻辑冲突
|
||||||
|
const target = item.getAttribute('href').substring(1);
|
||||||
|
if (target === 'shield' && typeof initShieldPage === 'function') {
|
||||||
|
initShieldPage();
|
||||||
|
} else if (target === 'hosts' && typeof initHostsPage === 'function') {
|
||||||
|
initHostsPage();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -33,52 +40,57 @@ function setupNavigation() {
|
|||||||
|
|
||||||
// 打开侧边栏函数
|
// 打开侧边栏函数
|
||||||
function openSidebar() {
|
function openSidebar() {
|
||||||
console.log('Opening sidebar...');
|
console.log('打开侧边栏');
|
||||||
if (sidebar) {
|
if (sidebar) {
|
||||||
sidebar.classList.remove('-translate-x-full');
|
sidebar.classList.remove('-translate-x-full');
|
||||||
sidebar.classList.add('translate-x-0');
|
sidebar.classList.add('translate-x-0');
|
||||||
}
|
}
|
||||||
if (sidebarOverlay) {
|
if (sidebarOverlay) {
|
||||||
sidebarOverlay.classList.remove('hidden');
|
sidebarOverlay.classList.remove('hidden');
|
||||||
sidebarOverlay.classList.add('block');
|
sidebarOverlay.classList.add('flex');
|
||||||
}
|
}
|
||||||
// 防止页面滚动
|
// 防止页面滚动
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
console.log('Sidebar opened successfully');
|
document.body.style.touchAction = 'none'; // 防止触摸滚动
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭侧边栏函数
|
// 关闭侧边栏函数
|
||||||
function closeSidebar() {
|
function closeSidebar() {
|
||||||
console.log('Closing sidebar...');
|
console.log('关闭侧边栏');
|
||||||
if (sidebar) {
|
if (sidebar) {
|
||||||
sidebar.classList.add('-translate-x-full');
|
sidebar.classList.add('-translate-x-full');
|
||||||
sidebar.classList.remove('translate-x-0');
|
sidebar.classList.remove('translate-x-0');
|
||||||
}
|
}
|
||||||
if (sidebarOverlay) {
|
if (sidebarOverlay) {
|
||||||
sidebarOverlay.classList.add('hidden');
|
sidebarOverlay.classList.add('hidden');
|
||||||
sidebarOverlay.classList.remove('block');
|
sidebarOverlay.classList.remove('flex');
|
||||||
}
|
}
|
||||||
// 恢复页面滚动
|
// 恢复页面滚动
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
console.log('Sidebar closed successfully');
|
document.body.style.touchAction = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换侧边栏函数
|
// 切换侧边栏函数
|
||||||
function toggleSidebarVisibility() {
|
function toggleSidebarVisibility() {
|
||||||
console.log('Toggling sidebar visibility...');
|
console.log('切换侧边栏');
|
||||||
console.log('Current sidebar classes:', sidebar ? sidebar.className : 'sidebar not found');
|
if (sidebar) {
|
||||||
if (sidebar && sidebar.classList.contains('-translate-x-full')) {
|
if (sidebar.classList.contains('-translate-x-full')) {
|
||||||
console.log('Sidebar is hidden, opening...');
|
|
||||||
openSidebar();
|
openSidebar();
|
||||||
} else {
|
} else {
|
||||||
console.log('Sidebar is visible, closing...');
|
|
||||||
closeSidebar();
|
closeSidebar();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 绑定切换按钮事件
|
// 绑定切换按钮事件
|
||||||
if (toggleSidebar) {
|
if (toggleSidebar) {
|
||||||
toggleSidebar.addEventListener('click', toggleSidebarVisibility);
|
// 移除可能存在的旧事件监听器
|
||||||
|
toggleSidebar.removeEventListener('click', toggleSidebarVisibility);
|
||||||
|
// 重新添加事件监听器
|
||||||
|
toggleSidebar.addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation(); // 阻止事件冒泡
|
||||||
|
toggleSidebarVisibility();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绑定关闭按钮事件
|
// 绑定关闭按钮事件
|
||||||
@@ -109,84 +121,15 @@ function setupNavigation() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 页面初始化函数 - 根据当前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() {
|
function init() {
|
||||||
// 设置导航
|
// 设置导航
|
||||||
setupNavigation();
|
setupNavigation();
|
||||||
|
|
||||||
// 初始化页面
|
// 加载仪表盘数据
|
||||||
initPageByHash();
|
if (typeof loadDashboardData === 'function') {
|
||||||
|
loadDashboardData();
|
||||||
// 添加hashchange事件监听,处理浏览器前进/后退按钮
|
}
|
||||||
window.addEventListener('hashchange', initPageByHash);
|
|
||||||
|
|
||||||
// 定期更新系统状态
|
// 定期更新系统状态
|
||||||
setInterval(updateSystemStatus, 5000);
|
setInterval(updateSystemStatus, 5000);
|
||||||
@@ -231,175 +174,5 @@ function formatUptime(milliseconds) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 账户功能 - 下拉菜单、注销和修改密码
|
|
||||||
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);
|
window.addEventListener('DOMContentLoaded', init);
|
||||||
+31
-5
@@ -52,8 +52,21 @@ function displayQueryResult(result, domain) {
|
|||||||
const statusClass = result.blocked ? 'text-danger' : 'text-success';
|
const statusClass = result.blocked ? 'text-danger' : 'text-success';
|
||||||
const blockType = result.blocked ? result.blockRuleType || '未知' : '正常';
|
const blockType = result.blocked ? result.blockRuleType || '未知' : '正常';
|
||||||
const blockRule = result.blocked ? result.blockRule || '未知' : '无';
|
const blockRule = result.blocked ? result.blockRule || '未知' : '无';
|
||||||
const blockSource = result.blocked ? result.blocksource || '未知' : '无';
|
|
||||||
|
// 优先使用API返回的blacklistName字段,如果没有则使用blocksource
|
||||||
|
let displaySource = '无';
|
||||||
|
if (result.blocked) {
|
||||||
|
if (result.blacklistName && result.blacklistName !== '') {
|
||||||
|
displaySource = result.blacklistName;
|
||||||
|
} else if (result.blocksource) {
|
||||||
|
displaySource = result.blocksource;
|
||||||
|
} else {
|
||||||
|
displaySource = '未知';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const timestamp = new Date(result.timestamp).toLocaleString();
|
const timestamp = new Date(result.timestamp).toLocaleString();
|
||||||
|
const blockSource = result.blocked ? result.blocksource || '未知' : '无';
|
||||||
|
|
||||||
// 更新结果显示
|
// 更新结果显示
|
||||||
document.getElementById('result-domain').textContent = domain;
|
document.getElementById('result-domain').textContent = domain;
|
||||||
@@ -101,7 +114,7 @@ function displayQueryResult(result, domain) {
|
|||||||
|
|
||||||
// 更新屏蔽来源显示
|
// 更新屏蔽来源显示
|
||||||
if (blockSourceElement) {
|
if (blockSourceElement) {
|
||||||
blockSourceElement.textContent = blockSource;
|
blockSourceElement.textContent = displaySource;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('result-time').textContent = timestamp;
|
document.getElementById('result-time').textContent = timestamp;
|
||||||
@@ -121,7 +134,8 @@ function saveQueryHistory(domain, result) {
|
|||||||
blocked: result.blocked,
|
blocked: result.blocked,
|
||||||
blockRuleType: result.blockRuleType,
|
blockRuleType: result.blockRuleType,
|
||||||
blockRule: result.blockRule,
|
blockRule: result.blockRule,
|
||||||
blocksource: result.blocksource
|
blocksource: result.blocksource,
|
||||||
|
blacklistName: result.blacklistName
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -156,7 +170,19 @@ function loadQueryHistory() {
|
|||||||
const statusText = item.result.blocked ? '被屏蔽' : '正常';
|
const statusText = item.result.blocked ? '被屏蔽' : '正常';
|
||||||
const blockType = item.result.blocked ? item.result.blockRuleType : '正常';
|
const blockType = item.result.blocked ? item.result.blockRuleType : '正常';
|
||||||
const blockRule = item.result.blocked ? item.result.blockRule : '无';
|
const blockRule = item.result.blocked ? item.result.blockRule : '无';
|
||||||
const blockSource = item.result.blocked ? item.result.blocksource : '无';
|
|
||||||
|
// 优先显示blacklistName,如果没有则显示blocksource
|
||||||
|
let sourceDisplay = '无';
|
||||||
|
if (item.result.blocked) {
|
||||||
|
if (item.result.blacklistName && item.result.blacklistName !== '') {
|
||||||
|
sourceDisplay = item.result.blacklistName;
|
||||||
|
} else if (item.result.blocksource) {
|
||||||
|
sourceDisplay = item.result.blocksource;
|
||||||
|
} else {
|
||||||
|
sourceDisplay = '未知';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const formattedTime = new Date(item.timestamp).toLocaleString();
|
const formattedTime = new Date(item.timestamp).toLocaleString();
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@@ -168,7 +194,7 @@ function loadQueryHistory() {
|
|||||||
<span class="text-xs text-gray-500">${blockType}</span>
|
<span class="text-xs text-gray-500">${blockType}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-gray-500 mt-1">规则: ${blockRule}</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">来源: ${sourceDisplay}</div>
|
||||||
<div class="text-xs text-gray-500 mt-1">${formattedTime}</div>
|
<div class="text-xs text-gray-500 mt-1">${formattedTime}</div>
|
||||||
</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}')">
|
<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}')">
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"dns-server/config"
|
||||||
|
"dns-server/shield"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 创建一个简单的配置
|
||||||
|
cfg := &config.ShieldConfig{
|
||||||
|
LocalRulesFile: "",
|
||||||
|
RemoteRulesCacheDir: "/tmp",
|
||||||
|
UpdateInterval: 3600,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建屏蔽管理器
|
||||||
|
manager := shield.NewShieldManager(cfg)
|
||||||
|
|
||||||
|
// 添加测试规则
|
||||||
|
testRules := []string{
|
||||||
|
"||example.com", // 应该只屏蔽 example.com
|
||||||
|
"||www.example.com", // 应该只屏蔽 www.example.com
|
||||||
|
"/text/", // 应该屏蔽包含 text 的域名
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rule := range testRules {
|
||||||
|
manager.AddRule(rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试域名
|
||||||
|
testDomains := []string{
|
||||||
|
"example.com",
|
||||||
|
"www.example.com",
|
||||||
|
"subdomain.example.com",
|
||||||
|
"anotherexample.com",
|
||||||
|
"google.com",
|
||||||
|
"www.anytext.com",
|
||||||
|
"text.example.com",
|
||||||
|
"example.text.com",
|
||||||
|
"example.com.text",
|
||||||
|
"examplewithouttext.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("测试结果:")
|
||||||
|
fmt.Println("----------------------------------------")
|
||||||
|
|
||||||
|
for _, domain := range testDomains {
|
||||||
|
details := manager.CheckDomainBlockDetails(domain)
|
||||||
|
blocked := details["blocked"].(bool)
|
||||||
|
blockRule := details["blockRule"].(string)
|
||||||
|
blockRuleType := details["blockRuleType"].(string)
|
||||||
|
|
||||||
|
status := "允许"
|
||||||
|
if blocked {
|
||||||
|
status = fmt.Sprintf("屏蔽 (规则: %s, 类型: %s)", blockRule, blockRuleType)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s: %s\n", domain, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("----------------------------------------")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user