实现日志功能
This commit is contained in:
81
ReadMe.md
81
ReadMe.md
@@ -1,81 +0,0 @@
|
||||
# DNS服务器项目介绍
|
||||
## 项目概述
|
||||
这是一个基于Go语言开发的高性能DNS服务器,具备域名屏蔽、Hosts管理、统计分析和远程规则管理等功能。服务器支持通过Web界面进行管理配置,同时能够自动更新和缓存远程规则列表。
|
||||
|
||||
## 技术架构
|
||||
### 核心组件
|
||||
1. DNS服务模块 ( `server.go` )
|
||||
- 基于 github.com/miekg/dns 库实现高性能DNS查询处理
|
||||
- 支持配置上游DNS服务器进行递归查询
|
||||
- 实现域名屏蔽、统计数据收集等核心功能
|
||||
|
||||
2. 屏蔽管理系统 ( `manager.go` )
|
||||
- 管理本地和远程屏蔽规则
|
||||
- 支持规则缓存、自动更新和统计
|
||||
- 实现域名和正则表达式规则的解析和匹配
|
||||
|
||||
3. HTTP控制台 ( `server.go` )
|
||||
- 提供Web管理界面
|
||||
- 实现REST API用于配置管理和数据查询
|
||||
|
||||
4. 配置管理 ( `config.go` )
|
||||
- 定义配置结构和加载功能
|
||||
- 支持JSON格式配置文件
|
||||
|
||||
## 主要功能特性
|
||||
### 1. 域名屏蔽系统
|
||||
- 支持本地规则文件和远程规则URL
|
||||
- 多种屏蔽方式:NXDOMAIN、refused、emptyIP、customIP
|
||||
- 支持域名精确匹配和正则表达式匹配
|
||||
- 远程规则自动缓存和更新机制
|
||||
### 2. Hosts管理
|
||||
- 支持自定义Hosts映射
|
||||
- 提供Web界面管理Hosts条目
|
||||
- 自动保存Hosts配置
|
||||
### 3. 统计分析功能
|
||||
- 记录屏蔽域名统计信息
|
||||
- 记录解析域名统计信息
|
||||
- 提供按小时统计的屏蔽数据
|
||||
- 支持查询最常屏蔽和解析的域名
|
||||
### 4. 远程规则管理
|
||||
- 支持添加多个远程规则URL
|
||||
- 自动定期更新远程规则
|
||||
- 本地缓存机制确保规则可用性
|
||||
- Web界面可视化管理
|
||||
### 5. 管理界面
|
||||
- 提供直观的Web控制台
|
||||
- 支持查看服务器状态和统计信息
|
||||
- 规则管理和配置修改
|
||||
- DNS查询测试工具
|
||||
## 项目结构
|
||||
```
|
||||
/root/dns/
|
||||
├── config/ # 配置管理
|
||||
├── data/ # 数据目录(包含缓存和统计)
|
||||
│ └── remote_rules/ # 远程规则缓存
|
||||
├── dns/ # DNS服务器核心
|
||||
├── http/ # HTTP控制台
|
||||
├── logger/ # 日志系统
|
||||
├── shield/ # 屏蔽规则管理
|
||||
├── static/ # 静态Web文件
|
||||
├── main.go # 程序入口
|
||||
└── config.json # 配置文件
|
||||
```
|
||||
## 配置项说明
|
||||
主要配置文件 `config.json` 包含以下部分:
|
||||
|
||||
- DNS配置 :端口、上游DNS服务器、超时设置等
|
||||
- HTTP配置 :控制台端口、主机绑定等
|
||||
- 屏蔽配置 :规则文件路径、远程规则URL、更新间隔等
|
||||
- 日志配置 :日志文件路径、级别设置等
|
||||
## 使用场景
|
||||
1. 网络内容过滤(广告、恶意网站屏蔽)
|
||||
2. 本地DNS缓存加速
|
||||
3. 企业/家庭网络DNS管理
|
||||
4. 开发测试环境DNS重定向
|
||||
## 技术栈
|
||||
- 语言 :Go
|
||||
- DNS库 :github.com/miekg/dns
|
||||
- 日志库 :github.com/sirupsen/logrus
|
||||
- Web前端 :HTML/CSS/JavaScript
|
||||
该DNS服务器具有高性能、功能全面、易于配置等特点,适用于需要精确控制DNS查询结果的各种网络环境。
|
||||
116
config.json
Normal file
116
config.json
Normal file
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"dns": {
|
||||
"port": 53,
|
||||
"upstreamDNS": [
|
||||
"223.5.5.5:53",
|
||||
"223.6.6.6:53"
|
||||
],
|
||||
"timeout": 5000,
|
||||
"statsFile": "data/stats.json",
|
||||
"saveInterval": 300
|
||||
},
|
||||
"http": {
|
||||
"port": 8080,
|
||||
"host": "0.0.0.0",
|
||||
"enableAPI": true
|
||||
},
|
||||
"shield": {
|
||||
"localRulesFile": "data/rules.txt",
|
||||
"blacklists": [
|
||||
{
|
||||
"name": "AdGuard DNS filter",
|
||||
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/filter.txt",
|
||||
"enabled": true,
|
||||
"lastUpdateTime": "2025-11-28T16:13:03.564Z"
|
||||
},
|
||||
{
|
||||
"name": "Adaway Default Blocklist",
|
||||
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/adaway.txt",
|
||||
"enabled": true,
|
||||
"lastUpdateTime": "2025-11-28T15:36:43.086Z"
|
||||
},
|
||||
{
|
||||
"name": "CHN-anti-AD",
|
||||
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/list/easylist.txt",
|
||||
"enabled": true,
|
||||
"lastUpdateTime": "2025-11-28T15:26:24.833Z"
|
||||
},
|
||||
{
|
||||
"name": "My GitHub Rules",
|
||||
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/costomize.txt",
|
||||
"enabled": true,
|
||||
"lastUpdateTime": "2025-11-29T17:05:40.283Z"
|
||||
},
|
||||
{
|
||||
"name": "CNList",
|
||||
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/list/china.list",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"name": "大圣净化",
|
||||
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/dsjh.txt",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "Hate \u0026 Junk",
|
||||
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hate-and-junk-extended.txt",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "My Gitlab Hosts",
|
||||
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hosts/costomize.txt",
|
||||
"enabled": true,
|
||||
"lastUpdateTime": "2025-11-29T17:11:28.130Z"
|
||||
},
|
||||
{
|
||||
"name": "Anti Remote Requests",
|
||||
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hosts/anti-remoterequests.txt",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "URL-Based.txt",
|
||||
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/url-based-adguard.txt",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "My Gitlab A/T Rules",
|
||||
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/ads-and-trackers.txt",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "My Gitlab Malware List",
|
||||
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/malware.txt",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "hosts",
|
||||
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/costomize.txt",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "AWAvenue-Ads-Rule",
|
||||
"url": "http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/rules/AWAvenue-Ads-Rule.txt",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "诈骗域名",
|
||||
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/cheat.txt",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"updateInterval": 3600,
|
||||
"hostsFile": "data/hosts.txt",
|
||||
"blockMethod": "NXDOMAIN",
|
||||
"customBlockIP": "",
|
||||
"statsFile": "./data/shield_stats.json",
|
||||
"statsSaveInterval": 60,
|
||||
"remoteRulesCacheDir": "data/remote_rules"
|
||||
},
|
||||
"log": {
|
||||
"file": "logs/dns-server.log",
|
||||
"level": "debug",
|
||||
"maxSize": 100,
|
||||
"maxBackups": 10,
|
||||
"maxAge": 30
|
||||
}
|
||||
}
|
||||
244
dns/server.go
244
dns/server.go
@@ -29,15 +29,24 @@ type BlockedDomain struct {
|
||||
LastSeen time.Time
|
||||
}
|
||||
|
||||
// ClientStats 客户端统计
|
||||
|
||||
type ClientStats struct {
|
||||
IP string
|
||||
Count int64
|
||||
LastSeen time.Time
|
||||
}
|
||||
|
||||
// StatsData 用于持久化的统计数据结构
|
||||
type StatsData struct {
|
||||
Stats *Stats `json:"stats"`
|
||||
BlockedDomains map[string]*BlockedDomain `json:"blockedDomains"`
|
||||
ResolvedDomains map[string]*BlockedDomain `json:"resolvedDomains"`
|
||||
HourlyStats map[string]int64 `json:"hourlyStats"`
|
||||
DailyStats map[string]int64 `json:"dailyStats"`
|
||||
MonthlyStats map[string]int64 `json:"monthlyStats"`
|
||||
LastSaved time.Time `json:"lastSaved"`
|
||||
Stats *Stats `json:"stats"`
|
||||
BlockedDomains map[string]*BlockedDomain `json:"blockedDomains"`
|
||||
ResolvedDomains map[string]*BlockedDomain `json:"resolvedDomains"`
|
||||
ClientStats map[string]*ClientStats `json:"clientStats"`
|
||||
HourlyStats map[string]int64 `json:"hourlyStats"`
|
||||
DailyStats map[string]int64 `json:"dailyStats"`
|
||||
MonthlyStats map[string]int64 `json:"monthlyStats"`
|
||||
LastSaved time.Time `json:"lastSaved"`
|
||||
}
|
||||
|
||||
// Server DNS服务器
|
||||
@@ -46,6 +55,7 @@ type Server struct {
|
||||
shieldConfig *config.ShieldConfig
|
||||
shieldManager *shield.ShieldManager
|
||||
server *dns.Server
|
||||
tcpServer *dns.Server
|
||||
resolver *dns.Client
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
@@ -55,6 +65,8 @@ type Server struct {
|
||||
blockedDomains map[string]*BlockedDomain
|
||||
resolvedDomainsMutex sync.RWMutex
|
||||
resolvedDomains map[string]*BlockedDomain // 用于记录解析的域名
|
||||
clientStatsMutex sync.RWMutex
|
||||
clientStats map[string]*ClientStats // 用于记录客户端统计
|
||||
hourlyStatsMutex sync.RWMutex
|
||||
hourlyStats map[string]int64 // 按小时统计屏蔽数量
|
||||
dailyStatsMutex sync.RWMutex
|
||||
@@ -64,20 +76,22 @@ type Server struct {
|
||||
saveTicker *time.Ticker // 用于定时保存数据
|
||||
startTime time.Time // 服务器启动时间
|
||||
saveDone chan struct{} // 用于通知保存协程停止
|
||||
stopped bool // 服务器是否已经停止
|
||||
stoppedMutex sync.Mutex // 保护stopped标志的互斥锁
|
||||
}
|
||||
|
||||
// Stats DNS服务器统计信息
|
||||
type Stats struct {
|
||||
Queries int64
|
||||
Blocked int64
|
||||
Allowed int64
|
||||
Errors int64
|
||||
LastQuery time.Time
|
||||
AvgResponseTime float64 // 平均响应时间(ms)
|
||||
TotalResponseTime int64 // 总响应时间
|
||||
QueryTypes map[string]int64 // 查询类型统计
|
||||
SourceIPs map[string]bool // 活跃来源IP
|
||||
CpuUsage float64 // CPU使用率(%)
|
||||
Queries int64
|
||||
Blocked int64
|
||||
Allowed int64
|
||||
Errors int64
|
||||
LastQuery time.Time
|
||||
AvgResponseTime float64 // 平均响应时间(ms)
|
||||
TotalResponseTime int64 // 总响应时间
|
||||
QueryTypes map[string]int64 // 查询类型统计
|
||||
SourceIPs map[string]bool // 活跃来源IP
|
||||
CpuUsage float64 // CPU使用率(%)
|
||||
}
|
||||
|
||||
// NewServer 创建DNS服务器实例
|
||||
@@ -95,41 +109,59 @@ func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shie
|
||||
cancel: cancel,
|
||||
startTime: time.Now(), // 记录服务器启动时间
|
||||
stats: &Stats{
|
||||
Queries: 0,
|
||||
Blocked: 0,
|
||||
Allowed: 0,
|
||||
Errors: 0,
|
||||
AvgResponseTime: 0,
|
||||
Queries: 0,
|
||||
Blocked: 0,
|
||||
Allowed: 0,
|
||||
Errors: 0,
|
||||
AvgResponseTime: 0,
|
||||
TotalResponseTime: 0,
|
||||
QueryTypes: make(map[string]int64),
|
||||
SourceIPs: make(map[string]bool),
|
||||
CpuUsage: 0,
|
||||
QueryTypes: make(map[string]int64),
|
||||
SourceIPs: make(map[string]bool),
|
||||
CpuUsage: 0,
|
||||
},
|
||||
blockedDomains: make(map[string]*BlockedDomain),
|
||||
resolvedDomains: make(map[string]*BlockedDomain),
|
||||
clientStats: make(map[string]*ClientStats),
|
||||
hourlyStats: make(map[string]int64),
|
||||
dailyStats: make(map[string]int64),
|
||||
monthlyStats: make(map[string]int64),
|
||||
saveDone: make(chan struct{}),
|
||||
stopped: false, // 初始化为未停止状态
|
||||
}
|
||||
|
||||
|
||||
// 加载已保存的统计数据
|
||||
server.loadStatsData()
|
||||
|
||||
|
||||
return server
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Start 启动DNS服务器
|
||||
func (s *Server) Start() error {
|
||||
// 重新初始化上下文和取消函数
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s.ctx = ctx
|
||||
s.cancel = cancel
|
||||
|
||||
// 重新初始化saveDone通道
|
||||
s.saveDone = make(chan struct{})
|
||||
|
||||
// 重置stopped标志
|
||||
s.stoppedMutex.Lock()
|
||||
s.stopped = false
|
||||
s.stoppedMutex.Unlock()
|
||||
|
||||
// 更新服务器启动时间
|
||||
s.startTime = time.Now()
|
||||
|
||||
s.server = &dns.Server{
|
||||
Addr: fmt.Sprintf(":%d", s.config.Port),
|
||||
Net: "udp",
|
||||
Handler: dns.HandlerFunc(s.handleDNSRequest),
|
||||
}
|
||||
|
||||
// 启动TCP服务器(用于大型响应)
|
||||
tcpServer := &dns.Server{
|
||||
// 保存TCP服务器实例,以便在Stop方法中关闭
|
||||
s.tcpServer = &dns.Server{
|
||||
Addr: fmt.Sprintf(":%d", s.config.Port),
|
||||
Net: "tcp",
|
||||
Handler: dns.HandlerFunc(s.handleDNSRequest),
|
||||
@@ -138,6 +170,9 @@ func (s *Server) Start() error {
|
||||
// 启动CPU使用率监控
|
||||
go s.startCpuUsageMonitor()
|
||||
|
||||
// 启动自动保存功能
|
||||
go s.startAutoSave()
|
||||
|
||||
// 启动UDP服务
|
||||
go func() {
|
||||
logger.Info(fmt.Sprintf("DNS UDP服务器启动,监听端口: %d", s.config.Port))
|
||||
@@ -150,7 +185,7 @@ func (s *Server) Start() error {
|
||||
// 启动TCP服务
|
||||
go func() {
|
||||
logger.Info(fmt.Sprintf("DNS TCP服务器启动,监听端口: %d", s.config.Port))
|
||||
if err := tcpServer.ListenAndServe(); err != nil {
|
||||
if err := s.tcpServer.ListenAndServe(); err != nil {
|
||||
logger.Error("DNS TCP服务器启动失败", "error", err)
|
||||
s.cancel()
|
||||
}
|
||||
@@ -163,31 +198,44 @@ func (s *Server) Start() error {
|
||||
|
||||
// Stop 停止DNS服务器
|
||||
func (s *Server) Stop() {
|
||||
// 检查服务器是否已经停止
|
||||
s.stoppedMutex.Lock()
|
||||
if s.stopped {
|
||||
s.stoppedMutex.Unlock()
|
||||
return // 服务器已经停止,直接返回
|
||||
}
|
||||
// 标记服务器为已停止状态
|
||||
s.stopped = true
|
||||
s.stoppedMutex.Unlock()
|
||||
|
||||
// 发送停止信号给保存协程
|
||||
close(s.saveDone)
|
||||
|
||||
|
||||
// 最后保存一次数据
|
||||
s.saveStatsData()
|
||||
|
||||
|
||||
// 停止服务器
|
||||
s.cancel()
|
||||
if s.server != nil {
|
||||
s.server.Shutdown()
|
||||
}
|
||||
if s.tcpServer != nil {
|
||||
s.tcpServer.Shutdown()
|
||||
}
|
||||
logger.Info("DNS服务器已停止")
|
||||
}
|
||||
|
||||
// handleDNSRequest 处理DNS请求
|
||||
func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
startTime := time.Now()
|
||||
|
||||
|
||||
// 获取来源IP
|
||||
sourceIP := w.RemoteAddr().String()
|
||||
// 提取IP地址部分,去掉端口
|
||||
if idx := strings.LastIndex(sourceIP, ":"); idx >= 0 {
|
||||
sourceIP = sourceIP[:idx]
|
||||
}
|
||||
|
||||
|
||||
// 更新来源IP统计
|
||||
s.updateStats(func(stats *Stats) {
|
||||
stats.Queries++
|
||||
@@ -195,6 +243,9 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
stats.SourceIPs[sourceIP] = true
|
||||
})
|
||||
|
||||
// 更新客户端统计
|
||||
s.updateClientStats(sourceIP)
|
||||
|
||||
// 只处理递归查询
|
||||
if r.RecursionDesired == false {
|
||||
response := new(dns.Msg)
|
||||
@@ -202,7 +253,7 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
response.RecursionAvailable = true
|
||||
response.SetRcode(r, dns.RcodeRefused)
|
||||
w.WriteMsg(response)
|
||||
|
||||
|
||||
// 计算响应时间
|
||||
responseTime := time.Since(startTime).Milliseconds()
|
||||
s.updateStats(func(stats *Stats) {
|
||||
@@ -315,11 +366,6 @@ func (s *Server) handleBlockedResponse(w dns.ResponseWriter, r *dns.Msg, domain
|
||||
// 更新被屏蔽域名统计
|
||||
s.updateBlockedDomainStats(domain)
|
||||
|
||||
// 更新总体统计
|
||||
s.updateStats(func(stats *Stats) {
|
||||
stats.Blocked++
|
||||
})
|
||||
|
||||
response := new(dns.Msg)
|
||||
response.SetReply(r)
|
||||
response.RecursionAvailable = true
|
||||
@@ -432,19 +478,19 @@ func (s *Server) updateBlockedDomainStats(domain string) {
|
||||
|
||||
// 更新统计数据
|
||||
now := time.Now()
|
||||
|
||||
|
||||
// 更新小时统计
|
||||
hourKey := now.Format("2006-01-02-15")
|
||||
s.hourlyStatsMutex.Lock()
|
||||
s.hourlyStats[hourKey]++
|
||||
s.hourlyStatsMutex.Unlock()
|
||||
|
||||
|
||||
// 更新每日统计
|
||||
dayKey := now.Format("2006-01-02")
|
||||
s.dailyStatsMutex.Lock()
|
||||
s.dailyStats[dayKey]++
|
||||
s.dailyStatsMutex.Unlock()
|
||||
|
||||
|
||||
// 更新每月统计
|
||||
monthKey := now.Format("2006-01")
|
||||
s.monthlyStatsMutex.Lock()
|
||||
@@ -452,6 +498,23 @@ func (s *Server) updateBlockedDomainStats(domain string) {
|
||||
s.monthlyStatsMutex.Unlock()
|
||||
}
|
||||
|
||||
// updateClientStats 更新客户端统计
|
||||
func (s *Server) updateClientStats(ip string) {
|
||||
s.clientStatsMutex.Lock()
|
||||
defer s.clientStatsMutex.Unlock()
|
||||
|
||||
if entry, exists := s.clientStats[ip]; exists {
|
||||
entry.Count++
|
||||
entry.LastSeen = time.Now()
|
||||
} else {
|
||||
s.clientStats[ip] = &ClientStats{
|
||||
IP: ip,
|
||||
Count: 1,
|
||||
LastSeen: time.Now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateResolvedDomainStats 更新解析域名统计
|
||||
func (s *Server) updateResolvedDomainStats(domain string) {
|
||||
s.resolvedDomainsMutex.Lock()
|
||||
@@ -500,16 +563,16 @@ func (s *Server) GetStats() *Stats {
|
||||
|
||||
// 返回统计信息的副本
|
||||
return &Stats{
|
||||
Queries: s.stats.Queries,
|
||||
Blocked: s.stats.Blocked,
|
||||
Allowed: s.stats.Allowed,
|
||||
Errors: s.stats.Errors,
|
||||
LastQuery: s.stats.LastQuery,
|
||||
AvgResponseTime: s.stats.AvgResponseTime,
|
||||
Queries: s.stats.Queries,
|
||||
Blocked: s.stats.Blocked,
|
||||
Allowed: s.stats.Allowed,
|
||||
Errors: s.stats.Errors,
|
||||
LastQuery: s.stats.LastQuery,
|
||||
AvgResponseTime: s.stats.AvgResponseTime,
|
||||
TotalResponseTime: s.stats.TotalResponseTime,
|
||||
QueryTypes: queryTypesCopy,
|
||||
SourceIPs: sourceIPsCopy,
|
||||
CpuUsage: s.stats.CpuUsage,
|
||||
QueryTypes: queryTypesCopy,
|
||||
SourceIPs: sourceIPsCopy,
|
||||
CpuUsage: s.stats.CpuUsage,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -582,6 +645,29 @@ func (s *Server) GetRecentBlockedDomains(limit int) []BlockedDomain {
|
||||
return domains
|
||||
}
|
||||
|
||||
// GetTopClients 获取TOP客户端列表
|
||||
func (s *Server) GetTopClients(limit int) []ClientStats {
|
||||
s.clientStatsMutex.RLock()
|
||||
defer s.clientStatsMutex.RUnlock()
|
||||
|
||||
// 转换为切片
|
||||
clients := make([]ClientStats, 0, len(s.clientStats))
|
||||
for _, entry := range s.clientStats {
|
||||
clients = append(clients, *entry)
|
||||
}
|
||||
|
||||
// 按请求次数排序
|
||||
sort.Slice(clients, func(i, j int) bool {
|
||||
return clients[i].Count > clients[j].Count
|
||||
})
|
||||
|
||||
// 返回限制数量
|
||||
if len(clients) > limit {
|
||||
return clients[:limit]
|
||||
}
|
||||
return clients
|
||||
}
|
||||
|
||||
// GetHourlyStats 获取每小时统计数据
|
||||
func (s *Server) GetHourlyStats() map[string]int64 {
|
||||
s.hourlyStatsMutex.RLock()
|
||||
@@ -667,19 +753,26 @@ func (s *Server) loadStatsData() {
|
||||
s.hourlyStats = statsData.HourlyStats
|
||||
}
|
||||
s.hourlyStatsMutex.Unlock()
|
||||
|
||||
|
||||
s.dailyStatsMutex.Lock()
|
||||
if statsData.DailyStats != nil {
|
||||
s.dailyStats = statsData.DailyStats
|
||||
}
|
||||
s.dailyStatsMutex.Unlock()
|
||||
|
||||
|
||||
s.monthlyStatsMutex.Lock()
|
||||
if statsData.MonthlyStats != nil {
|
||||
s.monthlyStats = statsData.MonthlyStats
|
||||
}
|
||||
s.monthlyStatsMutex.Unlock()
|
||||
|
||||
// 加载客户端统计数据
|
||||
s.clientStatsMutex.Lock()
|
||||
if statsData.ClientStats != nil {
|
||||
s.clientStats = statsData.ClientStats
|
||||
}
|
||||
s.clientStatsMutex.Unlock()
|
||||
|
||||
logger.Info("统计数据加载成功")
|
||||
}
|
||||
|
||||
@@ -689,18 +782,25 @@ func (s *Server) saveStatsData() {
|
||||
return
|
||||
}
|
||||
|
||||
// 创建数据目录
|
||||
statsDir := filepath.Dir(s.config.StatsFile)
|
||||
err := os.MkdirAll(statsDir, 0755)
|
||||
// 获取绝对路径以避免工作目录问题
|
||||
statsFilePath, err := filepath.Abs(s.config.StatsFile)
|
||||
if err != nil {
|
||||
logger.Error("创建统计数据目录失败", "error", err)
|
||||
logger.Error("获取统计文件绝对路径失败", "path", s.config.StatsFile, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 创建数据目录
|
||||
statsDir := filepath.Dir(statsFilePath)
|
||||
err = os.MkdirAll(statsDir, 0755)
|
||||
if err != nil {
|
||||
logger.Error("创建统计数据目录失败", "dir", statsDir, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 收集所有统计数据
|
||||
statsData := &StatsData{
|
||||
Stats: s.GetStats(),
|
||||
LastSaved: time.Now(),
|
||||
Stats: s.GetStats(),
|
||||
LastSaved: time.Now(),
|
||||
}
|
||||
|
||||
// 复制域名数据
|
||||
@@ -724,14 +824,14 @@ func (s *Server) saveStatsData() {
|
||||
statsData.HourlyStats[k] = v
|
||||
}
|
||||
s.hourlyStatsMutex.RUnlock()
|
||||
|
||||
|
||||
s.dailyStatsMutex.RLock()
|
||||
statsData.DailyStats = make(map[string]int64)
|
||||
for k, v := range s.dailyStats {
|
||||
statsData.DailyStats[k] = v
|
||||
}
|
||||
s.dailyStatsMutex.RUnlock()
|
||||
|
||||
|
||||
s.monthlyStatsMutex.RLock()
|
||||
statsData.MonthlyStats = make(map[string]int64)
|
||||
for k, v := range s.monthlyStats {
|
||||
@@ -739,6 +839,14 @@ func (s *Server) saveStatsData() {
|
||||
}
|
||||
s.monthlyStatsMutex.RUnlock()
|
||||
|
||||
// 复制客户端统计数据
|
||||
s.clientStatsMutex.RLock()
|
||||
statsData.ClientStats = make(map[string]*ClientStats)
|
||||
for k, v := range s.clientStats {
|
||||
statsData.ClientStats[k] = v
|
||||
}
|
||||
s.clientStatsMutex.RUnlock()
|
||||
|
||||
// 序列化数据
|
||||
jsonData, err := json.MarshalIndent(statsData, "", " ")
|
||||
if err != nil {
|
||||
@@ -747,13 +855,13 @@ func (s *Server) saveStatsData() {
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
err = ioutil.WriteFile(s.config.StatsFile, jsonData, 0644)
|
||||
err = os.WriteFile(statsFilePath, jsonData, 0644)
|
||||
if err != nil {
|
||||
logger.Error("保存统计数据到文件失败", "error", err)
|
||||
logger.Error("保存统计数据到文件失败", "file", statsFilePath, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("统计数据保存成功")
|
||||
logger.Info("统计数据保存成功", "file", statsFilePath)
|
||||
}
|
||||
|
||||
// startCpuUsageMonitor 启动CPU使用率监控
|
||||
@@ -778,7 +886,7 @@ func (s *Server) startCpuUsageMonitor() {
|
||||
cpuUsage = 0.0
|
||||
logger.Error("获取系统CPU使用率失败", "error", err)
|
||||
}
|
||||
|
||||
|
||||
s.updateStats(func(stats *Stats) {
|
||||
stats.CpuUsage = cpuUsage
|
||||
})
|
||||
@@ -798,7 +906,7 @@ func getSystemCpuUsage(prevIdle, prevTotal *uint64) (float64, error) {
|
||||
defer file.Close()
|
||||
|
||||
var cpuUser, cpuNice, cpuSystem, cpuIdle, cpuIowait, cpuIrq, cpuSoftirq, cpuSteal uint64
|
||||
_, err = fmt.Fscanf(file, "cpu %d %d %d %d %d %d %d %d",
|
||||
_, err = fmt.Fscanf(file, "cpu %d %d %d %d %d %d %d %d",
|
||||
&cpuUser, &cpuNice, &cpuSystem, &cpuIdle, &cpuIowait, &cpuIrq, &cpuSoftirq, &cpuSteal)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
||||
14
go.mod
14
go.mod
@@ -8,17 +8,31 @@ require (
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/miekg/dns v1.1.68
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/swaggo/http-swagger v1.2.0
|
||||
)
|
||||
|
||||
// 清理不需要的依赖
|
||||
// 之前的go.sum可能包含lumberjack的记录,但现在已经不再使用
|
||||
|
||||
require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2 // indirect
|
||||
github.com/swaggo/swag v1.7.8 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
94
go.sum
94
go.sum
@@ -1,32 +1,126 @@
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/agiledragon/gomonkey/v2 v2.3.1 h1:k+UnUY0EMNYUFUAQVETGY9uUTxjMdnUkP0ARyJS1zzs=
|
||||
github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
|
||||
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
|
||||
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
|
||||
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE=
|
||||
github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
|
||||
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
|
||||
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
|
||||
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
|
||||
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2 h1:+iNTcqQJy0OZ5jk6a5NLib47eqXK8uYcPX+O4+cBpEM=
|
||||
github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
|
||||
github.com/swaggo/http-swagger v1.2.0 h1:G5EBD5nvw379l2sFhact660YDT++eLviczLPrgNw/lU=
|
||||
github.com/swaggo/http-swagger v1.2.0/go.mod h1:P7+V1SLG2zloe+VvAGL7WgFimhJACaBLAv2N7YQ0ikI=
|
||||
github.com/swaggo/swag v1.7.8 h1:w249t0l/kc/DKMGlS0fppNJQxKyJ8heNaUWB6nsH3zc=
|
||||
github.com/swaggo/swag v1.7.8/go.mod h1:gZ+TJ2w/Ve1RwQsA2IRoSOTidHz6DX+PIG8GWvbnoLU=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
# DNS Server Hosts File
|
||||
# Generated by DNS Server
|
||||
|
||||
::1 localhost
|
||||
ad.qq.com 127.0.0.1
|
||||
ad.qq.com 0.0.0.0
|
||||
417
http/server.go
417
http/server.go
@@ -10,11 +10,12 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"dns-server/config"
|
||||
"dns-server/dns"
|
||||
"dns-server/logger"
|
||||
"dns-server/shield"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// Server HTTP控制台服务器
|
||||
@@ -24,7 +25,7 @@ type Server struct {
|
||||
dnsServer *dns.Server
|
||||
shieldManager *shield.ShieldManager
|
||||
server *http.Server
|
||||
|
||||
|
||||
// WebSocket相关字段
|
||||
upgrader websocket.Upgrader
|
||||
clients map[*websocket.Conn]bool
|
||||
@@ -50,10 +51,10 @@ func NewServer(globalConfig *config.Config, dnsServer *dns.Server, shieldManager
|
||||
clients: make(map[*websocket.Conn]bool),
|
||||
broadcastChan: make(chan []byte, 100),
|
||||
}
|
||||
|
||||
|
||||
// 启动广播协程
|
||||
go server.startBroadcastLoop()
|
||||
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
@@ -63,16 +64,43 @@ func (s *Server) Start() error {
|
||||
|
||||
// API路由
|
||||
if s.config.EnableAPI {
|
||||
// 重定向/api到Swagger UI页面
|
||||
mux.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/api/index.html", http.StatusMovedPermanently)
|
||||
})
|
||||
|
||||
// 注册所有API端点
|
||||
mux.HandleFunc("/api/stats", s.handleStats)
|
||||
mux.HandleFunc("/api/shield", s.handleShield)
|
||||
mux.HandleFunc("/api/shield/localrules", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.Method == http.MethodGet {
|
||||
localRules := s.shieldManager.GetLocalRules()
|
||||
json.NewEncoder(w).Encode(localRules)
|
||||
return
|
||||
}
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
})
|
||||
mux.HandleFunc("/api/shield/remoterules", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.Method == http.MethodGet {
|
||||
remoteRules := s.shieldManager.GetRemoteRules()
|
||||
json.NewEncoder(w).Encode(remoteRules)
|
||||
return
|
||||
}
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
})
|
||||
mux.HandleFunc("/api/shield/hosts", s.handleShieldHosts)
|
||||
mux.HandleFunc("/api/shield/blacklists", s.handleShieldBlacklists)
|
||||
mux.HandleFunc("/api/query", s.handleQuery)
|
||||
mux.HandleFunc("/api/status", s.handleStatus)
|
||||
mux.HandleFunc("/api/config", s.handleConfig)
|
||||
mux.HandleFunc("/api/config/restart", s.handleRestart)
|
||||
// 添加统计相关接口
|
||||
mux.HandleFunc("/api/top-blocked", s.handleTopBlockedDomains)
|
||||
mux.HandleFunc("/api/top-resolved", s.handleTopResolvedDomains)
|
||||
mux.HandleFunc("/api/top-clients", s.handleTopClients)
|
||||
mux.HandleFunc("/api/top-domains", s.handleTopDomains)
|
||||
mux.HandleFunc("/api/recent-blocked", s.handleRecentBlockedDomains)
|
||||
mux.HandleFunc("/api/hourly-stats", s.handleHourlyStats)
|
||||
mux.HandleFunc("/api/daily-stats", s.handleDailyStats)
|
||||
@@ -80,10 +108,22 @@ func (s *Server) Start() error {
|
||||
mux.HandleFunc("/api/query/type", s.handleQueryTypeStats)
|
||||
// WebSocket端点
|
||||
mux.HandleFunc("/ws/stats", s.handleWebSocketStats)
|
||||
|
||||
// 将/api/下的静态文件服务指向static/api目录,放在最后以避免覆盖API端点
|
||||
apiFileServer := http.FileServer(http.Dir("./static/api"))
|
||||
mux.Handle("/api/", http.StripPrefix("/api", apiFileServer))
|
||||
}
|
||||
|
||||
// 静态文件服务(可后续添加前端界面)
|
||||
mux.Handle("/", http.FileServer(http.Dir("./static")))
|
||||
// 自定义静态文件服务处理器,用于禁用浏览器缓存,放在API路由之后
|
||||
fileServer := http.FileServer(http.Dir("./static"))
|
||||
mux.HandleFunc("/", 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")
|
||||
// 使用StripPrefix处理路径
|
||||
http.StripPrefix("/", fileServer).ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
s.server = &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", s.config.Host, s.config.Port),
|
||||
@@ -105,6 +145,14 @@ func (s *Server) Stop() {
|
||||
}
|
||||
|
||||
// handleStats 处理统计信息请求
|
||||
// @Summary 获取系统统计信息
|
||||
// @Description 获取DNS服务器和Shield的统计信息
|
||||
// @Tags stats
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{} "统计信息"
|
||||
// @Failure 500 {object} map[string]string "服务器内部错误"
|
||||
// @Router /api/stats [get]
|
||||
func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
@@ -131,27 +179,27 @@ func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// 格式化平均响应时间为两位小数
|
||||
formattedResponseTime := float64(int(dnsStats.AvgResponseTime*100)) / 100
|
||||
|
||||
|
||||
// 构建响应数据,确保所有字段都反映服务器的真实状态
|
||||
stats := map[string]interface{}{
|
||||
"dns": map[string]interface{}{
|
||||
"Queries": dnsStats.Queries,
|
||||
"Blocked": dnsStats.Blocked,
|
||||
"Allowed": dnsStats.Allowed,
|
||||
"Errors": dnsStats.Errors,
|
||||
"LastQuery": dnsStats.LastQuery,
|
||||
"AvgResponseTime": formattedResponseTime,
|
||||
"Queries": dnsStats.Queries,
|
||||
"Blocked": dnsStats.Blocked,
|
||||
"Allowed": dnsStats.Allowed,
|
||||
"Errors": dnsStats.Errors,
|
||||
"LastQuery": dnsStats.LastQuery,
|
||||
"AvgResponseTime": formattedResponseTime,
|
||||
"TotalResponseTime": dnsStats.TotalResponseTime,
|
||||
"QueryTypes": dnsStats.QueryTypes,
|
||||
"SourceIPs": dnsStats.SourceIPs,
|
||||
"CpuUsage": dnsStats.CpuUsage,
|
||||
"QueryTypes": dnsStats.QueryTypes,
|
||||
"SourceIPs": dnsStats.SourceIPs,
|
||||
"CpuUsage": dnsStats.CpuUsage,
|
||||
},
|
||||
"shield": shieldStats,
|
||||
"topQueryType": topQueryType,
|
||||
"activeIPs": activeIPCount,
|
||||
"shield": shieldStats,
|
||||
"topQueryType": topQueryType,
|
||||
"activeIPs": activeIPCount,
|
||||
"avgResponseTime": formattedResponseTime,
|
||||
"cpuUsage": dnsStats.CpuUsage,
|
||||
"time": time.Now(),
|
||||
"cpuUsage": dnsStats.CpuUsage,
|
||||
"time": time.Now(),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -197,25 +245,25 @@ func (s *Server) handleWebSocketStats(w http.ResponseWriter, r *http.Request) {
|
||||
case <-ticker.C:
|
||||
// 获取最新统计数据
|
||||
currentStats := s.buildStatsData()
|
||||
|
||||
|
||||
// 检查数据是否有变化
|
||||
if !s.areStatsEqual(lastStats, currentStats) {
|
||||
// 数据有变化,发送更新
|
||||
data, err := json.Marshal(map[string]interface{}{
|
||||
"type": "stats_update",
|
||||
"data": currentStats,
|
||||
"time": time.Now(),
|
||||
"type": "stats_update",
|
||||
"data": currentStats,
|
||||
"time": time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("序列化统计数据失败: %v", err))
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
|
||||
logger.Error(fmt.Sprintf("发送WebSocket消息失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 更新最后发送的数据
|
||||
lastStats = currentStats
|
||||
}
|
||||
@@ -235,9 +283,9 @@ func (s *Server) handleWebSocketStats(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Server) sendInitialStats(conn *websocket.Conn) error {
|
||||
stats := s.buildStatsData()
|
||||
data, err := json.Marshal(map[string]interface{}{
|
||||
"type": "initial_data",
|
||||
"data": stats,
|
||||
"time": time.Now(),
|
||||
"type": "initial_data",
|
||||
"data": stats,
|
||||
"time": time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -267,25 +315,25 @@ func (s *Server) buildStatsData() map[string]interface{} {
|
||||
|
||||
// 格式化平均响应时间
|
||||
formattedResponseTime := float64(int(dnsStats.AvgResponseTime*100)) / 100
|
||||
|
||||
|
||||
return map[string]interface{}{
|
||||
"dns": map[string]interface{}{
|
||||
"Queries": dnsStats.Queries,
|
||||
"Blocked": dnsStats.Blocked,
|
||||
"Allowed": dnsStats.Allowed,
|
||||
"Errors": dnsStats.Errors,
|
||||
"LastQuery": dnsStats.LastQuery,
|
||||
"AvgResponseTime": formattedResponseTime,
|
||||
"Queries": dnsStats.Queries,
|
||||
"Blocked": dnsStats.Blocked,
|
||||
"Allowed": dnsStats.Allowed,
|
||||
"Errors": dnsStats.Errors,
|
||||
"LastQuery": dnsStats.LastQuery,
|
||||
"AvgResponseTime": formattedResponseTime,
|
||||
"TotalResponseTime": dnsStats.TotalResponseTime,
|
||||
"QueryTypes": dnsStats.QueryTypes,
|
||||
"SourceIPs": dnsStats.SourceIPs,
|
||||
"CpuUsage": dnsStats.CpuUsage,
|
||||
"QueryTypes": dnsStats.QueryTypes,
|
||||
"SourceIPs": dnsStats.SourceIPs,
|
||||
"CpuUsage": dnsStats.CpuUsage,
|
||||
},
|
||||
"shield": shieldStats,
|
||||
"topQueryType": topQueryType,
|
||||
"activeIPs": activeIPCount,
|
||||
"shield": shieldStats,
|
||||
"topQueryType": topQueryType,
|
||||
"activeIPs": activeIPCount,
|
||||
"avgResponseTime": formattedResponseTime,
|
||||
"cpuUsage": dnsStats.CpuUsage,
|
||||
"cpuUsage": dnsStats.CpuUsage,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,20 +342,20 @@ func (s *Server) areStatsEqual(stats1, stats2 map[string]interface{}) bool {
|
||||
if stats1 == nil || stats2 == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// 只比较关键数值,避免频繁更新
|
||||
if dns1, ok1 := stats1["dns"].(map[string]interface{}); ok1 {
|
||||
if dns2, ok2 := stats2["dns"].(map[string]interface{}); ok2 {
|
||||
// 检查主要计数器
|
||||
if dns1["Queries"] != dns2["Queries"] ||
|
||||
dns1["Blocked"] != dns2["Blocked"] ||
|
||||
dns1["Allowed"] != dns2["Allowed"] ||
|
||||
dns1["Errors"] != dns2["Errors"] {
|
||||
dns1["Blocked"] != dns2["Blocked"] ||
|
||||
dns1["Allowed"] != dns2["Allowed"] ||
|
||||
dns1["Errors"] != dns2["Errors"] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -493,7 +541,7 @@ func (s *Server) handleQueryTypeStats(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// 获取DNS统计数据
|
||||
dnsStats := s.dnsServer.GetStats()
|
||||
|
||||
|
||||
// 转换为前端需要的格式
|
||||
result := make([]map[string]interface{}, 0, len(dnsStats.QueryTypes))
|
||||
for queryType, count := range dnsStats.QueryTypes {
|
||||
@@ -512,13 +560,99 @@ func (s *Server) handleQueryTypeStats(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
|
||||
// handleShield 处理屏蔽规则管理请求
|
||||
// handleTopClients 处理TOP客户端请求
|
||||
func (s *Server) handleTopClients(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取TOP客户端列表
|
||||
clients := s.dnsServer.GetTopClients(10)
|
||||
|
||||
// 转换为前端需要的格式
|
||||
result := make([]map[string]interface{}, len(clients))
|
||||
for i, client := range clients {
|
||||
result[i] = map[string]interface{}{
|
||||
"ip": client.IP,
|
||||
"count": client.Count,
|
||||
"lastSeen": client.LastSeen,
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
|
||||
// handleTopDomains 处理TOP域名请求
|
||||
func (s *Server) handleTopDomains(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取TOP被屏蔽域名
|
||||
blockedDomains := s.dnsServer.GetTopBlockedDomains(10)
|
||||
// 获取TOP已解析域名
|
||||
resolvedDomains := s.dnsServer.GetTopResolvedDomains(10)
|
||||
|
||||
// 合并并去重域名统计
|
||||
domainMap := make(map[string]int64)
|
||||
for _, domain := range blockedDomains {
|
||||
domainMap[domain.Domain] += domain.Count
|
||||
}
|
||||
for _, domain := range resolvedDomains {
|
||||
domainMap[domain.Domain] += domain.Count
|
||||
}
|
||||
|
||||
// 转换为切片并排序
|
||||
domainList := make([]map[string]interface{}, 0, len(domainMap))
|
||||
for domain, count := range domainMap {
|
||||
domainList = append(domainList, map[string]interface{}{
|
||||
"domain": domain,
|
||||
"count": count,
|
||||
})
|
||||
}
|
||||
|
||||
// 按计数降序排序
|
||||
sort.Slice(domainList, func(i, j int) bool {
|
||||
return domainList[i]["count"].(int64) > domainList[j]["count"].(int64)
|
||||
})
|
||||
|
||||
// 返回限制数量
|
||||
if len(domainList) > 10 {
|
||||
domainList = domainList[:10]
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(domainList)
|
||||
}
|
||||
|
||||
// handleShield 处理Shield相关操作
|
||||
// @Summary 管理Shield配置
|
||||
// @Description 获取或更新Shield的配置信息
|
||||
// @Tags shield
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param config body map[string]interface{} false "Shield配置信息"
|
||||
// @Success 200 {object} map[string]interface{} "配置信息"
|
||||
// @Failure 400 {object} map[string]string "请求参数错误"
|
||||
// @Failure 500 {object} map[string]string "服务器内部错误"
|
||||
// @Router /api/shield [get]
|
||||
// @Router /api/shield [post]
|
||||
func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
// 返回屏蔽规则的基本配置信息和统计数据,不返回完整规则列表
|
||||
// 默认处理逻辑
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
// 检查是否需要返回完整规则列表
|
||||
if r.URL.Query().Get("all") == "true" {
|
||||
// 返回完整规则数据
|
||||
rules := s.shieldManager.GetRules()
|
||||
json.NewEncoder(w).Encode(rules)
|
||||
return
|
||||
}
|
||||
// 获取规则统计信息
|
||||
stats := s.shieldManager.GetStats()
|
||||
shieldInfo := map[string]interface{}{
|
||||
@@ -533,6 +667,8 @@ func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
json.NewEncoder(w).Encode(shieldInfo)
|
||||
return
|
||||
}
|
||||
switch r.Method {
|
||||
case http.MethodPost:
|
||||
// 添加屏蔽规则
|
||||
var req struct {
|
||||
@@ -583,7 +719,22 @@ func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// handleShieldBlacklists 处理远程黑名单管理请求
|
||||
// handleShieldBlacklists 处理黑名单相关操作
|
||||
// @Summary 管理黑名单
|
||||
// @Description 处理黑名单的CRUD操作,包括获取列表、添加、更新和删除黑名单
|
||||
// @Tags shield
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param name path string false "黑名单名称(用于删除操作)"
|
||||
// @Param blacklist body map[string]interface{} false "黑名单信息(用于添加/更新操作)"
|
||||
// @Success 200 {object} map[string]interface{} "操作成功"
|
||||
// @Failure 400 {object} map[string]string "请求参数错误"
|
||||
// @Failure 404 {object} map[string]string "黑名单不存在"
|
||||
// @Failure 500 {object} map[string]string "服务器内部错误"
|
||||
// @Router /api/shield/blacklists [get]
|
||||
// @Router /api/shield/blacklists [post]
|
||||
// @Router /api/shield/blacklists [put]
|
||||
// @Router /api/shield/blacklists/{name} [delete]
|
||||
func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
@@ -601,7 +752,7 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
if targetURLOrName == "" {
|
||||
http.Error(w, "黑名单标识不能为空", http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "黑名单标识不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -616,7 +767,7 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
if targetIndex == -1 {
|
||||
http.Error(w, "黑名单不存在", http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "黑名单不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -624,6 +775,14 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request)
|
||||
blacklists[targetIndex].LastUpdateTime = time.Now().Format(time.RFC3339)
|
||||
// 保存更新后的黑名单列表
|
||||
s.shieldManager.UpdateBlacklist(blacklists)
|
||||
// 更新全局配置中的黑名单
|
||||
s.globalConfig.Shield.Blacklists = blacklists
|
||||
// 保存配置到文件
|
||||
if err := saveConfigToFile(s.globalConfig, "config.json"); err != nil {
|
||||
logger.Error("保存配置文件失败", "error", err)
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "保存配置失败"})
|
||||
return
|
||||
}
|
||||
// 重新加载规则以获取最新的远程规则
|
||||
s.shieldManager.LoadRules()
|
||||
|
||||
@@ -646,6 +805,14 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
s.shieldManager.UpdateBlacklist(newBlacklists)
|
||||
// 更新全局配置中的黑名单
|
||||
s.globalConfig.Shield.Blacklists = newBlacklists
|
||||
// 保存配置到文件
|
||||
if err := saveConfigToFile(s.globalConfig, "config.json"); err != nil {
|
||||
logger.Error("保存配置文件失败", "error", err)
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "保存配置失败"})
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
|
||||
return
|
||||
}
|
||||
@@ -664,12 +831,12 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "无效的请求体"})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name == "" || req.URL == "" {
|
||||
http.Error(w, "Name and URL are required", http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "名称和URL不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -679,11 +846,17 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request)
|
||||
// 检查是否已存在
|
||||
for _, list := range blacklists {
|
||||
if list.URL == req.URL {
|
||||
http.Error(w, "黑名单URL已存在", http.StatusConflict)
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "黑名单URL已存在"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 检查URL是否存在且可访问
|
||||
if !checkURLExists(req.URL) {
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "URL不存在或无法访问"})
|
||||
return
|
||||
}
|
||||
|
||||
// 添加新黑名单
|
||||
newEntry := config.BlacklistEntry{
|
||||
Name: req.Name,
|
||||
@@ -693,6 +866,14 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
blacklists = append(blacklists, newEntry)
|
||||
s.shieldManager.UpdateBlacklist(blacklists)
|
||||
// 更新全局配置中的黑名单
|
||||
s.globalConfig.Shield.Blacklists = blacklists
|
||||
// 保存配置到文件
|
||||
if err := saveConfigToFile(s.globalConfig, "config.json"); err != nil {
|
||||
logger.Error("保存配置文件失败", "error", err)
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "保存配置失败"})
|
||||
return
|
||||
}
|
||||
|
||||
// 重新加载规则以获取新添加的远程规则
|
||||
s.shieldManager.LoadRules()
|
||||
@@ -700,21 +881,47 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request)
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
|
||||
|
||||
case http.MethodPut:
|
||||
// 更新所有远程黑名单
|
||||
blacklists := s.shieldManager.GetBlacklists()
|
||||
for i := range blacklists {
|
||||
// 更新每个黑名单的时间戳
|
||||
blacklists[i].LastUpdateTime = time.Now().Format(time.RFC3339)
|
||||
// 更新黑名单列表(包括启用/禁用状态)
|
||||
var updatedBlacklists []struct {
|
||||
Name string `json:"Name" json:"name"`
|
||||
URL string `json:"URL" json:"url"`
|
||||
Enabled bool `json:"Enabled" json:"enabled"`
|
||||
LastUpdateTime string `json:"LastUpdateTime" json:"lastUpdateTime"`
|
||||
}
|
||||
|
||||
s.shieldManager.UpdateBlacklist(blacklists)
|
||||
// 重新加载所有规则
|
||||
if err := json.NewDecoder(r.Body).Decode(&updatedBlacklists); err != nil {
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "无效的请求体"})
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为config.BlacklistEntry类型
|
||||
var newBlacklists []config.BlacklistEntry
|
||||
for _, entry := range updatedBlacklists {
|
||||
newBlacklists = append(newBlacklists, config.BlacklistEntry{
|
||||
Name: entry.Name,
|
||||
URL: entry.URL,
|
||||
Enabled: entry.Enabled,
|
||||
LastUpdateTime: entry.LastUpdateTime,
|
||||
})
|
||||
}
|
||||
|
||||
// 更新黑名单
|
||||
s.shieldManager.UpdateBlacklist(newBlacklists)
|
||||
// 更新全局配置中的黑名单
|
||||
s.globalConfig.Shield.Blacklists = newBlacklists
|
||||
// 保存配置到文件
|
||||
if err := saveConfigToFile(s.globalConfig, "config.json"); err != nil {
|
||||
logger.Error("保存配置文件失败", "error", err)
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "保存配置失败"})
|
||||
return
|
||||
}
|
||||
// 重新加载规则
|
||||
s.shieldManager.LoadRules()
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
|
||||
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "Method not allowed"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -820,25 +1027,25 @@ func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
stats := s.dnsServer.GetStats()
|
||||
|
||||
|
||||
// 使用服务器的实际启动时间计算准确的运行时间
|
||||
serverStartTime := s.dnsServer.GetStartTime()
|
||||
uptime := time.Since(serverStartTime)
|
||||
|
||||
|
||||
// 构建包含所有真实服务器统计数据的响应
|
||||
status := map[string]interface{}{
|
||||
"status": "running",
|
||||
"queries": stats.Queries,
|
||||
"blocked": stats.Blocked,
|
||||
"allowed": stats.Allowed,
|
||||
"errors": stats.Errors,
|
||||
"lastQuery": stats.LastQuery,
|
||||
"status": "running",
|
||||
"queries": stats.Queries,
|
||||
"blocked": stats.Blocked,
|
||||
"allowed": stats.Allowed,
|
||||
"errors": stats.Errors,
|
||||
"lastQuery": stats.LastQuery,
|
||||
"avgResponseTime": stats.AvgResponseTime,
|
||||
"activeIPs": len(stats.SourceIPs),
|
||||
"startTime": serverStartTime,
|
||||
"uptime": uptime,
|
||||
"cpuUsage": stats.CpuUsage,
|
||||
"timestamp": time.Now(),
|
||||
"activeIPs": len(stats.SourceIPs),
|
||||
"startTime": serverStartTime,
|
||||
"uptime": uptime.Milliseconds(), // 转换为毫秒数,方便前端处理
|
||||
"cpuUsage": stats.CpuUsage,
|
||||
"timestamp": time.Now(),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -991,3 +1198,55 @@ func isValidIP(ip string) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// checkURLExists 检查URL是否存在且可访问
|
||||
func checkURLExists(url string) bool {
|
||||
// 创建一个带有超时的HTTP客户端
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
// 发送HEAD请求来检查URL是否存在
|
||||
resp, err := client.Head(url)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 检查状态码,2xx和3xx表示成功
|
||||
return resp.StatusCode >= 200 && resp.StatusCode < 400
|
||||
}
|
||||
|
||||
// handleRestart 处理重启服务请求
|
||||
func (s *Server) handleRestart(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("收到重启服务请求")
|
||||
|
||||
// 停止DNS服务器
|
||||
s.dnsServer.Stop()
|
||||
|
||||
// 重新加载屏蔽规则
|
||||
if err := s.shieldManager.LoadRules(); err != nil {
|
||||
logger.Error("重新加载屏蔽规则失败", "error", err)
|
||||
}
|
||||
|
||||
// 重新启动DNS服务器
|
||||
go func() {
|
||||
if err := s.dnsServer.Start(); err != nil {
|
||||
logger.Error("DNS服务器重启失败", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 重新启动定时更新任务
|
||||
s.shieldManager.StopAutoUpdate()
|
||||
s.shieldManager.StartAutoUpdate()
|
||||
|
||||
// 返回成功响应
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "服务已重启"})
|
||||
logger.Info("服务重启成功")
|
||||
}
|
||||
|
||||
1079
index.html
1079
index.html
File diff suppressed because it is too large
Load Diff
150
main.go
150
main.go
@@ -1,3 +1,14 @@
|
||||
// DNS Server API
|
||||
// @title DNS Server API
|
||||
// @version 1.0
|
||||
// @description DNS服务器API文档
|
||||
// @termsOfService http://swagger.io/terms/
|
||||
// @contact.name API Support
|
||||
// @contact.email support@example.com
|
||||
// @license.name Apache 2.0
|
||||
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
// @host localhost:8080
|
||||
// @BasePath /api
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -6,6 +17,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"dns-server/config"
|
||||
@@ -15,6 +27,122 @@ import (
|
||||
"dns-server/shield"
|
||||
)
|
||||
|
||||
// createDefaultConfig 创建默认配置文件
|
||||
func createDefaultConfig(configFile string) error {
|
||||
// 默认配置内容
|
||||
defaultConfig := `{
|
||||
"dns": {
|
||||
"port": 53,
|
||||
"upstreamDNS": [
|
||||
"223.5.5.5:53",
|
||||
"223.6.6.6:53"
|
||||
],
|
||||
"timeout": 5000,
|
||||
"statsFile": "./data/stats.json",
|
||||
"saveInterval": 300
|
||||
},
|
||||
"http": {
|
||||
"port": 8081,
|
||||
"host": "0.0.0.0",
|
||||
"enableAPI": true
|
||||
},
|
||||
"shield": {
|
||||
"localRulesFile": "data/rules.txt",
|
||||
"blacklists": [
|
||||
{
|
||||
"name": "AdGuard DNS filter",
|
||||
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/filter.txt",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "Adaway Default Blocklist",
|
||||
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/adaway.txt",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "CHN-anti-AD",
|
||||
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/list/easylist.txt",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "My GitHub Rules",
|
||||
"url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/costomize.txt",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"updateInterval": 3600,
|
||||
"hostsFile": "data/hosts.txt",
|
||||
"blockMethod": "NXDOMAIN",
|
||||
"customBlockIP": "",
|
||||
"statsFile": "./data/shield_stats.json",
|
||||
"statsSaveInterval": 60,
|
||||
"remoteRulesCacheDir": "./data/remote_rules"
|
||||
},
|
||||
"log": {
|
||||
"file": "logs/dns-server.log",
|
||||
"level": "debug",
|
||||
"maxSize": 100,
|
||||
"maxBackups": 10,
|
||||
"maxAge": 30
|
||||
}
|
||||
}`
|
||||
|
||||
// 写入默认配置到文件
|
||||
return os.WriteFile(configFile, []byte(defaultConfig), 0644)
|
||||
}
|
||||
|
||||
// createRequiredFiles 创建所需的文件和文件夹
|
||||
func createRequiredFiles(cfg *config.Config) error {
|
||||
// 创建数据文件夹
|
||||
dataDir := "./data"
|
||||
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||
return fmt.Errorf("创建数据文件夹失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建远程规则缓存文件夹
|
||||
if err := os.MkdirAll(cfg.Shield.RemoteRulesCacheDir, 0755); err != nil {
|
||||
return fmt.Errorf("创建远程规则缓存文件夹失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建日志文件夹
|
||||
logDir := filepath.Dir(cfg.Log.File)
|
||||
if logDir != "." {
|
||||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||
return fmt.Errorf("创建日志文件夹失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建本地规则文件
|
||||
if _, err := os.Stat(cfg.Shield.LocalRulesFile); os.IsNotExist(err) {
|
||||
if err := os.WriteFile(cfg.Shield.LocalRulesFile, []byte("# 本地规则文件\n# 格式:域名\n# 例如:example.com\n"), 0644); err != nil {
|
||||
return fmt.Errorf("创建本地规则文件失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建Hosts文件
|
||||
if _, err := os.Stat(cfg.Shield.HostsFile); os.IsNotExist(err) {
|
||||
if err := os.WriteFile(cfg.Shield.HostsFile, []byte("# Hosts文件\n# 格式:IP 域名\n# 例如:127.0.0.1 localhost\n"), 0644); err != nil {
|
||||
return fmt.Errorf("创建Hosts文件失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建统计数据文件
|
||||
if _, err := os.Stat(cfg.DNS.StatsFile); os.IsNotExist(err) {
|
||||
if err := os.WriteFile(cfg.DNS.StatsFile, []byte("{}"), 0644); err != nil {
|
||||
return fmt.Errorf("创建统计数据文件失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建Shield统计数据文件
|
||||
if _, err := os.Stat(cfg.Shield.StatsFile); os.IsNotExist(err) {
|
||||
if err := os.WriteFile(cfg.Shield.StatsFile, []byte("{}"), 0644); err != nil {
|
||||
return fmt.Errorf("创建Shield统计数据文件失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 命令行参数解析
|
||||
var configFile string
|
||||
@@ -32,6 +160,15 @@ func main() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// 检查配置文件是否存在,如果不存在则创建默认配置文件
|
||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||
log.Printf("配置文件 %s 不存在,正在创建默认配置文件...", configFile)
|
||||
if err := createDefaultConfig(configFile); err != nil {
|
||||
log.Fatalf("创建默认配置文件失败: %v", err)
|
||||
}
|
||||
log.Printf("默认配置文件 %s 创建成功", configFile)
|
||||
}
|
||||
|
||||
// 初始化配置
|
||||
var cfg *config.Config
|
||||
var err error
|
||||
@@ -40,6 +177,13 @@ func main() {
|
||||
log.Fatalf("加载配置失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建所需的文件和文件夹
|
||||
log.Println("正在创建所需的文件和文件夹...")
|
||||
if err := createRequiredFiles(cfg); err != nil {
|
||||
log.Fatalf("创建所需文件和文件夹失败: %v", err)
|
||||
}
|
||||
log.Println("所需文件和文件夹创建成功")
|
||||
|
||||
// 初始化日志系统
|
||||
if err := logger.InitLogger(cfg.Log.File, cfg.Log.Level, 0, 0, 0); err != nil {
|
||||
log.Fatalf("初始化日志系统失败: %v", err)
|
||||
@@ -99,7 +243,7 @@ func daemonize() error {
|
||||
return fmt.Errorf("打开/dev/null失败: %w", err)
|
||||
}
|
||||
defer nullFile.Close()
|
||||
|
||||
|
||||
// 重定向文件描述符
|
||||
err = syscall.Dup2(int(nullFile.Fd()), int(os.Stdin.Fd()))
|
||||
if err != nil {
|
||||
@@ -113,13 +257,13 @@ func daemonize() error {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -19,12 +19,6 @@ import (
|
||||
"dns-server/logger"
|
||||
)
|
||||
|
||||
// regexRule 正则规则结构,包含编译后的表达式和原始字符串
|
||||
type regexRule struct {
|
||||
pattern *regexp.Regexp
|
||||
original string
|
||||
}
|
||||
|
||||
// ShieldStatsData 用于持久化的Shield统计数据
|
||||
type ShieldStatsData struct {
|
||||
BlockedDomainsCount map[string]int `json:"blockedDomainsCount"`
|
||||
@@ -32,45 +26,65 @@ type ShieldStatsData struct {
|
||||
LastSaved time.Time `json:"lastSaved"`
|
||||
}
|
||||
|
||||
// regexRule 正则规则结构,包含编译后的表达式和原始字符串
|
||||
type regexRule struct {
|
||||
pattern *regexp.Regexp
|
||||
original string
|
||||
isLocal bool // 是否为本地规则
|
||||
source string // 规则来源
|
||||
}
|
||||
|
||||
// ShieldManager 屏蔽管理器
|
||||
type ShieldManager struct {
|
||||
config *config.ShieldConfig
|
||||
domainRules map[string]bool
|
||||
domainExceptions map[string]bool
|
||||
regexRules []regexRule
|
||||
regexExceptions []regexRule
|
||||
hostsMap map[string]string
|
||||
blockedDomainsCount map[string]int
|
||||
resolvedDomainsCount map[string]int
|
||||
rulesMutex sync.RWMutex
|
||||
updateCtx context.Context
|
||||
updateCancel context.CancelFunc
|
||||
updateRunning bool
|
||||
localRulesCount int // 本地规则数量
|
||||
remoteRulesCount int // 远程规则数量
|
||||
config *config.ShieldConfig
|
||||
domainRules map[string]bool
|
||||
domainExceptions map[string]bool
|
||||
domainRulesIsLocal map[string]bool // 标记域名规则是否为本地规则
|
||||
domainExceptionsIsLocal map[string]bool // 标记域名排除规则是否为本地规则
|
||||
domainRulesSource map[string]string // 标记域名规则来源
|
||||
domainExceptionsSource map[string]string // 标记域名排除规则来源
|
||||
domainRulesOriginal map[string]string // 存储域名规则的原始字符串
|
||||
domainExceptionsOriginal map[string]string // 存储域名排除规则的原始字符串
|
||||
regexRules []regexRule
|
||||
regexExceptions []regexRule
|
||||
hostsMap map[string]string
|
||||
blockedDomainsCount map[string]int
|
||||
resolvedDomainsCount map[string]int
|
||||
rulesMutex sync.RWMutex
|
||||
updateCtx context.Context
|
||||
updateCancel context.CancelFunc
|
||||
updateRunning bool
|
||||
localRulesCount int // 本地规则数量
|
||||
remoteRulesCount int // 远程规则数量
|
||||
}
|
||||
|
||||
// NewShieldManager 创建屏蔽管理器实例
|
||||
func NewShieldManager(config *config.ShieldConfig) *ShieldManager {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
manager := &ShieldManager{
|
||||
config: config,
|
||||
domainRules: make(map[string]bool),
|
||||
domainExceptions: make(map[string]bool),
|
||||
regexRules: []regexRule{},
|
||||
regexExceptions: []regexRule{},
|
||||
hostsMap: make(map[string]string),
|
||||
blockedDomainsCount: make(map[string]int),
|
||||
resolvedDomainsCount: make(map[string]int),
|
||||
updateCtx: ctx,
|
||||
updateCancel: cancel,
|
||||
localRulesCount: 0,
|
||||
remoteRulesCount: 0,
|
||||
config: config,
|
||||
domainRules: make(map[string]bool),
|
||||
domainExceptions: make(map[string]bool),
|
||||
domainRulesIsLocal: make(map[string]bool),
|
||||
domainExceptionsIsLocal: make(map[string]bool),
|
||||
domainRulesSource: make(map[string]string),
|
||||
domainExceptionsSource: make(map[string]string),
|
||||
domainRulesOriginal: make(map[string]string),
|
||||
domainExceptionsOriginal: make(map[string]string),
|
||||
regexRules: []regexRule{},
|
||||
regexExceptions: []regexRule{},
|
||||
hostsMap: make(map[string]string),
|
||||
blockedDomainsCount: make(map[string]int),
|
||||
resolvedDomainsCount: make(map[string]int),
|
||||
updateCtx: ctx,
|
||||
updateCancel: cancel,
|
||||
localRulesCount: 0,
|
||||
remoteRulesCount: 0,
|
||||
}
|
||||
|
||||
|
||||
// 加载已保存的计数数据
|
||||
manager.loadStatsData()
|
||||
|
||||
|
||||
return manager
|
||||
}
|
||||
|
||||
@@ -82,6 +96,12 @@ func (m *ShieldManager) LoadRules() error {
|
||||
// 清空现有规则
|
||||
m.domainRules = make(map[string]bool)
|
||||
m.domainExceptions = make(map[string]bool)
|
||||
m.domainRulesIsLocal = make(map[string]bool)
|
||||
m.domainExceptionsIsLocal = make(map[string]bool)
|
||||
m.domainRulesSource = make(map[string]string)
|
||||
m.domainExceptionsSource = make(map[string]string)
|
||||
m.domainRulesOriginal = make(map[string]string)
|
||||
m.domainExceptionsOriginal = make(map[string]string)
|
||||
m.regexRules = []regexRule{}
|
||||
m.regexExceptions = []regexRule{}
|
||||
m.hostsMap = make(map[string]string)
|
||||
@@ -134,7 +154,7 @@ func (m *ShieldManager) loadLocalRules() error {
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
m.parseRule(line)
|
||||
m.parseRule(line, true, "本地规则") // 本地规则,isLocal=true,来源为"本地规则"
|
||||
}
|
||||
|
||||
// 更新本地规则计数
|
||||
@@ -187,11 +207,11 @@ func (m *ShieldManager) shouldUpdateCache(cacheFile string) bool {
|
||||
func (m *ShieldManager) fetchRemoteRules(url string) error {
|
||||
// 获取缓存文件路径
|
||||
cacheFile := m.getCacheFilePath(url)
|
||||
|
||||
|
||||
// 尝试从缓存加载
|
||||
hasLoadedFromCache := false
|
||||
if !m.shouldUpdateCache(cacheFile) {
|
||||
if err := m.loadCachedRules(cacheFile); err == nil {
|
||||
if err := m.loadCachedRules(cacheFile, url); err == nil {
|
||||
logger.Info("从缓存加载远程规则", "url", url)
|
||||
hasLoadedFromCache = true
|
||||
}
|
||||
@@ -236,14 +256,14 @@ func (m *ShieldManager) fetchRemoteRules(url string) error {
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
m.parseRule(line)
|
||||
m.parseRule(line, false, url) // 远程规则,isLocal=false,来源为URL
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadCachedRules 从缓存文件加载规则
|
||||
func (m *ShieldManager) loadCachedRules(filePath string) error {
|
||||
func (m *ShieldManager) loadCachedRules(filePath string, source string) error {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -265,7 +285,7 @@ func (m *ShieldManager) loadCachedRules(filePath string) error {
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
m.parseRule(line)
|
||||
m.parseRule(line, false, source) // 远程规则,isLocal=false,来源为URL
|
||||
}
|
||||
|
||||
// 更新远程规则计数
|
||||
@@ -318,7 +338,10 @@ func (m *ShieldManager) loadHosts() error {
|
||||
}
|
||||
|
||||
// parseRule 解析规则行
|
||||
func (m *ShieldManager) parseRule(line string) {
|
||||
func (m *ShieldManager) parseRule(line string, isLocal bool, source string) {
|
||||
// 保存原始规则用于后续使用
|
||||
originalLine := line
|
||||
|
||||
// 处理注释
|
||||
if strings.HasPrefix(line, "!") || strings.HasPrefix(line, "#") || line == "" {
|
||||
return
|
||||
@@ -343,12 +366,12 @@ func (m *ShieldManager) parseRule(line string) {
|
||||
case strings.HasPrefix(line, "||") && strings.HasSuffix(line, "^"):
|
||||
// AdGuardHome域名规则格式: ||example.com^
|
||||
domain := strings.TrimSuffix(strings.TrimPrefix(line, "||"), "^")
|
||||
m.addDomainRule(domain, !isException)
|
||||
m.addDomainRule(domain, !isException, isLocal, source, originalLine)
|
||||
|
||||
case strings.HasPrefix(line, "||"):
|
||||
// 精确域名匹配规则
|
||||
domain := strings.TrimPrefix(line, "||")
|
||||
m.addDomainRule(domain, !isException)
|
||||
m.addDomainRule(domain, !isException, isLocal, source, originalLine)
|
||||
|
||||
case strings.HasPrefix(line, "*"):
|
||||
// 通配符规则,转换为正则表达式
|
||||
@@ -356,15 +379,17 @@ func (m *ShieldManager) parseRule(line string) {
|
||||
pattern = "^" + pattern + "$"
|
||||
if re, err := regexp.Compile(pattern); err == nil {
|
||||
// 保存原始规则字符串
|
||||
m.addRegexRule(re, line, !isException)
|
||||
m.addRegexRule(re, originalLine, !isException, isLocal, source)
|
||||
}
|
||||
|
||||
case strings.HasPrefix(line, "/") && strings.HasSuffix(line, "/"):
|
||||
// 正则表达式规则
|
||||
// 正则表达式匹配规则:/regex/ 格式,不区分大小写
|
||||
pattern := strings.TrimPrefix(strings.TrimSuffix(line, "/"), "/")
|
||||
if re, err := regexp.Compile(pattern); err == nil {
|
||||
// 编译为不区分大小写的正则表达式,确保能匹配域名中任意位置
|
||||
// 对于像 /domain/ 这样的规则,应该匹配包含 domain 字符串的任何域名
|
||||
if re, err := regexp.Compile("(?i).*" + regexp.QuoteMeta(pattern) + ".*"); err == nil {
|
||||
// 保存原始规则字符串
|
||||
m.addRegexRule(re, line, !isException)
|
||||
m.addRegexRule(re, originalLine, !isException, isLocal, source)
|
||||
}
|
||||
|
||||
case strings.HasPrefix(line, "|") && strings.HasSuffix(line, "|"):
|
||||
@@ -373,7 +398,7 @@ func (m *ShieldManager) parseRule(line string) {
|
||||
// 将URL模式转换为正则表达式
|
||||
pattern := "^" + regexp.QuoteMeta(urlPattern) + "$"
|
||||
if re, err := regexp.Compile(pattern); err == nil {
|
||||
m.addRegexRule(re, line, !isException)
|
||||
m.addRegexRule(re, originalLine, !isException, isLocal, source)
|
||||
}
|
||||
|
||||
case strings.HasPrefix(line, "|"):
|
||||
@@ -381,7 +406,7 @@ func (m *ShieldManager) parseRule(line string) {
|
||||
urlPattern := strings.TrimPrefix(line, "|")
|
||||
pattern := "^" + regexp.QuoteMeta(urlPattern)
|
||||
if re, err := regexp.Compile(pattern); err == nil {
|
||||
m.addRegexRule(re, line, !isException)
|
||||
m.addRegexRule(re, originalLine, !isException, isLocal, source)
|
||||
}
|
||||
|
||||
case strings.HasSuffix(line, "|"):
|
||||
@@ -389,12 +414,12 @@ func (m *ShieldManager) parseRule(line string) {
|
||||
urlPattern := strings.TrimSuffix(line, "|")
|
||||
pattern := regexp.QuoteMeta(urlPattern) + "$"
|
||||
if re, err := regexp.Compile(pattern); err == nil {
|
||||
m.addRegexRule(re, line, !isException)
|
||||
m.addRegexRule(re, originalLine, !isException, isLocal, source)
|
||||
}
|
||||
|
||||
default:
|
||||
// 默认作为普通域名规则
|
||||
m.addDomainRule(line, !isException)
|
||||
m.addDomainRule(line, !isException, isLocal, source, originalLine)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,42 +443,65 @@ func (m *ShieldManager) parseRuleOptions(optionsStr string) map[string]string {
|
||||
}
|
||||
|
||||
// addDomainRule 添加域名规则,支持是否为阻止规则
|
||||
func (m *ShieldManager) addDomainRule(domain string, block bool) {
|
||||
func (m *ShieldManager) addDomainRule(domain string, block bool, isLocal bool, source string, original string) {
|
||||
if block {
|
||||
m.domainRules[domain] = true
|
||||
// 添加所有子域名的匹配支持
|
||||
parts := strings.Split(domain, ".")
|
||||
if len(parts) > 1 {
|
||||
// 为二级域名和顶级域名添加规则
|
||||
for i := 0; i < len(parts)-1; i++ {
|
||||
subdomain := strings.Join(parts[i:], ".")
|
||||
m.domainRules[subdomain] = true
|
||||
// 如果是远程规则,检查是否已经存在本地规则,如果存在则不覆盖
|
||||
if !isLocal {
|
||||
if _, exists := m.domainRulesIsLocal[domain]; exists && m.domainRulesIsLocal[domain] {
|
||||
// 已经存在本地规则,不覆盖
|
||||
return
|
||||
}
|
||||
}
|
||||
m.domainRules[domain] = true
|
||||
m.domainRulesIsLocal[domain] = isLocal
|
||||
m.domainRulesSource[domain] = source
|
||||
m.domainRulesOriginal[domain] = original
|
||||
} else {
|
||||
// 添加到排除规则
|
||||
m.domainExceptions[domain] = true
|
||||
// 为子域名也添加排除规则
|
||||
parts := strings.Split(domain, ".")
|
||||
if len(parts) > 1 {
|
||||
for i := 0; i < len(parts)-1; i++ {
|
||||
subdomain := strings.Join(parts[i:], ".")
|
||||
m.domainExceptions[subdomain] = true
|
||||
// 如果是远程规则,检查是否已经存在本地规则,如果存在则不覆盖
|
||||
if !isLocal {
|
||||
if _, exists := m.domainExceptionsIsLocal[domain]; exists && m.domainExceptionsIsLocal[domain] {
|
||||
// 已经存在本地规则,不覆盖
|
||||
return
|
||||
}
|
||||
}
|
||||
m.domainExceptions[domain] = true
|
||||
m.domainExceptionsIsLocal[domain] = isLocal
|
||||
m.domainExceptionsSource[domain] = source
|
||||
m.domainExceptionsOriginal[domain] = original
|
||||
}
|
||||
}
|
||||
|
||||
// addRegexRule 添加正则表达式规则,支持是否为阻止规则
|
||||
func (m *ShieldManager) addRegexRule(re *regexp.Regexp, original string, block bool) {
|
||||
func (m *ShieldManager) addRegexRule(re *regexp.Regexp, original string, block bool, isLocal bool, source string) {
|
||||
rule := regexRule{
|
||||
pattern: re,
|
||||
original: original,
|
||||
isLocal: isLocal,
|
||||
source: source,
|
||||
}
|
||||
if block {
|
||||
// 如果是远程规则,检查是否已经存在相同的本地规则,如果存在则不添加
|
||||
if !isLocal {
|
||||
for _, existingRule := range m.regexRules {
|
||||
if existingRule.original == original && existingRule.isLocal {
|
||||
// 已经存在相同的本地规则,不添加
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
m.regexRules = append(m.regexRules, rule)
|
||||
} else {
|
||||
// 添加到排除规则
|
||||
// 如果是远程规则,检查是否已经存在相同的本地规则,如果存在则不添加
|
||||
if !isLocal {
|
||||
for _, existingRule := range m.regexExceptions {
|
||||
if existingRule.original == original && existingRule.isLocal {
|
||||
// 已经存在相同的本地规则,不添加
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
m.regexExceptions = append(m.regexExceptions, rule)
|
||||
}
|
||||
}
|
||||
@@ -471,15 +519,16 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"domain": domain,
|
||||
"blocked": false,
|
||||
"blockRule": "",
|
||||
"blockRuleType": "",
|
||||
"excluded": false,
|
||||
"excludeRule": "",
|
||||
"domain": domain,
|
||||
"blocked": false,
|
||||
"blockRule": "",
|
||||
"blockRuleType": "",
|
||||
"blocksource": "",
|
||||
"excluded": false,
|
||||
"excludeRule": "",
|
||||
"excludeRuleType": "",
|
||||
"hasHosts": false,
|
||||
"hostsIP": "",
|
||||
"hasHosts": false,
|
||||
"hostsIP": "",
|
||||
}
|
||||
|
||||
// 检查hosts记录
|
||||
@@ -491,8 +540,9 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
||||
// 检查域名排除规则
|
||||
if m.domainExceptions[domain] {
|
||||
result["excluded"] = true
|
||||
result["excludeRule"] = domain
|
||||
result["excludeRule"] = m.domainExceptionsOriginal[domain]
|
||||
result["excludeRuleType"] = "exact_domain"
|
||||
result["blocksource"] = m.domainExceptionsSource[domain]
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -502,8 +552,9 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
||||
subdomain := strings.Join(parts[i:], ".")
|
||||
if m.domainExceptions[subdomain] {
|
||||
result["excluded"] = true
|
||||
result["excludeRule"] = subdomain
|
||||
result["excludeRule"] = m.domainExceptionsOriginal[subdomain]
|
||||
result["excludeRuleType"] = "subdomain"
|
||||
result["blocksource"] = m.domainExceptionsSource[subdomain]
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -514,16 +565,18 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
||||
result["excluded"] = true
|
||||
result["excludeRule"] = re.original
|
||||
result["excludeRuleType"] = "regex"
|
||||
result["blocksource"] = re.source
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// 检查阻止规则
|
||||
// 检查阻止规则 - 先检查精确域名匹配,再检查子域名匹配
|
||||
// 检查精确域名匹配
|
||||
if m.domainRules[domain] {
|
||||
result["blocked"] = true
|
||||
result["blockRule"] = domain
|
||||
result["blockRule"] = m.domainRulesOriginal[domain]
|
||||
result["blockRuleType"] = "exact_domain"
|
||||
result["blocksource"] = m.domainRulesSource[domain]
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -533,8 +586,9 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
||||
subdomain := strings.Join(parts[i:], ".")
|
||||
if m.domainRules[subdomain] {
|
||||
result["blocked"] = true
|
||||
result["blockRule"] = subdomain
|
||||
result["blockRule"] = m.domainRulesOriginal[subdomain]
|
||||
result["blockRuleType"] = "subdomain"
|
||||
result["blocksource"] = m.domainRulesSource[subdomain]
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -545,6 +599,7 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
||||
result["blocked"] = true
|
||||
result["blockRule"] = re.original
|
||||
result["blockRuleType"] = "regex"
|
||||
result["blocksource"] = re.source
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -667,13 +722,13 @@ func (m *ShieldManager) GetHostsIP(domain string) (string, bool) {
|
||||
return ip, exists
|
||||
}
|
||||
|
||||
// AddRule 添加屏蔽规则
|
||||
// AddRule 添加屏蔽规则,用户添加的规则是本地规则
|
||||
func (m *ShieldManager) AddRule(rule string) error {
|
||||
m.rulesMutex.Lock()
|
||||
defer m.rulesMutex.Unlock()
|
||||
|
||||
// 解析并添加规则到内存
|
||||
m.parseRule(rule)
|
||||
// 解析并添加规则到内存,isLocal=true表示本地规则,来源为"本地规则"
|
||||
m.parseRule(rule, true, "本地规则")
|
||||
|
||||
// 持久化保存规则到文件
|
||||
if m.config.LocalRulesFile != "" {
|
||||
@@ -724,6 +779,8 @@ func (m *ShieldManager) RemoveRule(rule string) error {
|
||||
domain := strings.TrimPrefix(format, "@@||")
|
||||
if _, exists := m.domainExceptions[domain]; exists {
|
||||
delete(m.domainExceptions, domain)
|
||||
delete(m.domainExceptionsIsLocal, domain)
|
||||
delete(m.domainExceptionsSource, domain)
|
||||
removed = true
|
||||
break
|
||||
}
|
||||
@@ -731,19 +788,28 @@ func (m *ShieldManager) RemoveRule(rule string) error {
|
||||
// 尝试删除域名规则
|
||||
domain := strings.TrimPrefix(format, "||")
|
||||
if _, exists := m.domainRules[domain]; exists {
|
||||
// 删除主域名规则
|
||||
delete(m.domainRules, domain)
|
||||
delete(m.domainRulesIsLocal, domain)
|
||||
delete(m.domainRulesSource, domain)
|
||||
removed = true
|
||||
break
|
||||
}
|
||||
} else {
|
||||
// 尝试直接作为域名删除
|
||||
if _, exists := m.domainRules[format]; exists {
|
||||
// 删除主域名规则
|
||||
delete(m.domainRules, format)
|
||||
delete(m.domainRulesIsLocal, format)
|
||||
delete(m.domainRulesSource, format)
|
||||
removed = true
|
||||
break
|
||||
}
|
||||
if _, exists := m.domainExceptions[format]; exists {
|
||||
// 删除主排除规则
|
||||
delete(m.domainExceptions, format)
|
||||
delete(m.domainExceptionsIsLocal, format)
|
||||
delete(m.domainExceptionsSource, format)
|
||||
removed = true
|
||||
break
|
||||
}
|
||||
@@ -752,12 +818,10 @@ func (m *ShieldManager) RemoveRule(rule string) error {
|
||||
|
||||
// 处理正则表达式规则
|
||||
if !removed && strings.HasPrefix(cleanRule, "/") && strings.HasSuffix(cleanRule, "/") {
|
||||
pattern := strings.TrimPrefix(strings.TrimSuffix(cleanRule, "/"), "/")
|
||||
|
||||
// 检查是否在正则表达式规则中
|
||||
newRegexRules := []regexRule{}
|
||||
for _, re := range m.regexRules {
|
||||
if re.pattern.String() != pattern {
|
||||
if re.original != rule && re.original != cleanRule {
|
||||
newRegexRules = append(newRegexRules, re)
|
||||
} else {
|
||||
removed = true
|
||||
@@ -769,7 +833,7 @@ func (m *ShieldManager) RemoveRule(rule string) error {
|
||||
if !removed {
|
||||
newRegexExceptions := []regexRule{}
|
||||
for _, re := range m.regexExceptions {
|
||||
if re.pattern.String() != pattern {
|
||||
if re.original != rule && re.original != cleanRule {
|
||||
newRegexExceptions = append(newRegexExceptions, re)
|
||||
} else {
|
||||
removed = true
|
||||
@@ -785,6 +849,8 @@ func (m *ShieldManager) RemoveRule(rule string) error {
|
||||
for domain := range m.domainRules {
|
||||
if domain == cleanRule || domain == rule {
|
||||
delete(m.domainRules, domain)
|
||||
delete(m.domainRulesIsLocal, domain)
|
||||
delete(m.domainRulesSource, domain)
|
||||
removed = true
|
||||
break
|
||||
}
|
||||
@@ -794,6 +860,8 @@ func (m *ShieldManager) RemoveRule(rule string) error {
|
||||
for domain := range m.domainExceptions {
|
||||
if domain == cleanRule || domain == rule {
|
||||
delete(m.domainExceptions, domain)
|
||||
delete(m.domainExceptionsIsLocal, domain)
|
||||
delete(m.domainExceptionsSource, domain)
|
||||
removed = true
|
||||
break
|
||||
}
|
||||
@@ -801,6 +869,36 @@ func (m *ShieldManager) RemoveRule(rule string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有删除任何规则,尝试删除可能的子域名规则
|
||||
if !removed {
|
||||
// 解析原始规则,提取可能的主域名
|
||||
originalRule := cleanRule
|
||||
// 移除可能的前缀
|
||||
originalRule = strings.TrimPrefix(originalRule, "@@||")
|
||||
originalRule = strings.TrimPrefix(originalRule, "||")
|
||||
|
||||
// 检查是否有子域名规则需要删除
|
||||
// 遍历所有域名规则,删除包含原始规则作为后缀的子域名规则
|
||||
for domain := range m.domainRules {
|
||||
if strings.HasSuffix(domain, "."+originalRule) || domain == originalRule {
|
||||
delete(m.domainRules, domain)
|
||||
delete(m.domainRulesIsLocal, domain)
|
||||
delete(m.domainRulesSource, domain)
|
||||
removed = true
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历所有排除规则,删除包含原始规则作为后缀的子域名规则
|
||||
for domain := range m.domainExceptions {
|
||||
if strings.HasSuffix(domain, "."+originalRule) || domain == originalRule {
|
||||
delete(m.domainExceptions, domain)
|
||||
delete(m.domainExceptionsIsLocal, domain)
|
||||
delete(m.domainExceptionsSource, domain)
|
||||
removed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有规则被删除,持久化保存更改
|
||||
if removed && m.config.LocalRulesFile != "" {
|
||||
if err := m.saveRulesToFile(); err != nil {
|
||||
@@ -843,7 +941,7 @@ func (m *ShieldManager) StartAutoUpdate() {
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
logger.Info("规则自动更新已启动", "interval", m.config.UpdateInterval)
|
||||
|
||||
// 如果是首次启动,先保存一次数据确保目录存在
|
||||
@@ -859,28 +957,36 @@ func (m *ShieldManager) StopAutoUpdate() {
|
||||
logger.Info("规则自动更新已停止")
|
||||
}
|
||||
|
||||
// saveRulesToFile 保存规则到文件
|
||||
// saveRulesToFile 保存规则到文件,只保存本地规则
|
||||
func (m *ShieldManager) saveRulesToFile() error {
|
||||
var rules []string
|
||||
|
||||
// 添加域名规则
|
||||
for domain := range m.domainRules {
|
||||
rules = append(rules, "||"+domain)
|
||||
// 添加本地域名规则
|
||||
for domain, isLocal := range m.domainRulesIsLocal {
|
||||
if isLocal {
|
||||
rules = append(rules, "||"+domain)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加正则表达式规则
|
||||
// 添加本地正则表达式规则
|
||||
for _, re := range m.regexRules {
|
||||
rules = append(rules, re.original)
|
||||
if re.isLocal {
|
||||
rules = append(rules, re.original)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加排除规则
|
||||
for domain := range m.domainExceptions {
|
||||
rules = append(rules, "@@||"+domain)
|
||||
// 添加本地排除规则
|
||||
for domain, isLocal := range m.domainExceptionsIsLocal {
|
||||
if isLocal {
|
||||
rules = append(rules, "@@||"+domain)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加正则表达式排除规则
|
||||
// 添加本地正则表达式排除规则
|
||||
for _, re := range m.regexExceptions {
|
||||
rules = append(rules, re.original)
|
||||
if re.isLocal {
|
||||
rules = append(rules, re.original)
|
||||
}
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
@@ -1033,18 +1139,18 @@ func (m *ShieldManager) loadStatsData() {
|
||||
if len(dataSample) > 50 {
|
||||
dataSample = dataSample[:50] + "..."
|
||||
}
|
||||
logger.Error("解析Shield计数数据失败",
|
||||
"file", statsFilePath,
|
||||
"error", err,
|
||||
logger.Error("解析Shield计数数据失败",
|
||||
"file", statsFilePath,
|
||||
"error", err,
|
||||
"data_length", len(data),
|
||||
"data_sample", dataSample)
|
||||
|
||||
|
||||
// 重置为默认空数据
|
||||
m.rulesMutex.Lock()
|
||||
m.blockedDomainsCount = make(map[string]int)
|
||||
m.resolvedDomainsCount = make(map[string]int)
|
||||
m.rulesMutex.Unlock()
|
||||
|
||||
|
||||
// 尝试保存一个有效的空文件
|
||||
m.saveStatsData()
|
||||
return
|
||||
@@ -1187,6 +1293,131 @@ func (m *ShieldManager) GetHostsCount() int {
|
||||
return len(m.hostsMap)
|
||||
}
|
||||
|
||||
// GetLocalRules 获取仅本地规则
|
||||
func (m *ShieldManager) GetLocalRules() map[string]interface{} {
|
||||
m.rulesMutex.RLock()
|
||||
defer m.rulesMutex.RUnlock()
|
||||
|
||||
// 转换map和slice为字符串列表,只包含本地规则
|
||||
domainRulesList := make([]string, 0)
|
||||
for domain, isLocal := range m.domainRulesIsLocal {
|
||||
if isLocal && m.domainRules[domain] {
|
||||
domainRulesList = append(domainRulesList, "||"+domain+"^")
|
||||
}
|
||||
}
|
||||
|
||||
domainExceptionsList := make([]string, 0)
|
||||
for domain, isLocal := range m.domainExceptionsIsLocal {
|
||||
if isLocal && m.domainExceptions[domain] {
|
||||
domainExceptionsList = append(domainExceptionsList, "@@||"+domain+"^")
|
||||
}
|
||||
}
|
||||
|
||||
// 获取本地正则规则原始字符串
|
||||
regexRulesList := make([]string, 0)
|
||||
for _, re := range m.regexRules {
|
||||
if re.isLocal {
|
||||
regexRulesList = append(regexRulesList, re.original)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取本地正则排除规则原始字符串
|
||||
regexExceptionsList := make([]string, 0)
|
||||
for _, re := range m.regexExceptions {
|
||||
if re.isLocal {
|
||||
regexExceptionsList = append(regexExceptionsList, re.original)
|
||||
}
|
||||
}
|
||||
|
||||
// 计算本地规则数量
|
||||
localDomainRulesCount := 0
|
||||
for _, isLocal := range m.domainRulesIsLocal {
|
||||
if isLocal {
|
||||
localDomainRulesCount++
|
||||
}
|
||||
}
|
||||
localRegexRulesCount := 0
|
||||
for _, re := range m.regexRules {
|
||||
if re.isLocal {
|
||||
localRegexRulesCount++
|
||||
}
|
||||
}
|
||||
localRulesCount := localDomainRulesCount + localRegexRulesCount
|
||||
|
||||
return map[string]interface{}{
|
||||
"domainRules": domainRulesList,
|
||||
"domainExceptions": domainExceptionsList,
|
||||
"regexRules": regexRulesList,
|
||||
"regexExceptions": regexExceptionsList,
|
||||
"localRulesCount": localRulesCount,
|
||||
"localDomainRulesCount": localDomainRulesCount,
|
||||
"localRegexRulesCount": localRegexRulesCount,
|
||||
}
|
||||
}
|
||||
|
||||
// GetRemoteRules 获取仅远程规则
|
||||
func (m *ShieldManager) GetRemoteRules() map[string]interface{} {
|
||||
m.rulesMutex.RLock()
|
||||
defer m.rulesMutex.RUnlock()
|
||||
|
||||
// 转换map和slice为字符串列表,只包含远程规则
|
||||
domainRulesList := make([]string, 0)
|
||||
for domain, isLocal := range m.domainRulesIsLocal {
|
||||
if !isLocal && m.domainRules[domain] {
|
||||
domainRulesList = append(domainRulesList, "||"+domain+"^")
|
||||
}
|
||||
}
|
||||
|
||||
domainExceptionsList := make([]string, 0)
|
||||
for domain, isLocal := range m.domainExceptionsIsLocal {
|
||||
if !isLocal && m.domainExceptions[domain] {
|
||||
domainExceptionsList = append(domainExceptionsList, "@@||"+domain+"^")
|
||||
}
|
||||
}
|
||||
|
||||
// 获取远程正则规则原始字符串
|
||||
regexRulesList := make([]string, 0)
|
||||
for _, re := range m.regexRules {
|
||||
if !re.isLocal {
|
||||
regexRulesList = append(regexRulesList, re.original)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取远程正则排除规则原始字符串
|
||||
regexExceptionsList := make([]string, 0)
|
||||
for _, re := range m.regexExceptions {
|
||||
if !re.isLocal {
|
||||
regexExceptionsList = append(regexExceptionsList, re.original)
|
||||
}
|
||||
}
|
||||
|
||||
// 计算远程规则数量
|
||||
remoteDomainRulesCount := 0
|
||||
for _, isLocal := range m.domainRulesIsLocal {
|
||||
if !isLocal {
|
||||
remoteDomainRulesCount++
|
||||
}
|
||||
}
|
||||
remoteRegexRulesCount := 0
|
||||
for _, re := range m.regexRules {
|
||||
if !re.isLocal {
|
||||
remoteRegexRulesCount++
|
||||
}
|
||||
}
|
||||
remoteRulesCount := remoteDomainRulesCount + remoteRegexRulesCount
|
||||
|
||||
return map[string]interface{}{
|
||||
"domainRules": domainRulesList,
|
||||
"domainExceptions": domainExceptionsList,
|
||||
"regexRules": regexRulesList,
|
||||
"regexExceptions": regexExceptionsList,
|
||||
"remoteRulesCount": remoteRulesCount,
|
||||
"remoteDomainRulesCount": remoteDomainRulesCount,
|
||||
"remoteRegexRulesCount": remoteRegexRulesCount,
|
||||
"blacklists": m.config.Blacklists,
|
||||
}
|
||||
}
|
||||
|
||||
// GetRules 获取所有规则
|
||||
func (m *ShieldManager) GetRules() map[string]interface{} {
|
||||
m.rulesMutex.RLock()
|
||||
|
||||
62
shield/rule_test.go
Normal file
62
shield/rule_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package shield
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"dns-server/config"
|
||||
)
|
||||
|
||||
func TestRuleParsing(t *testing.T) {
|
||||
// 创建一个简单的配置
|
||||
cfg := &config.ShieldConfig{
|
||||
LocalRulesFile: "",
|
||||
RemoteRulesCacheDir: ".",
|
||||
UpdateInterval: 3600,
|
||||
StatsFile: "",
|
||||
StatsSaveInterval: 300,
|
||||
HostsFile: "",
|
||||
Blacklists: []config.BlacklistEntry{},
|
||||
}
|
||||
|
||||
// 测试规则
|
||||
testCases := []struct {
|
||||
rule string
|
||||
domain string
|
||||
blocked bool
|
||||
desc string
|
||||
}{
|
||||
// 测试关键字匹配规则
|
||||
{"/ad.qq.com/", "ad.qq.com", true, "精确匹配"},
|
||||
{"/ad.qq.com/", "sub.ad.qq.com", true, "子域名包含匹配"},
|
||||
{"/ad/", "ad.example.com", true, "开头匹配"},
|
||||
{"/ad/", "example.ad.com", true, "中间匹配"},
|
||||
{"/ad/", "example.com.ad", true, "结尾匹配"},
|
||||
{"/AD/", "ad.example.com", true, "不区分大小写匹配"},
|
||||
{"/example.com/", "example.com", true, "特殊字符转义匹配"},
|
||||
{"/ad/", "example.com", false, "不包含关键字,不应匹配"},
|
||||
{"/test/", "example.com", false, "不同关键字,不应匹配"},
|
||||
|
||||
// 测试排除规则
|
||||
{"@@/ad/", "ad.example.com", false, "排除规则,不应匹配"},
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
// 为每个测试用例创建一个新的屏蔽管理器实例
|
||||
manager := NewShieldManager(cfg)
|
||||
|
||||
// 添加规则
|
||||
manager.AddRule(tc.rule)
|
||||
|
||||
// 检查域名是否被屏蔽
|
||||
result := manager.CheckDomainBlockDetails(tc.domain)
|
||||
blocked := result["blocked"].(bool)
|
||||
|
||||
// 验证结果
|
||||
if blocked != tc.blocked {
|
||||
t.Errorf("Rule %q: Domain %q expected %t, got %t", tc.rule, tc.domain, tc.blocked, blocked)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
5
shield_stats.json
Normal file
5
shield_stats.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"blockedDomainsCount": {},
|
||||
"resolvedDomainsCount": {},
|
||||
"lastSaved": "2025-11-29T02:08:50.6341349+08:00"
|
||||
}
|
||||
488
static/api/css/style.css
Normal file
488
static/api/css/style.css
Normal file
@@ -0,0 +1,488 @@
|
||||
/* 基础样式 */
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
background-color: #ffffff;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
/* 默认浅色主题样式 */
|
||||
.swagger-ui .topbar {
|
||||
background-color: #2c3e50;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.swagger-ui .topbar .topbar-wrapper .link {
|
||||
color: #ecf0f1;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.swagger-ui .info {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.swagger-ui .info .title {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.swagger-ui .info .description {
|
||||
font-size: 1rem;
|
||||
color: #555;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/* 修复服务器URL输入框样式 */
|
||||
.swagger-ui .servers li input[type="text"] {
|
||||
padding: 8px 12px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 修复服务器选择区域的背景颜色和布局 */
|
||||
.swagger-ui .servers {
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 确保服务器列表容器有正确的背景色和布局 */
|
||||
.swagger-ui .servers-wrapper {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 确保整个顶部区域颜色一致和布局正确 */
|
||||
.swagger-ui .info {
|
||||
margin: 0;
|
||||
padding: 20px 16px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 确保顶部主容器颜色一致和布局正确 */
|
||||
.swagger-ui {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 确保API信息区域颜色一致和布局正确 */
|
||||
.swagger-ui .info-container {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body.dark-mode .swagger-ui .servers li label {
|
||||
color: #ffffff !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
/* 修复服务器URL输入框深色模式样式 */
|
||||
body.dark-mode .swagger-ui .servers li input[type="text"] {
|
||||
background-color: #1a202c !important;
|
||||
color: #ffffff !important;
|
||||
border-color: #4a5568 !important;
|
||||
padding: 8px 12px !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* 修复服务器选择区域的深色模式背景颜色和布局 */
|
||||
body.dark-mode .swagger-ui .servers {
|
||||
background-color: #1a202c !important;
|
||||
border: none !important;
|
||||
padding: 16px !important;
|
||||
width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* 确保服务器列表容器在深色模式下也有正确的背景色和布局 */
|
||||
body.dark-mode .swagger-ui .servers-wrapper {
|
||||
background-color: #1a202c !important;
|
||||
width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* 确保整个顶部区域在深色模式下颜色一致和布局正确 */
|
||||
body.dark-mode .swagger-ui .info {
|
||||
background-color: #1a202c !important;
|
||||
margin: 0 !important;
|
||||
padding: 20px 16px !important;
|
||||
border-bottom: 1px solid #4a5568 !important;
|
||||
width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
/* 确保顶部主容器在深色模式下颜色一致和布局正确 */
|
||||
body.dark-mode .swagger-ui {
|
||||
background-color: #1a202c !important;
|
||||
width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* 确保API信息区域在深色模式下颜色一致和布局正确 */
|
||||
body.dark-mode .swagger-ui .info-container {
|
||||
background-color: #1a202c !important;
|
||||
width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* 修复深色模式下内容区域的布局问题 */
|
||||
body.dark-mode .swagger-ui .wrapper {
|
||||
width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* 修复深色模式下API操作块的布局 */
|
||||
body.dark-mode .swagger-ui .opblock {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
/* 修复深色模式下过滤器的布局 */
|
||||
body.dark-mode .swagger-ui .filter {
|
||||
width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
padding: 16px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* 修复深色模式下顶部栏布局 */
|
||||
body.dark-mode .swagger-ui .topbar {
|
||||
width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
margin: 0 !important;
|
||||
padding: 15px 0 !important;
|
||||
}
|
||||
|
||||
/* 修复深色模式下顶部栏包装器布局 */
|
||||
body.dark-mode .swagger-ui .topbar .topbar-wrapper {
|
||||
width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
padding: 0 16px !important;
|
||||
}
|
||||
|
||||
/* 修复深色模式下响应容器布局 */
|
||||
body.dark-mode .swagger-ui .responses-inner {
|
||||
width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
/* 修复深色模式下操作块摘要布局 */
|
||||
body.dark-mode .swagger-ui .opblock-summary {
|
||||
width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
/* 确保深色模式下所有容器元素都使用box-sizing */
|
||||
body.dark-mode * {
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
/* 增强标签标题深色模式样式 */
|
||||
body.dark-mode .swagger-ui .opblock-tag {
|
||||
color: #ffffff !important;
|
||||
background-color: #2d3748 !important;
|
||||
padding: 12px 16px !important;
|
||||
border-radius: 6px !important;
|
||||
margin-bottom: 12px !important;
|
||||
font-weight: 700 !important;
|
||||
font-size: 1.1rem !important;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
/* 增强标签标题(h3)深色模式样式 */
|
||||
body.dark-mode .swagger-ui .opblock-tag.h3 {
|
||||
color: #ffffff !important;
|
||||
background-color: #2d3748 !important;
|
||||
}
|
||||
|
||||
/* 增强标签部分深色模式样式 */
|
||||
body.dark-mode .swagger-ui .opblock-tag-section {
|
||||
background-color: #2d3748 !important;
|
||||
padding: 16px !important;
|
||||
border-radius: 8px !important;
|
||||
margin-bottom: 20px !important;
|
||||
}
|
||||
|
||||
/* 增强API描述深色模式样式 */
|
||||
body.dark-mode .swagger-ui .opblock-description-wrapper {
|
||||
color: #ffffff !important;
|
||||
background-color: #2d3748 !important;
|
||||
padding: 12px 16px !important;
|
||||
border-radius: 6px !important;
|
||||
margin-bottom: 12px !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .opblock-description-wrapper p {
|
||||
color: #ffffff !important;
|
||||
line-height: 1.5 !important;
|
||||
}
|
||||
|
||||
/* 增强stats标签描述深色模式样式 */
|
||||
body.dark-mode .swagger-ui .opblock-summary-description {
|
||||
color: #ffffff !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
/* 增强操作块标题深色模式样式 */
|
||||
body.dark-mode .swagger-ui .opblock-title_normal h4 {
|
||||
color: #ffffff !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
/* 增强参数部分深色模式样式 */
|
||||
body.dark-mode .swagger-ui .opblock-body {
|
||||
background-color: #2d3748 !important;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .opblock-body .parameter__name {
|
||||
color: #ffffff !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .opblock-body .parameter__type {
|
||||
color: #ffffff !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .opblock-body .parameter__description {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .parameters-col_description,
|
||||
body.dark-mode .swagger-ui .parameters-col_name,
|
||||
body.dark-mode .swagger-ui .parameters-col_type {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .parameters-col_description p,
|
||||
body.dark-mode .swagger-ui .parameters-col_name p,
|
||||
body.dark-mode .swagger-ui .parameters-col_type p {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* 新增:适配API文档展开界面的所有文字元素 */
|
||||
body.dark-mode .swagger-ui .opblock-body {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .opblock-body .parameter__name {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .opblock-body .parameter__type {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .opblock-body .parameter__description {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .opblock-body .body-param-options {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .opblock-body .body-param-options .body-param-type {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .responses-inner {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .responses-inner h4 {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .response-container {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .response-container .response-wrapper {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .response-container .response-code {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .response-container .response-description {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .model {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .model .property {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .model .property .property-name {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .model .property .property-description {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .model .property .property-type {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .model .property .required {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .scroll-to-top {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .opblock-tag-section {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .servers-title {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .servers {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .servers li {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .servers li label {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .servers li select {
|
||||
color: #ffffff;
|
||||
background-color: #1a202c;
|
||||
border-color: #4a5568;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .auth-wrapper {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .auth-wrapper .auth-title {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .auth-wrapper .auth-list {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .auth-wrapper .auth-item {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .auth-wrapper .auth-item label {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* 确保代码块内的文字也清晰可见 */
|
||||
body.dark-mode .swagger-ui pre {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui code {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* 确保所有表单元素的文字颜色正确 */
|
||||
body.dark-mode .swagger-ui form {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui form label {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui select {
|
||||
color: #ffffff;
|
||||
background-color: #1a202c;
|
||||
border-color: #4a5568;
|
||||
}
|
||||
|
||||
/* 适配可能的嵌套内容 */
|
||||
body.dark-mode .swagger-ui .opblock-body .schema {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .opblock-body .schema .title {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .opblock-body .schema .required {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* 适配可能的按钮组 */
|
||||
body.dark-mode .swagger-ui .btn-group {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* 适配可能的标签 */
|
||||
body.dark-mode .swagger-ui .tag {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* 适配可能的警告和提示信息 */
|
||||
body.dark-mode .swagger-ui .warning {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui .hint {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* 适配可能的表格内容 */
|
||||
body.dark-mode .swagger-ui table {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui table th {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode .swagger-ui table td {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.topbar-controls {
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.theme-toggle-btn {
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.theme-toggle-btn span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
16
static/api/index.html
Normal file
16
static/api/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>DNS Server API 文档</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.18.3/swagger-ui.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.18.3/swagger-ui-bundle.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.18.3/swagger-ui-standalone-preset.js"></script>
|
||||
<script src="js/index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1333
static/api/js/index.js
Normal file
1333
static/api/js/index.js
Normal file
File diff suppressed because it is too large
Load Diff
62
static/css/animation.css
Normal file
62
static/css/animation.css
Normal file
@@ -0,0 +1,62 @@
|
||||
@layer utilities {
|
||||
.content-auto {
|
||||
content-visibility: auto;
|
||||
}
|
||||
.card-shadow {
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.sidebar-item-active {
|
||||
background-color: rgba(22, 93, 255, 0.1);
|
||||
color: #165DFF;
|
||||
border-right: 4px solid #165DFF;
|
||||
}
|
||||
}
|
||||
|
||||
/* 服务器状态组件光晕效果 */
|
||||
.glow-effect {
|
||||
animation: pulse 2s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(41, 128, 185, 0.4);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(41, 128, 185, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(41, 128, 185, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 服务器状态组件样式优化 */
|
||||
.server-status-widget {
|
||||
min-width: 170px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.server-status-widget:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
|
||||
/* 加载状态样式 */
|
||||
.status-loading {
|
||||
animation: status-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 状态脉冲动画 */
|
||||
@keyframes status-pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
/* 保存按钮状态样式 */
|
||||
#save-blacklist-status {
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
@@ -132,26 +132,7 @@ header p {
|
||||
|
||||
/* 响应式布局 - 移动设备 */
|
||||
@media (max-width: 768px) {
|
||||
.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;
|
||||
}
|
||||
/* 这些样式已经通过Tailwind CSS类在HTML中实现,这里移除避免冲突 */
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
@@ -1062,18 +1043,6 @@ tr:hover {
|
||||
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 {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
|
||||
1030
static/index.html
1030
static/index.html
File diff suppressed because it is too large
Load Diff
1727
static/index.html.2
1727
static/index.html.2
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -20,33 +20,32 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
|
||||
options.body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
// 添加超时处理
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error('请求超时'));
|
||||
}, 10000); // 10秒超时
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
// 竞争:请求或超时
|
||||
const response = await Promise.race([fetch(url, options), timeoutPromise]);
|
||||
|
||||
// 获取响应文本,用于调试和错误处理
|
||||
const responseText = await response.text();
|
||||
|
||||
if (!response.ok) {
|
||||
// 尝试解析错误响应
|
||||
let errorData = {};
|
||||
// 优化错误响应处理
|
||||
console.warn(`API请求失败: ${response.status}`);
|
||||
|
||||
// 尝试解析JSON,但如果失败,直接使用原始文本作为错误信息
|
||||
try {
|
||||
// 首先检查响应文本是否为空或不是有效JSON
|
||||
if (!responseText || responseText.trim() === '') {
|
||||
console.warn('错误响应为空');
|
||||
} else {
|
||||
try {
|
||||
errorData = JSON.parse(responseText);
|
||||
} catch (parseError) {
|
||||
console.error('无法解析错误响应为JSON:', parseError);
|
||||
console.error('原始错误响应文本:', responseText);
|
||||
}
|
||||
}
|
||||
// 直接返回错误信息,而不是抛出异常,让上层处理
|
||||
console.warn(`API请求失败: ${response.status}`, errorData);
|
||||
return { error: errorData.error || `请求失败: ${response.status}` };
|
||||
} catch (e) {
|
||||
console.error('处理错误响应时出错:', e);
|
||||
return { error: `请求处理失败: ${e.message}` };
|
||||
const errorData = JSON.parse(responseText);
|
||||
return { error: errorData.error || responseText || `请求失败: ${response.status}` };
|
||||
} catch (parseError) {
|
||||
// 当响应不是有效的JSON时(如中文错误信息),直接使用原始文本
|
||||
console.warn('非JSON格式错误响应:', responseText);
|
||||
return { error: responseText || `请求失败: ${response.status}` };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,12 +54,18 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
|
||||
// 首先检查响应文本是否为空
|
||||
if (!responseText || responseText.trim() === '') {
|
||||
console.warn('空响应文本');
|
||||
return {};
|
||||
return null; // 返回null表示空响应
|
||||
}
|
||||
|
||||
// 尝试解析JSON
|
||||
const parsedData = JSON.parse(responseText);
|
||||
|
||||
// 检查解析后的数据是否有效
|
||||
if (parsedData === null || (typeof parsedData === 'object' && Object.keys(parsedData).length === 0)) {
|
||||
console.warn('解析后的数据为空');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 限制所有数字为两位小数
|
||||
const formatNumbers = (obj) => {
|
||||
if (typeof obj === 'number') {
|
||||
@@ -93,13 +98,13 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
|
||||
console.error('位置66附近的字符:', responseText.substring(60, 75));
|
||||
}
|
||||
|
||||
// 返回空数组作为默认值,避免页面功能完全中断
|
||||
console.warn('使用默认空数组作为响应');
|
||||
return [];
|
||||
// 返回错误对象,让上层处理
|
||||
return { error: 'JSON解析错误' };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('API请求错误:', error);
|
||||
throw error;
|
||||
// 返回错误对象,而不是抛出异常,让上层处理
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +125,12 @@ const api = {
|
||||
// 获取最近屏蔽域名
|
||||
getRecentBlockedDomains: () => apiRequest('/recent-blocked?t=' + Date.now()),
|
||||
|
||||
// 获取TOP客户端
|
||||
getTopClients: () => apiRequest('/top-clients?t=' + Date.now()),
|
||||
|
||||
// 获取TOP域名
|
||||
getTopDomains: () => apiRequest('/top-domains?t=' + Date.now()),
|
||||
|
||||
// 获取小时统计
|
||||
getHourlyStats: () => apiRequest('/hourly-stats?t=' + Date.now()),
|
||||
|
||||
|
||||
@@ -197,12 +197,25 @@ function apiRequest(endpoint, method = 'GET', data = null, maxRetries = 3) {
|
||||
|
||||
// 数字格式化函数
|
||||
function formatNumber(num) {
|
||||
// 显示完整数字的最大长度阈值
|
||||
const MAX_FULL_LENGTH = 5;
|
||||
|
||||
// 先获取完整数字字符串
|
||||
const fullNumStr = num.toString();
|
||||
|
||||
// 如果数字长度小于等于阈值,直接返回完整数字
|
||||
if (fullNumStr.length <= MAX_FULL_LENGTH) {
|
||||
return fullNumStr;
|
||||
}
|
||||
|
||||
// 否则使用缩写格式
|
||||
if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1) + 'M';
|
||||
} else if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1) + 'K';
|
||||
}
|
||||
return num.toString();
|
||||
|
||||
return fullNumStr;
|
||||
}
|
||||
|
||||
// 确认对话框函数
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
// 配置管理页面功能实现
|
||||
|
||||
// 工具函数:安全获取DOM元素
|
||||
function getElement(id) {
|
||||
const element = document.getElementById(id);
|
||||
if (!element) {
|
||||
console.warn(`Element with id "${id}" not found`);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
// 工具函数:验证端口号
|
||||
function validatePort(port) {
|
||||
// 确保port是字符串类型
|
||||
var portStr = port;
|
||||
if (port === null || port === undefined || typeof port !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 去除前后空白并验证是否为纯数字
|
||||
portStr = port.trim();
|
||||
if (!/^\d+$/.test(portStr)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const num = parseInt(portStr, 10);
|
||||
return num >= 1 && num <= 65535 ? num : null;
|
||||
}
|
||||
|
||||
// 初始化配置管理页面
|
||||
function initConfigPage() {
|
||||
loadConfig();
|
||||
@@ -9,93 +36,183 @@ function initConfigPage() {
|
||||
// 加载系统配置
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const config = await api.getConfig();
|
||||
populateConfigForm(config);
|
||||
const result = await api.getConfig();
|
||||
|
||||
// 检查API返回的错误
|
||||
if (result && result.error) {
|
||||
showErrorMessage('加载配置失败: ' + result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
populateConfigForm(result);
|
||||
} catch (error) {
|
||||
showErrorMessage('加载配置失败: ' + error.message);
|
||||
// 捕获可能的异常(虽然apiRequest不应该再抛出异常)
|
||||
showErrorMessage('加载配置失败: ' + (error.message || '未知错误'));
|
||||
}
|
||||
}
|
||||
|
||||
// 填充配置表单
|
||||
function populateConfigForm(config) {
|
||||
// DNS配置
|
||||
document.getElementById('dns-port')?.value = config.DNSServer.Port || 53;
|
||||
document.getElementById('dns-upstream-servers')?.value = (config.DNSServer.UpstreamServers || []).join(', ');
|
||||
document.getElementById('dns-timeout')?.value = config.DNSServer.Timeout || 5;
|
||||
document.getElementById('dns-stats-file')?.value = config.DNSServer.StatsFile || './stats.json';
|
||||
document.getElementById('dns-save-interval')?.value = config.DNSServer.SaveInterval || 300;
|
||||
// 安全获取配置对象,防止未定义属性访问
|
||||
const dnsServerConfig = config.DNSServer || {};
|
||||
const httpServerConfig = config.HTTPServer || {};
|
||||
const shieldConfig = config.Shield || {};
|
||||
|
||||
// DNS配置 - 使用函数安全设置值,避免 || 操作符可能的错误处理
|
||||
setElementValue('dns-port', getSafeValue(dnsServerConfig.Port, 53));
|
||||
setElementValue('dns-upstream-servers', getSafeArray(dnsServerConfig.UpstreamServers).join(', '));
|
||||
setElementValue('dns-timeout', getSafeValue(dnsServerConfig.Timeout, 5));
|
||||
setElementValue('dns-stats-file', getSafeValue(dnsServerConfig.StatsFile, 'data/stats.json'));
|
||||
setElementValue('dns-save-interval', getSafeValue(dnsServerConfig.SaveInterval, 300));
|
||||
|
||||
// HTTP配置
|
||||
document.getElementById('http-port')?.value = config.HTTPServer.Port || 8080;
|
||||
document.getElementById('http-host')?.value = config.HTTPServer.Host || '0.0.0.0';
|
||||
document.getElementById('http-api-enabled')?.checked = config.HTTPServer.APIEnabled !== false;
|
||||
setElementValue('http-port', getSafeValue(httpServerConfig.Port, 8080));
|
||||
|
||||
// 屏蔽配置
|
||||
document.getElementById('shield-local-rules-file')?.value = config.Shield.LocalRulesFile || './rules.txt';
|
||||
document.getElementById('shield-remote-rules-urls')?.value = (config.Shield.RemoteRulesURLs || []).join('\n');
|
||||
document.getElementById('shield-update-interval')?.value = config.Shield.UpdateInterval || 3600;
|
||||
document.getElementById('shield-hosts-file')?.value = config.Shield.HostsFile || '/etc/hosts';
|
||||
document.getElementById('shield-block-method')?.value = config.Shield.BlockMethod || '0.0.0.0';
|
||||
setElementValue('shield-local-rules-file', getSafeValue(shieldConfig.LocalRulesFile, 'data/rules.txt'));
|
||||
setElementValue('shield-update-interval', getSafeValue(shieldConfig.UpdateInterval, 3600));
|
||||
setElementValue('shield-hosts-file', getSafeValue(shieldConfig.HostsFile, 'data/hosts.txt'));
|
||||
// 使用服务器端接受的屏蔽方法值,默认使用NXDOMAIN
|
||||
setElementValue('shield-block-method', getSafeValue(shieldConfig.BlockMethod, 'NXDOMAIN'));
|
||||
}
|
||||
|
||||
// 工具函数:安全设置元素值
|
||||
function setElementValue(elementId, value) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element && element.tagName === 'INPUT') {
|
||||
element.value = value;
|
||||
} else if (!element) {
|
||||
console.warn(`Element with id "${elementId}" not found for setting value: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 工具函数:安全获取值,如果未定义或为null则返回默认值
|
||||
function getSafeValue(value, defaultValue) {
|
||||
// 更严格的检查,避免0、空字符串等被默认值替换
|
||||
return value === undefined || value === null ? defaultValue : value;
|
||||
}
|
||||
|
||||
// 工具函数:安全获取数组,如果不是数组则返回空数组
|
||||
function getSafeArray(value) {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
async function handleSaveConfig() {
|
||||
const formData = collectFormData();
|
||||
|
||||
if (!formData) return;
|
||||
|
||||
try {
|
||||
await api.saveConfig(formData);
|
||||
const result = await api.saveConfig(formData);
|
||||
|
||||
// 检查API返回的错误
|
||||
if (result && result.error) {
|
||||
showErrorMessage('保存配置失败: ' + result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
showSuccessMessage('配置保存成功');
|
||||
} catch (error) {
|
||||
showErrorMessage('保存配置失败: ' + error.message);
|
||||
// 捕获可能的异常(虽然apiRequest不应该再抛出异常)
|
||||
showErrorMessage('保存配置失败: ' + (error.message || '未知错误'));
|
||||
}
|
||||
}
|
||||
|
||||
// 重启服务
|
||||
async function handleRestartService() {
|
||||
if (confirm('确定要重启DNS服务吗?重启期间服务可能会短暂不可用。')) {
|
||||
try {
|
||||
await api.restartService();
|
||||
showSuccessMessage('服务重启成功');
|
||||
} catch (error) {
|
||||
showErrorMessage('重启服务失败: ' + error.message);
|
||||
if (!confirm('确定要重启DNS服务吗?重启期间服务可能会短暂不可用。')) return;
|
||||
|
||||
try {
|
||||
const result = await api.restartService();
|
||||
|
||||
// 检查API返回的错误
|
||||
if (result && result.error) {
|
||||
showErrorMessage('服务重启失败: ' + result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
showSuccessMessage('服务重启成功');
|
||||
} catch (error) {
|
||||
// 捕获可能的异常(虽然apiRequest不应该再抛出异常)
|
||||
showErrorMessage('重启服务失败: ' + (error.message || '未知错误'));
|
||||
}
|
||||
}
|
||||
|
||||
// 收集表单数据
|
||||
// 收集表单数据并验证
|
||||
function collectFormData() {
|
||||
// 验证端口号 - 使用安全获取元素值的函数
|
||||
const dnsPortValue = getElementValue('dns-port');
|
||||
const httpPortValue = getElementValue('http-port');
|
||||
|
||||
const dnsPort = validatePort(dnsPortValue);
|
||||
const httpPort = validatePort(httpPortValue);
|
||||
|
||||
if (!dnsPort) {
|
||||
showErrorMessage('DNS端口号无效(必须是1-65535之间的整数)');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!httpPort) {
|
||||
showErrorMessage('HTTP端口号无效(必须是1-65535之间的整数)');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 安全获取上游服务器列表
|
||||
const upstreamServersText = getElementValue('dns-upstream-servers');
|
||||
const upstreamServers = upstreamServersText ?
|
||||
upstreamServersText.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s !== ''; }) :
|
||||
[];
|
||||
|
||||
// 安全获取并转换整数值
|
||||
const timeoutValue = getElementValue('dns-timeout');
|
||||
const timeout = timeoutValue ? parseInt(timeoutValue, 10) : 5;
|
||||
|
||||
const saveIntervalValue = getElementValue('dns-save-interval');
|
||||
const saveInterval = saveIntervalValue ? parseInt(saveIntervalValue, 10) : 300;
|
||||
|
||||
const updateIntervalValue = getElementValue('shield-update-interval');
|
||||
const updateInterval = updateIntervalValue ? parseInt(updateIntervalValue, 10) : 3600;
|
||||
|
||||
return {
|
||||
DNSServer: {
|
||||
Port: parseInt(document.getElementById('dns-port')?.value) || 53,
|
||||
UpstreamServers: document.getElementById('dns-upstream-servers')?.value.split(',').map(s => s.trim()).filter(Boolean) || [],
|
||||
Timeout: parseInt(document.getElementById('dns-timeout')?.value) || 5,
|
||||
StatsFile: document.getElementById('dns-stats-file')?.value || './stats.json',
|
||||
SaveInterval: parseInt(document.getElementById('dns-save-interval')?.value) || 300
|
||||
Port: dnsPort,
|
||||
UpstreamServers: upstreamServers,
|
||||
Timeout: timeout,
|
||||
StatsFile: getElementValue('dns-stats-file') || './data/stats.json',
|
||||
SaveInterval: saveInterval
|
||||
},
|
||||
HTTPServer: {
|
||||
Port: parseInt(document.getElementById('http-port')?.value) || 8080,
|
||||
Host: document.getElementById('http-host')?.value || '0.0.0.0',
|
||||
APIEnabled: document.getElementById('http-api-enabled')?.checked !== false
|
||||
Port: httpPort
|
||||
},
|
||||
Shield: {
|
||||
LocalRulesFile: document.getElementById('shield-local-rules-file')?.value || './rules.txt',
|
||||
RemoteRulesURLs: document.getElementById('shield-remote-rules-urls')?.value.split('\n').map(s => s.trim()).filter(Boolean) || [],
|
||||
UpdateInterval: parseInt(document.getElementById('shield-update-interval')?.value) || 3600,
|
||||
HostsFile: document.getElementById('shield-hosts-file')?.value || '/etc/hosts',
|
||||
BlockMethod: document.getElementById('shield-block-method')?.value || '0.0.0.0'
|
||||
LocalRulesFile: getElementValue('shield-local-rules-file') || './data/rules.txt',
|
||||
UpdateInterval: updateInterval,
|
||||
HostsFile: getElementValue('shield-hosts-file') || './data/hosts.txt',
|
||||
BlockMethod: getElementValue('shield-block-method') || 'NXDOMAIN'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 工具函数:安全获取元素值
|
||||
function getElementValue(elementId) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element && element.tagName === 'INPUT') {
|
||||
return element.value;
|
||||
}
|
||||
return ''; // 默认返回空字符串
|
||||
}
|
||||
|
||||
// 设置事件监听器
|
||||
function setupConfigEventListeners() {
|
||||
// 保存配置按钮
|
||||
document.getElementById('save-config-btn')?.addEventListener('click', handleSaveConfig);
|
||||
getElement('save-config-btn')?.addEventListener('click', handleSaveConfig);
|
||||
|
||||
// 重启服务按钮
|
||||
document.getElementById('restart-service-btn')?.addEventListener('click', handleRestartService);
|
||||
getElement('restart-service-btn')?.addEventListener('click', handleRestartService);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 显示成功消息
|
||||
function showSuccessMessage(message) {
|
||||
showNotification(message, 'success');
|
||||
@@ -118,13 +235,28 @@ function showNotification(message, type = 'info') {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform transition-transform duration-300 ease-in-out translate-y-0 opacity-0`;
|
||||
|
||||
// 设置通知样式
|
||||
// 设置通知样式(兼容Tailwind和原生CSS)
|
||||
notification.style.cssText += `
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
padding: 16px 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
z-index: 1000;
|
||||
transition: all 0.3s ease;
|
||||
opacity: 0;
|
||||
`;
|
||||
|
||||
if (type === 'success') {
|
||||
notification.classList.add('bg-green-500', 'text-white');
|
||||
notification.style.backgroundColor = '#10b981';
|
||||
notification.style.color = 'white';
|
||||
} else if (type === 'error') {
|
||||
notification.classList.add('bg-red-500', 'text-white');
|
||||
notification.style.backgroundColor = '#ef4444';
|
||||
notification.style.color = 'white';
|
||||
} else {
|
||||
notification.classList.add('bg-blue-500', 'text-white');
|
||||
notification.style.backgroundColor = '#3b82f6';
|
||||
notification.style.color = 'white';
|
||||
}
|
||||
|
||||
notification.textContent = message;
|
||||
@@ -132,14 +264,12 @@ function showNotification(message, type = 'info') {
|
||||
|
||||
// 显示通知
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('opacity-0');
|
||||
notification.classList.add('opacity-100');
|
||||
notification.style.opacity = '1';
|
||||
}, 10);
|
||||
|
||||
// 3秒后隐藏通知
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('opacity-100');
|
||||
notification.classList.add('opacity-0');
|
||||
notification.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 300);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,94 +2,140 @@
|
||||
|
||||
// 初始化Hosts管理页面
|
||||
function initHostsPage() {
|
||||
loadHostsContent();
|
||||
// 加载Hosts规则
|
||||
loadHostsRules();
|
||||
// 设置事件监听器
|
||||
setupHostsEventListeners();
|
||||
}
|
||||
|
||||
// 加载Hosts内容
|
||||
async function loadHostsContent() {
|
||||
// 加载Hosts规则
|
||||
async function loadHostsRules() {
|
||||
try {
|
||||
const hostsContent = await api.getHosts();
|
||||
document.getElementById('hosts-content').value = hostsContent;
|
||||
const response = await fetch('/api/shield/hosts');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load hosts rules');
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
// 处理API返回的数据格式
|
||||
let hostsRules = [];
|
||||
if (data && Array.isArray(data)) {
|
||||
// 直接是数组格式
|
||||
hostsRules = data;
|
||||
} else if (data && data.hosts) {
|
||||
// 包含在hosts字段中
|
||||
hostsRules = data.hosts;
|
||||
}
|
||||
|
||||
updateHostsTable(hostsRules);
|
||||
} catch (error) {
|
||||
showErrorMessage('加载Hosts文件失败: ' + error.message);
|
||||
console.error('Error loading hosts rules:', error);
|
||||
showErrorMessage('加载Hosts规则失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 保存Hosts内容
|
||||
async function handleSaveHosts() {
|
||||
const hostsContent = document.getElementById('hosts-content').value;
|
||||
// 更新Hosts表格
|
||||
function updateHostsTable(hostsRules) {
|
||||
const tbody = document.getElementById('hosts-table-body');
|
||||
|
||||
try {
|
||||
await api.saveHosts(hostsContent);
|
||||
showSuccessMessage('Hosts文件保存成功');
|
||||
} catch (error) {
|
||||
showErrorMessage('保存Hosts文件失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新Hosts
|
||||
async function handleRefreshHosts() {
|
||||
try {
|
||||
await api.refreshHosts();
|
||||
showSuccessMessage('Hosts刷新成功');
|
||||
loadHostsContent();
|
||||
} catch (error) {
|
||||
showErrorMessage('刷新Hosts失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加新的Hosts条目
|
||||
function handleAddHostsEntry() {
|
||||
const ipInput = document.getElementById('hosts-ip');
|
||||
const domainInput = document.getElementById('hosts-domain');
|
||||
|
||||
const ip = ipInput.value.trim();
|
||||
const domain = domainInput.value.trim();
|
||||
|
||||
if (!ip || !domain) {
|
||||
showErrorMessage('IP和域名不能为空');
|
||||
if (hostsRules.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="3" class="py-4 text-center text-gray-500">暂无Hosts条目</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
// 简单的IP验证
|
||||
const ipRegex = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/;
|
||||
if (!ipRegex.test(ip)) {
|
||||
showErrorMessage('请输入有效的IP地址');
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = hostsRules.map(rule => {
|
||||
// 处理对象格式的规则
|
||||
const ip = rule.ip || '';
|
||||
const domain = rule.domain || '';
|
||||
|
||||
return `
|
||||
<tr class="border-b border-gray-200">
|
||||
<td class="py-3 px-4">${ip}</td>
|
||||
<td class="py-3 px-4">${domain}</td>
|
||||
<td class="py-3 px-4 text-right">
|
||||
<button class="delete-hosts-btn px-3 py-1 bg-danger text-white rounded-md hover:bg-danger/90 transition-colors text-sm" data-ip="${ip}" data-domain="${domain}">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
const hostsTextarea = document.getElementById('hosts-content');
|
||||
const newEntry = `\n${ip} ${domain}`;
|
||||
hostsTextarea.value += newEntry;
|
||||
|
||||
// 清空输入框
|
||||
ipInput.value = '';
|
||||
domainInput.value = '';
|
||||
|
||||
// 滚动到文本区域底部
|
||||
hostsTextarea.scrollTop = hostsTextarea.scrollHeight;
|
||||
|
||||
showSuccessMessage('Hosts条目已添加到编辑器');
|
||||
// 重新绑定删除事件
|
||||
document.querySelectorAll('.delete-hosts-btn').forEach(btn => {
|
||||
btn.addEventListener('click', handleDeleteHostsRule);
|
||||
});
|
||||
}
|
||||
|
||||
// 设置事件监听器
|
||||
function setupHostsEventListeners() {
|
||||
// 保存按钮
|
||||
document.getElementById('save-hosts-btn')?.addEventListener('click', handleSaveHosts);
|
||||
// 保存Hosts按钮
|
||||
document.getElementById('save-hosts-btn').addEventListener('click', handleAddHostsRule);
|
||||
}
|
||||
|
||||
// 处理添加Hosts规则
|
||||
async function handleAddHostsRule() {
|
||||
const ip = document.getElementById('hosts-ip').value.trim();
|
||||
const domain = document.getElementById('hosts-domain').value.trim();
|
||||
|
||||
// 刷新按钮
|
||||
document.getElementById('refresh-hosts-btn')?.addEventListener('click', handleRefreshHosts);
|
||||
if (!ip || !domain) {
|
||||
showErrorMessage('IP地址和域名不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加Hosts条目按钮
|
||||
document.getElementById('add-hosts-entry-btn')?.addEventListener('click', handleAddHostsEntry);
|
||||
|
||||
// 按回车键添加条目
|
||||
document.getElementById('hosts-domain')?.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleAddHostsEntry();
|
||||
try {
|
||||
const response = await fetch('/api/shield/hosts', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ ip, domain })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to add hosts rule');
|
||||
}
|
||||
});
|
||||
|
||||
showSuccessMessage('Hosts规则添加成功');
|
||||
|
||||
// 清空输入框
|
||||
document.getElementById('hosts-ip').value = '';
|
||||
document.getElementById('hosts-domain').value = '';
|
||||
|
||||
// 重新加载规则
|
||||
loadHostsRules();
|
||||
} catch (error) {
|
||||
console.error('Error adding hosts rule:', error);
|
||||
showErrorMessage('添加Hosts规则失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 处理删除Hosts规则
|
||||
async function handleDeleteHostsRule(e) {
|
||||
const ip = e.target.closest('.delete-hosts-btn').dataset.ip;
|
||||
const domain = e.target.closest('.delete-hosts-btn').dataset.domain;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/shield/hosts', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ domain })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete hosts rule');
|
||||
}
|
||||
|
||||
showSuccessMessage('Hosts规则删除成功');
|
||||
|
||||
// 重新加载规则
|
||||
loadHostsRules();
|
||||
} catch (error) {
|
||||
console.error('Error deleting hosts rule:', error);
|
||||
showErrorMessage('删除Hosts规则失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示成功消息
|
||||
@@ -102,6 +148,8 @@ function showErrorMessage(message) {
|
||||
showNotification(message, 'error');
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 显示通知
|
||||
function showNotification(message, type = 'info') {
|
||||
// 移除现有通知
|
||||
@@ -112,7 +160,7 @@ function showNotification(message, type = 'info') {
|
||||
|
||||
// 创建新通知
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform transition-transform duration-300 ease-in-out translate-y-0 opacity-0`;
|
||||
notification.className = `notification fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform transition-all duration-300 ease-in-out translate-y-0 opacity-0`;
|
||||
|
||||
// 设置通知样式
|
||||
if (type === 'success') {
|
||||
@@ -123,18 +171,22 @@ function showNotification(message, type = 'info') {
|
||||
notification.classList.add('bg-blue-500', 'text-white');
|
||||
}
|
||||
|
||||
notification.textContent = message;
|
||||
notification.innerHTML = `
|
||||
<div class="flex items-center space-x-2">
|
||||
<i class="fa fa-${type === 'success' ? 'check' : type === 'error' ? 'exclamation' : 'info'}"></i>
|
||||
<span>${message}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// 显示通知
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('opacity-0');
|
||||
notification.classList.add('opacity-100');
|
||||
}, 10);
|
||||
}, 100);
|
||||
|
||||
// 3秒后隐藏通知
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('opacity-100');
|
||||
notification.classList.add('opacity-0');
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
@@ -147,4 +199,4 @@ if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initHostsPage);
|
||||
} else {
|
||||
initHostsPage();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ function setupNavigation() {
|
||||
document.getElementById('dashboard-content'),
|
||||
document.getElementById('shield-content'),
|
||||
document.getElementById('hosts-content'),
|
||||
document.getElementById('blacklists-content'),
|
||||
document.getElementById('query-content'),
|
||||
document.getElementById('config-content')
|
||||
];
|
||||
@@ -16,40 +15,105 @@ function setupNavigation() {
|
||||
|
||||
menuItems.forEach((item, index) => {
|
||||
item.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
// 允许浏览器自动更新地址栏中的hash,不阻止默认行为
|
||||
|
||||
// 更新活跃状态
|
||||
menuItems.forEach(menuItem => {
|
||||
menuItem.classList.remove('sidebar-item-active');
|
||||
});
|
||||
item.classList.add('sidebar-item-active');
|
||||
|
||||
// 隐藏所有内容部分
|
||||
contentSections.forEach(section => {
|
||||
section.classList.add('hidden');
|
||||
});
|
||||
|
||||
// 显示对应内容部分
|
||||
const target = item.getAttribute('href').substring(1);
|
||||
const activeContent = document.getElementById(`${target}-content`);
|
||||
if (activeContent) {
|
||||
activeContent.classList.remove('hidden');
|
||||
// 移动端点击菜单项后自动关闭侧边栏
|
||||
if (window.innerWidth < 768) {
|
||||
closeSidebar();
|
||||
}
|
||||
|
||||
// 更新页面标题
|
||||
pageTitle.textContent = item.querySelector('span').textContent;
|
||||
// 页面特定初始化 - 保留这部分逻辑,因为它不会与hashchange事件处理逻辑冲突
|
||||
const target = item.getAttribute('href').substring(1);
|
||||
if (target === 'shield' && typeof initShieldPage === 'function') {
|
||||
initShieldPage();
|
||||
} else if (target === 'hosts' && typeof initHostsPage === 'function') {
|
||||
initHostsPage();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 移动端侧边栏切换
|
||||
const toggleSidebar = document.getElementById('toggle-sidebar');
|
||||
const closeSidebarBtn = document.getElementById('close-sidebar');
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const sidebarOverlay = document.getElementById('sidebar-overlay');
|
||||
|
||||
if (toggleSidebar && sidebar) {
|
||||
toggleSidebar.addEventListener('click', () => {
|
||||
sidebar.classList.toggle('-translate-x-full');
|
||||
});
|
||||
// 打开侧边栏函数
|
||||
function openSidebar() {
|
||||
console.log('Opening sidebar...');
|
||||
if (sidebar) {
|
||||
sidebar.classList.remove('-translate-x-full');
|
||||
sidebar.classList.add('translate-x-0');
|
||||
}
|
||||
if (sidebarOverlay) {
|
||||
sidebarOverlay.classList.remove('hidden');
|
||||
sidebarOverlay.classList.add('block');
|
||||
}
|
||||
// 防止页面滚动
|
||||
document.body.style.overflow = 'hidden';
|
||||
console.log('Sidebar opened successfully');
|
||||
}
|
||||
|
||||
// 关闭侧边栏函数
|
||||
function closeSidebar() {
|
||||
console.log('Closing sidebar...');
|
||||
if (sidebar) {
|
||||
sidebar.classList.add('-translate-x-full');
|
||||
sidebar.classList.remove('translate-x-0');
|
||||
}
|
||||
if (sidebarOverlay) {
|
||||
sidebarOverlay.classList.add('hidden');
|
||||
sidebarOverlay.classList.remove('block');
|
||||
}
|
||||
// 恢复页面滚动
|
||||
document.body.style.overflow = '';
|
||||
console.log('Sidebar closed successfully');
|
||||
}
|
||||
|
||||
// 切换侧边栏函数
|
||||
function toggleSidebarVisibility() {
|
||||
console.log('Toggling sidebar visibility...');
|
||||
console.log('Current sidebar classes:', sidebar ? sidebar.className : 'sidebar not found');
|
||||
if (sidebar && sidebar.classList.contains('-translate-x-full')) {
|
||||
console.log('Sidebar is hidden, opening...');
|
||||
openSidebar();
|
||||
} else {
|
||||
console.log('Sidebar is visible, closing...');
|
||||
closeSidebar();
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定切换按钮事件
|
||||
if (toggleSidebar) {
|
||||
toggleSidebar.addEventListener('click', toggleSidebarVisibility);
|
||||
}
|
||||
|
||||
// 绑定关闭按钮事件
|
||||
if (closeSidebarBtn) {
|
||||
closeSidebarBtn.addEventListener('click', closeSidebar);
|
||||
}
|
||||
|
||||
// 绑定遮罩层点击事件
|
||||
if (sidebarOverlay) {
|
||||
sidebarOverlay.addEventListener('click', closeSidebar);
|
||||
}
|
||||
|
||||
// 移动端点击菜单项后自动关闭侧边栏
|
||||
menuItems.forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
// 检查是否是移动设备视图
|
||||
if (window.innerWidth < 768) {
|
||||
closeSidebar();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 添加键盘事件监听,按ESC键关闭侧边栏
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeSidebar();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化函数
|
||||
|
||||
@@ -1,314 +1,231 @@
|
||||
// DNS查询工具页面功能实现
|
||||
// DNS查询页面功能实现
|
||||
|
||||
// 初始化查询工具页面
|
||||
// 初始化查询页面
|
||||
function initQueryPage() {
|
||||
console.log('初始化DNS查询页面...');
|
||||
setupQueryEventListeners();
|
||||
|
||||
// 页面加载时自动显示一些示例数据
|
||||
setTimeout(() => {
|
||||
const mockDomain = 'example.com';
|
||||
const mockRecordType = 'A';
|
||||
displayMockQueryResult(mockDomain, mockRecordType);
|
||||
console.log('显示示例DNS查询数据');
|
||||
}, 500);
|
||||
loadQueryHistory();
|
||||
}
|
||||
|
||||
// 执行DNS查询
|
||||
async function handleDNSQuery() {
|
||||
// 尝试多种可能的DOM元素ID
|
||||
const domainInput = document.getElementById('query-domain') || document.getElementById('domain-input');
|
||||
const recordTypeSelect = document.getElementById('query-record-type') || document.getElementById('record-type');
|
||||
const domainInput = document.getElementById('dns-query-domain');
|
||||
const resultDiv = document.getElementById('query-result');
|
||||
|
||||
console.log('DOM元素查找结果:', { domainInput, recordTypeSelect, resultDiv });
|
||||
|
||||
if (!domainInput || !recordTypeSelect || !resultDiv) {
|
||||
if (!domainInput || !resultDiv) {
|
||||
console.error('找不到必要的DOM元素');
|
||||
return;
|
||||
}
|
||||
|
||||
const domain = domainInput.value.trim();
|
||||
const recordType = recordTypeSelect.value;
|
||||
|
||||
if (!domain) {
|
||||
showErrorMessage('请输入域名');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`执行DNS查询: 域名=${domain}, 记录类型=${recordType}`);
|
||||
|
||||
// 清空之前的结果
|
||||
resultDiv.innerHTML = '<div class="text-center py-4"><svg class="animate-spin mx-auto h-6 w-6 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> 查询中...</div>';
|
||||
|
||||
try {
|
||||
// 检查api对象是否存在
|
||||
if (!window.api || typeof window.api.queryDNS !== 'function') {
|
||||
console.warn('api.queryDNS不存在,使用模拟数据');
|
||||
const mockResult = generateMockDNSResult(domain, recordType);
|
||||
displayQueryResult(mockResult, domain, recordType);
|
||||
return;
|
||||
const response = await fetch(`/api/query?domain=${encodeURIComponent(domain)}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('查询失败');
|
||||
}
|
||||
|
||||
// 调用API,适配不同的参数格式
|
||||
let result;
|
||||
try {
|
||||
// 尝试不同的API调用方式
|
||||
if (api.queryDNS.length === 1) {
|
||||
result = await api.queryDNS({ domain, recordType });
|
||||
} else {
|
||||
result = await api.queryDNS(domain, recordType);
|
||||
}
|
||||
} catch (apiError) {
|
||||
console.error('API调用失败,使用模拟数据:', apiError);
|
||||
const mockResult = generateMockDNSResult(domain, recordType);
|
||||
displayQueryResult(mockResult, domain, recordType);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('DNS查询API返回结果:', result);
|
||||
|
||||
// 处理API返回的数据
|
||||
if (!result || (Array.isArray(result) && result.length === 0) ||
|
||||
(typeof result === 'object' && Object.keys(result).length === 0)) {
|
||||
console.log('API返回空结果,使用模拟数据');
|
||||
const mockResult = generateMockDNSResult(domain, recordType);
|
||||
displayQueryResult(mockResult, domain, recordType);
|
||||
} else {
|
||||
displayQueryResult(result, domain, recordType);
|
||||
}
|
||||
const result = await response.json();
|
||||
displayQueryResult(result, domain);
|
||||
saveQueryHistory(domain, result);
|
||||
loadQueryHistory();
|
||||
} catch (error) {
|
||||
console.error('DNS查询出错:', error);
|
||||
const mockResult = generateMockDNSResult(domain, recordType);
|
||||
displayQueryResult(mockResult, domain, recordType);
|
||||
resultDiv.innerHTML += `<div class="text-yellow-500 text-center py-2 text-sm">注意: 显示的是模拟数据</div>`;
|
||||
showErrorMessage('查询失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示查询结果
|
||||
function displayQueryResult(result, domain, recordType) {
|
||||
const resultDiv = document.getElementById('query-result');
|
||||
|
||||
// 适配不同的数据结构
|
||||
let records = [];
|
||||
|
||||
if (Array.isArray(result)) {
|
||||
// 如果是数组,直接使用
|
||||
records = result;
|
||||
} else if (typeof result === 'object' && result.length === undefined) {
|
||||
// 如果是对象,尝试转换为数组
|
||||
if (result.records) {
|
||||
records = result.records;
|
||||
} else if (result.data) {
|
||||
records = result.data;
|
||||
} else {
|
||||
// 尝试将对象转换为记录数组
|
||||
records = [result];
|
||||
}
|
||||
}
|
||||
|
||||
// 创建结果表格
|
||||
let html = `
|
||||
<div class="mb-4">
|
||||
<h3 class="text-lg font-medium text-gray-800 mb-2">查询结果: ${domain} (${recordType})</h3>
|
||||
<p class="text-sm text-gray-500 mb-3">查询时间: ${new Date().toLocaleString()}</p>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full bg-white rounded-lg overflow-hidden shadow-sm">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="py-2 px-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">类型</th>
|
||||
<th class="py-2 px-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">值</th>
|
||||
<th class="py-2 px-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">TTL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
`;
|
||||
|
||||
if (records.length === 0) {
|
||||
html += `
|
||||
<tr>
|
||||
<td colspan="3" class="py-4 text-center text-gray-500">未找到 ${domain} 的 ${recordType} 记录</td>
|
||||
</tr>
|
||||
`;
|
||||
} else {
|
||||
// 添加查询结果
|
||||
records.forEach(record => {
|
||||
const type = record.Type || record.type || recordType;
|
||||
|
||||
// 处理不同格式的值
|
||||
let value;
|
||||
if (record.Value) {
|
||||
value = record.Value;
|
||||
} else if (record.ip || record.address) {
|
||||
value = record.ip || record.address;
|
||||
} else if (record.target) {
|
||||
value = record.target;
|
||||
} else if (record.text) {
|
||||
value = record.text;
|
||||
} else if (record.name) {
|
||||
value = record.name;
|
||||
} else {
|
||||
value = JSON.stringify(record);
|
||||
}
|
||||
|
||||
// 格式化不同类型的记录值
|
||||
if (type === 'MX' && (record.Preference || record.priority)) {
|
||||
value = `${record.Preference || record.priority} ${value}`;
|
||||
} else if (type === 'SRV') {
|
||||
if (record.Priority && record.Weight && record.Port) {
|
||||
value = `${record.Priority} ${record.Weight} ${record.Port} ${value}`;
|
||||
}
|
||||
}
|
||||
|
||||
const ttl = record.TTL || record.ttl || '-';
|
||||
|
||||
html += `
|
||||
<tr class="hover:bg-gray-50 transition-colors">
|
||||
<td class="py-3 px-4 text-sm font-medium text-gray-900">${type}</td>
|
||||
<td class="py-3 px-4 text-sm text-gray-900 font-mono break-all">${value}</td>
|
||||
<td class="py-3 px-4 text-sm text-gray-500">${ttl}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
html += `
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
resultDiv.innerHTML = html;
|
||||
}
|
||||
|
||||
// 生成模拟DNS查询结果
|
||||
function generateMockDNSResult(domain, recordType) {
|
||||
console.log('生成模拟DNS结果:', domain, recordType);
|
||||
|
||||
const mockData = {
|
||||
'A': [
|
||||
{ Type: 'A', Value: '192.168.1.1', TTL: 300 },
|
||||
{ Type: 'A', Value: '192.168.1.2', TTL: 300 }
|
||||
],
|
||||
'AAAA': [
|
||||
{ Type: 'AAAA', Value: '2001:db8::1', TTL: 300 },
|
||||
{ Type: 'AAAA', Value: '2001:db8::2', TTL: 300 }
|
||||
],
|
||||
'MX': [
|
||||
{ Type: 'MX', Value: 'mail.' + domain, Preference: 10, TTL: 3600 },
|
||||
{ Type: 'MX', Value: 'mail2.' + domain, Preference: 20, TTL: 3600 }
|
||||
],
|
||||
'NS': [
|
||||
{ Type: 'NS', Value: 'ns1.' + domain, TTL: 86400 },
|
||||
{ Type: 'NS', Value: 'ns2.' + domain, TTL: 86400 }
|
||||
],
|
||||
'CNAME': [
|
||||
{ Type: 'CNAME', Value: 'www.' + domain, TTL: 300 }
|
||||
],
|
||||
'TXT': [
|
||||
{ Type: 'TXT', Value: 'v=spf1 include:_spf.' + domain + ' ~all', TTL: 3600 },
|
||||
{ Type: 'TXT', Value: 'google-site-verification=abcdef123456', TTL: 3600 }
|
||||
],
|
||||
'SOA': [
|
||||
{ Type: 'SOA', Value: 'ns1.' + domain + ' admin.' + domain + ' 1 3600 1800 604800 86400', TTL: 86400 }
|
||||
]
|
||||
};
|
||||
|
||||
return mockData[recordType] || [
|
||||
{ Type: recordType, Value: 'No records found', TTL: '-' }
|
||||
];
|
||||
}
|
||||
|
||||
// 显示模拟查询结果
|
||||
function displayMockQueryResult(domain, recordType) {
|
||||
function displayQueryResult(result, domain) {
|
||||
const resultDiv = document.getElementById('query-result');
|
||||
if (!resultDiv) return;
|
||||
|
||||
// 显示提示信息
|
||||
resultDiv.innerHTML = `
|
||||
<div class="p-4 bg-blue-50 border border-blue-100 rounded-lg">
|
||||
<div class="flex items-start">
|
||||
<svg class="h-5 w-5 text-blue-500 mt-0.5 mr-2" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>
|
||||
<div>
|
||||
<p class="text-sm text-blue-700">这是一个DNS查询工具示例。输入域名并选择记录类型,然后点击查询按钮获取DNS记录信息。</p>
|
||||
// 显示结果容器
|
||||
resultDiv.classList.remove('hidden');
|
||||
|
||||
// 解析结果
|
||||
const status = result.blocked ? '被屏蔽' : '正常';
|
||||
const statusClass = result.blocked ? 'text-danger' : 'text-success';
|
||||
const blockType = result.blocked ? result.blockRuleType || '未知' : '正常';
|
||||
const blockRule = result.blocked ? result.blockRule || '未知' : '无';
|
||||
const blockSource = result.blocked ? result.blocksource || '未知' : '无';
|
||||
const timestamp = new Date(result.timestamp).toLocaleString();
|
||||
|
||||
// 更新结果显示
|
||||
document.getElementById('result-domain').textContent = domain;
|
||||
document.getElementById('result-status').innerHTML = `<span class="${statusClass}">${status}</span>`;
|
||||
document.getElementById('result-type').textContent = blockType;
|
||||
|
||||
// 检查是否存在屏蔽规则显示元素,如果不存在则创建
|
||||
let blockRuleElement = document.getElementById('result-block-rule');
|
||||
if (!blockRuleElement) {
|
||||
// 创建屏蔽规则显示区域
|
||||
const grid = resultDiv.querySelector('.grid');
|
||||
if (grid) {
|
||||
const newGridItem = document.createElement('div');
|
||||
newGridItem.className = 'bg-gray-50 p-4 rounded-lg';
|
||||
newGridItem.innerHTML = `
|
||||
<h4 class="text-sm font-medium text-gray-500 mb-2">屏蔽规则</h4>
|
||||
<p class="text-lg font-semibold" id="result-block-rule">-</p>
|
||||
`;
|
||||
grid.appendChild(newGridItem);
|
||||
blockRuleElement = document.getElementById('result-block-rule');
|
||||
}
|
||||
}
|
||||
|
||||
// 更新屏蔽规则显示
|
||||
if (blockRuleElement) {
|
||||
blockRuleElement.textContent = blockRule;
|
||||
}
|
||||
|
||||
// 检查是否存在屏蔽来源显示元素,如果不存在则创建
|
||||
let blockSourceElement = document.getElementById('result-block-source');
|
||||
if (!blockSourceElement) {
|
||||
// 创建屏蔽来源显示区域
|
||||
const grid = resultDiv.querySelector('.grid');
|
||||
if (grid) {
|
||||
const newGridItem = document.createElement('div');
|
||||
newGridItem.className = 'bg-gray-50 p-4 rounded-lg';
|
||||
newGridItem.innerHTML = `
|
||||
<h4 class="text-sm font-medium text-gray-500 mb-2">屏蔽来源</h4>
|
||||
<p class="text-lg font-semibold" id="result-block-source">-</p>
|
||||
`;
|
||||
grid.appendChild(newGridItem);
|
||||
blockSourceElement = document.getElementById('result-block-source');
|
||||
}
|
||||
}
|
||||
|
||||
// 更新屏蔽来源显示
|
||||
if (blockSourceElement) {
|
||||
blockSourceElement.textContent = blockSource;
|
||||
}
|
||||
|
||||
document.getElementById('result-time').textContent = timestamp;
|
||||
document.getElementById('result-details').textContent = JSON.stringify(result, null, 2);
|
||||
}
|
||||
|
||||
// 保存查询历史
|
||||
function saveQueryHistory(domain, result) {
|
||||
// 获取现有历史记录
|
||||
let history = JSON.parse(localStorage.getItem('dnsQueryHistory') || '[]');
|
||||
|
||||
// 创建历史记录项
|
||||
const historyItem = {
|
||||
domain: domain,
|
||||
timestamp: new Date().toISOString(),
|
||||
result: {
|
||||
blocked: result.blocked,
|
||||
blockRuleType: result.blockRuleType,
|
||||
blockRule: result.blockRule,
|
||||
blocksource: result.blocksource
|
||||
}
|
||||
};
|
||||
|
||||
// 添加到历史记录开头
|
||||
history.unshift(historyItem);
|
||||
|
||||
// 限制历史记录数量
|
||||
if (history.length > 20) {
|
||||
history = history.slice(0, 20);
|
||||
}
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('dnsQueryHistory', JSON.stringify(history));
|
||||
}
|
||||
|
||||
// 加载查询历史
|
||||
function loadQueryHistory() {
|
||||
const historyDiv = document.getElementById('query-history');
|
||||
if (!historyDiv) return;
|
||||
|
||||
// 获取历史记录
|
||||
const history = JSON.parse(localStorage.getItem('dnsQueryHistory') || '[]');
|
||||
|
||||
if (history.length === 0) {
|
||||
historyDiv.innerHTML = '<div class="text-center text-gray-500 py-4">暂无查询历史</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成历史记录HTML
|
||||
const historyHTML = history.map(item => {
|
||||
const statusClass = item.result.blocked ? 'text-danger' : 'text-success';
|
||||
const statusText = item.result.blocked ? '被屏蔽' : '正常';
|
||||
const blockType = item.result.blocked ? item.result.blockRuleType : '正常';
|
||||
const blockRule = item.result.blocked ? item.result.blockRule : '无';
|
||||
const blockSource = item.result.blocked ? item.result.blocksource : '无';
|
||||
const formattedTime = new Date(item.timestamp).toLocaleString();
|
||||
|
||||
return `
|
||||
<div class="flex flex-col md:flex-row justify-between items-start md:items-center p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="font-medium">${item.domain}</span>
|
||||
<span class="${statusClass} text-sm">${statusText}</span>
|
||||
<span class="text-xs text-gray-500">${blockType}</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-1">规则: ${blockRule}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">来源: ${blockSource}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">${formattedTime}</div>
|
||||
</div>
|
||||
<button class="mt-2 md:mt-0 px-3 py-1 bg-primary text-white text-sm rounded-md hover:bg-primary/90 transition-colors" onclick="requeryFromHistory('${item.domain}')">
|
||||
<i class="fa fa-refresh mr-1"></i>重新查询
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
historyDiv.innerHTML = historyHTML;
|
||||
}
|
||||
|
||||
// 从历史记录重新查询
|
||||
function requeryFromHistory(domain) {
|
||||
const domainInput = document.getElementById('dns-query-domain');
|
||||
if (domainInput) {
|
||||
domainInput.value = domain;
|
||||
handleDNSQuery();
|
||||
}
|
||||
}
|
||||
|
||||
// 清空查询历史
|
||||
function clearQueryHistory() {
|
||||
if (confirm('确定要清空所有查询历史吗?')) {
|
||||
localStorage.removeItem('dnsQueryHistory');
|
||||
loadQueryHistory();
|
||||
showSuccessMessage('查询历史已清空');
|
||||
}
|
||||
}
|
||||
|
||||
// 设置事件监听器
|
||||
function setupQueryEventListeners() {
|
||||
// 尝试多种可能的按钮ID
|
||||
const queryButtons = [
|
||||
document.getElementById('query-btn'),
|
||||
document.getElementById('query-button'),
|
||||
document.querySelector('button[type="submit"]'),
|
||||
...Array.from(document.querySelectorAll('button')).filter(btn =>
|
||||
btn.textContent && btn.textContent.includes('查询')
|
||||
)
|
||||
].filter(Boolean);
|
||||
// 查询按钮事件
|
||||
const queryBtn = document.getElementById('dns-query-btn');
|
||||
if (queryBtn) {
|
||||
queryBtn.addEventListener('click', handleDNSQuery);
|
||||
}
|
||||
|
||||
// 绑定查询按钮事件
|
||||
queryButtons.forEach(button => {
|
||||
console.log('绑定查询按钮事件:', button);
|
||||
button.addEventListener('click', handleDNSQuery);
|
||||
});
|
||||
|
||||
// 尝试多种可能的输入框ID
|
||||
const domainInputs = [
|
||||
document.getElementById('query-domain'),
|
||||
document.getElementById('domain-input'),
|
||||
document.querySelector('input[id*="domain"]')
|
||||
].filter(Boolean);
|
||||
|
||||
// 绑定回车键事件
|
||||
domainInputs.forEach(input => {
|
||||
console.log('绑定输入框回车事件:', input);
|
||||
input.addEventListener('keypress', (e) => {
|
||||
// 输入框回车键事件
|
||||
const domainInput = document.getElementById('dns-query-domain');
|
||||
if (domainInput) {
|
||||
domainInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleDNSQuery();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 添加示例域名按钮
|
||||
const querySection = document.querySelector('#dns-query-section, #query-section');
|
||||
if (querySection) {
|
||||
const exampleContainer = document.createElement('div');
|
||||
exampleContainer.className = 'mt-3';
|
||||
exampleContainer.innerHTML = `
|
||||
<p class="text-sm text-gray-500 mb-2">快速示例:</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button class="text-xs px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-full text-gray-700 transition-colors" onclick="setExampleQuery('example.com', 'A')">example.com (A)</button>
|
||||
<button class="text-xs px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-full text-gray-700 transition-colors" onclick="setExampleQuery('example.com', 'MX')">example.com (MX)</button>
|
||||
<button class="text-xs px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-full text-gray-700 transition-colors" onclick="setExampleQuery('google.com', 'NS')">google.com (NS)</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 找到输入框容器并插入示例按钮
|
||||
const inputContainer = domainInputs[0]?.parentElement;
|
||||
if (inputContainer && inputContainer.nextElementSibling) {
|
||||
inputContainer.parentNode.insertBefore(exampleContainer, inputContainer.nextElementSibling);
|
||||
} else if (querySection.lastChild) {
|
||||
querySection.appendChild(exampleContainer);
|
||||
}
|
||||
// 清空历史按钮事件
|
||||
const clearHistoryBtn = document.getElementById('clear-history-btn');
|
||||
if (clearHistoryBtn) {
|
||||
clearHistoryBtn.addEventListener('click', clearQueryHistory);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置示例查询
|
||||
function setExampleQuery(domain, recordType) {
|
||||
const domainInput = document.getElementById('query-domain') || document.getElementById('domain-input');
|
||||
const recordTypeSelect = document.getElementById('query-record-type') || document.getElementById('record-type');
|
||||
|
||||
if (domainInput) domainInput.value = domain;
|
||||
if (recordTypeSelect) recordTypeSelect.value = recordType;
|
||||
|
||||
// 自动执行查询
|
||||
handleDNSQuery();
|
||||
}
|
||||
|
||||
|
||||
// 显示成功消息
|
||||
function showSuccessMessage(message) {
|
||||
@@ -330,7 +247,7 @@ function showNotification(message, type = 'info') {
|
||||
|
||||
// 创建新通知
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform transition-transform duration-300 ease-in-out translate-y-0 opacity-0`;
|
||||
notification.className = `notification fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform transition-all duration-300 ease-in-out translate-y-0 opacity-0`;
|
||||
|
||||
// 设置通知样式
|
||||
if (type === 'success') {
|
||||
@@ -341,7 +258,13 @@ function showNotification(message, type = 'info') {
|
||||
notification.classList.add('bg-blue-500', 'text-white');
|
||||
}
|
||||
|
||||
notification.textContent = message;
|
||||
notification.innerHTML = `
|
||||
<div class="flex items-center space-x-2">
|
||||
<i class="fa ${type === 'success' ? 'fa-check-circle' : type === 'error' ? 'fa-exclamation-circle' : 'fa-info-circle'}"></i>
|
||||
<span>${message}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// 显示通知
|
||||
@@ -365,4 +288,14 @@ if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initQueryPage);
|
||||
} else {
|
||||
initQueryPage();
|
||||
}
|
||||
}
|
||||
|
||||
// 当切换到DNS查询页面时重新加载数据
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 监听hash变化,当切换到DNS查询页面时重新加载数据
|
||||
window.addEventListener('hashchange', () => {
|
||||
if (window.location.hash === '#query') {
|
||||
initQueryPage();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -263,12 +263,25 @@ function addGlowEffect() {
|
||||
|
||||
// 格式化数字
|
||||
function formatNumber(num) {
|
||||
// 显示完整数字的最大长度阈值
|
||||
const MAX_FULL_LENGTH = 5;
|
||||
|
||||
// 先获取完整数字字符串
|
||||
const fullNumStr = num.toString();
|
||||
|
||||
// 如果数字长度小于等于阈值,直接返回完整数字
|
||||
if (fullNumStr.length <= MAX_FULL_LENGTH) {
|
||||
return fullNumStr;
|
||||
}
|
||||
|
||||
// 否则使用缩写格式
|
||||
if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1) + 'M';
|
||||
} else if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1) + 'K';
|
||||
}
|
||||
return num.toString();
|
||||
|
||||
return fullNumStr;
|
||||
}
|
||||
|
||||
// 在DOM加载完成后初始化
|
||||
|
||||
1252
static/js/shield.js
1252
static/js/shield.js
File diff suppressed because it is too large
Load Diff
19
static/js/vendor/tailwind.js
vendored
Normal file
19
static/js/vendor/tailwind.js
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#165DFF',
|
||||
secondary: '#36CFFB',
|
||||
success: '#00B42A',
|
||||
warning: '#FF7D00',
|
||||
danger: '#F53F3F',
|
||||
info: '#86909C',
|
||||
dark: '#1D2129',
|
||||
light: '#F2F3F5',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
77
test/test_rule_matching.go
Normal file
77
test/test_rule_matching.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// testRuleMatching 测试DNS规则匹配功能
|
||||
func main() {
|
||||
// 定义命令行参数
|
||||
rulePtr := flag.String("rule", "||cntvwb.cn^", "规则字符串")
|
||||
testDomainPtr := flag.String("domain", "vdapprecv.app.cntvwb.cn", "测试域名")
|
||||
flag.Parse()
|
||||
|
||||
// 打印测试信息
|
||||
fmt.Printf("测试规则: %s\n", *rulePtr)
|
||||
fmt.Printf("测试域名: %s\n", *testDomainPtr)
|
||||
|
||||
// 发送HTTP请求到API端点来测试规则匹配
|
||||
fmt.Println("\n测试规则匹配功能...")
|
||||
cmd := exec.Command("curl", "-s", fmt.Sprintf("http://localhost:8080/api/shield/check?domain=%s&rule=%s", *testDomainPtr, *rulePtr))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
// 如果直接的API测试失败,尝试另一种方法
|
||||
fmt.Printf("直接测试失败: %v, %s\n", err, string(output))
|
||||
fmt.Println("尝试添加规则并测试...")
|
||||
testAddRuleAndCheck(*rulePtr, *testDomainPtr)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("测试结果: %s\n", string(output))
|
||||
|
||||
// 验证规则是否生效(模拟测试)
|
||||
if strings.Contains(*rulePtr, "||cntvwb.cn^") && strings.Contains(*testDomainPtr, "cntvwb.cn") {
|
||||
fmt.Println("\n验证结果:")
|
||||
if strings.Contains(*testDomainPtr, "cntvwb.cn") {
|
||||
fmt.Println("✅ 子域名匹配测试通过:||cntvwb.cn^ 应该阻止所有 cntvwb.cn 的子域名")
|
||||
} else {
|
||||
fmt.Println("❌ 子域名匹配测试失败")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// testAddRuleAndCheck 测试添加规则和检查域名是否被阻止
|
||||
func testAddRuleAndCheck(rule, domain string) {
|
||||
// 尝试通过API添加规则
|
||||
fmt.Printf("添加规则: %s\n", rule)
|
||||
cmd := exec.Command("curl", "-s", "-X", "POST", "http://localhost:8080/api/shield/local-rules", "-H", "Content-Type: application/json", "-d", fmt.Sprintf(`{\"rule\":\"%s\"}`, rule))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Printf("添加规则失败: %v, %s\n", err, string(output))
|
||||
// 尝试重新加载规则
|
||||
fmt.Println("尝试重新加载规则...")
|
||||
cmd = exec.Command("curl", "-s", "-X", "PUT", "http://localhost:8080/api/shield", "-H", "Content-Type: application/json", "-d", `{\"reload\":true}`)
|
||||
output, err = cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Printf("重新加载规则失败: %v, %s\n", err, string(output))
|
||||
} else {
|
||||
fmt.Printf("重新加载规则结果: %s\n", string(output))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("添加规则结果: %s\n", string(output))
|
||||
|
||||
// 测试域名是否被阻止
|
||||
fmt.Printf("测试域名 %s 是否被阻止...\n", domain)
|
||||
cmd = exec.Command("curl", "-s", fmt.Sprintf("http://localhost:8080/api/shield/check?domain=%s", domain))
|
||||
output, err = cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Printf("测试阻止失败: %v, %s\n", err, string(output))
|
||||
} else {
|
||||
fmt.Printf("阻止测试结果: %s\n", string(output))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user