实现日志功能

This commit is contained in:
Alex Yang
2025-11-30 02:25:36 +08:00
32 changed files with 6816 additions and 5666 deletions

View File

@@ -1,81 +0,0 @@
# DNS服务器项目介绍
## 项目概述
这是一个基于Go语言开发的高性能DNS服务器具备域名屏蔽、Hosts管理、统计分析和远程规则管理等功能。服务器支持通过Web界面进行管理配置同时能够自动更新和缓存远程规则列表。
## 技术架构
### 核心组件
1. DNS服务模块 ( `server.go` )
- 基于 github.com/miekg/dns 库实现高性能DNS查询处理
- 支持配置上游DNS服务器进行递归查询
- 实现域名屏蔽、统计数据收集等核心功能
2. 屏蔽管理系统 ( `manager.go` )
- 管理本地和远程屏蔽规则
- 支持规则缓存、自动更新和统计
- 实现域名和正则表达式规则的解析和匹配
3. HTTP控制台 ( `server.go` )
- 提供Web管理界面
- 实现REST API用于配置管理和数据查询
4. 配置管理 ( `config.go` )
- 定义配置结构和加载功能
- 支持JSON格式配置文件
## 主要功能特性
### 1. 域名屏蔽系统
- 支持本地规则文件和远程规则URL
- 多种屏蔽方式NXDOMAIN、refused、emptyIP、customIP
- 支持域名精确匹配和正则表达式匹配
- 远程规则自动缓存和更新机制
### 2. Hosts管理
- 支持自定义Hosts映射
- 提供Web界面管理Hosts条目
- 自动保存Hosts配置
### 3. 统计分析功能
- 记录屏蔽域名统计信息
- 记录解析域名统计信息
- 提供按小时统计的屏蔽数据
- 支持查询最常屏蔽和解析的域名
### 4. 远程规则管理
- 支持添加多个远程规则URL
- 自动定期更新远程规则
- 本地缓存机制确保规则可用性
- Web界面可视化管理
### 5. 管理界面
- 提供直观的Web控制台
- 支持查看服务器状态和统计信息
- 规则管理和配置修改
- DNS查询测试工具
## 项目结构
```
/root/dns/
├── config/          # 配置管理
├── data/            # 数据目录(包含缓存和统计)
   └── remote_rules/ # 远程规则缓存
├── dns/             # DNS服务器核心
├── http/            # HTTP控制台
├── logger/          # 日志系统
├── shield/          # 屏蔽规则管理
├── static/          # 静态Web文件
├── main.go          # 程序入口
└── config.json      # 配置文件
```
## 配置项说明
主要配置文件 `config.json` 包含以下部分:
- DNS配置 端口、上游DNS服务器、超时设置等
- HTTP配置 :控制台端口、主机绑定等
- 屏蔽配置 规则文件路径、远程规则URL、更新间隔等
- 日志配置 :日志文件路径、级别设置等
## 使用场景
1. 网络内容过滤(广告、恶意网站屏蔽)
2. 本地DNS缓存加速
3. 企业/家庭网络DNS管理
4. 开发测试环境DNS重定向
## 技术栈
- 语言 Go
- DNS库 github.com/miekg/dns
- 日志库 github.com/sirupsen/logrus
- Web前端 HTML/CSS/JavaScript
该DNS服务器具有高性能、功能全面、易于配置等特点适用于需要精确控制DNS查询结果的各种网络环境。

116
config.json Normal file
View 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
}
}

View File

@@ -29,11 +29,20 @@ type BlockedDomain struct {
LastSeen time.Time LastSeen time.Time
} }
// ClientStats 客户端统计
type ClientStats struct {
IP string
Count int64
LastSeen time.Time
}
// StatsData 用于持久化的统计数据结构 // StatsData 用于持久化的统计数据结构
type StatsData struct { type StatsData struct {
Stats *Stats `json:"stats"` Stats *Stats `json:"stats"`
BlockedDomains map[string]*BlockedDomain `json:"blockedDomains"` BlockedDomains map[string]*BlockedDomain `json:"blockedDomains"`
ResolvedDomains map[string]*BlockedDomain `json:"resolvedDomains"` ResolvedDomains map[string]*BlockedDomain `json:"resolvedDomains"`
ClientStats map[string]*ClientStats `json:"clientStats"`
HourlyStats map[string]int64 `json:"hourlyStats"` HourlyStats map[string]int64 `json:"hourlyStats"`
DailyStats map[string]int64 `json:"dailyStats"` DailyStats map[string]int64 `json:"dailyStats"`
MonthlyStats map[string]int64 `json:"monthlyStats"` MonthlyStats map[string]int64 `json:"monthlyStats"`
@@ -46,6 +55,7 @@ type Server struct {
shieldConfig *config.ShieldConfig shieldConfig *config.ShieldConfig
shieldManager *shield.ShieldManager shieldManager *shield.ShieldManager
server *dns.Server server *dns.Server
tcpServer *dns.Server
resolver *dns.Client resolver *dns.Client
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
@@ -55,6 +65,8 @@ type Server struct {
blockedDomains map[string]*BlockedDomain blockedDomains map[string]*BlockedDomain
resolvedDomainsMutex sync.RWMutex resolvedDomainsMutex sync.RWMutex
resolvedDomains map[string]*BlockedDomain // 用于记录解析的域名 resolvedDomains map[string]*BlockedDomain // 用于记录解析的域名
clientStatsMutex sync.RWMutex
clientStats map[string]*ClientStats // 用于记录客户端统计
hourlyStatsMutex sync.RWMutex hourlyStatsMutex sync.RWMutex
hourlyStats map[string]int64 // 按小时统计屏蔽数量 hourlyStats map[string]int64 // 按小时统计屏蔽数量
dailyStatsMutex sync.RWMutex dailyStatsMutex sync.RWMutex
@@ -64,6 +76,8 @@ type Server struct {
saveTicker *time.Ticker // 用于定时保存数据 saveTicker *time.Ticker // 用于定时保存数据
startTime time.Time // 服务器启动时间 startTime time.Time // 服务器启动时间
saveDone chan struct{} // 用于通知保存协程停止 saveDone chan struct{} // 用于通知保存协程停止
stopped bool // 服务器是否已经停止
stoppedMutex sync.Mutex // 保护stopped标志的互斥锁
} }
// Stats DNS服务器统计信息 // Stats DNS服务器统计信息
@@ -107,10 +121,12 @@ func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shie
}, },
blockedDomains: make(map[string]*BlockedDomain), blockedDomains: make(map[string]*BlockedDomain),
resolvedDomains: make(map[string]*BlockedDomain), resolvedDomains: make(map[string]*BlockedDomain),
clientStats: make(map[string]*ClientStats),
hourlyStats: make(map[string]int64), hourlyStats: make(map[string]int64),
dailyStats: make(map[string]int64), dailyStats: make(map[string]int64),
monthlyStats: make(map[string]int64), monthlyStats: make(map[string]int64),
saveDone: make(chan struct{}), saveDone: make(chan struct{}),
stopped: false, // 初始化为未停止状态
} }
// 加载已保存的统计数据 // 加载已保存的统计数据
@@ -122,14 +138,30 @@ func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shie
// Start 启动DNS服务器 // Start 启动DNS服务器
func (s *Server) Start() error { 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{ s.server = &dns.Server{
Addr: fmt.Sprintf(":%d", s.config.Port), Addr: fmt.Sprintf(":%d", s.config.Port),
Net: "udp", Net: "udp",
Handler: dns.HandlerFunc(s.handleDNSRequest), Handler: dns.HandlerFunc(s.handleDNSRequest),
} }
// 启动TCP服务器(用于大型响应) // 保存TCP服务器实例以便在Stop方法中关闭
tcpServer := &dns.Server{ s.tcpServer = &dns.Server{
Addr: fmt.Sprintf(":%d", s.config.Port), Addr: fmt.Sprintf(":%d", s.config.Port),
Net: "tcp", Net: "tcp",
Handler: dns.HandlerFunc(s.handleDNSRequest), Handler: dns.HandlerFunc(s.handleDNSRequest),
@@ -138,6 +170,9 @@ func (s *Server) Start() error {
// 启动CPU使用率监控 // 启动CPU使用率监控
go s.startCpuUsageMonitor() go s.startCpuUsageMonitor()
// 启动自动保存功能
go s.startAutoSave()
// 启动UDP服务 // 启动UDP服务
go func() { go func() {
logger.Info(fmt.Sprintf("DNS UDP服务器启动监听端口: %d", s.config.Port)) logger.Info(fmt.Sprintf("DNS UDP服务器启动监听端口: %d", s.config.Port))
@@ -150,7 +185,7 @@ func (s *Server) Start() error {
// 启动TCP服务 // 启动TCP服务
go func() { go func() {
logger.Info(fmt.Sprintf("DNS TCP服务器启动监听端口: %d", s.config.Port)) 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) logger.Error("DNS TCP服务器启动失败", "error", err)
s.cancel() s.cancel()
} }
@@ -163,6 +198,16 @@ func (s *Server) Start() error {
// Stop 停止DNS服务器 // Stop 停止DNS服务器
func (s *Server) Stop() { func (s *Server) Stop() {
// 检查服务器是否已经停止
s.stoppedMutex.Lock()
if s.stopped {
s.stoppedMutex.Unlock()
return // 服务器已经停止,直接返回
}
// 标记服务器为已停止状态
s.stopped = true
s.stoppedMutex.Unlock()
// 发送停止信号给保存协程 // 发送停止信号给保存协程
close(s.saveDone) close(s.saveDone)
@@ -174,6 +219,9 @@ func (s *Server) Stop() {
if s.server != nil { if s.server != nil {
s.server.Shutdown() s.server.Shutdown()
} }
if s.tcpServer != nil {
s.tcpServer.Shutdown()
}
logger.Info("DNS服务器已停止") logger.Info("DNS服务器已停止")
} }
@@ -195,6 +243,9 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
stats.SourceIPs[sourceIP] = true stats.SourceIPs[sourceIP] = true
}) })
// 更新客户端统计
s.updateClientStats(sourceIP)
// 只处理递归查询 // 只处理递归查询
if r.RecursionDesired == false { if r.RecursionDesired == false {
response := new(dns.Msg) response := new(dns.Msg)
@@ -315,11 +366,6 @@ func (s *Server) handleBlockedResponse(w dns.ResponseWriter, r *dns.Msg, domain
// 更新被屏蔽域名统计 // 更新被屏蔽域名统计
s.updateBlockedDomainStats(domain) s.updateBlockedDomainStats(domain)
// 更新总体统计
s.updateStats(func(stats *Stats) {
stats.Blocked++
})
response := new(dns.Msg) response := new(dns.Msg)
response.SetReply(r) response.SetReply(r)
response.RecursionAvailable = true response.RecursionAvailable = true
@@ -452,6 +498,23 @@ func (s *Server) updateBlockedDomainStats(domain string) {
s.monthlyStatsMutex.Unlock() 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 更新解析域名统计 // updateResolvedDomainStats 更新解析域名统计
func (s *Server) updateResolvedDomainStats(domain string) { func (s *Server) updateResolvedDomainStats(domain string) {
s.resolvedDomainsMutex.Lock() s.resolvedDomainsMutex.Lock()
@@ -582,6 +645,29 @@ func (s *Server) GetRecentBlockedDomains(limit int) []BlockedDomain {
return domains 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 获取每小时统计数据 // GetHourlyStats 获取每小时统计数据
func (s *Server) GetHourlyStats() map[string]int64 { func (s *Server) GetHourlyStats() map[string]int64 {
s.hourlyStatsMutex.RLock() s.hourlyStatsMutex.RLock()
@@ -680,6 +766,13 @@ func (s *Server) loadStatsData() {
} }
s.monthlyStatsMutex.Unlock() s.monthlyStatsMutex.Unlock()
// 加载客户端统计数据
s.clientStatsMutex.Lock()
if statsData.ClientStats != nil {
s.clientStats = statsData.ClientStats
}
s.clientStatsMutex.Unlock()
logger.Info("统计数据加载成功") logger.Info("统计数据加载成功")
} }
@@ -689,11 +782,18 @@ func (s *Server) saveStatsData() {
return return
} }
// 创建数据目录 // 获取绝对路径以避免工作目录问题
statsDir := filepath.Dir(s.config.StatsFile) statsFilePath, err := filepath.Abs(s.config.StatsFile)
err := os.MkdirAll(statsDir, 0755)
if err != nil { 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 return
} }
@@ -739,6 +839,14 @@ func (s *Server) saveStatsData() {
} }
s.monthlyStatsMutex.RUnlock() 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, "", " ") jsonData, err := json.MarshalIndent(statsData, "", " ")
if err != nil { 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 { if err != nil {
logger.Error("保存统计数据到文件失败", "error", err) logger.Error("保存统计数据到文件失败", "file", statsFilePath, "error", err)
return return
} }
logger.Info("统计数据保存成功") logger.Info("统计数据保存成功", "file", statsFilePath)
} }
// startCpuUsageMonitor 启动CPU使用率监控 // startCpuUsageMonitor 启动CPU使用率监控

14
go.mod
View File

@@ -8,17 +8,31 @@ require (
github.com/gorilla/websocket v1.5.1 github.com/gorilla/websocket v1.5.1
github.com/miekg/dns v1.1.68 github.com/miekg/dns v1.1.68
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/swaggo/http-swagger v1.2.0
) )
// 清理不需要的依赖 // 清理不需要的依赖
// 之前的go.sum可能包含lumberjack的记录但现在已经不再使用 // 之前的go.sum可能包含lumberjack的记录但现在已经不再使用
require ( 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/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/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/mod v0.25.0 // indirect
golang.org/x/net v0.42.0 // indirect golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.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 golang.org/x/tools v0.34.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
) )

94
go.sum
View File

@@ -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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/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 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= 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 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
github.com/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 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/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.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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 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 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= 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 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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.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 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= 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 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-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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,6 +0,0 @@
# DNS Server Hosts File
# Generated by DNS Server
::1 localhost
ad.qq.com 127.0.0.1
ad.qq.com 0.0.0.0

View File

@@ -10,11 +10,12 @@ import (
"sync" "sync"
"time" "time"
"github.com/gorilla/websocket"
"dns-server/config" "dns-server/config"
"dns-server/dns" "dns-server/dns"
"dns-server/logger" "dns-server/logger"
"dns-server/shield" "dns-server/shield"
"github.com/gorilla/websocket"
) )
// Server HTTP控制台服务器 // Server HTTP控制台服务器
@@ -63,16 +64,43 @@ func (s *Server) Start() error {
// API路由 // API路由
if s.config.EnableAPI { 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/stats", s.handleStats)
mux.HandleFunc("/api/shield", s.handleShield) 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/hosts", s.handleShieldHosts)
mux.HandleFunc("/api/shield/blacklists", s.handleShieldBlacklists) mux.HandleFunc("/api/shield/blacklists", s.handleShieldBlacklists)
mux.HandleFunc("/api/query", s.handleQuery) mux.HandleFunc("/api/query", s.handleQuery)
mux.HandleFunc("/api/status", s.handleStatus) mux.HandleFunc("/api/status", s.handleStatus)
mux.HandleFunc("/api/config", s.handleConfig) mux.HandleFunc("/api/config", s.handleConfig)
mux.HandleFunc("/api/config/restart", s.handleRestart)
// 添加统计相关接口 // 添加统计相关接口
mux.HandleFunc("/api/top-blocked", s.handleTopBlockedDomains) mux.HandleFunc("/api/top-blocked", s.handleTopBlockedDomains)
mux.HandleFunc("/api/top-resolved", s.handleTopResolvedDomains) 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/recent-blocked", s.handleRecentBlockedDomains)
mux.HandleFunc("/api/hourly-stats", s.handleHourlyStats) mux.HandleFunc("/api/hourly-stats", s.handleHourlyStats)
mux.HandleFunc("/api/daily-stats", s.handleDailyStats) mux.HandleFunc("/api/daily-stats", s.handleDailyStats)
@@ -80,10 +108,22 @@ func (s *Server) Start() error {
mux.HandleFunc("/api/query/type", s.handleQueryTypeStats) mux.HandleFunc("/api/query/type", s.handleQueryTypeStats)
// WebSocket端点 // WebSocket端点
mux.HandleFunc("/ws/stats", s.handleWebSocketStats) mux.HandleFunc("/ws/stats", s.handleWebSocketStats)
// 将/api/下的静态文件服务指向static/api目录放在最后以避免覆盖API端点
apiFileServer := http.FileServer(http.Dir("./static/api"))
mux.Handle("/api/", http.StripPrefix("/api", apiFileServer))
} }
// 静态文件服务(可后续添加前端界面) // 自定义静态文件服务处理器用于禁用浏览器缓存放在API路由之后
mux.Handle("/", http.FileServer(http.Dir("./static"))) 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{ s.server = &http.Server{
Addr: fmt.Sprintf("%s:%d", s.config.Host, s.config.Port), Addr: fmt.Sprintf("%s:%d", s.config.Host, s.config.Port),
@@ -105,6 +145,14 @@ func (s *Server) Stop() {
} }
// handleStats 处理统计信息请求 // 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) { func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
@@ -512,13 +560,99 @@ func (s *Server) handleQueryTypeStats(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(result) 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) { func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
// 返回屏蔽规则的基本配置信息和统计数据,不返回完整规则列表 // 默认处理逻辑
switch r.Method { switch r.Method {
case http.MethodGet: case http.MethodGet:
// 检查是否需要返回完整规则列表
if r.URL.Query().Get("all") == "true" {
// 返回完整规则数据
rules := s.shieldManager.GetRules()
json.NewEncoder(w).Encode(rules)
return
}
// 获取规则统计信息 // 获取规则统计信息
stats := s.shieldManager.GetStats() stats := s.shieldManager.GetStats()
shieldInfo := map[string]interface{}{ shieldInfo := map[string]interface{}{
@@ -533,6 +667,8 @@ func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) {
} }
json.NewEncoder(w).Encode(shieldInfo) json.NewEncoder(w).Encode(shieldInfo)
return return
}
switch r.Method {
case http.MethodPost: case http.MethodPost:
// 添加屏蔽规则 // 添加屏蔽规则
var req struct { 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) { func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@@ -601,7 +752,7 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request)
} }
if targetURLOrName == "" { if targetURLOrName == "" {
http.Error(w, "黑名单标识不能为空", http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "黑名单标识不能为空"})
return return
} }
@@ -616,7 +767,7 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request)
} }
if targetIndex == -1 { if targetIndex == -1 {
http.Error(w, "黑名单不存在", http.StatusNotFound) json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "黑名单不存在"})
return return
} }
@@ -624,6 +775,14 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request)
blacklists[targetIndex].LastUpdateTime = time.Now().Format(time.RFC3339) blacklists[targetIndex].LastUpdateTime = time.Now().Format(time.RFC3339)
// 保存更新后的黑名单列表 // 保存更新后的黑名单列表
s.shieldManager.UpdateBlacklist(blacklists) 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() s.shieldManager.LoadRules()
@@ -646,6 +805,14 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request)
} }
s.shieldManager.UpdateBlacklist(newBlacklists) 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"}) json.NewEncoder(w).Encode(map[string]string{"status": "success"})
return 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 { 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 return
} }
if req.Name == "" || req.URL == "" { 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 return
} }
@@ -679,11 +846,17 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request)
// 检查是否已存在 // 检查是否已存在
for _, list := range blacklists { for _, list := range blacklists {
if list.URL == req.URL { if list.URL == req.URL {
http.Error(w, "黑名单URL已存在", http.StatusConflict) json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "黑名单URL已存在"})
return return
} }
} }
// 检查URL是否存在且可访问
if !checkURLExists(req.URL) {
json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "URL不存在或无法访问"})
return
}
// 添加新黑名单 // 添加新黑名单
newEntry := config.BlacklistEntry{ newEntry := config.BlacklistEntry{
Name: req.Name, Name: req.Name,
@@ -693,6 +866,14 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request)
blacklists = append(blacklists, newEntry) blacklists = append(blacklists, newEntry)
s.shieldManager.UpdateBlacklist(blacklists) 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() 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"}) json.NewEncoder(w).Encode(map[string]string{"status": "success"})
case http.MethodPut: case http.MethodPut:
// 更新所有远程黑名单 // 更新黑名单列表(包括启用/禁用状态)
blacklists := s.shieldManager.GetBlacklists() var updatedBlacklists []struct {
for i := range blacklists { Name string `json:"Name" json:"name"`
// 更新每个黑名单的时间戳 URL string `json:"URL" json:"url"`
blacklists[i].LastUpdateTime = time.Now().Format(time.RFC3339) 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() s.shieldManager.LoadRules()
json.NewEncoder(w).Encode(map[string]string{"status": "success"}) json.NewEncoder(w).Encode(map[string]string{"status": "success"})
default: default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "Method not allowed"})
} }
} }
@@ -836,7 +1043,7 @@ func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
"avgResponseTime": stats.AvgResponseTime, "avgResponseTime": stats.AvgResponseTime,
"activeIPs": len(stats.SourceIPs), "activeIPs": len(stats.SourceIPs),
"startTime": serverStartTime, "startTime": serverStartTime,
"uptime": uptime, "uptime": uptime.Milliseconds(), // 转换为毫秒数,方便前端处理
"cpuUsage": stats.CpuUsage, "cpuUsage": stats.CpuUsage,
"timestamp": time.Now(), "timestamp": time.Now(),
} }
@@ -991,3 +1198,55 @@ func isValidIP(ip string) bool {
} }
return true 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

File diff suppressed because it is too large Load Diff

144
main.go
View File

@@ -1,3 +1,14 @@
// DNS Server API
// @title DNS Server API
// @version 1.0
// @description DNS服务器API文档
// @termsOfService http://swagger.io/terms/
// @contact.name API Support
// @contact.email support@example.com
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host localhost:8080
// @BasePath /api
package main package main
import ( import (
@@ -6,6 +17,7 @@ import (
"log" "log"
"os" "os"
"os/signal" "os/signal"
"path/filepath"
"syscall" "syscall"
"dns-server/config" "dns-server/config"
@@ -15,6 +27,122 @@ import (
"dns-server/shield" "dns-server/shield"
) )
// createDefaultConfig 创建默认配置文件
func createDefaultConfig(configFile string) error {
// 默认配置内容
defaultConfig := `{
"dns": {
"port": 53,
"upstreamDNS": [
"223.5.5.5:53",
"223.6.6.6:53"
],
"timeout": 5000,
"statsFile": "./data/stats.json",
"saveInterval": 300
},
"http": {
"port": 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() { func main() {
// 命令行参数解析 // 命令行参数解析
var configFile string var configFile string
@@ -32,6 +160,15 @@ func main() {
os.Exit(0) 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 cfg *config.Config
var err error var err error
@@ -40,6 +177,13 @@ func main() {
log.Fatalf("加载配置失败: %v", err) log.Fatalf("加载配置失败: %v", err)
} }
// 创建所需的文件和文件夹
log.Println("正在创建所需的文件和文件夹...")
if err := createRequiredFiles(cfg); err != nil {
log.Fatalf("创建所需文件和文件夹失败: %v", err)
}
log.Println("所需文件和文件夹创建成功")
// 初始化日志系统 // 初始化日志系统
if err := logger.InitLogger(cfg.Log.File, cfg.Log.Level, 0, 0, 0); err != nil { if err := logger.InitLogger(cfg.Log.File, cfg.Log.Level, 0, 0, 0); err != nil {
log.Fatalf("初始化日志系统失败: %v", err) log.Fatalf("初始化日志系统失败: %v", err)

View File

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

View File

@@ -19,12 +19,6 @@ import (
"dns-server/logger" "dns-server/logger"
) )
// regexRule 正则规则结构,包含编译后的表达式和原始字符串
type regexRule struct {
pattern *regexp.Regexp
original string
}
// ShieldStatsData 用于持久化的Shield统计数据 // ShieldStatsData 用于持久化的Shield统计数据
type ShieldStatsData struct { type ShieldStatsData struct {
BlockedDomainsCount map[string]int `json:"blockedDomainsCount"` BlockedDomainsCount map[string]int `json:"blockedDomainsCount"`
@@ -32,11 +26,25 @@ type ShieldStatsData struct {
LastSaved time.Time `json:"lastSaved"` LastSaved time.Time `json:"lastSaved"`
} }
// regexRule 正则规则结构,包含编译后的表达式和原始字符串
type regexRule struct {
pattern *regexp.Regexp
original string
isLocal bool // 是否为本地规则
source string // 规则来源
}
// ShieldManager 屏蔽管理器 // ShieldManager 屏蔽管理器
type ShieldManager struct { type ShieldManager struct {
config *config.ShieldConfig config *config.ShieldConfig
domainRules map[string]bool domainRules map[string]bool
domainExceptions map[string]bool domainExceptions map[string]bool
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 regexRules []regexRule
regexExceptions []regexRule regexExceptions []regexRule
hostsMap map[string]string hostsMap map[string]string
@@ -57,6 +65,12 @@ func NewShieldManager(config *config.ShieldConfig) *ShieldManager {
config: config, config: config,
domainRules: make(map[string]bool), domainRules: make(map[string]bool),
domainExceptions: make(map[string]bool), domainExceptions: make(map[string]bool),
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{}, regexRules: []regexRule{},
regexExceptions: []regexRule{}, regexExceptions: []regexRule{},
hostsMap: make(map[string]string), hostsMap: make(map[string]string),
@@ -82,6 +96,12 @@ func (m *ShieldManager) LoadRules() error {
// 清空现有规则 // 清空现有规则
m.domainRules = make(map[string]bool) m.domainRules = make(map[string]bool)
m.domainExceptions = make(map[string]bool) m.domainExceptions = make(map[string]bool)
m.domainRulesIsLocal = make(map[string]bool)
m.domainExceptionsIsLocal = make(map[string]bool)
m.domainRulesSource = make(map[string]string)
m.domainExceptionsSource = make(map[string]string)
m.domainRulesOriginal = make(map[string]string)
m.domainExceptionsOriginal = make(map[string]string)
m.regexRules = []regexRule{} m.regexRules = []regexRule{}
m.regexExceptions = []regexRule{} m.regexExceptions = []regexRule{}
m.hostsMap = make(map[string]string) m.hostsMap = make(map[string]string)
@@ -134,7 +154,7 @@ func (m *ShieldManager) loadLocalRules() error {
if line == "" || strings.HasPrefix(line, "#") { if line == "" || strings.HasPrefix(line, "#") {
continue continue
} }
m.parseRule(line) m.parseRule(line, true, "本地规则") // 本地规则isLocal=true来源为"本地规则"
} }
// 更新本地规则计数 // 更新本地规则计数
@@ -191,7 +211,7 @@ func (m *ShieldManager) fetchRemoteRules(url string) error {
// 尝试从缓存加载 // 尝试从缓存加载
hasLoadedFromCache := false hasLoadedFromCache := false
if !m.shouldUpdateCache(cacheFile) { if !m.shouldUpdateCache(cacheFile) {
if err := m.loadCachedRules(cacheFile); err == nil { if err := m.loadCachedRules(cacheFile, url); err == nil {
logger.Info("从缓存加载远程规则", "url", url) logger.Info("从缓存加载远程规则", "url", url)
hasLoadedFromCache = true hasLoadedFromCache = true
} }
@@ -236,14 +256,14 @@ func (m *ShieldManager) fetchRemoteRules(url string) error {
if line == "" || strings.HasPrefix(line, "#") { if line == "" || strings.HasPrefix(line, "#") {
continue continue
} }
m.parseRule(line) m.parseRule(line, false, url) // 远程规则isLocal=false来源为URL
} }
return nil return nil
} }
// loadCachedRules 从缓存文件加载规则 // loadCachedRules 从缓存文件加载规则
func (m *ShieldManager) loadCachedRules(filePath string) error { func (m *ShieldManager) loadCachedRules(filePath string, source string) error {
file, err := os.Open(filePath) file, err := os.Open(filePath)
if err != nil { if err != nil {
return err return err
@@ -265,7 +285,7 @@ func (m *ShieldManager) loadCachedRules(filePath string) error {
if line == "" || strings.HasPrefix(line, "#") { if line == "" || strings.HasPrefix(line, "#") {
continue continue
} }
m.parseRule(line) m.parseRule(line, false, source) // 远程规则isLocal=false来源为URL
} }
// 更新远程规则计数 // 更新远程规则计数
@@ -318,7 +338,10 @@ func (m *ShieldManager) loadHosts() error {
} }
// parseRule 解析规则行 // parseRule 解析规则行
func (m *ShieldManager) parseRule(line string) { func (m *ShieldManager) parseRule(line string, isLocal bool, source string) {
// 保存原始规则用于后续使用
originalLine := line
// 处理注释 // 处理注释
if strings.HasPrefix(line, "!") || strings.HasPrefix(line, "#") || line == "" { if strings.HasPrefix(line, "!") || strings.HasPrefix(line, "#") || line == "" {
return return
@@ -343,12 +366,12 @@ func (m *ShieldManager) parseRule(line string) {
case strings.HasPrefix(line, "||") && strings.HasSuffix(line, "^"): case strings.HasPrefix(line, "||") && strings.HasSuffix(line, "^"):
// AdGuardHome域名规则格式: ||example.com^ // AdGuardHome域名规则格式: ||example.com^
domain := strings.TrimSuffix(strings.TrimPrefix(line, "||"), "^") domain := strings.TrimSuffix(strings.TrimPrefix(line, "||"), "^")
m.addDomainRule(domain, !isException) m.addDomainRule(domain, !isException, isLocal, source, originalLine)
case strings.HasPrefix(line, "||"): case strings.HasPrefix(line, "||"):
// 精确域名匹配规则 // 精确域名匹配规则
domain := strings.TrimPrefix(line, "||") domain := strings.TrimPrefix(line, "||")
m.addDomainRule(domain, !isException) m.addDomainRule(domain, !isException, isLocal, source, originalLine)
case strings.HasPrefix(line, "*"): case strings.HasPrefix(line, "*"):
// 通配符规则,转换为正则表达式 // 通配符规则,转换为正则表达式
@@ -356,15 +379,17 @@ func (m *ShieldManager) parseRule(line string) {
pattern = "^" + pattern + "$" pattern = "^" + pattern + "$"
if re, err := regexp.Compile(pattern); err == nil { if re, err := regexp.Compile(pattern); err == nil {
// 保存原始规则字符串 // 保存原始规则字符串
m.addRegexRule(re, line, !isException) m.addRegexRule(re, originalLine, !isException, isLocal, source)
} }
case strings.HasPrefix(line, "/") && strings.HasSuffix(line, "/"): case strings.HasPrefix(line, "/") && strings.HasSuffix(line, "/"):
// 正则表达式规则 // 正则表达式匹配规则:/regex/ 格式,不区分大小写
pattern := strings.TrimPrefix(strings.TrimSuffix(line, "/"), "/") pattern := strings.TrimPrefix(strings.TrimSuffix(line, "/"), "/")
if re, err := regexp.Compile(pattern); err == nil { // 编译为不区分大小写的正则表达式,确保能匹配域名中任意位置
// 对于像 /domain/ 这样的规则,应该匹配包含 domain 字符串的任何域名
if re, err := regexp.Compile("(?i).*" + regexp.QuoteMeta(pattern) + ".*"); err == nil {
// 保存原始规则字符串 // 保存原始规则字符串
m.addRegexRule(re, line, !isException) m.addRegexRule(re, originalLine, !isException, isLocal, source)
} }
case strings.HasPrefix(line, "|") && strings.HasSuffix(line, "|"): case strings.HasPrefix(line, "|") && strings.HasSuffix(line, "|"):
@@ -373,7 +398,7 @@ func (m *ShieldManager) parseRule(line string) {
// 将URL模式转换为正则表达式 // 将URL模式转换为正则表达式
pattern := "^" + regexp.QuoteMeta(urlPattern) + "$" pattern := "^" + regexp.QuoteMeta(urlPattern) + "$"
if re, err := regexp.Compile(pattern); err == nil { if re, err := regexp.Compile(pattern); err == nil {
m.addRegexRule(re, line, !isException) m.addRegexRule(re, originalLine, !isException, isLocal, source)
} }
case strings.HasPrefix(line, "|"): case strings.HasPrefix(line, "|"):
@@ -381,7 +406,7 @@ func (m *ShieldManager) parseRule(line string) {
urlPattern := strings.TrimPrefix(line, "|") urlPattern := strings.TrimPrefix(line, "|")
pattern := "^" + regexp.QuoteMeta(urlPattern) pattern := "^" + regexp.QuoteMeta(urlPattern)
if re, err := regexp.Compile(pattern); err == nil { if re, err := regexp.Compile(pattern); err == nil {
m.addRegexRule(re, line, !isException) m.addRegexRule(re, originalLine, !isException, isLocal, source)
} }
case strings.HasSuffix(line, "|"): case strings.HasSuffix(line, "|"):
@@ -389,12 +414,12 @@ func (m *ShieldManager) parseRule(line string) {
urlPattern := strings.TrimSuffix(line, "|") urlPattern := strings.TrimSuffix(line, "|")
pattern := regexp.QuoteMeta(urlPattern) + "$" pattern := regexp.QuoteMeta(urlPattern) + "$"
if re, err := regexp.Compile(pattern); err == nil { if re, err := regexp.Compile(pattern); err == nil {
m.addRegexRule(re, line, !isException) m.addRegexRule(re, originalLine, !isException, isLocal, source)
} }
default: default:
// 默认作为普通域名规则 // 默认作为普通域名规则
m.addDomainRule(line, !isException) m.addDomainRule(line, !isException, isLocal, source, originalLine)
} }
} }
@@ -418,42 +443,65 @@ func (m *ShieldManager) parseRuleOptions(optionsStr string) map[string]string {
} }
// addDomainRule 添加域名规则,支持是否为阻止规则 // addDomainRule 添加域名规则,支持是否为阻止规则
func (m *ShieldManager) addDomainRule(domain string, block bool) { func (m *ShieldManager) addDomainRule(domain string, block bool, isLocal bool, source string, original string) {
if block { if block {
// 如果是远程规则,检查是否已经存在本地规则,如果存在则不覆盖
if !isLocal {
if _, exists := m.domainRulesIsLocal[domain]; exists && m.domainRulesIsLocal[domain] {
// 已经存在本地规则,不覆盖
return
}
}
m.domainRules[domain] = true m.domainRules[domain] = true
// 添加所有子域名的匹配支持 m.domainRulesIsLocal[domain] = isLocal
parts := strings.Split(domain, ".") m.domainRulesSource[domain] = source
if len(parts) > 1 { m.domainRulesOriginal[domain] = original
// 为二级域名和顶级域名添加规则
for i := 0; i < len(parts)-1; i++ {
subdomain := strings.Join(parts[i:], ".")
m.domainRules[subdomain] = true
}
}
} else { } else {
// 添加到排除规则 // 添加到排除规则
// 如果是远程规则,检查是否已经存在本地规则,如果存在则不覆盖
if !isLocal {
if _, exists := m.domainExceptionsIsLocal[domain]; exists && m.domainExceptionsIsLocal[domain] {
// 已经存在本地规则,不覆盖
return
}
}
m.domainExceptions[domain] = true m.domainExceptions[domain] = true
// 为子域名也添加排除规则 m.domainExceptionsIsLocal[domain] = isLocal
parts := strings.Split(domain, ".") m.domainExceptionsSource[domain] = source
if len(parts) > 1 { m.domainExceptionsOriginal[domain] = original
for i := 0; i < len(parts)-1; i++ {
subdomain := strings.Join(parts[i:], ".")
m.domainExceptions[subdomain] = true
}
}
} }
} }
// addRegexRule 添加正则表达式规则,支持是否为阻止规则 // addRegexRule 添加正则表达式规则,支持是否为阻止规则
func (m *ShieldManager) addRegexRule(re *regexp.Regexp, original string, block bool) { func (m *ShieldManager) addRegexRule(re *regexp.Regexp, original string, block bool, isLocal bool, source string) {
rule := regexRule{ rule := regexRule{
pattern: re, pattern: re,
original: original, original: original,
isLocal: isLocal,
source: source,
} }
if block { if block {
// 如果是远程规则,检查是否已经存在相同的本地规则,如果存在则不添加
if !isLocal {
for _, existingRule := range m.regexRules {
if existingRule.original == original && existingRule.isLocal {
// 已经存在相同的本地规则,不添加
return
}
}
}
m.regexRules = append(m.regexRules, rule) m.regexRules = append(m.regexRules, rule)
} else { } else {
// 添加到排除规则 // 添加到排除规则
// 如果是远程规则,检查是否已经存在相同的本地规则,如果存在则不添加
if !isLocal {
for _, existingRule := range m.regexExceptions {
if existingRule.original == original && existingRule.isLocal {
// 已经存在相同的本地规则,不添加
return
}
}
}
m.regexExceptions = append(m.regexExceptions, rule) m.regexExceptions = append(m.regexExceptions, rule)
} }
} }
@@ -475,6 +523,7 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
"blocked": false, "blocked": false,
"blockRule": "", "blockRule": "",
"blockRuleType": "", "blockRuleType": "",
"blocksource": "",
"excluded": false, "excluded": false,
"excludeRule": "", "excludeRule": "",
"excludeRuleType": "", "excludeRuleType": "",
@@ -491,8 +540,9 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
// 检查域名排除规则 // 检查域名排除规则
if m.domainExceptions[domain] { if m.domainExceptions[domain] {
result["excluded"] = true result["excluded"] = true
result["excludeRule"] = domain result["excludeRule"] = m.domainExceptionsOriginal[domain]
result["excludeRuleType"] = "exact_domain" result["excludeRuleType"] = "exact_domain"
result["blocksource"] = m.domainExceptionsSource[domain]
return result return result
} }
@@ -502,8 +552,9 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
subdomain := strings.Join(parts[i:], ".") subdomain := strings.Join(parts[i:], ".")
if m.domainExceptions[subdomain] { if m.domainExceptions[subdomain] {
result["excluded"] = true result["excluded"] = true
result["excludeRule"] = subdomain result["excludeRule"] = m.domainExceptionsOriginal[subdomain]
result["excludeRuleType"] = "subdomain" result["excludeRuleType"] = "subdomain"
result["blocksource"] = m.domainExceptionsSource[subdomain]
return result return result
} }
} }
@@ -514,16 +565,18 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
result["excluded"] = true result["excluded"] = true
result["excludeRule"] = re.original result["excludeRule"] = re.original
result["excludeRuleType"] = "regex" result["excludeRuleType"] = "regex"
result["blocksource"] = re.source
return result return result
} }
} }
// 检查阻止规则 // 检查阻止规则 - 先检查精确域名匹配,再检查子域名匹配
// 检查精确域名匹配 // 检查精确域名匹配
if m.domainRules[domain] { if m.domainRules[domain] {
result["blocked"] = true result["blocked"] = true
result["blockRule"] = domain result["blockRule"] = m.domainRulesOriginal[domain]
result["blockRuleType"] = "exact_domain" result["blockRuleType"] = "exact_domain"
result["blocksource"] = m.domainRulesSource[domain]
return result return result
} }
@@ -533,8 +586,9 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
subdomain := strings.Join(parts[i:], ".") subdomain := strings.Join(parts[i:], ".")
if m.domainRules[subdomain] { if m.domainRules[subdomain] {
result["blocked"] = true result["blocked"] = true
result["blockRule"] = subdomain result["blockRule"] = m.domainRulesOriginal[subdomain]
result["blockRuleType"] = "subdomain" result["blockRuleType"] = "subdomain"
result["blocksource"] = m.domainRulesSource[subdomain]
return result return result
} }
} }
@@ -545,6 +599,7 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
result["blocked"] = true result["blocked"] = true
result["blockRule"] = re.original result["blockRule"] = re.original
result["blockRuleType"] = "regex" result["blockRuleType"] = "regex"
result["blocksource"] = re.source
return result return result
} }
} }
@@ -667,13 +722,13 @@ func (m *ShieldManager) GetHostsIP(domain string) (string, bool) {
return ip, exists return ip, exists
} }
// AddRule 添加屏蔽规则 // AddRule 添加屏蔽规则,用户添加的规则是本地规则
func (m *ShieldManager) AddRule(rule string) error { func (m *ShieldManager) AddRule(rule string) error {
m.rulesMutex.Lock() m.rulesMutex.Lock()
defer m.rulesMutex.Unlock() defer m.rulesMutex.Unlock()
// 解析并添加规则到内存 // 解析并添加规则到内存isLocal=true表示本地规则来源为"本地规则"
m.parseRule(rule) m.parseRule(rule, true, "本地规则")
// 持久化保存规则到文件 // 持久化保存规则到文件
if m.config.LocalRulesFile != "" { if m.config.LocalRulesFile != "" {
@@ -724,6 +779,8 @@ func (m *ShieldManager) RemoveRule(rule string) error {
domain := strings.TrimPrefix(format, "@@||") domain := strings.TrimPrefix(format, "@@||")
if _, exists := m.domainExceptions[domain]; exists { if _, exists := m.domainExceptions[domain]; exists {
delete(m.domainExceptions, domain) delete(m.domainExceptions, domain)
delete(m.domainExceptionsIsLocal, domain)
delete(m.domainExceptionsSource, domain)
removed = true removed = true
break break
} }
@@ -731,19 +788,28 @@ func (m *ShieldManager) RemoveRule(rule string) error {
// 尝试删除域名规则 // 尝试删除域名规则
domain := strings.TrimPrefix(format, "||") domain := strings.TrimPrefix(format, "||")
if _, exists := m.domainRules[domain]; exists { if _, exists := m.domainRules[domain]; exists {
// 删除主域名规则
delete(m.domainRules, domain) delete(m.domainRules, domain)
delete(m.domainRulesIsLocal, domain)
delete(m.domainRulesSource, domain)
removed = true removed = true
break break
} }
} else { } else {
// 尝试直接作为域名删除 // 尝试直接作为域名删除
if _, exists := m.domainRules[format]; exists { if _, exists := m.domainRules[format]; exists {
// 删除主域名规则
delete(m.domainRules, format) delete(m.domainRules, format)
delete(m.domainRulesIsLocal, format)
delete(m.domainRulesSource, format)
removed = true removed = true
break break
} }
if _, exists := m.domainExceptions[format]; exists { if _, exists := m.domainExceptions[format]; exists {
// 删除主排除规则
delete(m.domainExceptions, format) delete(m.domainExceptions, format)
delete(m.domainExceptionsIsLocal, format)
delete(m.domainExceptionsSource, format)
removed = true removed = true
break break
} }
@@ -752,12 +818,10 @@ func (m *ShieldManager) RemoveRule(rule string) error {
// 处理正则表达式规则 // 处理正则表达式规则
if !removed && strings.HasPrefix(cleanRule, "/") && strings.HasSuffix(cleanRule, "/") { if !removed && strings.HasPrefix(cleanRule, "/") && strings.HasSuffix(cleanRule, "/") {
pattern := strings.TrimPrefix(strings.TrimSuffix(cleanRule, "/"), "/")
// 检查是否在正则表达式规则中 // 检查是否在正则表达式规则中
newRegexRules := []regexRule{} newRegexRules := []regexRule{}
for _, re := range m.regexRules { for _, re := range m.regexRules {
if re.pattern.String() != pattern { if re.original != rule && re.original != cleanRule {
newRegexRules = append(newRegexRules, re) newRegexRules = append(newRegexRules, re)
} else { } else {
removed = true removed = true
@@ -769,7 +833,7 @@ func (m *ShieldManager) RemoveRule(rule string) error {
if !removed { if !removed {
newRegexExceptions := []regexRule{} newRegexExceptions := []regexRule{}
for _, re := range m.regexExceptions { for _, re := range m.regexExceptions {
if re.pattern.String() != pattern { if re.original != rule && re.original != cleanRule {
newRegexExceptions = append(newRegexExceptions, re) newRegexExceptions = append(newRegexExceptions, re)
} else { } else {
removed = true removed = true
@@ -785,6 +849,8 @@ func (m *ShieldManager) RemoveRule(rule string) error {
for domain := range m.domainRules { for domain := range m.domainRules {
if domain == cleanRule || domain == rule { if domain == cleanRule || domain == rule {
delete(m.domainRules, domain) delete(m.domainRules, domain)
delete(m.domainRulesIsLocal, domain)
delete(m.domainRulesSource, domain)
removed = true removed = true
break break
} }
@@ -794,6 +860,8 @@ func (m *ShieldManager) RemoveRule(rule string) error {
for domain := range m.domainExceptions { for domain := range m.domainExceptions {
if domain == cleanRule || domain == rule { if domain == cleanRule || domain == rule {
delete(m.domainExceptions, domain) delete(m.domainExceptions, domain)
delete(m.domainExceptionsIsLocal, domain)
delete(m.domainExceptionsSource, domain)
removed = true removed = true
break break
} }
@@ -801,6 +869,36 @@ func (m *ShieldManager) RemoveRule(rule string) error {
} }
} }
// 如果没有删除任何规则,尝试删除可能的子域名规则
if !removed {
// 解析原始规则,提取可能的主域名
originalRule := cleanRule
// 移除可能的前缀
originalRule = strings.TrimPrefix(originalRule, "@@||")
originalRule = strings.TrimPrefix(originalRule, "||")
// 检查是否有子域名规则需要删除
// 遍历所有域名规则,删除包含原始规则作为后缀的子域名规则
for domain := range m.domainRules {
if strings.HasSuffix(domain, "."+originalRule) || domain == originalRule {
delete(m.domainRules, domain)
delete(m.domainRulesIsLocal, domain)
delete(m.domainRulesSource, domain)
removed = true
}
}
// 遍历所有排除规则,删除包含原始规则作为后缀的子域名规则
for domain := range m.domainExceptions {
if strings.HasSuffix(domain, "."+originalRule) || domain == originalRule {
delete(m.domainExceptions, domain)
delete(m.domainExceptionsIsLocal, domain)
delete(m.domainExceptionsSource, domain)
removed = true
}
}
}
// 如果有规则被删除,持久化保存更改 // 如果有规则被删除,持久化保存更改
if removed && m.config.LocalRulesFile != "" { if removed && m.config.LocalRulesFile != "" {
if err := m.saveRulesToFile(); err != nil { if err := m.saveRulesToFile(); err != nil {
@@ -859,29 +957,37 @@ func (m *ShieldManager) StopAutoUpdate() {
logger.Info("规则自动更新已停止") logger.Info("规则自动更新已停止")
} }
// saveRulesToFile 保存规则到文件 // saveRulesToFile 保存规则到文件,只保存本地规则
func (m *ShieldManager) saveRulesToFile() error { func (m *ShieldManager) saveRulesToFile() error {
var rules []string var rules []string
// 添加域名规则 // 添加本地域名规则
for domain := range m.domainRules { for domain, isLocal := range m.domainRulesIsLocal {
if isLocal {
rules = append(rules, "||"+domain) rules = append(rules, "||"+domain)
} }
// 添加正则表达式规则
for _, re := range m.regexRules {
rules = append(rules, re.original)
} }
// 添加排除规则 // 添加本地正则表达式规则
for domain := range m.domainExceptions { for _, re := range m.regexRules {
if re.isLocal {
rules = append(rules, re.original)
}
}
// 添加本地排除规则
for domain, isLocal := range m.domainExceptionsIsLocal {
if isLocal {
rules = append(rules, "@@||"+domain) rules = append(rules, "@@||"+domain)
} }
}
// 添加正则表达式排除规则 // 添加本地正则表达式排除规则
for _, re := range m.regexExceptions { for _, re := range m.regexExceptions {
if re.isLocal {
rules = append(rules, re.original) rules = append(rules, re.original)
} }
}
// 写入文件 // 写入文件
content := strings.Join(rules, "\n") content := strings.Join(rules, "\n")
@@ -1187,6 +1293,131 @@ func (m *ShieldManager) GetHostsCount() int {
return len(m.hostsMap) return len(m.hostsMap)
} }
// GetLocalRules 获取仅本地规则
func (m *ShieldManager) GetLocalRules() map[string]interface{} {
m.rulesMutex.RLock()
defer m.rulesMutex.RUnlock()
// 转换map和slice为字符串列表只包含本地规则
domainRulesList := make([]string, 0)
for domain, isLocal := range m.domainRulesIsLocal {
if isLocal && m.domainRules[domain] {
domainRulesList = append(domainRulesList, "||"+domain+"^")
}
}
domainExceptionsList := make([]string, 0)
for domain, isLocal := range m.domainExceptionsIsLocal {
if isLocal && m.domainExceptions[domain] {
domainExceptionsList = append(domainExceptionsList, "@@||"+domain+"^")
}
}
// 获取本地正则规则原始字符串
regexRulesList := make([]string, 0)
for _, re := range m.regexRules {
if re.isLocal {
regexRulesList = append(regexRulesList, re.original)
}
}
// 获取本地正则排除规则原始字符串
regexExceptionsList := make([]string, 0)
for _, re := range m.regexExceptions {
if re.isLocal {
regexExceptionsList = append(regexExceptionsList, re.original)
}
}
// 计算本地规则数量
localDomainRulesCount := 0
for _, isLocal := range m.domainRulesIsLocal {
if isLocal {
localDomainRulesCount++
}
}
localRegexRulesCount := 0
for _, re := range m.regexRules {
if re.isLocal {
localRegexRulesCount++
}
}
localRulesCount := localDomainRulesCount + localRegexRulesCount
return map[string]interface{}{
"domainRules": domainRulesList,
"domainExceptions": domainExceptionsList,
"regexRules": regexRulesList,
"regexExceptions": regexExceptionsList,
"localRulesCount": localRulesCount,
"localDomainRulesCount": localDomainRulesCount,
"localRegexRulesCount": localRegexRulesCount,
}
}
// GetRemoteRules 获取仅远程规则
func (m *ShieldManager) GetRemoteRules() map[string]interface{} {
m.rulesMutex.RLock()
defer m.rulesMutex.RUnlock()
// 转换map和slice为字符串列表只包含远程规则
domainRulesList := make([]string, 0)
for domain, isLocal := range m.domainRulesIsLocal {
if !isLocal && m.domainRules[domain] {
domainRulesList = append(domainRulesList, "||"+domain+"^")
}
}
domainExceptionsList := make([]string, 0)
for domain, isLocal := range m.domainExceptionsIsLocal {
if !isLocal && m.domainExceptions[domain] {
domainExceptionsList = append(domainExceptionsList, "@@||"+domain+"^")
}
}
// 获取远程正则规则原始字符串
regexRulesList := make([]string, 0)
for _, re := range m.regexRules {
if !re.isLocal {
regexRulesList = append(regexRulesList, re.original)
}
}
// 获取远程正则排除规则原始字符串
regexExceptionsList := make([]string, 0)
for _, re := range m.regexExceptions {
if !re.isLocal {
regexExceptionsList = append(regexExceptionsList, re.original)
}
}
// 计算远程规则数量
remoteDomainRulesCount := 0
for _, isLocal := range m.domainRulesIsLocal {
if !isLocal {
remoteDomainRulesCount++
}
}
remoteRegexRulesCount := 0
for _, re := range m.regexRules {
if !re.isLocal {
remoteRegexRulesCount++
}
}
remoteRulesCount := remoteDomainRulesCount + remoteRegexRulesCount
return map[string]interface{}{
"domainRules": domainRulesList,
"domainExceptions": domainExceptionsList,
"regexRules": regexRulesList,
"regexExceptions": regexExceptionsList,
"remoteRulesCount": remoteRulesCount,
"remoteDomainRulesCount": remoteDomainRulesCount,
"remoteRegexRulesCount": remoteRegexRulesCount,
"blacklists": m.config.Blacklists,
}
}
// GetRules 获取所有规则 // GetRules 获取所有规则
func (m *ShieldManager) GetRules() map[string]interface{} { func (m *ShieldManager) GetRules() map[string]interface{} {
m.rulesMutex.RLock() m.rulesMutex.RLock()

62
shield/rule_test.go Normal file
View File

@@ -0,0 +1,62 @@
package shield
import (
"testing"
"dns-server/config"
)
func TestRuleParsing(t *testing.T) {
// 创建一个简单的配置
cfg := &config.ShieldConfig{
LocalRulesFile: "",
RemoteRulesCacheDir: ".",
UpdateInterval: 3600,
StatsFile: "",
StatsSaveInterval: 300,
HostsFile: "",
Blacklists: []config.BlacklistEntry{},
}
// 测试规则
testCases := []struct {
rule string
domain string
blocked bool
desc string
}{
// 测试关键字匹配规则
{"/ad.qq.com/", "ad.qq.com", true, "精确匹配"},
{"/ad.qq.com/", "sub.ad.qq.com", true, "子域名包含匹配"},
{"/ad/", "ad.example.com", true, "开头匹配"},
{"/ad/", "example.ad.com", true, "中间匹配"},
{"/ad/", "example.com.ad", true, "结尾匹配"},
{"/AD/", "ad.example.com", true, "不区分大小写匹配"},
{"/example.com/", "example.com", true, "特殊字符转义匹配"},
{"/ad/", "example.com", false, "不包含关键字,不应匹配"},
{"/test/", "example.com", false, "不同关键字,不应匹配"},
// 测试排除规则
{"@@/ad/", "ad.example.com", false, "排除规则,不应匹配"},
}
// 运行测试
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
// 为每个测试用例创建一个新的屏蔽管理器实例
manager := NewShieldManager(cfg)
// 添加规则
manager.AddRule(tc.rule)
// 检查域名是否被屏蔽
result := manager.CheckDomainBlockDetails(tc.domain)
blocked := result["blocked"].(bool)
// 验证结果
if blocked != tc.blocked {
t.Errorf("Rule %q: Domain %q expected %t, got %t", tc.rule, tc.domain, tc.blocked, blocked)
}
})
}
}

5
shield_stats.json Normal file
View File

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

488
static/api/css/style.css Normal file
View File

@@ -0,0 +1,488 @@
/* 基础样式 */
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #ffffff;
color: #333333;
}
/* 默认浅色主题样式 */
.swagger-ui .topbar {
background-color: #2c3e50;
padding: 15px 0;
}
.swagger-ui .topbar .topbar-wrapper .link {
color: #ecf0f1;
font-size: 1.2rem;
}
.swagger-ui .info {
margin: 20px 0;
}
.swagger-ui .info .title {
font-size: 2rem;
margin-bottom: 10px;
color: #333;
}
.swagger-ui .info .description {
font-size: 1rem;
color: #555;
margin-bottom: 15px;
}
/* 修复服务器URL输入框样式 */
.swagger-ui .servers li input[type="text"] {
padding: 8px 12px;
width: 100%;
box-sizing: border-box;
}
/* 修复服务器选择区域的背景颜色和布局 */
.swagger-ui .servers {
padding: 16px;
width: 100%;
box-sizing: border-box;
margin: 0;
}
/* 确保服务器列表容器有正确的背景色和布局 */
.swagger-ui .servers-wrapper {
width: 100%;
box-sizing: border-box;
margin: 0;
}
/* 确保整个顶部区域颜色一致和布局正确 */
.swagger-ui .info {
margin: 0;
padding: 20px 16px;
width: 100%;
box-sizing: border-box;
}
/* 确保顶部主容器颜色一致和布局正确 */
.swagger-ui {
width: 100%;
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* 确保API信息区域颜色一致和布局正确 */
.swagger-ui .info-container {
width: 100%;
box-sizing: border-box;
}
body.dark-mode .swagger-ui .servers li label {
color: #ffffff !important;
font-weight: 500 !important;
}
/* 修复服务器URL输入框深色模式样式 */
body.dark-mode .swagger-ui .servers li input[type="text"] {
background-color: #1a202c !important;
color: #ffffff !important;
border-color: #4a5568 !important;
padding: 8px 12px !important;
width: 100% !important;
}
/* 修复服务器选择区域的深色模式背景颜色和布局 */
body.dark-mode .swagger-ui .servers {
background-color: #1a202c !important;
border: none !important;
padding: 16px !important;
width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
}
/* 确保服务器列表容器在深色模式下也有正确的背景色和布局 */
body.dark-mode .swagger-ui .servers-wrapper {
background-color: #1a202c !important;
width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
}
/* 确保整个顶部区域在深色模式下颜色一致和布局正确 */
body.dark-mode .swagger-ui .info {
background-color: #1a202c !important;
margin: 0 !important;
padding: 20px 16px !important;
border-bottom: 1px solid #4a5568 !important;
width: 100% !important;
box-sizing: border-box !important;
}
/* 确保顶部主容器在深色模式下颜色一致和布局正确 */
body.dark-mode .swagger-ui {
background-color: #1a202c !important;
width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
padding: 0 !important;
}
/* 确保API信息区域在深色模式下颜色一致和布局正确 */
body.dark-mode .swagger-ui .info-container {
background-color: #1a202c !important;
width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
padding: 0 !important;
}
/* 修复深色模式下内容区域的布局问题 */
body.dark-mode .swagger-ui .wrapper {
width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
padding: 0 !important;
}
/* 修复深色模式下API操作块的布局 */
body.dark-mode .swagger-ui .opblock {
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
box-sizing: border-box !important;
}
/* 修复深色模式下过滤器的布局 */
body.dark-mode .swagger-ui .filter {
width: 100% !important;
box-sizing: border-box !important;
padding: 16px !important;
margin: 0 !important;
}
/* 修复深色模式下顶部栏布局 */
body.dark-mode .swagger-ui .topbar {
width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
padding: 15px 0 !important;
}
/* 修复深色模式下顶部栏包装器布局 */
body.dark-mode .swagger-ui .topbar .topbar-wrapper {
width: 100% !important;
box-sizing: border-box !important;
padding: 0 16px !important;
}
/* 修复深色模式下响应容器布局 */
body.dark-mode .swagger-ui .responses-inner {
width: 100% !important;
box-sizing: border-box !important;
}
/* 修复深色模式下操作块摘要布局 */
body.dark-mode .swagger-ui .opblock-summary {
width: 100% !important;
box-sizing: border-box !important;
}
/* 确保深色模式下所有容器元素都使用box-sizing */
body.dark-mode * {
box-sizing: border-box !important;
}
/* 增强标签标题深色模式样式 */
body.dark-mode .swagger-ui .opblock-tag {
color: #ffffff !important;
background-color: #2d3748 !important;
padding: 12px 16px !important;
border-radius: 6px !important;
margin-bottom: 12px !important;
font-weight: 700 !important;
font-size: 1.1rem !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3) !important;
}
/* 增强标签标题h3深色模式样式 */
body.dark-mode .swagger-ui .opblock-tag.h3 {
color: #ffffff !important;
background-color: #2d3748 !important;
}
/* 增强标签部分深色模式样式 */
body.dark-mode .swagger-ui .opblock-tag-section {
background-color: #2d3748 !important;
padding: 16px !important;
border-radius: 8px !important;
margin-bottom: 20px !important;
}
/* 增强API描述深色模式样式 */
body.dark-mode .swagger-ui .opblock-description-wrapper {
color: #ffffff !important;
background-color: #2d3748 !important;
padding: 12px 16px !important;
border-radius: 6px !important;
margin-bottom: 12px !important;
font-weight: 500 !important;
}
body.dark-mode .swagger-ui .opblock-description-wrapper p {
color: #ffffff !important;
line-height: 1.5 !important;
}
/* 增强stats标签描述深色模式样式 */
body.dark-mode .swagger-ui .opblock-summary-description {
color: #ffffff !important;
font-weight: 500 !important;
}
/* 增强操作块标题深色模式样式 */
body.dark-mode .swagger-ui .opblock-title_normal h4 {
color: #ffffff !important;
font-weight: 600 !important;
}
/* 增强参数部分深色模式样式 */
body.dark-mode .swagger-ui .opblock-body {
background-color: #2d3748 !important;
}
body.dark-mode .swagger-ui .opblock-body .parameter__name {
color: #ffffff !important;
font-weight: 600 !important;
}
body.dark-mode .swagger-ui .opblock-body .parameter__type {
color: #ffffff !important;
font-weight: 500 !important;
}
body.dark-mode .swagger-ui .opblock-body .parameter__description {
color: #ffffff !important;
}
body.dark-mode .swagger-ui .parameters-col_description,
body.dark-mode .swagger-ui .parameters-col_name,
body.dark-mode .swagger-ui .parameters-col_type {
color: #ffffff !important;
}
body.dark-mode .swagger-ui .parameters-col_description p,
body.dark-mode .swagger-ui .parameters-col_name p,
body.dark-mode .swagger-ui .parameters-col_type p {
color: #ffffff !important;
}
/* 新增适配API文档展开界面的所有文字元素 */
body.dark-mode .swagger-ui .opblock-body {
color: #ffffff;
}
body.dark-mode .swagger-ui .opblock-body .parameter__name {
color: #ffffff;
}
body.dark-mode .swagger-ui .opblock-body .parameter__type {
color: #ffffff;
}
body.dark-mode .swagger-ui .opblock-body .parameter__description {
color: #ffffff;
}
body.dark-mode .swagger-ui .opblock-body .body-param-options {
color: #ffffff;
}
body.dark-mode .swagger-ui .opblock-body .body-param-options .body-param-type {
color: #ffffff;
}
body.dark-mode .swagger-ui .responses-inner {
color: #ffffff;
}
body.dark-mode .swagger-ui .responses-inner h4 {
color: #ffffff;
}
body.dark-mode .swagger-ui .response-container {
color: #ffffff;
}
body.dark-mode .swagger-ui .response-container .response-wrapper {
color: #ffffff;
}
body.dark-mode .swagger-ui .response-container .response-code {
color: #ffffff;
}
body.dark-mode .swagger-ui .response-container .response-description {
color: #ffffff;
}
body.dark-mode .swagger-ui .model {
color: #ffffff;
}
body.dark-mode .swagger-ui .model .property {
color: #ffffff;
}
body.dark-mode .swagger-ui .model .property .property-name {
color: #ffffff;
}
body.dark-mode .swagger-ui .model .property .property-description {
color: #ffffff;
}
body.dark-mode .swagger-ui .model .property .property-type {
color: #ffffff;
}
body.dark-mode .swagger-ui .model .property .required {
color: #ffffff;
}
body.dark-mode .swagger-ui .scroll-to-top {
color: #ffffff;
}
body.dark-mode .swagger-ui .opblock-tag-section {
color: #ffffff;
}
body.dark-mode .swagger-ui .servers-title {
color: #ffffff;
}
body.dark-mode .swagger-ui .servers {
color: #ffffff;
}
body.dark-mode .swagger-ui .servers li {
color: #ffffff;
}
body.dark-mode .swagger-ui .servers li label {
color: #ffffff;
}
body.dark-mode .swagger-ui .servers li select {
color: #ffffff;
background-color: #1a202c;
border-color: #4a5568;
}
body.dark-mode .swagger-ui .auth-wrapper {
color: #ffffff;
}
body.dark-mode .swagger-ui .auth-wrapper .auth-title {
color: #ffffff;
}
body.dark-mode .swagger-ui .auth-wrapper .auth-list {
color: #ffffff;
}
body.dark-mode .swagger-ui .auth-wrapper .auth-item {
color: #ffffff;
}
body.dark-mode .swagger-ui .auth-wrapper .auth-item label {
color: #ffffff;
}
/* 确保代码块内的文字也清晰可见 */
body.dark-mode .swagger-ui pre {
color: #ffffff;
}
body.dark-mode .swagger-ui code {
color: #ffffff;
}
/* 确保所有表单元素的文字颜色正确 */
body.dark-mode .swagger-ui form {
color: #ffffff;
}
body.dark-mode .swagger-ui form label {
color: #ffffff;
}
body.dark-mode .swagger-ui select {
color: #ffffff;
background-color: #1a202c;
border-color: #4a5568;
}
/* 适配可能的嵌套内容 */
body.dark-mode .swagger-ui .opblock-body .schema {
color: #ffffff;
}
body.dark-mode .swagger-ui .opblock-body .schema .title {
color: #ffffff;
}
body.dark-mode .swagger-ui .opblock-body .schema .required {
color: #ffffff;
}
/* 适配可能的按钮组 */
body.dark-mode .swagger-ui .btn-group {
color: #ffffff;
}
/* 适配可能的标签 */
body.dark-mode .swagger-ui .tag {
color: #ffffff;
}
/* 适配可能的警告和提示信息 */
body.dark-mode .swagger-ui .warning {
color: #ffffff;
}
body.dark-mode .swagger-ui .hint {
color: #ffffff;
}
/* 适配可能的表格内容 */
body.dark-mode .swagger-ui table {
color: #ffffff;
}
body.dark-mode .swagger-ui table th {
color: #ffffff;
}
body.dark-mode .swagger-ui table td {
color: #ffffff;
}
/* 响应式设计 */
@media (max-width: 768px) {
.topbar-controls {
flex-direction: column;
align-items: flex-end;
gap: 10px;
}
.theme-toggle-btn {
padding: 6px 10px;
font-size: 12px;
}
.theme-toggle-btn span {
display: none;
}
}

16
static/api/index.html Normal file
View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>DNS Server API 文档</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.18.3/swagger-ui.css">
<link rel="stylesheet" type="text/css" href="css/style.css">
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.18.3/swagger-ui-bundle.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.18.3/swagger-ui-standalone-preset.js"></script>
<script src="js/index.js"></script>
</body>
</html>

1333
static/api/js/index.js Normal file

File diff suppressed because it is too large Load Diff

62
static/css/animation.css Normal file
View File

@@ -0,0 +1,62 @@
@layer utilities {
.content-auto {
content-visibility: auto;
}
.card-shadow {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.sidebar-item-active {
background-color: rgba(22, 93, 255, 0.1);
color: #165DFF;
border-right: 4px solid #165DFF;
}
}
/* 服务器状态组件光晕效果 */
.glow-effect {
animation: pulse 2s ease-in-out;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(41, 128, 185, 0.4);
}
70% {
box-shadow: 0 0 0 10px rgba(41, 128, 185, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(41, 128, 185, 0);
}
}
/* 服务器状态组件样式优化 */
.server-status-widget {
min-width: 170px;
transition: all 0.3s ease;
}
.server-status-widget:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* 加载状态样式 */
.status-loading {
animation: status-pulse 1.5s ease-in-out infinite;
}
/* 状态脉冲动画 */
@keyframes status-pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
/* 保存按钮状态样式 */
#save-blacklist-status {
transition: all 0.3s ease-in-out;
}

View File

@@ -132,26 +132,7 @@ header p {
/* 响应式布局 - 移动设备 */ /* 响应式布局 - 移动设备 */
@media (max-width: 768px) { @media (max-width: 768px) {
.sidebar { /* 这些样式已经通过Tailwind CSS类在HTML中实现这里移除避免冲突 */
position: fixed;
left: -var(--sidebar-width);
top: var(--header-height);
z-index: 99;
height: calc(100vh - var(--header-height));
}
.sidebar.open {
left: 0;
width: var(--sidebar-width);
}
.sidebar.open .nav-item span {
display: block;
}
.sidebar.open .nav-item i {
margin-right: 1rem;
}
} }
.nav-menu { .nav-menu {
@@ -1062,18 +1043,6 @@ tr:hover {
font-size: 0.9rem; font-size: 0.9rem;
} }
} }
/* 加载动画 */
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin { @keyframes spin {
0% { transform: rotate(0deg); } 0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); } 100% { transform: rotate(360deg); }

View File

@@ -8,170 +8,23 @@
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome --> <!-- Font Awesome -->
<link href="https://cdn.staticfile.org/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet"> <link href="https://cdn.staticfile.org/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<!-- Chart.js -->
<!-- Chart.js 本地备用 --> <!-- Chart.js 本地备用 -->
<script src="js/vendor/chart.umd.min.js" onerror="this.onerror=null;this.src='js/chart.umd.min.js';"></script> <script src="js/vendor/chart.umd.min.js" onerror="this.onerror=null;this.src='js/chart.umd.min.js';"></script>
<!-- Tailwind 配置 --> <!-- Tailwind 配置 -->
<script> <script src="js/vendor/tailwind.js"></script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#165DFF',
secondary: '#36CFFB',
success: '#00B42A',
warning: '#FF7D00',
danger: '#F53F3F',
info: '#86909C',
dark: '#1D2129',
light: '#F2F3F5',
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
},
}
}
</script>
<!-- 自定义工具类 --> <!-- 自定义工具类 -->
<style type="text/tailwindcss"> <style type="text/tailwindcss" src="css/index.css"></style>
@layer utilities {
.content-auto {
content-visibility: auto;
}
.card-shadow {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.sidebar-item-active {
background-color: rgba(22, 93, 255, 0.1);
color: #165DFF;
border-right: 4px solid #165DFF;
}
}
</style>
<!-- 数字光晕效果样式 -->
<style>
/* 数字光晕效果基础样式 */
.number-glow {
animation: glow-pulse 2s ease-in-out;
}
/* 服务器状态组件光晕效果 */
.glow-effect {
animation: pulse 2s ease-in-out;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(41, 128, 185, 0.4);
}
70% {
box-shadow: 0 0 0 10px rgba(41, 128, 185, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(41, 128, 185, 0);
}
}
/* 服务器状态组件样式优化 */
.server-status-widget {
min-width: 170px;
transition: all 0.3s ease;
}
.server-status-widget:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* 蓝色光晕效果 */
.number-glow-blue {
animation: glow-blue 2s ease-in-out;
}
/* 红色光晕效果 */
.number-glow-red {
animation: glow-red 2s ease-in-out;
}
/* 绿色光晕效果 */
.number-glow-green {
animation: glow-green 2s ease-in-out;
}
/* 黄色光晕效果 */
.number-glow-yellow {
animation: glow-yellow 2s ease-in-out;
}
/* 光晕动画定义 */
@keyframes glow-pulse {
0% {
text-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
}
50% {
text-shadow: 0 0 20px rgba(0, 0, 0, 0.7);
}
100% {
text-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
}
}
@keyframes glow-blue {
0% {
text-shadow: 0 0 5px rgba(59, 130, 246, 0.3);
}
50% {
text-shadow: 0 0 20px rgba(59, 130, 246, 0.7);
}
100% {
text-shadow: 0 0 5px rgba(59, 130, 246, 0.3);
}
}
@keyframes glow-red {
0% {
text-shadow: 0 0 5px rgba(239, 68, 68, 0.3);
}
50% {
text-shadow: 0 0 20px rgba(239, 68, 68, 0.7);
}
100% {
text-shadow: 0 0 5px rgba(239, 68, 68, 0.3);
}
}
@keyframes glow-green {
0% {
text-shadow: 0 0 5px rgba(16, 185, 129, 0.3);
}
50% {
text-shadow: 0 0 20px rgba(16, 185, 129, 0.7);
}
100% {
text-shadow: 0 0 5px rgba(16, 185, 129, 0.3);
}
}
@keyframes glow-yellow {
0% {
text-shadow: 0 0 5px rgba(250, 204, 21, 0.3);
}
50% {
text-shadow: 0 0 20px rgba(250, 204, 21, 0.7);
}
100% {
text-shadow: 0 0 5px rgba(250, 204, 21, 0.3);
}
}
</style>
</head> </head>
<body class="bg-gray-50 text-dark font-sans"> <body class="bg-gray-50 text-dark font-sans">
<div class="flex h-screen overflow-hidden"> <div class="flex h-screen overflow-hidden">
<!-- 侧边栏 --> <!-- 侧边栏 -->
<aside id="sidebar" class="w-64 bg-white border-r border-gray-200 flex flex-col transition-all duration-300 z-10"> <aside id="sidebar" class="fixed inset-y-0 left-0 w-64 bg-white border-r border-gray-200 flex flex-col transition-transform duration-300 z-50 md:relative md:translate-x-0 -translate-x-full shadow-lg">
<!-- 移动端关闭按钮 -->
<div class="absolute top-4 right-4 md:hidden">
<button id="close-sidebar" class="p-2 text-gray-500 hover:text-gray-700 focus:outline-none">
<i class="fa fa-times text-xl"></i>
</button>
</div>
<!-- Logo --> <!-- Logo -->
<div class="flex items-center justify-center h-16 border-b border-gray-200"> <div class="flex items-center justify-center h-16 border-b border-gray-200">
<i class="fa fa-server text-3xl text-primary mr-3"></i> <i class="fa fa-server text-3xl text-primary mr-3"></i>
@@ -199,16 +52,11 @@
<span>Hosts管理</span> <span>Hosts管理</span>
</a> </a>
</li> </li>
<li>
<a href="#blacklists" class="flex items-center px-4 py-3 text-gray-700 hover:bg-gray-100 rounded-md transition-all">
<i class="fa fa-ban mr-3 text-lg"></i>
<span>黑名单管理</span>
</a>
</li>
<li> <li>
<a href="#query" class="flex items-center px-4 py-3 text-gray-700 hover:bg-gray-100 rounded-md transition-all"> <a href="#query" class="flex items-center px-4 py-3 text-gray-700 hover:bg-gray-100 rounded-md transition-all">
<i class="fa fa-search mr-3 text-lg"></i> <i class="fa fa-search mr-3 text-lg"></i>
<span>DNS查询</span> <span>DNS屏蔽查询</span>
</a> </a>
</li> </li>
<li> <li>
@@ -227,12 +75,15 @@
</div> </div>
</aside> </aside>
<!-- 侧边栏遮罩层 -->
<div id="sidebar-overlay" class="fixed inset-0 bg-black bg-opacity-50 z-40 hidden md:hidden"></div>
<!-- 主内容区 --> <!-- 主内容区 -->
<main class="flex-1 overflow-y-auto"> <main class="flex-1 overflow-y-auto">
<!-- 顶部导航栏 --> <!-- 顶部导航栏 -->
<header class="bg-white border-b border-gray-200 h-16 flex items-center justify-between px-6"> <header class="bg-white border-b border-gray-200 h-16 flex items-center justify-between px-6">
<div class="flex items-center"> <div class="flex items-center">
<button id="toggle-sidebar" class="lg:hidden text-gray-500 hover:text-gray-700"> <button id="toggle-sidebar" class="block md:hidden text-gray-500 hover:text-gray-700 focus:outline-none">
<i class="fa fa-bars text-xl"></i> <i class="fa fa-bars text-xl"></i>
</button> </button>
<h2 class="ml-4 text-xl font-semibold" id="page-title">仪表盘</h2> <h2 class="ml-4 text-xl font-semibold" id="page-title">仪表盘</h2>
@@ -240,7 +91,7 @@
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<!-- 服务器状态组件 --> <!-- 服务器状态组件 -->
<div class="relative bg-white rounded-lg shadow-md px-3 py-2 flex items-center space-x-2 server-status-widget" id="server-status-widget"> <div class="relative bg-white rounded-lg shadow-md px-3 py-2 flex items-center space-x-2 server-status-widget md:min-w-[300px] sm:min-w-[250px] min-w-[180px]" id="server-status-widget">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex items-center"> <div class="flex items-center">
<span class="text-xs font-medium text-gray-500">CPU</span> <span class="text-xs font-medium text-gray-500">CPU</span>
@@ -261,7 +112,7 @@
</div> </div>
</div> </div>
<!-- 额外指标区域 - 初始隐藏,只在非首页显示 --> <!-- 额外指标区域 - 初始隐藏,只在非首页显示 -->
<div id="server-additional-stats" class="hidden flex items-center"> <div id="server-additional-stats" class="hidden md:flex items-center">
<div class="w-1 h-8 bg-gray-200 rounded-full mx-1"></div> <div class="w-1 h-8 bg-gray-200 rounded-full mx-1"></div>
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex items-center"> <div class="flex items-center">
@@ -304,9 +155,9 @@
<!-- 仪表盘部分 --> <!-- 仪表盘部分 -->
<div id="dashboard-content" class="space-y-6"> <div id="dashboard-content" class="space-y-6">
<!-- 统计卡片 --> <!-- 统计卡片 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-4 gap-6">
<!-- 查询总量卡片 --> <!-- 查询总量卡片 -->
<div class="bg-blue-50 rounded-lg p-6 card-shadow relative overflow-hidden"> <div class="bg-blue-50 rounded-lg p-4 card-shadow relative overflow-hidden">
<!-- 颜色蒙版 --> <!-- 颜色蒙版 -->
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-primary opacity-10"></div> <div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-primary opacity-10"></div>
<div class="relative z-10"> <div class="relative z-10">
@@ -324,15 +175,12 @@
<span id="queries-percent">0%</span> <span id="queries-percent">0%</span>
</span> </span>
</div> </div>
<div class="h-16 mt-2">
<canvas id="query-chart"></canvas>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- 屏蔽数量卡片 --> <!-- 屏蔽数量卡片 -->
<div class="bg-red-50 rounded-lg p-6 card-shadow relative overflow-hidden"> <div class="bg-red-50 rounded-lg p-4 card-shadow relative overflow-hidden">
<!-- 颜色蒙版 --> <!-- 颜色蒙版 -->
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-danger opacity-10"></div> <div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-danger opacity-10"></div>
<div class="relative z-10"> <div class="relative z-10">
@@ -350,15 +198,12 @@
<span id="blocked-percent">0%</span> <span id="blocked-percent">0%</span>
</span> </span>
</div> </div>
<div class="h-16 mt-2">
<canvas id="blocked-chart"></canvas>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- 正常解析卡片 --> <!-- 正常解析卡片 -->
<div class="bg-green-50 rounded-lg p-6 card-shadow relative overflow-hidden"> <div class="bg-green-50 rounded-lg p-4 card-shadow relative overflow-hidden">
<!-- 颜色蒙版 --> <!-- 颜色蒙版 -->
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-success opacity-10"></div> <div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-success opacity-10"></div>
<div class="relative z-10"> <div class="relative z-10">
@@ -376,15 +221,12 @@
<span id="allowed-percent">0%</span> <span id="allowed-percent">0%</span>
</span> </span>
</div> </div>
<div class="h-16 mt-2">
<canvas id="allowed-chart"></canvas>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- 错误数量卡片 --> <!-- 错误数量卡片 -->
<div class="bg-yellow-50 rounded-lg p-6 card-shadow relative overflow-hidden"> <div class="bg-yellow-50 rounded-lg p-4 card-shadow relative overflow-hidden">
<!-- 颜色蒙版 --> <!-- 颜色蒙版 -->
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-warning opacity-10"></div> <div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-warning opacity-10"></div>
<div class="relative z-10"> <div class="relative z-10">
@@ -402,15 +244,12 @@
<span id="error-percent">0%</span> <span id="error-percent">0%</span>
</span> </span>
</div> </div>
<div class="h-16 mt-2">
<canvas id="error-chart"></canvas>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- 平均响应时间卡片 --> <!-- 平均响应时间卡片 -->
<div class="bg-white rounded-lg p-6 card-shadow relative overflow-hidden"> <div class="bg-white rounded-lg p-4 card-shadow relative overflow-hidden">
<!-- 颜色蒙版 --> <!-- 颜色蒙版 -->
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-info opacity-10"></div> <div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-info opacity-10"></div>
<div class="relative z-10"> <div class="relative z-10">
@@ -428,15 +267,12 @@
<span id="response-time-percent">0%</span> <span id="response-time-percent">0%</span>
</span> </span>
</div> </div>
<div class="h-16 mt-2">
<canvas id="response-time-chart"></canvas>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- 最常用查询类型卡片 --> <!-- 最常用查询类型卡片 -->
<div class="bg-white rounded-lg p-6 card-shadow relative overflow-hidden"> <div class="bg-white rounded-lg p-4 card-shadow relative overflow-hidden">
<!-- 颜色蒙版 --> <!-- 颜色蒙版 -->
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-secondary opacity-10"></div> <div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-secondary opacity-10"></div>
<div class="relative z-10"> <div class="relative z-10">
@@ -457,7 +293,7 @@
</div> </div>
<!-- 活跃来源IP数卡片 --> <!-- 活跃来源IP数卡片 -->
<div class="bg-white rounded-lg p-6 card-shadow relative overflow-hidden"> <div class="bg-white rounded-lg p-4 card-shadow relative overflow-hidden">
<!-- 颜色蒙版 --> <!-- 颜色蒙版 -->
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-success opacity-10"></div> <div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-success opacity-10"></div>
<div class="relative z-10"> <div class="relative z-10">
@@ -475,42 +311,15 @@
<span id="active-ips-percent">0%</span> <span id="active-ips-percent">0%</span>
</span> </span>
</div> </div>
<div class="h-16 mt-2">
<canvas id="ips-chart"></canvas>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- CPU使用率卡片 -->
<div class="bg-white rounded-lg p-6 card-shadow relative overflow-hidden">
<!-- 颜色蒙版 -->
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-warning opacity-10"></div>
<div class="relative z-10">
<div class="flex items-center justify-between mb-4">
<h3 class="text-gray-500 font-medium">CPU使用率</h3>
<div class="p-2 rounded-full bg-warning/10 text-warning">
<i class="fa fa-microchip"></i>
</div>
</div>
<div class="mb-2">
<div class="flex items-end justify-between">
<p class="text-3xl font-bold" id="cpu-usage">0%</p>
<span class="text-warning text-sm flex items-center">
<i class="fa fa-bolt mr-1"></i>
<span id="cpu-status">正常</span>
</span>
</div>
<div class="h-16 mt-2">
<canvas id="cpu-chart"></canvas>
</div>
</div>
</div>
</div>
</div> </div>
<!-- 图表和数据表格 --> <!-- 图表和数据表格 -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- 三个图表在同一行显示 --> <!-- 三个图表在同一行显示 -->
<div class="bg-white rounded-lg p-6 card-shadow lg:col-span-1 md:col-span-1"> <div class="bg-white rounded-lg p-6 card-shadow lg:col-span-1 md:col-span-1">
<h3 class="text-lg font-semibold mb-6">解析与屏蔽比例</h3> <h3 class="text-lg font-semibold mb-6">解析与屏蔽比例</h3>
@@ -560,7 +369,7 @@
</div> </div>
</div> </div>
<div class="p-6"> <div class="p-6">
<div class="h-[600px]"> <div class="h-full max-h-[calc(90vh-120px)]">
<canvas id="detailed-dns-requests-chart"></canvas> <canvas id="detailed-dns-requests-chart"></canvas>
</div> </div>
</div> </div>
@@ -569,82 +378,387 @@
<!-- 最近活动表格 --> <!-- 最近活动表格 -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- 最常屏蔽域名 --> <!-- 被拦截域名排行 -->
<div class="bg-white rounded-lg p-6 card-shadow"> <div class="bg-white rounded-lg p-6 card-shadow">
<h3 class="text-lg font-semibold mb-6">最常屏蔽域名</h3> <h3 class="text-lg font-semibold mb-4">被拦截域名排行</h3>
<div class="overflow-x-auto"> <div class="h-64 overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent">
<table class="min-w-full"> <div class="space-y-3" id="top-blocked-table">
<thead> <div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-danger">
<tr class="border-b border-gray-200"> <div class="flex-1 min-w-0">
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">域名</th> <div class="flex items-center">
<th class="text-right py-3 px-4 text-sm font-medium text-gray-500">屏蔽次数</th> <span class="w-6 h-6 flex items-center justify-center rounded-full bg-danger/10 text-danger text-xs font-medium mr-3">1</span>
</tr> <span class="font-medium truncate">example1.com</span>
</thead> </div>
<tbody id="top-blocked-table"> </div>
<tr> <span class="ml-4 flex-shrink-0 font-semibold text-danger">150</span>
<td colspan="2" class="py-4 text-center text-gray-500">加载中...</td> </div>
</tr> <div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-danger">
</tbody> <div class="flex-1 min-w-0">
</table> <div class="flex items-center">
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-danger/10 text-danger text-xs font-medium mr-3">2</span>
<span class="font-medium truncate">example2.com</span>
</div>
</div>
<span class="ml-4 flex-shrink-0 font-semibold text-danger">130</span>
</div>
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-danger">
<div class="flex-1 min-w-0">
<div class="flex items-center">
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-danger/10 text-danger text-xs font-medium mr-3">3</span>
<span class="font-medium truncate">example3.com</span>
</div>
</div>
<span class="ml-4 flex-shrink-0 font-semibold text-danger">120</span>
</div>
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-danger">
<div class="flex-1 min-w-0">
<div class="flex items-center">
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-danger/10 text-danger text-xs font-medium mr-3">4</span>
<span class="font-medium truncate">example4.com</span>
</div>
</div>
<span class="ml-4 flex-shrink-0 font-semibold text-danger">110</span>
</div>
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-danger">
<div class="flex-1 min-w-0">
<div class="flex items-center">
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-danger/10 text-danger text-xs font-medium mr-3">5</span>
<span class="font-medium truncate">example5.com</span>
</div>
</div>
<span class="ml-4 flex-shrink-0 font-semibold text-danger">100</span>
</div>
</div>
</div> </div>
</div> </div>
<!-- 最近屏蔽域名 --> <!-- 请求域名排行 -->
<div class="bg-white rounded-lg p-6 card-shadow"> <div class="bg-white rounded-lg p-6 card-shadow">
<h3 class="text-lg font-semibold mb-6">最近屏蔽域名</h3> <h3 class="text-lg font-semibold mb-4">请求域名排行</h3>
<div class="overflow-x-auto"> <div class="h-64 overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent">
<table class="min-w-full"> <div class="space-y-3" id="top-domains-table">
<thead> <div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-success">
<tr class="border-b border-gray-200"> <div class="flex-1 min-w-0">
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">域名</th> <div class="flex items-center">
<th class="text-right py-3 px-4 text-sm font-medium text-gray-500">时间</th> <span class="w-6 h-6 flex items-center justify-center rounded-full bg-success/10 text-success text-xs font-medium mr-3">1</span>
</tr> <span class="font-medium truncate">example.com</span>
</thead> </div>
<tbody id="recent-blocked-table"> </div>
<tr> <span class="ml-4 flex-shrink-0 font-semibold text-success">50</span>
<td colspan="2" class="py-4 text-center text-gray-500">加载中...</td> </div>
</tr> </div>
</tbody> </div>
</table> </div>
</div>
<!-- 排行表格 -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
<!-- 客户端排行 -->
<div class="bg-white rounded-lg p-6 card-shadow">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold">客户端排行</h3>
<div id="top-clients-loading" class="flex items-center text-sm text-gray-500">
<i class="fa fa-spinner fa-spin mr-2"></i>
<span>加载中...</span>
</div>
<div id="top-clients-error" class="flex items-center text-sm text-danger hidden">
<i class="fa fa-exclamation-circle mr-2"></i>
<span>加载失败</span>
<button id="retry-top-clients" class="ml-2 text-primary hover:underline">重试</button>
</div>
</div>
<div class="h-64 overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent">
<div class="space-y-3" id="top-clients-table">
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-primary">
<div class="flex-1 min-w-0">
<div class="flex items-center">
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-medium mr-3">1</span>
<span class="font-medium truncate">192.168.1.1</span>
</div>
</div>
<span class="ml-4 flex-shrink-0 font-semibold text-primary">500</span>
</div>
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-primary">
<div class="flex-1 min-w-0">
<div class="flex items-center">
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-medium mr-3">2</span>
<span class="font-medium truncate">192.168.1.2</span>
</div>
</div>
<span class="ml-4 flex-shrink-0 font-semibold text-primary">450</span>
</div>
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-primary">
<div class="flex-1 min-w-0">
<div class="flex items-center">
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-medium mr-3">3</span>
<span class="font-medium truncate">192.168.1.3</span>
</div>
</div>
<span class="ml-4 flex-shrink-0 font-semibold text-primary">400</span>
</div>
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-primary">
<div class="flex-1 min-w-0">
<div class="flex items-center">
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-medium mr-3">4</span>
<span class="font-medium truncate">192.168.1.4</span>
</div>
</div>
<span class="ml-4 flex-shrink-0 font-semibold text-primary">350</span>
</div>
<div class="flex items-center justify-between p-3 rounded-md hover:bg-gray-50 transition-colors border-l-4 border-primary">
<div class="flex-1 min-w-0">
<div class="flex items-center">
<span class="w-6 h-6 flex items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-medium mr-3">5</span>
<span class="font-medium truncate">192.168.1.5</span>
</div>
</div>
<span class="ml-4 flex-shrink-0 font-semibold text-primary">300</span>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- 其他页面内容(初始隐藏) --> <!-- 其他页面内容(初始隐藏) -->
<div id="shield-content" class="hidden"> <div id="shield-content" class="hidden space-y-6">
<!-- 屏蔽管理页面内容 --> <!-- 屏蔽规则统计信息 -->
<div class="bg-white rounded-lg p-6 card-shadow"> <div class="bg-white rounded-lg p-6 card-shadow">
<h3 class="text-lg font-semibold mb-6">屏蔽规则管理</h3> <h3 class="text-lg font-semibold mb-6">屏蔽规则统计</h3>
<!-- 这里将添加屏蔽规则管理相关内容 --> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<p>屏蔽管理页面内容待实现</p> <div class="bg-blue-50 p-4 rounded-lg">
<div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-medium text-gray-500">域名规则</h4>
<i class="fa fa-list text-blue-500"></i>
</div>
<p class="text-2xl font-bold" id="domain-rules-count">0</p>
</div>
<div class="bg-green-50 p-4 rounded-lg">
<div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-medium text-gray-500">域名例外</h4>
<i class="fa fa-check-circle text-green-500"></i>
</div>
<p class="text-2xl font-bold counter" id="domain-exceptions-count">0</p>
</div>
<div class="bg-purple-50 p-4 rounded-lg">
<div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-medium text-gray-500">正则规则</h4>
<i class="fa fa-code text-purple-500"></i>
</div>
<p class="text-2xl font-bold counter" id="regex-rules-count">0</p>
</div>
<div class="bg-yellow-50 p-4 rounded-lg">
<div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-medium text-gray-500">正则例外</h4>
<i class="fa fa-exclamation-circle text-yellow-500"></i>
</div>
<p class="text-2xl font-bold counter" id="regex-exceptions-count">0</p>
</div>
<div class="bg-red-50 p-4 rounded-lg">
<div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-medium text-gray-500">Hosts规则</h4>
<i class="fa fa-file-text text-red-500"></i>
</div>
<p class="text-2xl font-bold counter" id="hosts-rules-count">0</p>
</div>
<div class="bg-indigo-50 p-4 rounded-lg">
<div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-medium text-gray-500">黑名单数量</h4>
<i class="fa fa-ban text-indigo-500"></i>
</div>
<p class="text-2xl font-bold counter" id="blacklist-count">0</p>
<div class="flex items-center justify-between mt-2">
<h5 class="text-xs font-medium text-gray-500">禁用数量</h5>
<p class="text-sm font-bold text-red-600 counter" id="blacklist-disabled-count">0</p>
</div>
</div>
</div> </div>
</div> </div>
<div id="hosts-content" class="hidden"> <!-- 本地规则管理 -->
<div class="bg-white rounded-lg p-6 card-shadow">
<h3 class="text-lg font-semibold mb-6">本地规则管理</h3>
<!-- 添加规则表单 -->
<div id="add-rule-form" class="mb-6 bg-gray-50 p-4 rounded-lg">
<div class="flex items-center space-x-4">
<input type="text" id="new-rule" placeholder="输入规则例如example.com 或 regex:/example\.com/" class="flex-1 px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
<button id="save-rule-btn" class="px-4 py-2 bg-success text-white rounded-md hover:bg-success/90 transition-colors">
保存
</button>
<div id="save-rule-status" class="flex items-center text-sm"></div>
</div>
</div>
<!-- 规则列表 -->
<div class="overflow-x-auto">
<table class="min-w-full">
<thead>
<tr class="border-b border-gray-200">
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">规则</th>
<th class="text-center py-3 px-4 text-sm font-medium text-gray-500">状态</th>
<th class="text-right py-3 px-4 text-sm font-medium text-gray-500">操作</th>
</tr>
</thead>
<tbody id="rules-table-body">
<tr>
<td colspan="3" class="py-4 text-center text-gray-500">暂无规则</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 远程黑名单管理 -->
<div class="bg-white rounded-lg p-6 card-shadow">
<h3 class="text-lg font-semibold mb-6">远程黑名单管理</h3>
<!-- 添加黑名单表单 -->
<div id="add-blacklist-form" class="mb-6 bg-gray-50 p-4 rounded-lg">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label for="blacklist-name" class="block text-sm font-medium text-gray-700 mb-1">名称</label>
<input type="text" id="blacklist-name" placeholder="输入黑名单名称" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
</div>
<div>
<label for="blacklist-url" class="block text-sm font-medium text-gray-700 mb-1">URL</label>
<input type="text" id="blacklist-url" placeholder="输入黑名单URL" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
</div>
<div class="flex items-end">
<div class="flex items-center space-x-2">
<button id="save-blacklist-btn" class="px-4 py-2 bg-success text-white rounded-md hover:bg-success/90 transition-colors">
保存
</button>
<div id="save-blacklist-status" class="flex items-center text-sm"></div>
</div>
</div>
</div>
</div>
<!-- 黑名单列表 -->
<div class="overflow-x-auto">
<table class="min-w-full">
<thead>
<tr class="border-b border-gray-200">
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">名称</th>
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">URL</th>
<th class="text-center py-3 px-4 text-sm font-medium text-gray-500">状态</th>
<th class="text-center py-3 px-4 text-sm font-medium text-gray-500"></th>
<th class="text-right py-3 px-4 text-sm font-medium text-gray-500">操作</th>
</tr>
</thead>
<tbody id="blacklists-table-body">
<tr>
<td colspan="5" class="py-4 text-center text-gray-500">暂无黑名单</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div id="hosts-content" class="hidden space-y-6">
<!-- Hosts管理页面内容 --> <!-- Hosts管理页面内容 -->
<div class="bg-white rounded-lg p-6 card-shadow"> <div class="bg-white rounded-lg p-6 card-shadow">
<h3 class="text-lg font-semibold mb-6">Hosts管理</h3> <h3 class="text-lg font-semibold mb-6">Hosts条目管理</h3>
<!-- 这里将添加Hosts管理相关内容 -->
<p>Hosts管理页面内容待实现</p> <!-- 添加hosts条目表单 -->
<div id="add-hosts-form" class="mb-6 bg-gray-50 p-4 rounded-lg">
<div class="flex items-center space-x-4">
<input type="text" id="hosts-ip" placeholder="IP地址" class="w-32 px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
<input type="text" id="hosts-domain" placeholder="域名" class="flex-1 px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
<button id="save-hosts-btn" class="px-4 py-2 bg-success text-white rounded-md hover:bg-success/90 transition-colors">
保存
</button>
</div> </div>
</div> </div>
<div id="blacklists-content" class="hidden"> <!-- Hosts列表 -->
<!-- 黑名单管理页面内容 --> <div class="overflow-x-auto">
<div class="bg-white rounded-lg p-6 card-shadow"> <table class="min-w-full">
<h3 class="text-lg font-semibold mb-6">黑名单管理</h3> <thead>
<!-- 这里将添加黑名单管理相关内容 --> <tr class="border-b border-gray-200">
<p>黑名单管理页面内容待实现</p> <th class="text-left py-3 px-4 text-sm font-medium text-gray-500">IP地址</th>
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">域名</th>
<th class="text-right py-3 px-4 text-sm font-medium text-gray-500">操作</th>
</tr>
</thead>
<tbody id="hosts-table-body">
<tr>
<td colspan="3" class="py-4 text-center text-gray-500">暂无Hosts条目</td>
</tr>
</tbody>
</table>
</div>
</div> </div>
</div> </div>
<div id="query-content" class="hidden">
<!-- DNS查询页面内容 -->
<div id="query-content" class="hidden space-y-6">
<!-- DNS查询表单 -->
<div class="bg-white rounded-lg p-6 card-shadow"> <div class="bg-white rounded-lg p-6 card-shadow">
<h3 class="text-lg font-semibold mb-6">DNS查询</h3> <h3 class="text-lg font-semibold mb-6">DNS查询</h3>
<!-- 这里将添加DNS查询相关内容 -->
<p>DNS查询页面内容待实现</p> <!-- 查询表单 -->
<div class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4">
<div class="flex-1">
<input type="text" id="dns-query-domain" placeholder="输入域名例如example.com" class="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
</div>
<button id="dns-query-btn" class="px-6 py-3 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors">
<i class="fa fa-search mr-2"></i>查询
</button>
</div>
</div>
<!-- 查询结果展示 -->
<div id="query-result" class="bg-white rounded-lg p-6 card-shadow hidden">
<h3 class="text-lg font-semibold mb-4">查询结果</h3>
<div class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="text-sm font-medium text-gray-500 mb-2">域名</h4>
<p class="text-lg font-semibold" id="result-domain">-</p>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="text-sm font-medium text-gray-500 mb-2">状态</h4>
<p class="text-lg font-semibold" id="result-status">-</p>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="text-sm font-medium text-gray-500 mb-2">屏蔽类型</h4>
<p class="text-lg font-semibold" id="result-type">-</p>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="text-sm font-medium text-gray-500 mb-2">查询时间</h4>
<p class="text-lg font-semibold" id="result-time">-</p>
</div>
</div>
<!-- 详细信息 -->
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="text-sm font-medium text-gray-500 mb-2">详细信息</h4>
<pre class="bg-white p-4 rounded-md border border-gray-200 overflow-x-auto" id="result-details">-</pre>
</div>
</div>
</div>
<!-- 查询历史记录 -->
<div class="bg-white rounded-lg p-6 card-shadow">
<div class="flex items-center justify-between mb-6">
<h3 class="text-lg font-semibold">查询历史</h3>
<button id="clear-history-btn" class="text-sm text-gray-500 hover:text-danger transition-colors">
<i class="fa fa-trash mr-1"></i>清空历史
</button>
</div>
<!-- 历史记录列表 -->
<div id="query-history" class="space-y-3">
<div class="text-center text-gray-500 py-4">
暂无查询历史
</div>
</div>
</div> </div>
</div> </div>
@@ -652,8 +766,86 @@
<!-- 系统设置页面内容 --> <!-- 系统设置页面内容 -->
<div class="bg-white rounded-lg p-6 card-shadow"> <div class="bg-white rounded-lg p-6 card-shadow">
<h3 class="text-lg font-semibold mb-6">系统设置</h3> <h3 class="text-lg font-semibold mb-6">系统设置</h3>
<!-- 这里将添加系统设置相关内容 -->
<p>系统设置页面内容待实现</p> <!-- 配置表单 -->
<form id="config-form">
<!-- DNS配置 -->
<div class="mb-8">
<h4 class="text-md font-medium mb-4">DNS服务器配置</h4>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 gap-6">
<div>
<label for="dns-port" class="block text-sm font-medium text-gray-700 mb-1">端口</label>
<input type="number" id="dns-port" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="53">
</div>
<div>
<label for="dns-timeout" class="block text-sm font-medium text-gray-700 mb-1">超时时间 (秒)</label>
<input type="number" id="dns-timeout" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="5">
</div>
<div class="md:col-span-2">
<label for="dns-upstream-servers" class="block text-sm font-medium text-gray-700 mb-1">上游DNS服务器 (逗号分隔)</label>
<input type="text" id="dns-upstream-servers" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="8.8.8.8, 1.1.1.1">
</div>
<div>
<label for="dns-stats-file" class="block text-sm font-medium text-gray-700 mb-1">统计文件路径</label>
<input type="text" id="dns-stats-file" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="./stats.json">
</div>
<div>
<label for="dns-save-interval" class="block text-sm font-medium text-gray-700 mb-1">保存间隔 (秒)</label>
<input type="number" id="dns-save-interval" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="300">
</div>
</div>
</div>
<!-- HTTP配置 -->
<div class="mb-8">
<h4 class="text-md font-medium mb-4">HTTP服务器配置</h4>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 gap-6">
<div>
<label for="http-port" class="block text-sm font-medium text-gray-700 mb-1">端口</label>
<input type="number" id="http-port" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="8080">
</div>
</div>
</div>
<!-- 屏蔽配置 -->
<div class="mb-8">
<h4 class="text-md font-medium mb-4">屏蔽配置</h4>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 gap-6">
<div>
<label for="shield-local-rules-file" class="block text-sm font-medium text-gray-700 mb-1">本地规则文件</label>
<input type="text" id="shield-local-rules-file" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="./rules.txt">
</div>
<div>
<label for="shield-hosts-file" class="block text-sm font-medium text-gray-700 mb-1">Hosts文件</label>
<input type="text" id="shield-hosts-file" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="/etc/hosts">
</div>
<div>
<label for="shield-update-interval" class="block text-sm font-medium text-gray-700 mb-1">更新间隔 (秒)</label>
<input type="number" id="shield-update-interval" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="3600">
</div>
<div>
<label for="shield-block-method" class="block text-sm font-medium text-gray-700 mb-1">屏蔽方法</label>
<select id="shield-block-method" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
<option value="0.0.0.0">返回0.0.0.0</option>
<option value="NXDOMAIN">返回NXDOMAIN</option>
<option value="refused">返回refused</option>
<option value="emptyIP">返回空IP</option>
<option value="customIP">返回自定义IP</option>
</select>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="flex justify-end space-x-4">
<button type="button" id="restart-service-btn" class="px-6 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:border-transparent">
重启服务
</button>
<button type="button" id="save-config-btn" class="px-6 py-2 bg-primary text-white rounded-md hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
保存配置
</button>
</div>
</form>
</div> </div>
</div> </div>
</div> </div>
@@ -669,5 +861,7 @@
<script src="js/hosts.js"></script> <script src="js/hosts.js"></script>
<script src="js/query.js"></script> <script src="js/query.js"></script>
<script src="js/config.js"></script> <script src="js/config.js"></script>
<!-- 直接渲染滚动列表的静态HTML内容 -->
</body> </body>
</html> </html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -20,33 +20,32 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
options.body = JSON.stringify(data); options.body = JSON.stringify(data);
} }
// 添加超时处理
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('请求超时'));
}, 10000); // 10秒超时
});
try { try {
const response = await fetch(url, options); // 竞争:请求或超时
const response = await Promise.race([fetch(url, options), timeoutPromise]);
// 获取响应文本,用于调试和错误处理 // 获取响应文本,用于调试和错误处理
const responseText = await response.text(); const responseText = await response.text();
if (!response.ok) { if (!response.ok) {
// 尝试解析错误响应 // 优化错误响应处理
let errorData = {}; console.warn(`API请求失败: ${response.status}`);
// 尝试解析JSON但如果失败直接使用原始文本作为错误信息
try { try {
// 首先检查响应文本是否为空或不是有效JSON const errorData = JSON.parse(responseText);
if (!responseText || responseText.trim() === '') { return { error: errorData.error || responseText || `请求失败: ${response.status}` };
console.warn('错误响应为空');
} else {
try {
errorData = JSON.parse(responseText);
} catch (parseError) { } catch (parseError) {
console.error('无法解析错误响应为JSON:', parseError); // 当响应不是有效的JSON时如中文错误信息直接使用原始文本
console.error('原始错误响应文本:', responseText); console.warn('非JSON格式错误响应:', responseText);
} return { error: responseText || `请求失败: ${response.status}` };
}
// 直接返回错误信息,而不是抛出异常,让上层处理
console.warn(`API请求失败: ${response.status}`, errorData);
return { error: errorData.error || `请求失败: ${response.status}` };
} catch (e) {
console.error('处理错误响应时出错:', e);
return { error: `请求处理失败: ${e.message}` };
} }
} }
@@ -55,12 +54,18 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
// 首先检查响应文本是否为空 // 首先检查响应文本是否为空
if (!responseText || responseText.trim() === '') { if (!responseText || responseText.trim() === '') {
console.warn('空响应文本'); console.warn('空响应文本');
return {}; return null; // 返回null表示空响应
} }
// 尝试解析JSON // 尝试解析JSON
const parsedData = JSON.parse(responseText); const parsedData = JSON.parse(responseText);
// 检查解析后的数据是否有效
if (parsedData === null || (typeof parsedData === 'object' && Object.keys(parsedData).length === 0)) {
console.warn('解析后的数据为空');
return null;
}
// 限制所有数字为两位小数 // 限制所有数字为两位小数
const formatNumbers = (obj) => { const formatNumbers = (obj) => {
if (typeof obj === 'number') { if (typeof obj === 'number') {
@@ -93,13 +98,13 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
console.error('位置66附近的字符:', responseText.substring(60, 75)); console.error('位置66附近的字符:', responseText.substring(60, 75));
} }
// 返回空数组作为默认值,避免页面功能完全中断 // 返回错误对象,让上层处理
console.warn('使用默认空数组作为响应'); return { error: 'JSON解析错误' };
return [];
} }
} catch (error) { } catch (error) {
console.error('API请求错误:', error); console.error('API请求错误:', error);
throw error; // 返回错误对象,而不是抛出异常,让上层处理
return { error: error.message };
} }
} }
@@ -120,6 +125,12 @@ const api = {
// 获取最近屏蔽域名 // 获取最近屏蔽域名
getRecentBlockedDomains: () => apiRequest('/recent-blocked?t=' + Date.now()), getRecentBlockedDomains: () => apiRequest('/recent-blocked?t=' + Date.now()),
// 获取TOP客户端
getTopClients: () => apiRequest('/top-clients?t=' + Date.now()),
// 获取TOP域名
getTopDomains: () => apiRequest('/top-domains?t=' + Date.now()),
// 获取小时统计 // 获取小时统计
getHourlyStats: () => apiRequest('/hourly-stats?t=' + Date.now()), getHourlyStats: () => apiRequest('/hourly-stats?t=' + Date.now()),

View File

@@ -197,12 +197,25 @@ function apiRequest(endpoint, method = 'GET', data = null, maxRetries = 3) {
// 数字格式化函数 // 数字格式化函数
function formatNumber(num) { function formatNumber(num) {
// 显示完整数字的最大长度阈值
const MAX_FULL_LENGTH = 5;
// 先获取完整数字字符串
const fullNumStr = num.toString();
// 如果数字长度小于等于阈值,直接返回完整数字
if (fullNumStr.length <= MAX_FULL_LENGTH) {
return fullNumStr;
}
// 否则使用缩写格式
if (num >= 1000000) { if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M'; return (num / 1000000).toFixed(1) + 'M';
} else if (num >= 1000) { } else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K'; return (num / 1000).toFixed(1) + 'K';
} }
return num.toString();
return fullNumStr;
} }
// 确认对话框函数 // 确认对话框函数

View File

@@ -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() { function initConfigPage() {
loadConfig(); loadConfig();
@@ -9,93 +36,183 @@ function initConfigPage() {
// 加载系统配置 // 加载系统配置
async function loadConfig() { async function loadConfig() {
try { try {
const config = await api.getConfig(); const result = await api.getConfig();
populateConfigForm(config);
// 检查API返回的错误
if (result && result.error) {
showErrorMessage('加载配置失败: ' + result.error);
return;
}
populateConfigForm(result);
} catch (error) { } catch (error) {
showErrorMessage('加载配置失败: ' + error.message); // 捕获可能的异常虽然apiRequest不应该再抛出异常
showErrorMessage('加载配置失败: ' + (error.message || '未知错误'));
} }
} }
// 填充配置表单 // 填充配置表单
function populateConfigForm(config) { function populateConfigForm(config) {
// DNS配置 // 安全获取配置对象,防止未定义属性访问
document.getElementById('dns-port')?.value = config.DNSServer.Port || 53; const dnsServerConfig = config.DNSServer || {};
document.getElementById('dns-upstream-servers')?.value = (config.DNSServer.UpstreamServers || []).join(', '); const httpServerConfig = config.HTTPServer || {};
document.getElementById('dns-timeout')?.value = config.DNSServer.Timeout || 5; const shieldConfig = config.Shield || {};
document.getElementById('dns-stats-file')?.value = config.DNSServer.StatsFile || './stats.json';
document.getElementById('dns-save-interval')?.value = config.DNSServer.SaveInterval || 300; // 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配置 // HTTP配置
document.getElementById('http-port')?.value = config.HTTPServer.Port || 8080; setElementValue('http-port', getSafeValue(httpServerConfig.Port, 8080));
document.getElementById('http-host')?.value = config.HTTPServer.Host || '0.0.0.0';
document.getElementById('http-api-enabled')?.checked = config.HTTPServer.APIEnabled !== false;
// 屏蔽配置 // 屏蔽配置
document.getElementById('shield-local-rules-file')?.value = config.Shield.LocalRulesFile || './rules.txt'; setElementValue('shield-local-rules-file', getSafeValue(shieldConfig.LocalRulesFile, 'data/rules.txt'));
document.getElementById('shield-remote-rules-urls')?.value = (config.Shield.RemoteRulesURLs || []).join('\n'); setElementValue('shield-update-interval', getSafeValue(shieldConfig.UpdateInterval, 3600));
document.getElementById('shield-update-interval')?.value = config.Shield.UpdateInterval || 3600; setElementValue('shield-hosts-file', getSafeValue(shieldConfig.HostsFile, 'data/hosts.txt'));
document.getElementById('shield-hosts-file')?.value = config.Shield.HostsFile || '/etc/hosts'; // 使用服务器端接受的屏蔽方法值默认使用NXDOMAIN
document.getElementById('shield-block-method')?.value = config.Shield.BlockMethod || '0.0.0.0'; 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() { async function handleSaveConfig() {
const formData = collectFormData(); const formData = collectFormData();
if (!formData) return;
try { try {
await api.saveConfig(formData); const result = await api.saveConfig(formData);
// 检查API返回的错误
if (result && result.error) {
showErrorMessage('保存配置失败: ' + result.error);
return;
}
showSuccessMessage('配置保存成功'); showSuccessMessage('配置保存成功');
} catch (error) { } catch (error) {
showErrorMessage('保存配置失败: ' + error.message); // 捕获可能的异常虽然apiRequest不应该再抛出异常
showErrorMessage('保存配置失败: ' + (error.message || '未知错误'));
} }
} }
// 重启服务 // 重启服务
async function handleRestartService() { async function handleRestartService() {
if (confirm('确定要重启DNS服务吗重启期间服务可能会短暂不可用。')) { if (!confirm('确定要重启DNS服务吗重启期间服务可能会短暂不可用。')) return;
try { try {
await api.restartService(); const result = await api.restartService();
// 检查API返回的错误
if (result && result.error) {
showErrorMessage('服务重启失败: ' + result.error);
return;
}
showSuccessMessage('服务重启成功'); showSuccessMessage('服务重启成功');
} catch (error) { } catch (error) {
showErrorMessage('重启服务失败: ' + error.message); // 捕获可能的异常虽然apiRequest不应该再抛出异常
} showErrorMessage('重启服务失败: ' + (error.message || '未知错误'));
} }
} }
// 收集表单数据 // 收集表单数据并验证
function collectFormData() { 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 { return {
DNSServer: { DNSServer: {
Port: parseInt(document.getElementById('dns-port')?.value) || 53, Port: dnsPort,
UpstreamServers: document.getElementById('dns-upstream-servers')?.value.split(',').map(s => s.trim()).filter(Boolean) || [], UpstreamServers: upstreamServers,
Timeout: parseInt(document.getElementById('dns-timeout')?.value) || 5, Timeout: timeout,
StatsFile: document.getElementById('dns-stats-file')?.value || './stats.json', StatsFile: getElementValue('dns-stats-file') || './data/stats.json',
SaveInterval: parseInt(document.getElementById('dns-save-interval')?.value) || 300 SaveInterval: saveInterval
}, },
HTTPServer: { HTTPServer: {
Port: parseInt(document.getElementById('http-port')?.value) || 8080, Port: httpPort
Host: document.getElementById('http-host')?.value || '0.0.0.0',
APIEnabled: document.getElementById('http-api-enabled')?.checked !== false
}, },
Shield: { Shield: {
LocalRulesFile: document.getElementById('shield-local-rules-file')?.value || './rules.txt', LocalRulesFile: getElementValue('shield-local-rules-file') || './data/rules.txt',
RemoteRulesURLs: document.getElementById('shield-remote-rules-urls')?.value.split('\n').map(s => s.trim()).filter(Boolean) || [], UpdateInterval: updateInterval,
UpdateInterval: parseInt(document.getElementById('shield-update-interval')?.value) || 3600, HostsFile: getElementValue('shield-hosts-file') || './data/hosts.txt',
HostsFile: document.getElementById('shield-hosts-file')?.value || '/etc/hosts', BlockMethod: getElementValue('shield-block-method') || 'NXDOMAIN'
BlockMethod: document.getElementById('shield-block-method')?.value || '0.0.0.0'
} }
}; };
} }
// 工具函数:安全获取元素值
function getElementValue(elementId) {
const element = document.getElementById(elementId);
if (element && element.tagName === 'INPUT') {
return element.value;
}
return ''; // 默认返回空字符串
}
// 设置事件监听器 // 设置事件监听器
function setupConfigEventListeners() { 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) { function showSuccessMessage(message) {
showNotification(message, 'success'); showNotification(message, 'success');
@@ -118,13 +235,28 @@ function showNotification(message, type = 'info') {
const notification = document.createElement('div'); 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-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') { if (type === 'success') {
notification.classList.add('bg-green-500', 'text-white'); notification.style.backgroundColor = '#10b981';
notification.style.color = 'white';
} else if (type === 'error') { } else if (type === 'error') {
notification.classList.add('bg-red-500', 'text-white'); notification.style.backgroundColor = '#ef4444';
notification.style.color = 'white';
} else { } else {
notification.classList.add('bg-blue-500', 'text-white'); notification.style.backgroundColor = '#3b82f6';
notification.style.color = 'white';
} }
notification.textContent = message; notification.textContent = message;
@@ -132,14 +264,12 @@ function showNotification(message, type = 'info') {
// 显示通知 // 显示通知
setTimeout(() => { setTimeout(() => {
notification.classList.remove('opacity-0'); notification.style.opacity = '1';
notification.classList.add('opacity-100');
}, 10); }, 10);
// 3秒后隐藏通知 // 3秒后隐藏通知
setTimeout(() => { setTimeout(() => {
notification.classList.remove('opacity-100'); notification.style.opacity = '0';
notification.classList.add('opacity-0');
setTimeout(() => { setTimeout(() => {
notification.remove(); notification.remove();
}, 300); }, 300);

File diff suppressed because it is too large Load Diff

View File

@@ -2,94 +2,140 @@
// 初始化Hosts管理页面 // 初始化Hosts管理页面
function initHostsPage() { function initHostsPage() {
loadHostsContent(); // 加载Hosts规则
loadHostsRules();
// 设置事件监听器
setupHostsEventListeners(); setupHostsEventListeners();
} }
// 加载Hosts内容 // 加载Hosts规则
async function loadHostsContent() { async function loadHostsRules() {
try { try {
const hostsContent = await api.getHosts(); const response = await fetch('/api/shield/hosts');
document.getElementById('hosts-content').value = hostsContent; 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) { } catch (error) {
showErrorMessage('加载Hosts文件失败: ' + error.message); console.error('Error loading hosts rules:', error);
showErrorMessage('加载Hosts规则失败');
} }
} }
// 保存Hosts内容 // 更新Hosts表格
async function handleSaveHosts() { function updateHostsTable(hostsRules) {
const hostsContent = document.getElementById('hosts-content').value; const tbody = document.getElementById('hosts-table-body');
try { if (hostsRules.length === 0) {
await api.saveHosts(hostsContent); tbody.innerHTML = '<tr><td colspan="3" class="py-4 text-center text-gray-500">暂无Hosts条目</td></tr>';
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和域名不能为空');
return; return;
} }
// 简单的IP验证 tbody.innerHTML = hostsRules.map(rule => {
const ipRegex = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/; // 处理对象格式的规则
if (!ipRegex.test(ip)) { const ip = rule.ip || '';
showErrorMessage('请输入有效的IP地址'); const domain = rule.domain || '';
return;
}
const hostsTextarea = document.getElementById('hosts-content'); return `
const newEntry = `\n${ip} ${domain}`; <tr class="border-b border-gray-200">
hostsTextarea.value += newEntry; <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('');
// 清空输入框 // 重新绑定删除事件
ipInput.value = ''; document.querySelectorAll('.delete-hosts-btn').forEach(btn => {
domainInput.value = ''; btn.addEventListener('click', handleDeleteHostsRule);
});
// 滚动到文本区域底部
hostsTextarea.scrollTop = hostsTextarea.scrollHeight;
showSuccessMessage('Hosts条目已添加到编辑器');
} }
// 设置事件监听器 // 设置事件监听器
function setupHostsEventListeners() { function setupHostsEventListeners() {
// 保存按钮 // 保存Hosts按钮
document.getElementById('save-hosts-btn')?.addEventListener('click', handleSaveHosts); document.getElementById('save-hosts-btn').addEventListener('click', handleAddHostsRule);
// 刷新按钮
document.getElementById('refresh-hosts-btn')?.addEventListener('click', handleRefreshHosts);
// 添加Hosts条目按钮
document.getElementById('add-hosts-entry-btn')?.addEventListener('click', handleAddHostsEntry);
// 按回车键添加条目
document.getElementById('hosts-domain')?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
handleAddHostsEntry();
} }
// 处理添加Hosts规则
async function handleAddHostsRule() {
const ip = document.getElementById('hosts-ip').value.trim();
const domain = document.getElementById('hosts-domain').value.trim();
if (!ip || !domain) {
showErrorMessage('IP地址和域名不能为空');
return;
}
try {
const response = await fetch('/api/shield/hosts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ ip, domain })
}); });
if (!response.ok) {
throw new Error('Failed to add hosts rule');
}
showSuccessMessage('Hosts规则添加成功');
// 清空输入框
document.getElementById('hosts-ip').value = '';
document.getElementById('hosts-domain').value = '';
// 重新加载规则
loadHostsRules();
} catch (error) {
console.error('Error adding hosts rule:', error);
showErrorMessage('添加Hosts规则失败');
}
}
// 处理删除Hosts规则
async function handleDeleteHostsRule(e) {
const ip = e.target.closest('.delete-hosts-btn').dataset.ip;
const domain = e.target.closest('.delete-hosts-btn').dataset.domain;
try {
const response = await fetch('/api/shield/hosts', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ domain })
});
if (!response.ok) {
throw new Error('Failed to delete hosts rule');
}
showSuccessMessage('Hosts规则删除成功');
// 重新加载规则
loadHostsRules();
} catch (error) {
console.error('Error deleting hosts rule:', error);
showErrorMessage('删除Hosts规则失败');
}
} }
// 显示成功消息 // 显示成功消息
@@ -102,6 +148,8 @@ function showErrorMessage(message) {
showNotification(message, 'error'); showNotification(message, 'error');
} }
// 显示通知 // 显示通知
function showNotification(message, type = 'info') { function showNotification(message, type = 'info') {
// 移除现有通知 // 移除现有通知
@@ -112,7 +160,7 @@ function showNotification(message, type = 'info') {
// 创建新通知 // 创建新通知
const notification = document.createElement('div'); 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') { if (type === 'success') {
@@ -123,18 +171,22 @@ function showNotification(message, type = 'info') {
notification.classList.add('bg-blue-500', 'text-white'); 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); document.body.appendChild(notification);
// 显示通知 // 显示通知
setTimeout(() => { setTimeout(() => {
notification.classList.remove('opacity-0'); notification.classList.remove('opacity-0');
notification.classList.add('opacity-100'); }, 100);
}, 10);
// 3秒后隐藏通知 // 3秒后隐藏通知
setTimeout(() => { setTimeout(() => {
notification.classList.remove('opacity-100');
notification.classList.add('opacity-0'); notification.classList.add('opacity-0');
setTimeout(() => { setTimeout(() => {
notification.remove(); notification.remove();

View File

@@ -8,7 +8,6 @@ function setupNavigation() {
document.getElementById('dashboard-content'), document.getElementById('dashboard-content'),
document.getElementById('shield-content'), document.getElementById('shield-content'),
document.getElementById('hosts-content'), document.getElementById('hosts-content'),
document.getElementById('blacklists-content'),
document.getElementById('query-content'), document.getElementById('query-content'),
document.getElementById('config-content') document.getElementById('config-content')
]; ];
@@ -16,40 +15,105 @@ function setupNavigation() {
menuItems.forEach((item, index) => { menuItems.forEach((item, index) => {
item.addEventListener('click', (e) => { item.addEventListener('click', (e) => {
e.preventDefault(); // 允许浏览器自动更新地址栏中的hash不阻止默认行为
// 更新活跃状态 // 移动端点击菜单项后自动关闭侧边栏
menuItems.forEach(menuItem => { if (window.innerWidth < 768) {
menuItem.classList.remove('sidebar-item-active'); closeSidebar();
});
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');
} }
// 更新页面标题 // 页面特定初始化 - 保留这部分逻辑因为它不会与hashchange事件处理逻辑冲突
pageTitle.textContent = item.querySelector('span').textContent; 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 toggleSidebar = document.getElementById('toggle-sidebar');
const closeSidebarBtn = document.getElementById('close-sidebar');
const sidebar = document.getElementById('sidebar'); const sidebar = document.getElementById('sidebar');
const sidebarOverlay = document.getElementById('sidebar-overlay');
if (toggleSidebar && sidebar) { // 打开侧边栏函数
toggleSidebar.addEventListener('click', () => { function openSidebar() {
sidebar.classList.toggle('-translate-x-full'); 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();
}
});
} }
// 初始化函数 // 初始化函数

View File

@@ -1,314 +1,231 @@
// DNS查询工具页面功能实现 // DNS查询页面功能实现
// 初始化查询工具页面 // 初始化查询页面
function initQueryPage() { function initQueryPage() {
console.log('初始化DNS查询页面...'); console.log('初始化DNS查询页面...');
setupQueryEventListeners(); setupQueryEventListeners();
loadQueryHistory();
// 页面加载时自动显示一些示例数据
setTimeout(() => {
const mockDomain = 'example.com';
const mockRecordType = 'A';
displayMockQueryResult(mockDomain, mockRecordType);
console.log('显示示例DNS查询数据');
}, 500);
} }
// 执行DNS查询 // 执行DNS查询
async function handleDNSQuery() { async function handleDNSQuery() {
// 尝试多种可能的DOM元素ID const domainInput = document.getElementById('dns-query-domain');
const domainInput = document.getElementById('query-domain') || document.getElementById('domain-input');
const recordTypeSelect = document.getElementById('query-record-type') || document.getElementById('record-type');
const resultDiv = document.getElementById('query-result'); const resultDiv = document.getElementById('query-result');
console.log('DOM元素查找结果:', { domainInput, recordTypeSelect, resultDiv }); if (!domainInput || !resultDiv) {
if (!domainInput || !recordTypeSelect || !resultDiv) {
console.error('找不到必要的DOM元素'); console.error('找不到必要的DOM元素');
return; return;
} }
const domain = domainInput.value.trim(); const domain = domainInput.value.trim();
const recordType = recordTypeSelect.value;
if (!domain) { if (!domain) {
showErrorMessage('请输入域名'); showErrorMessage('请输入域名');
return; 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 { try {
// 检查api对象是否存在 const response = await fetch(`/api/query?domain=${encodeURIComponent(domain)}`);
if (!window.api || typeof window.api.queryDNS !== 'function') { if (!response.ok) {
console.warn('api.queryDNS不存在使用模拟数据'); throw new Error('查询失败');
const mockResult = generateMockDNSResult(domain, recordType);
displayQueryResult(mockResult, domain, recordType);
return;
} }
// 调用API适配不同的参数格式 const result = await response.json();
let result; displayQueryResult(result, domain);
try { saveQueryHistory(domain, result);
// 尝试不同的API调用方式 loadQueryHistory();
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);
}
} catch (error) { } catch (error) {
console.error('DNS查询出错:', error); console.error('DNS查询出错:', error);
const mockResult = generateMockDNSResult(domain, recordType); showErrorMessage('查询失败,请稍后重试');
displayQueryResult(mockResult, domain, recordType);
resultDiv.innerHTML += `<div class="text-yellow-500 text-center py-2 text-sm">注意: 显示的是模拟数据</div>`;
} }
} }
// 显示查询结果 // 显示查询结果
function displayQueryResult(result, domain, recordType) { function displayQueryResult(result, domain) {
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) {
const resultDiv = document.getElementById('query-result'); const resultDiv = document.getElementById('query-result');
if (!resultDiv) return; if (!resultDiv) return;
// 显示提示信息 // 显示结果容器
resultDiv.innerHTML = ` resultDiv.classList.remove('hidden');
<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> const status = result.blocked ? '被屏蔽' : '正常';
<div> const statusClass = result.blocked ? 'text-danger' : 'text-success';
<p class="text-sm text-blue-700">这是一个DNS查询工具示例。输入域名并选择记录类型然后点击查询按钮获取DNS记录信息。</p> 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>
<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> </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() { function setupQueryEventListeners() {
// 尝试多种可能的按钮ID // 查询按钮事件
const queryButtons = [ const queryBtn = document.getElementById('dns-query-btn');
document.getElementById('query-btn'), if (queryBtn) {
document.getElementById('query-button'), queryBtn.addEventListener('click', handleDNSQuery);
document.querySelector('button[type="submit"]'), }
...Array.from(document.querySelectorAll('button')).filter(btn =>
btn.textContent && btn.textContent.includes('查询')
)
].filter(Boolean);
// 绑定查询按钮事件 // 输入框回车键事件
queryButtons.forEach(button => { const domainInput = document.getElementById('dns-query-domain');
console.log('绑定查询按钮事件:', button); if (domainInput) {
button.addEventListener('click', handleDNSQuery); domainInput.addEventListener('keypress', (e) => {
});
// 尝试多种可能的输入框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) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
handleDNSQuery(); 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);
}
}
} }
// 设置示例查询 // 清空历史按钮事件
function setExampleQuery(domain, recordType) { const clearHistoryBtn = document.getElementById('clear-history-btn');
const domainInput = document.getElementById('query-domain') || document.getElementById('domain-input'); if (clearHistoryBtn) {
const recordTypeSelect = document.getElementById('query-record-type') || document.getElementById('record-type'); clearHistoryBtn.addEventListener('click', clearQueryHistory);
if (domainInput) domainInput.value = domain;
if (recordTypeSelect) recordTypeSelect.value = recordType;
// 自动执行查询
handleDNSQuery();
} }
}
// 显示成功消息 // 显示成功消息
function showSuccessMessage(message) { function showSuccessMessage(message) {
@@ -330,7 +247,7 @@ function showNotification(message, type = 'info') {
// 创建新通知 // 创建新通知
const notification = document.createElement('div'); 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') { if (type === 'success') {
@@ -341,7 +258,13 @@ function showNotification(message, type = 'info') {
notification.classList.add('bg-blue-500', 'text-white'); 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); document.body.appendChild(notification);
// 显示通知 // 显示通知
@@ -366,3 +289,13 @@ if (document.readyState === 'loading') {
} else { } else {
initQueryPage(); initQueryPage();
} }
// 当切换到DNS查询页面时重新加载数据
document.addEventListener('DOMContentLoaded', () => {
// 监听hash变化当切换到DNS查询页面时重新加载数据
window.addEventListener('hashchange', () => {
if (window.location.hash === '#query') {
initQueryPage();
}
});
});

View File

@@ -263,12 +263,25 @@ function addGlowEffect() {
// 格式化数字 // 格式化数字
function formatNumber(num) { function formatNumber(num) {
// 显示完整数字的最大长度阈值
const MAX_FULL_LENGTH = 5;
// 先获取完整数字字符串
const fullNumStr = num.toString();
// 如果数字长度小于等于阈值,直接返回完整数字
if (fullNumStr.length <= MAX_FULL_LENGTH) {
return fullNumStr;
}
// 否则使用缩写格式
if (num >= 1000000) { if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M'; return (num / 1000000).toFixed(1) + 'M';
} else if (num >= 1000) { } else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K'; return (num / 1000).toFixed(1) + 'K';
} }
return num.toString();
return fullNumStr;
} }
// 在DOM加载完成后初始化 // 在DOM加载完成后初始化

File diff suppressed because it is too large Load Diff

19
static/js/vendor/tailwind.js vendored Normal file
View File

@@ -0,0 +1,19 @@
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#165DFF',
secondary: '#36CFFB',
success: '#00B42A',
warning: '#FF7D00',
danger: '#F53F3F',
info: '#86909C',
dark: '#1D2129',
light: '#F2F3F5',
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
},
}
}

View File

@@ -0,0 +1,77 @@
package main
import (
"flag"
"fmt"
"os/exec"
"strings"
)
// testRuleMatching 测试DNS规则匹配功能
func main() {
// 定义命令行参数
rulePtr := flag.String("rule", "||cntvwb.cn^", "规则字符串")
testDomainPtr := flag.String("domain", "vdapprecv.app.cntvwb.cn", "测试域名")
flag.Parse()
// 打印测试信息
fmt.Printf("测试规则: %s\n", *rulePtr)
fmt.Printf("测试域名: %s\n", *testDomainPtr)
// 发送HTTP请求到API端点来测试规则匹配
fmt.Println("\n测试规则匹配功能...")
cmd := exec.Command("curl", "-s", fmt.Sprintf("http://localhost:8080/api/shield/check?domain=%s&rule=%s", *testDomainPtr, *rulePtr))
output, err := cmd.CombinedOutput()
if err != nil {
// 如果直接的API测试失败尝试另一种方法
fmt.Printf("直接测试失败: %v, %s\n", err, string(output))
fmt.Println("尝试添加规则并测试...")
testAddRuleAndCheck(*rulePtr, *testDomainPtr)
return
}
fmt.Printf("测试结果: %s\n", string(output))
// 验证规则是否生效(模拟测试)
if strings.Contains(*rulePtr, "||cntvwb.cn^") && strings.Contains(*testDomainPtr, "cntvwb.cn") {
fmt.Println("\n验证结果:")
if strings.Contains(*testDomainPtr, "cntvwb.cn") {
fmt.Println("✅ 子域名匹配测试通过:||cntvwb.cn^ 应该阻止所有 cntvwb.cn 的子域名")
} else {
fmt.Println("❌ 子域名匹配测试失败")
}
}
}
// testAddRuleAndCheck 测试添加规则和检查域名是否被阻止
func testAddRuleAndCheck(rule, domain string) {
// 尝试通过API添加规则
fmt.Printf("添加规则: %s\n", rule)
cmd := exec.Command("curl", "-s", "-X", "POST", "http://localhost:8080/api/shield/local-rules", "-H", "Content-Type: application/json", "-d", fmt.Sprintf(`{\"rule\":\"%s\"}`, rule))
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("添加规则失败: %v, %s\n", err, string(output))
// 尝试重新加载规则
fmt.Println("尝试重新加载规则...")
cmd = exec.Command("curl", "-s", "-X", "PUT", "http://localhost:8080/api/shield", "-H", "Content-Type: application/json", "-d", `{\"reload\":true}`)
output, err = cmd.CombinedOutput()
if err != nil {
fmt.Printf("重新加载规则失败: %v, %s\n", err, string(output))
} else {
fmt.Printf("重新加载规则结果: %s\n", string(output))
}
return
}
fmt.Printf("添加规则结果: %s\n", string(output))
// 测试域名是否被阻止
fmt.Printf("测试域名 %s 是否被阻止...\n", domain)
cmd = exec.Command("curl", "-s", fmt.Sprintf("http://localhost:8080/api/shield/check?domain=%s", domain))
output, err = cmd.CombinedOutput()
if err != nil {
fmt.Printf("测试阻止失败: %v, %s\n", err, string(output))
} else {
fmt.Printf("阻止测试结果: %s\n", string(output))
}
}