diff --git a/ReadMe.md b/ReadMe.md
deleted file mode 100644
index 73b8109..0000000
--- a/ReadMe.md
+++ /dev/null
@@ -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查询结果的各种网络环境。
\ No newline at end of file
diff --git a/config.json b/config.json
new file mode 100644
index 0000000..d996e92
--- /dev/null
+++ b/config.json
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/dns/server.go b/dns/server.go
index 730828d..d8fb52f 100644
--- a/dns/server.go
+++ b/dns/server.go
@@ -29,15 +29,24 @@ type BlockedDomain struct {
LastSeen time.Time
}
+// ClientStats 客户端统计
+
+type ClientStats struct {
+ IP string
+ Count int64
+ LastSeen time.Time
+}
+
// StatsData 用于持久化的统计数据结构
type StatsData struct {
- Stats *Stats `json:"stats"`
- BlockedDomains map[string]*BlockedDomain `json:"blockedDomains"`
- ResolvedDomains map[string]*BlockedDomain `json:"resolvedDomains"`
- HourlyStats map[string]int64 `json:"hourlyStats"`
- DailyStats map[string]int64 `json:"dailyStats"`
- MonthlyStats map[string]int64 `json:"monthlyStats"`
- LastSaved time.Time `json:"lastSaved"`
+ Stats *Stats `json:"stats"`
+ BlockedDomains map[string]*BlockedDomain `json:"blockedDomains"`
+ ResolvedDomains map[string]*BlockedDomain `json:"resolvedDomains"`
+ ClientStats map[string]*ClientStats `json:"clientStats"`
+ HourlyStats map[string]int64 `json:"hourlyStats"`
+ DailyStats map[string]int64 `json:"dailyStats"`
+ MonthlyStats map[string]int64 `json:"monthlyStats"`
+ LastSaved time.Time `json:"lastSaved"`
}
// Server DNS服务器
@@ -46,6 +55,7 @@ type Server struct {
shieldConfig *config.ShieldConfig
shieldManager *shield.ShieldManager
server *dns.Server
+ tcpServer *dns.Server
resolver *dns.Client
ctx context.Context
cancel context.CancelFunc
@@ -55,6 +65,8 @@ type Server struct {
blockedDomains map[string]*BlockedDomain
resolvedDomainsMutex sync.RWMutex
resolvedDomains map[string]*BlockedDomain // 用于记录解析的域名
+ clientStatsMutex sync.RWMutex
+ clientStats map[string]*ClientStats // 用于记录客户端统计
hourlyStatsMutex sync.RWMutex
hourlyStats map[string]int64 // 按小时统计屏蔽数量
dailyStatsMutex sync.RWMutex
@@ -64,20 +76,22 @@ type Server struct {
saveTicker *time.Ticker // 用于定时保存数据
startTime time.Time // 服务器启动时间
saveDone chan struct{} // 用于通知保存协程停止
+ stopped bool // 服务器是否已经停止
+ stoppedMutex sync.Mutex // 保护stopped标志的互斥锁
}
// Stats DNS服务器统计信息
type Stats struct {
- Queries int64
- Blocked int64
- Allowed int64
- Errors int64
- LastQuery time.Time
- AvgResponseTime float64 // 平均响应时间(ms)
- TotalResponseTime int64 // 总响应时间
- QueryTypes map[string]int64 // 查询类型统计
- SourceIPs map[string]bool // 活跃来源IP
- CpuUsage float64 // CPU使用率(%)
+ Queries int64
+ Blocked int64
+ Allowed int64
+ Errors int64
+ LastQuery time.Time
+ AvgResponseTime float64 // 平均响应时间(ms)
+ TotalResponseTime int64 // 总响应时间
+ QueryTypes map[string]int64 // 查询类型统计
+ SourceIPs map[string]bool // 活跃来源IP
+ CpuUsage float64 // CPU使用率(%)
}
// NewServer 创建DNS服务器实例
@@ -95,41 +109,59 @@ func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shie
cancel: cancel,
startTime: time.Now(), // 记录服务器启动时间
stats: &Stats{
- Queries: 0,
- Blocked: 0,
- Allowed: 0,
- Errors: 0,
- AvgResponseTime: 0,
+ Queries: 0,
+ Blocked: 0,
+ Allowed: 0,
+ Errors: 0,
+ AvgResponseTime: 0,
TotalResponseTime: 0,
- QueryTypes: make(map[string]int64),
- SourceIPs: make(map[string]bool),
- CpuUsage: 0,
+ QueryTypes: make(map[string]int64),
+ SourceIPs: make(map[string]bool),
+ CpuUsage: 0,
},
blockedDomains: make(map[string]*BlockedDomain),
resolvedDomains: make(map[string]*BlockedDomain),
+ clientStats: make(map[string]*ClientStats),
hourlyStats: make(map[string]int64),
dailyStats: make(map[string]int64),
monthlyStats: make(map[string]int64),
saveDone: make(chan struct{}),
+ stopped: false, // 初始化为未停止状态
}
-
+
// 加载已保存的统计数据
server.loadStatsData()
-
+
return server
-
+
}
// Start 启动DNS服务器
func (s *Server) Start() error {
+ // 重新初始化上下文和取消函数
+ ctx, cancel := context.WithCancel(context.Background())
+ s.ctx = ctx
+ s.cancel = cancel
+
+ // 重新初始化saveDone通道
+ s.saveDone = make(chan struct{})
+
+ // 重置stopped标志
+ s.stoppedMutex.Lock()
+ s.stopped = false
+ s.stoppedMutex.Unlock()
+
+ // 更新服务器启动时间
+ s.startTime = time.Now()
+
s.server = &dns.Server{
Addr: fmt.Sprintf(":%d", s.config.Port),
Net: "udp",
Handler: dns.HandlerFunc(s.handleDNSRequest),
}
- // 启动TCP服务器(用于大型响应)
- tcpServer := &dns.Server{
+ // 保存TCP服务器实例,以便在Stop方法中关闭
+ s.tcpServer = &dns.Server{
Addr: fmt.Sprintf(":%d", s.config.Port),
Net: "tcp",
Handler: dns.HandlerFunc(s.handleDNSRequest),
@@ -138,6 +170,9 @@ func (s *Server) Start() error {
// 启动CPU使用率监控
go s.startCpuUsageMonitor()
+ // 启动自动保存功能
+ go s.startAutoSave()
+
// 启动UDP服务
go func() {
logger.Info(fmt.Sprintf("DNS UDP服务器启动,监听端口: %d", s.config.Port))
@@ -150,7 +185,7 @@ func (s *Server) Start() error {
// 启动TCP服务
go func() {
logger.Info(fmt.Sprintf("DNS TCP服务器启动,监听端口: %d", s.config.Port))
- if err := tcpServer.ListenAndServe(); err != nil {
+ if err := s.tcpServer.ListenAndServe(); err != nil {
logger.Error("DNS TCP服务器启动失败", "error", err)
s.cancel()
}
@@ -163,31 +198,44 @@ func (s *Server) Start() error {
// Stop 停止DNS服务器
func (s *Server) Stop() {
+ // 检查服务器是否已经停止
+ s.stoppedMutex.Lock()
+ if s.stopped {
+ s.stoppedMutex.Unlock()
+ return // 服务器已经停止,直接返回
+ }
+ // 标记服务器为已停止状态
+ s.stopped = true
+ s.stoppedMutex.Unlock()
+
// 发送停止信号给保存协程
close(s.saveDone)
-
+
// 最后保存一次数据
s.saveStatsData()
-
+
// 停止服务器
s.cancel()
if s.server != nil {
s.server.Shutdown()
}
+ if s.tcpServer != nil {
+ s.tcpServer.Shutdown()
+ }
logger.Info("DNS服务器已停止")
}
// handleDNSRequest 处理DNS请求
func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
startTime := time.Now()
-
+
// 获取来源IP
sourceIP := w.RemoteAddr().String()
// 提取IP地址部分,去掉端口
if idx := strings.LastIndex(sourceIP, ":"); idx >= 0 {
sourceIP = sourceIP[:idx]
}
-
+
// 更新来源IP统计
s.updateStats(func(stats *Stats) {
stats.Queries++
@@ -195,6 +243,9 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
stats.SourceIPs[sourceIP] = true
})
+ // 更新客户端统计
+ s.updateClientStats(sourceIP)
+
// 只处理递归查询
if r.RecursionDesired == false {
response := new(dns.Msg)
@@ -202,7 +253,7 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
response.RecursionAvailable = true
response.SetRcode(r, dns.RcodeRefused)
w.WriteMsg(response)
-
+
// 计算响应时间
responseTime := time.Since(startTime).Milliseconds()
s.updateStats(func(stats *Stats) {
@@ -315,11 +366,6 @@ func (s *Server) handleBlockedResponse(w dns.ResponseWriter, r *dns.Msg, domain
// 更新被屏蔽域名统计
s.updateBlockedDomainStats(domain)
- // 更新总体统计
- s.updateStats(func(stats *Stats) {
- stats.Blocked++
- })
-
response := new(dns.Msg)
response.SetReply(r)
response.RecursionAvailable = true
@@ -432,19 +478,19 @@ func (s *Server) updateBlockedDomainStats(domain string) {
// 更新统计数据
now := time.Now()
-
+
// 更新小时统计
hourKey := now.Format("2006-01-02-15")
s.hourlyStatsMutex.Lock()
s.hourlyStats[hourKey]++
s.hourlyStatsMutex.Unlock()
-
+
// 更新每日统计
dayKey := now.Format("2006-01-02")
s.dailyStatsMutex.Lock()
s.dailyStats[dayKey]++
s.dailyStatsMutex.Unlock()
-
+
// 更新每月统计
monthKey := now.Format("2006-01")
s.monthlyStatsMutex.Lock()
@@ -452,6 +498,23 @@ func (s *Server) updateBlockedDomainStats(domain string) {
s.monthlyStatsMutex.Unlock()
}
+// updateClientStats 更新客户端统计
+func (s *Server) updateClientStats(ip string) {
+ s.clientStatsMutex.Lock()
+ defer s.clientStatsMutex.Unlock()
+
+ if entry, exists := s.clientStats[ip]; exists {
+ entry.Count++
+ entry.LastSeen = time.Now()
+ } else {
+ s.clientStats[ip] = &ClientStats{
+ IP: ip,
+ Count: 1,
+ LastSeen: time.Now(),
+ }
+ }
+}
+
// updateResolvedDomainStats 更新解析域名统计
func (s *Server) updateResolvedDomainStats(domain string) {
s.resolvedDomainsMutex.Lock()
@@ -500,16 +563,16 @@ func (s *Server) GetStats() *Stats {
// 返回统计信息的副本
return &Stats{
- Queries: s.stats.Queries,
- Blocked: s.stats.Blocked,
- Allowed: s.stats.Allowed,
- Errors: s.stats.Errors,
- LastQuery: s.stats.LastQuery,
- AvgResponseTime: s.stats.AvgResponseTime,
+ Queries: s.stats.Queries,
+ Blocked: s.stats.Blocked,
+ Allowed: s.stats.Allowed,
+ Errors: s.stats.Errors,
+ LastQuery: s.stats.LastQuery,
+ AvgResponseTime: s.stats.AvgResponseTime,
TotalResponseTime: s.stats.TotalResponseTime,
- QueryTypes: queryTypesCopy,
- SourceIPs: sourceIPsCopy,
- CpuUsage: s.stats.CpuUsage,
+ QueryTypes: queryTypesCopy,
+ SourceIPs: sourceIPsCopy,
+ CpuUsage: s.stats.CpuUsage,
}
}
@@ -582,6 +645,29 @@ func (s *Server) GetRecentBlockedDomains(limit int) []BlockedDomain {
return domains
}
+// GetTopClients 获取TOP客户端列表
+func (s *Server) GetTopClients(limit int) []ClientStats {
+ s.clientStatsMutex.RLock()
+ defer s.clientStatsMutex.RUnlock()
+
+ // 转换为切片
+ clients := make([]ClientStats, 0, len(s.clientStats))
+ for _, entry := range s.clientStats {
+ clients = append(clients, *entry)
+ }
+
+ // 按请求次数排序
+ sort.Slice(clients, func(i, j int) bool {
+ return clients[i].Count > clients[j].Count
+ })
+
+ // 返回限制数量
+ if len(clients) > limit {
+ return clients[:limit]
+ }
+ return clients
+}
+
// GetHourlyStats 获取每小时统计数据
func (s *Server) GetHourlyStats() map[string]int64 {
s.hourlyStatsMutex.RLock()
@@ -667,19 +753,26 @@ func (s *Server) loadStatsData() {
s.hourlyStats = statsData.HourlyStats
}
s.hourlyStatsMutex.Unlock()
-
+
s.dailyStatsMutex.Lock()
if statsData.DailyStats != nil {
s.dailyStats = statsData.DailyStats
}
s.dailyStatsMutex.Unlock()
-
+
s.monthlyStatsMutex.Lock()
if statsData.MonthlyStats != nil {
s.monthlyStats = statsData.MonthlyStats
}
s.monthlyStatsMutex.Unlock()
+ // 加载客户端统计数据
+ s.clientStatsMutex.Lock()
+ if statsData.ClientStats != nil {
+ s.clientStats = statsData.ClientStats
+ }
+ s.clientStatsMutex.Unlock()
+
logger.Info("统计数据加载成功")
}
@@ -689,18 +782,25 @@ func (s *Server) saveStatsData() {
return
}
- // 创建数据目录
- statsDir := filepath.Dir(s.config.StatsFile)
- err := os.MkdirAll(statsDir, 0755)
+ // 获取绝对路径以避免工作目录问题
+ statsFilePath, err := filepath.Abs(s.config.StatsFile)
if err != nil {
- logger.Error("创建统计数据目录失败", "error", err)
+ logger.Error("获取统计文件绝对路径失败", "path", s.config.StatsFile, "error", err)
+ return
+ }
+
+ // 创建数据目录
+ statsDir := filepath.Dir(statsFilePath)
+ err = os.MkdirAll(statsDir, 0755)
+ if err != nil {
+ logger.Error("创建统计数据目录失败", "dir", statsDir, "error", err)
return
}
// 收集所有统计数据
statsData := &StatsData{
- Stats: s.GetStats(),
- LastSaved: time.Now(),
+ Stats: s.GetStats(),
+ LastSaved: time.Now(),
}
// 复制域名数据
@@ -724,14 +824,14 @@ func (s *Server) saveStatsData() {
statsData.HourlyStats[k] = v
}
s.hourlyStatsMutex.RUnlock()
-
+
s.dailyStatsMutex.RLock()
statsData.DailyStats = make(map[string]int64)
for k, v := range s.dailyStats {
statsData.DailyStats[k] = v
}
s.dailyStatsMutex.RUnlock()
-
+
s.monthlyStatsMutex.RLock()
statsData.MonthlyStats = make(map[string]int64)
for k, v := range s.monthlyStats {
@@ -739,6 +839,14 @@ func (s *Server) saveStatsData() {
}
s.monthlyStatsMutex.RUnlock()
+ // 复制客户端统计数据
+ s.clientStatsMutex.RLock()
+ statsData.ClientStats = make(map[string]*ClientStats)
+ for k, v := range s.clientStats {
+ statsData.ClientStats[k] = v
+ }
+ s.clientStatsMutex.RUnlock()
+
// 序列化数据
jsonData, err := json.MarshalIndent(statsData, "", " ")
if err != nil {
@@ -747,13 +855,13 @@ func (s *Server) saveStatsData() {
}
// 写入文件
- err = ioutil.WriteFile(s.config.StatsFile, jsonData, 0644)
+ err = os.WriteFile(statsFilePath, jsonData, 0644)
if err != nil {
- logger.Error("保存统计数据到文件失败", "error", err)
+ logger.Error("保存统计数据到文件失败", "file", statsFilePath, "error", err)
return
}
- logger.Info("统计数据保存成功")
+ logger.Info("统计数据保存成功", "file", statsFilePath)
}
// startCpuUsageMonitor 启动CPU使用率监控
@@ -778,7 +886,7 @@ func (s *Server) startCpuUsageMonitor() {
cpuUsage = 0.0
logger.Error("获取系统CPU使用率失败", "error", err)
}
-
+
s.updateStats(func(stats *Stats) {
stats.CpuUsage = cpuUsage
})
@@ -798,7 +906,7 @@ func getSystemCpuUsage(prevIdle, prevTotal *uint64) (float64, error) {
defer file.Close()
var cpuUser, cpuNice, cpuSystem, cpuIdle, cpuIowait, cpuIrq, cpuSoftirq, cpuSteal uint64
- _, err = fmt.Fscanf(file, "cpu %d %d %d %d %d %d %d %d",
+ _, err = fmt.Fscanf(file, "cpu %d %d %d %d %d %d %d %d",
&cpuUser, &cpuNice, &cpuSystem, &cpuIdle, &cpuIowait, &cpuIrq, &cpuSoftirq, &cpuSteal)
if err != nil {
return 0, err
diff --git a/go.mod b/go.mod
index 433ba33..d0d9f6e 100644
--- a/go.mod
+++ b/go.mod
@@ -8,17 +8,31 @@ require (
github.com/gorilla/websocket v1.5.1
github.com/miekg/dns v1.1.68
github.com/sirupsen/logrus v1.9.3
+ github.com/swaggo/http-swagger v1.2.0
)
// 清理不需要的依赖
// 之前的go.sum可能包含lumberjack的记录,但现在已经不再使用
require (
+ github.com/KyleBanks/depth v1.2.1 // indirect
+ github.com/PuerkitoBio/purell v1.1.1 // indirect
+ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
+ github.com/go-openapi/jsonpointer v0.19.5 // indirect
+ github.com/go-openapi/jsonreference v0.19.6 // indirect
+ github.com/go-openapi/spec v0.20.4 // indirect
+ github.com/go-openapi/swag v0.19.15 // indirect
github.com/google/go-cmp v0.7.0 // indirect
+ github.com/josharian/intern v1.0.0 // indirect
+ github.com/mailru/easyjson v0.7.6 // indirect
github.com/stretchr/testify v1.10.0 // indirect
+ github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2 // indirect
+ github.com/swaggo/swag v1.7.8 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
+ golang.org/x/text v0.27.0 // indirect
golang.org/x/tools v0.34.0 // indirect
+ gopkg.in/yaml.v2 v2.4.0 // indirect
)
diff --git a/go.sum b/go.sum
index 068bbb2..d7334cd 100644
--- a/go.sum
+++ b/go.sum
@@ -1,32 +1,126 @@
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
+github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
+github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
+github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/agiledragon/gomonkey/v2 v2.3.1 h1:k+UnUY0EMNYUFUAQVETGY9uUTxjMdnUkP0ARyJS1zzs=
+github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
+github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
+github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
+github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
+github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
+github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
+github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
+github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE=
+github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
+github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
+github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
+github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
+github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2 h1:+iNTcqQJy0OZ5jk6a5NLib47eqXK8uYcPX+O4+cBpEM=
+github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
+github.com/swaggo/http-swagger v1.2.0 h1:G5EBD5nvw379l2sFhact660YDT++eLviczLPrgNw/lU=
+github.com/swaggo/http-swagger v1.2.0/go.mod h1:P7+V1SLG2zloe+VvAGL7WgFimhJACaBLAv2N7YQ0ikI=
+github.com/swaggo/swag v1.7.8 h1:w249t0l/kc/DKMGlS0fppNJQxKyJ8heNaUWB6nsH3zc=
+github.com/swaggo/swag v1.7.8/go.mod h1:gZ+TJ2w/Ve1RwQsA2IRoSOTidHz6DX+PIG8GWvbnoLU=
+github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
+github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
+golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
+golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/hosts.txt b/hosts.txt
deleted file mode 100644
index 6f4c647..0000000
--- a/hosts.txt
+++ /dev/null
@@ -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
\ No newline at end of file
diff --git a/http/server.go b/http/server.go
index bcd83e2..76f6e62 100644
--- a/http/server.go
+++ b/http/server.go
@@ -10,11 +10,12 @@ import (
"sync"
"time"
- "github.com/gorilla/websocket"
"dns-server/config"
"dns-server/dns"
"dns-server/logger"
"dns-server/shield"
+
+ "github.com/gorilla/websocket"
)
// Server HTTP控制台服务器
@@ -24,7 +25,7 @@ type Server struct {
dnsServer *dns.Server
shieldManager *shield.ShieldManager
server *http.Server
-
+
// WebSocket相关字段
upgrader websocket.Upgrader
clients map[*websocket.Conn]bool
@@ -50,10 +51,10 @@ func NewServer(globalConfig *config.Config, dnsServer *dns.Server, shieldManager
clients: make(map[*websocket.Conn]bool),
broadcastChan: make(chan []byte, 100),
}
-
+
// 启动广播协程
go server.startBroadcastLoop()
-
+
return server
}
@@ -63,16 +64,43 @@ func (s *Server) Start() error {
// API路由
if s.config.EnableAPI {
+ // 重定向/api到Swagger UI页面
+ mux.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
+ http.Redirect(w, r, "/api/index.html", http.StatusMovedPermanently)
+ })
+
+ // 注册所有API端点
mux.HandleFunc("/api/stats", s.handleStats)
mux.HandleFunc("/api/shield", s.handleShield)
+ mux.HandleFunc("/api/shield/localrules", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ if r.Method == http.MethodGet {
+ localRules := s.shieldManager.GetLocalRules()
+ json.NewEncoder(w).Encode(localRules)
+ return
+ }
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ })
+ mux.HandleFunc("/api/shield/remoterules", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ if r.Method == http.MethodGet {
+ remoteRules := s.shieldManager.GetRemoteRules()
+ json.NewEncoder(w).Encode(remoteRules)
+ return
+ }
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ })
mux.HandleFunc("/api/shield/hosts", s.handleShieldHosts)
mux.HandleFunc("/api/shield/blacklists", s.handleShieldBlacklists)
mux.HandleFunc("/api/query", s.handleQuery)
mux.HandleFunc("/api/status", s.handleStatus)
mux.HandleFunc("/api/config", s.handleConfig)
+ mux.HandleFunc("/api/config/restart", s.handleRestart)
// 添加统计相关接口
mux.HandleFunc("/api/top-blocked", s.handleTopBlockedDomains)
mux.HandleFunc("/api/top-resolved", s.handleTopResolvedDomains)
+ mux.HandleFunc("/api/top-clients", s.handleTopClients)
+ mux.HandleFunc("/api/top-domains", s.handleTopDomains)
mux.HandleFunc("/api/recent-blocked", s.handleRecentBlockedDomains)
mux.HandleFunc("/api/hourly-stats", s.handleHourlyStats)
mux.HandleFunc("/api/daily-stats", s.handleDailyStats)
@@ -80,10 +108,22 @@ func (s *Server) Start() error {
mux.HandleFunc("/api/query/type", s.handleQueryTypeStats)
// WebSocket端点
mux.HandleFunc("/ws/stats", s.handleWebSocketStats)
+
+ // 将/api/下的静态文件服务指向static/api目录,放在最后以避免覆盖API端点
+ apiFileServer := http.FileServer(http.Dir("./static/api"))
+ mux.Handle("/api/", http.StripPrefix("/api", apiFileServer))
}
- // 静态文件服务(可后续添加前端界面)
- mux.Handle("/", http.FileServer(http.Dir("./static")))
+ // 自定义静态文件服务处理器,用于禁用浏览器缓存,放在API路由之后
+ fileServer := http.FileServer(http.Dir("./static"))
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ // 添加Cache-Control头,禁用浏览器缓存
+ w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
+ w.Header().Set("Pragma", "no-cache")
+ w.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
+ // 使用StripPrefix处理路径
+ http.StripPrefix("/", fileServer).ServeHTTP(w, r)
+ })
s.server = &http.Server{
Addr: fmt.Sprintf("%s:%d", s.config.Host, s.config.Port),
@@ -105,6 +145,14 @@ func (s *Server) Stop() {
}
// handleStats 处理统计信息请求
+// @Summary 获取系统统计信息
+// @Description 获取DNS服务器和Shield的统计信息
+// @Tags stats
+// @Accept json
+// @Produce json
+// @Success 200 {object} map[string]interface{} "统计信息"
+// @Failure 500 {object} map[string]string "服务器内部错误"
+// @Router /api/stats [get]
func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
@@ -131,27 +179,27 @@ func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
// 格式化平均响应时间为两位小数
formattedResponseTime := float64(int(dnsStats.AvgResponseTime*100)) / 100
-
+
// 构建响应数据,确保所有字段都反映服务器的真实状态
stats := map[string]interface{}{
"dns": map[string]interface{}{
- "Queries": dnsStats.Queries,
- "Blocked": dnsStats.Blocked,
- "Allowed": dnsStats.Allowed,
- "Errors": dnsStats.Errors,
- "LastQuery": dnsStats.LastQuery,
- "AvgResponseTime": formattedResponseTime,
+ "Queries": dnsStats.Queries,
+ "Blocked": dnsStats.Blocked,
+ "Allowed": dnsStats.Allowed,
+ "Errors": dnsStats.Errors,
+ "LastQuery": dnsStats.LastQuery,
+ "AvgResponseTime": formattedResponseTime,
"TotalResponseTime": dnsStats.TotalResponseTime,
- "QueryTypes": dnsStats.QueryTypes,
- "SourceIPs": dnsStats.SourceIPs,
- "CpuUsage": dnsStats.CpuUsage,
+ "QueryTypes": dnsStats.QueryTypes,
+ "SourceIPs": dnsStats.SourceIPs,
+ "CpuUsage": dnsStats.CpuUsage,
},
- "shield": shieldStats,
- "topQueryType": topQueryType,
- "activeIPs": activeIPCount,
+ "shield": shieldStats,
+ "topQueryType": topQueryType,
+ "activeIPs": activeIPCount,
"avgResponseTime": formattedResponseTime,
- "cpuUsage": dnsStats.CpuUsage,
- "time": time.Now(),
+ "cpuUsage": dnsStats.CpuUsage,
+ "time": time.Now(),
}
w.Header().Set("Content-Type", "application/json")
@@ -197,25 +245,25 @@ func (s *Server) handleWebSocketStats(w http.ResponseWriter, r *http.Request) {
case <-ticker.C:
// 获取最新统计数据
currentStats := s.buildStatsData()
-
+
// 检查数据是否有变化
if !s.areStatsEqual(lastStats, currentStats) {
// 数据有变化,发送更新
data, err := json.Marshal(map[string]interface{}{
- "type": "stats_update",
- "data": currentStats,
- "time": time.Now(),
+ "type": "stats_update",
+ "data": currentStats,
+ "time": time.Now(),
})
if err != nil {
logger.Error(fmt.Sprintf("序列化统计数据失败: %v", err))
continue
}
-
+
if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
logger.Error(fmt.Sprintf("发送WebSocket消息失败: %v", err))
return
}
-
+
// 更新最后发送的数据
lastStats = currentStats
}
@@ -235,9 +283,9 @@ func (s *Server) handleWebSocketStats(w http.ResponseWriter, r *http.Request) {
func (s *Server) sendInitialStats(conn *websocket.Conn) error {
stats := s.buildStatsData()
data, err := json.Marshal(map[string]interface{}{
- "type": "initial_data",
- "data": stats,
- "time": time.Now(),
+ "type": "initial_data",
+ "data": stats,
+ "time": time.Now(),
})
if err != nil {
return err
@@ -267,25 +315,25 @@ func (s *Server) buildStatsData() map[string]interface{} {
// 格式化平均响应时间
formattedResponseTime := float64(int(dnsStats.AvgResponseTime*100)) / 100
-
+
return map[string]interface{}{
"dns": map[string]interface{}{
- "Queries": dnsStats.Queries,
- "Blocked": dnsStats.Blocked,
- "Allowed": dnsStats.Allowed,
- "Errors": dnsStats.Errors,
- "LastQuery": dnsStats.LastQuery,
- "AvgResponseTime": formattedResponseTime,
+ "Queries": dnsStats.Queries,
+ "Blocked": dnsStats.Blocked,
+ "Allowed": dnsStats.Allowed,
+ "Errors": dnsStats.Errors,
+ "LastQuery": dnsStats.LastQuery,
+ "AvgResponseTime": formattedResponseTime,
"TotalResponseTime": dnsStats.TotalResponseTime,
- "QueryTypes": dnsStats.QueryTypes,
- "SourceIPs": dnsStats.SourceIPs,
- "CpuUsage": dnsStats.CpuUsage,
+ "QueryTypes": dnsStats.QueryTypes,
+ "SourceIPs": dnsStats.SourceIPs,
+ "CpuUsage": dnsStats.CpuUsage,
},
- "shield": shieldStats,
- "topQueryType": topQueryType,
- "activeIPs": activeIPCount,
+ "shield": shieldStats,
+ "topQueryType": topQueryType,
+ "activeIPs": activeIPCount,
"avgResponseTime": formattedResponseTime,
- "cpuUsage": dnsStats.CpuUsage,
+ "cpuUsage": dnsStats.CpuUsage,
}
}
@@ -294,20 +342,20 @@ func (s *Server) areStatsEqual(stats1, stats2 map[string]interface{}) bool {
if stats1 == nil || stats2 == nil {
return false
}
-
+
// 只比较关键数值,避免频繁更新
if dns1, ok1 := stats1["dns"].(map[string]interface{}); ok1 {
if dns2, ok2 := stats2["dns"].(map[string]interface{}); ok2 {
// 检查主要计数器
if dns1["Queries"] != dns2["Queries"] ||
- dns1["Blocked"] != dns2["Blocked"] ||
- dns1["Allowed"] != dns2["Allowed"] ||
- dns1["Errors"] != dns2["Errors"] {
+ dns1["Blocked"] != dns2["Blocked"] ||
+ dns1["Allowed"] != dns2["Allowed"] ||
+ dns1["Errors"] != dns2["Errors"] {
return false
}
}
}
-
+
return true
}
@@ -493,7 +541,7 @@ func (s *Server) handleQueryTypeStats(w http.ResponseWriter, r *http.Request) {
// 获取DNS统计数据
dnsStats := s.dnsServer.GetStats()
-
+
// 转换为前端需要的格式
result := make([]map[string]interface{}, 0, len(dnsStats.QueryTypes))
for queryType, count := range dnsStats.QueryTypes {
@@ -512,13 +560,99 @@ func (s *Server) handleQueryTypeStats(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(result)
}
-// handleShield 处理屏蔽规则管理请求
+// handleTopClients 处理TOP客户端请求
+func (s *Server) handleTopClients(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ // 获取TOP客户端列表
+ clients := s.dnsServer.GetTopClients(10)
+
+ // 转换为前端需要的格式
+ result := make([]map[string]interface{}, len(clients))
+ for i, client := range clients {
+ result[i] = map[string]interface{}{
+ "ip": client.IP,
+ "count": client.Count,
+ "lastSeen": client.LastSeen,
+ }
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(result)
+}
+
+// handleTopDomains 处理TOP域名请求
+func (s *Server) handleTopDomains(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ // 获取TOP被屏蔽域名
+ blockedDomains := s.dnsServer.GetTopBlockedDomains(10)
+ // 获取TOP已解析域名
+ resolvedDomains := s.dnsServer.GetTopResolvedDomains(10)
+
+ // 合并并去重域名统计
+ domainMap := make(map[string]int64)
+ for _, domain := range blockedDomains {
+ domainMap[domain.Domain] += domain.Count
+ }
+ for _, domain := range resolvedDomains {
+ domainMap[domain.Domain] += domain.Count
+ }
+
+ // 转换为切片并排序
+ domainList := make([]map[string]interface{}, 0, len(domainMap))
+ for domain, count := range domainMap {
+ domainList = append(domainList, map[string]interface{}{
+ "domain": domain,
+ "count": count,
+ })
+ }
+
+ // 按计数降序排序
+ sort.Slice(domainList, func(i, j int) bool {
+ return domainList[i]["count"].(int64) > domainList[j]["count"].(int64)
+ })
+
+ // 返回限制数量
+ if len(domainList) > 10 {
+ domainList = domainList[:10]
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(domainList)
+}
+
+// handleShield 处理Shield相关操作
+// @Summary 管理Shield配置
+// @Description 获取或更新Shield的配置信息
+// @Tags shield
+// @Accept json
+// @Produce json
+// @Param config body map[string]interface{} false "Shield配置信息"
+// @Success 200 {object} map[string]interface{} "配置信息"
+// @Failure 400 {object} map[string]string "请求参数错误"
+// @Failure 500 {object} map[string]string "服务器内部错误"
+// @Router /api/shield [get]
+// @Router /api/shield [post]
func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
- // 返回屏蔽规则的基本配置信息和统计数据,不返回完整规则列表
+ // 默认处理逻辑
switch r.Method {
case http.MethodGet:
+ // 检查是否需要返回完整规则列表
+ if r.URL.Query().Get("all") == "true" {
+ // 返回完整规则数据
+ rules := s.shieldManager.GetRules()
+ json.NewEncoder(w).Encode(rules)
+ return
+ }
// 获取规则统计信息
stats := s.shieldManager.GetStats()
shieldInfo := map[string]interface{}{
@@ -533,6 +667,8 @@ func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) {
}
json.NewEncoder(w).Encode(shieldInfo)
return
+ }
+ switch r.Method {
case http.MethodPost:
// 添加屏蔽规则
var req struct {
@@ -583,7 +719,22 @@ func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) {
}
}
-// handleShieldBlacklists 处理远程黑名单管理请求
+// handleShieldBlacklists 处理黑名单相关操作
+// @Summary 管理黑名单
+// @Description 处理黑名单的CRUD操作,包括获取列表、添加、更新和删除黑名单
+// @Tags shield
+// @Accept json
+// @Produce json
+// @Param name path string false "黑名单名称(用于删除操作)"
+// @Param blacklist body map[string]interface{} false "黑名单信息(用于添加/更新操作)"
+// @Success 200 {object} map[string]interface{} "操作成功"
+// @Failure 400 {object} map[string]string "请求参数错误"
+// @Failure 404 {object} map[string]string "黑名单不存在"
+// @Failure 500 {object} map[string]string "服务器内部错误"
+// @Router /api/shield/blacklists [get]
+// @Router /api/shield/blacklists [post]
+// @Router /api/shield/blacklists [put]
+// @Router /api/shield/blacklists/{name} [delete]
func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
@@ -601,7 +752,7 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request)
}
if targetURLOrName == "" {
- http.Error(w, "黑名单标识不能为空", http.StatusBadRequest)
+ json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "黑名单标识不能为空"})
return
}
@@ -616,7 +767,7 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request)
}
if targetIndex == -1 {
- http.Error(w, "黑名单不存在", http.StatusNotFound)
+ json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "黑名单不存在"})
return
}
@@ -624,6 +775,14 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request)
blacklists[targetIndex].LastUpdateTime = time.Now().Format(time.RFC3339)
// 保存更新后的黑名单列表
s.shieldManager.UpdateBlacklist(blacklists)
+ // 更新全局配置中的黑名单
+ s.globalConfig.Shield.Blacklists = blacklists
+ // 保存配置到文件
+ if err := saveConfigToFile(s.globalConfig, "config.json"); err != nil {
+ logger.Error("保存配置文件失败", "error", err)
+ json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "保存配置失败"})
+ return
+ }
// 重新加载规则以获取最新的远程规则
s.shieldManager.LoadRules()
@@ -646,6 +805,14 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request)
}
s.shieldManager.UpdateBlacklist(newBlacklists)
+ // 更新全局配置中的黑名单
+ s.globalConfig.Shield.Blacklists = newBlacklists
+ // 保存配置到文件
+ if err := saveConfigToFile(s.globalConfig, "config.json"); err != nil {
+ logger.Error("保存配置文件失败", "error", err)
+ json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "保存配置失败"})
+ return
+ }
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
return
}
@@ -664,12 +831,12 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request)
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- http.Error(w, "Invalid request body", http.StatusBadRequest)
+ json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "无效的请求体"})
return
}
if req.Name == "" || req.URL == "" {
- http.Error(w, "Name and URL are required", http.StatusBadRequest)
+ json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "名称和URL不能为空"})
return
}
@@ -679,11 +846,17 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request)
// 检查是否已存在
for _, list := range blacklists {
if list.URL == req.URL {
- http.Error(w, "黑名单URL已存在", http.StatusConflict)
+ json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "黑名单URL已存在"})
return
}
}
+ // 检查URL是否存在且可访问
+ if !checkURLExists(req.URL) {
+ json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "URL不存在或无法访问"})
+ return
+ }
+
// 添加新黑名单
newEntry := config.BlacklistEntry{
Name: req.Name,
@@ -693,6 +866,14 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request)
blacklists = append(blacklists, newEntry)
s.shieldManager.UpdateBlacklist(blacklists)
+ // 更新全局配置中的黑名单
+ s.globalConfig.Shield.Blacklists = blacklists
+ // 保存配置到文件
+ if err := saveConfigToFile(s.globalConfig, "config.json"); err != nil {
+ logger.Error("保存配置文件失败", "error", err)
+ json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "保存配置失败"})
+ return
+ }
// 重新加载规则以获取新添加的远程规则
s.shieldManager.LoadRules()
@@ -700,21 +881,47 @@ func (s *Server) handleShieldBlacklists(w http.ResponseWriter, r *http.Request)
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
case http.MethodPut:
- // 更新所有远程黑名单
- blacklists := s.shieldManager.GetBlacklists()
- for i := range blacklists {
- // 更新每个黑名单的时间戳
- blacklists[i].LastUpdateTime = time.Now().Format(time.RFC3339)
+ // 更新黑名单列表(包括启用/禁用状态)
+ var updatedBlacklists []struct {
+ Name string `json:"Name" json:"name"`
+ URL string `json:"URL" json:"url"`
+ Enabled bool `json:"Enabled" json:"enabled"`
+ LastUpdateTime string `json:"LastUpdateTime" json:"lastUpdateTime"`
}
- s.shieldManager.UpdateBlacklist(blacklists)
- // 重新加载所有规则
+ if err := json.NewDecoder(r.Body).Decode(&updatedBlacklists); err != nil {
+ json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "无效的请求体"})
+ return
+ }
+
+ // 转换为config.BlacklistEntry类型
+ var newBlacklists []config.BlacklistEntry
+ for _, entry := range updatedBlacklists {
+ newBlacklists = append(newBlacklists, config.BlacklistEntry{
+ Name: entry.Name,
+ URL: entry.URL,
+ Enabled: entry.Enabled,
+ LastUpdateTime: entry.LastUpdateTime,
+ })
+ }
+
+ // 更新黑名单
+ s.shieldManager.UpdateBlacklist(newBlacklists)
+ // 更新全局配置中的黑名单
+ s.globalConfig.Shield.Blacklists = newBlacklists
+ // 保存配置到文件
+ if err := saveConfigToFile(s.globalConfig, "config.json"); err != nil {
+ logger.Error("保存配置文件失败", "error", err)
+ json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "保存配置失败"})
+ return
+ }
+ // 重新加载规则
s.shieldManager.LoadRules()
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
default:
- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ json.NewEncoder(w).Encode(map[string]string{"status": "error", "message": "Method not allowed"})
}
}
@@ -820,25 +1027,25 @@ func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
}
stats := s.dnsServer.GetStats()
-
+
// 使用服务器的实际启动时间计算准确的运行时间
serverStartTime := s.dnsServer.GetStartTime()
uptime := time.Since(serverStartTime)
-
+
// 构建包含所有真实服务器统计数据的响应
status := map[string]interface{}{
- "status": "running",
- "queries": stats.Queries,
- "blocked": stats.Blocked,
- "allowed": stats.Allowed,
- "errors": stats.Errors,
- "lastQuery": stats.LastQuery,
+ "status": "running",
+ "queries": stats.Queries,
+ "blocked": stats.Blocked,
+ "allowed": stats.Allowed,
+ "errors": stats.Errors,
+ "lastQuery": stats.LastQuery,
"avgResponseTime": stats.AvgResponseTime,
- "activeIPs": len(stats.SourceIPs),
- "startTime": serverStartTime,
- "uptime": uptime,
- "cpuUsage": stats.CpuUsage,
- "timestamp": time.Now(),
+ "activeIPs": len(stats.SourceIPs),
+ "startTime": serverStartTime,
+ "uptime": uptime.Milliseconds(), // 转换为毫秒数,方便前端处理
+ "cpuUsage": stats.CpuUsage,
+ "timestamp": time.Now(),
}
w.Header().Set("Content-Type", "application/json")
@@ -991,3 +1198,55 @@ func isValidIP(ip string) bool {
}
return true
}
+
+// checkURLExists 检查URL是否存在且可访问
+func checkURLExists(url string) bool {
+ // 创建一个带有超时的HTTP客户端
+ client := &http.Client{
+ Timeout: 5 * time.Second,
+ }
+
+ // 发送HEAD请求来检查URL是否存在
+ resp, err := client.Head(url)
+ if err != nil {
+ return false
+ }
+ defer resp.Body.Close()
+
+ // 检查状态码,2xx和3xx表示成功
+ return resp.StatusCode >= 200 && resp.StatusCode < 400
+}
+
+// handleRestart 处理重启服务请求
+func (s *Server) handleRestart(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ logger.Info("收到重启服务请求")
+
+ // 停止DNS服务器
+ s.dnsServer.Stop()
+
+ // 重新加载屏蔽规则
+ if err := s.shieldManager.LoadRules(); err != nil {
+ logger.Error("重新加载屏蔽规则失败", "error", err)
+ }
+
+ // 重新启动DNS服务器
+ go func() {
+ if err := s.dnsServer.Start(); err != nil {
+ logger.Error("DNS服务器重启失败", "error", err)
+ }
+ }()
+
+ // 重新启动定时更新任务
+ s.shieldManager.StopAutoUpdate()
+ s.shieldManager.StartAutoUpdate()
+
+ // 返回成功响应
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "服务已重启"})
+ logger.Info("服务重启成功")
+}
diff --git a/index.html b/index.html
deleted file mode 100644
index e4934e0..0000000
--- a/index.html
+++ /dev/null
@@ -1,1079 +0,0 @@
-
-
-
-
-
- DNS服务器控制台
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
仪表盘
-
-
-
-
-
-
-
-
-

-
管理员
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
DNS请求趋势
-
-
-
-
-
-
-
-
-
-
-
-
-
-
DNS请求趋势详细图表
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
客户端排行
-
-
- 加载中...
-
-
-
- 加载失败
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
本地规则管理
-
-
-
-
-
-
-
-
-
- | 规则 |
- 状态 |
- 操作 |
-
-
-
-
- | 暂无规则 |
-
-
-
-
-
-
-
-
-
远程黑名单管理
-
-
-
-
-
-
-
-
-
- | 名称 |
- URL |
- 状态 |
- |
- 操作 |
-
-
-
-
- | 暂无黑名单 |
-
-
-
-
-
-
-
-
-
-
-
-
-
Hosts条目管理
-
-
-
-
-
-
-
-
-
- | IP地址 |
- 域名 |
- 操作 |
-
-
-
-
- | 暂无Hosts条目 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
查询历史
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/main.go b/main.go
index 9f1e7d9..7c3cf6c 100644
--- a/main.go
+++ b/main.go
@@ -1,3 +1,14 @@
+// DNS Server API
+// @title DNS Server API
+// @version 1.0
+// @description DNS服务器API文档
+// @termsOfService http://swagger.io/terms/
+// @contact.name API Support
+// @contact.email support@example.com
+// @license.name Apache 2.0
+// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
+// @host localhost:8080
+// @BasePath /api
package main
import (
@@ -6,6 +17,7 @@ import (
"log"
"os"
"os/signal"
+ "path/filepath"
"syscall"
"dns-server/config"
@@ -15,6 +27,122 @@ import (
"dns-server/shield"
)
+// createDefaultConfig 创建默认配置文件
+func createDefaultConfig(configFile string) error {
+ // 默认配置内容
+ defaultConfig := `{
+ "dns": {
+ "port": 53,
+ "upstreamDNS": [
+ "223.5.5.5:53",
+ "223.6.6.6:53"
+ ],
+ "timeout": 5000,
+ "statsFile": "./data/stats.json",
+ "saveInterval": 300
+ },
+ "http": {
+ "port": 8081,
+ "host": "0.0.0.0",
+ "enableAPI": true
+ },
+ "shield": {
+ "localRulesFile": "data/rules.txt",
+ "blacklists": [
+ {
+ "name": "AdGuard DNS filter",
+ "url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/filter.txt",
+ "enabled": true
+ },
+ {
+ "name": "Adaway Default Blocklist",
+ "url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/adaway.txt",
+ "enabled": true
+ },
+ {
+ "name": "CHN-anti-AD",
+ "url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/list/easylist.txt",
+ "enabled": true
+ },
+ {
+ "name": "My GitHub Rules",
+ "url": "https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/costomize.txt",
+ "enabled": true
+ }
+ ],
+ "updateInterval": 3600,
+ "hostsFile": "data/hosts.txt",
+ "blockMethod": "NXDOMAIN",
+ "customBlockIP": "",
+ "statsFile": "./data/shield_stats.json",
+ "statsSaveInterval": 60,
+ "remoteRulesCacheDir": "./data/remote_rules"
+ },
+ "log": {
+ "file": "logs/dns-server.log",
+ "level": "debug",
+ "maxSize": 100,
+ "maxBackups": 10,
+ "maxAge": 30
+ }
+}`
+
+ // 写入默认配置到文件
+ return os.WriteFile(configFile, []byte(defaultConfig), 0644)
+}
+
+// createRequiredFiles 创建所需的文件和文件夹
+func createRequiredFiles(cfg *config.Config) error {
+ // 创建数据文件夹
+ dataDir := "./data"
+ if err := os.MkdirAll(dataDir, 0755); err != nil {
+ return fmt.Errorf("创建数据文件夹失败: %w", err)
+ }
+
+ // 创建远程规则缓存文件夹
+ if err := os.MkdirAll(cfg.Shield.RemoteRulesCacheDir, 0755); err != nil {
+ return fmt.Errorf("创建远程规则缓存文件夹失败: %w", err)
+ }
+
+ // 创建日志文件夹
+ logDir := filepath.Dir(cfg.Log.File)
+ if logDir != "." {
+ if err := os.MkdirAll(logDir, 0755); err != nil {
+ return fmt.Errorf("创建日志文件夹失败: %w", err)
+ }
+ }
+
+ // 创建本地规则文件
+ if _, err := os.Stat(cfg.Shield.LocalRulesFile); os.IsNotExist(err) {
+ if err := os.WriteFile(cfg.Shield.LocalRulesFile, []byte("# 本地规则文件\n# 格式:域名\n# 例如:example.com\n"), 0644); err != nil {
+ return fmt.Errorf("创建本地规则文件失败: %w", err)
+ }
+ }
+
+ // 创建Hosts文件
+ if _, err := os.Stat(cfg.Shield.HostsFile); os.IsNotExist(err) {
+ if err := os.WriteFile(cfg.Shield.HostsFile, []byte("# Hosts文件\n# 格式:IP 域名\n# 例如:127.0.0.1 localhost\n"), 0644); err != nil {
+ return fmt.Errorf("创建Hosts文件失败: %w", err)
+ }
+ }
+
+ // 创建统计数据文件
+ if _, err := os.Stat(cfg.DNS.StatsFile); os.IsNotExist(err) {
+ if err := os.WriteFile(cfg.DNS.StatsFile, []byte("{}"), 0644); err != nil {
+ return fmt.Errorf("创建统计数据文件失败: %w", err)
+ }
+ }
+
+ // 创建Shield统计数据文件
+ if _, err := os.Stat(cfg.Shield.StatsFile); os.IsNotExist(err) {
+ if err := os.WriteFile(cfg.Shield.StatsFile, []byte("{}"), 0644); err != nil {
+ return fmt.Errorf("创建Shield统计数据文件失败: %w", err)
+ }
+ }
+
+ return nil
+}
+
func main() {
// 命令行参数解析
var configFile string
@@ -32,6 +160,15 @@ func main() {
os.Exit(0)
}
+ // 检查配置文件是否存在,如果不存在则创建默认配置文件
+ if _, err := os.Stat(configFile); os.IsNotExist(err) {
+ log.Printf("配置文件 %s 不存在,正在创建默认配置文件...", configFile)
+ if err := createDefaultConfig(configFile); err != nil {
+ log.Fatalf("创建默认配置文件失败: %v", err)
+ }
+ log.Printf("默认配置文件 %s 创建成功", configFile)
+ }
+
// 初始化配置
var cfg *config.Config
var err error
@@ -40,6 +177,13 @@ func main() {
log.Fatalf("加载配置失败: %v", err)
}
+ // 创建所需的文件和文件夹
+ log.Println("正在创建所需的文件和文件夹...")
+ if err := createRequiredFiles(cfg); err != nil {
+ log.Fatalf("创建所需文件和文件夹失败: %v", err)
+ }
+ log.Println("所需文件和文件夹创建成功")
+
// 初始化日志系统
if err := logger.InitLogger(cfg.Log.File, cfg.Log.Level, 0, 0, 0); err != nil {
log.Fatalf("初始化日志系统失败: %v", err)
@@ -99,7 +243,7 @@ func daemonize() error {
return fmt.Errorf("打开/dev/null失败: %w", err)
}
defer nullFile.Close()
-
+
// 重定向文件描述符
err = syscall.Dup2(int(nullFile.Fd()), int(os.Stdin.Fd()))
if err != nil {
@@ -113,13 +257,13 @@ func daemonize() error {
if err != nil {
return fmt.Errorf("重定向stderr失败: %w", err)
}
-
+
// 2. 创建新的会话和进程组
_, err = syscall.Setsid()
if err != nil {
return fmt.Errorf("创建新会话失败: %w", err)
}
-
+
fmt.Println("守护进程已启动")
return nil
}
diff --git a/rules.txt b/rules.txt
deleted file mode 100644
index bd67aa8..0000000
--- a/rules.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-||hm.baidu.com
-||baidu.com
-/.*tracking.*/
-/adjust.net/
-/ad./
\ No newline at end of file
diff --git a/shield/manager.go b/shield/manager.go
index 356cbd6..18c0463 100644
--- a/shield/manager.go
+++ b/shield/manager.go
@@ -19,12 +19,6 @@ import (
"dns-server/logger"
)
-// regexRule 正则规则结构,包含编译后的表达式和原始字符串
-type regexRule struct {
- pattern *regexp.Regexp
- original string
-}
-
// ShieldStatsData 用于持久化的Shield统计数据
type ShieldStatsData struct {
BlockedDomainsCount map[string]int `json:"blockedDomainsCount"`
@@ -32,45 +26,65 @@ type ShieldStatsData struct {
LastSaved time.Time `json:"lastSaved"`
}
+// regexRule 正则规则结构,包含编译后的表达式和原始字符串
+type regexRule struct {
+ pattern *regexp.Regexp
+ original string
+ isLocal bool // 是否为本地规则
+ source string // 规则来源
+}
+
// ShieldManager 屏蔽管理器
type ShieldManager struct {
- config *config.ShieldConfig
- domainRules map[string]bool
- domainExceptions map[string]bool
- regexRules []regexRule
- regexExceptions []regexRule
- hostsMap map[string]string
- blockedDomainsCount map[string]int
- resolvedDomainsCount map[string]int
- rulesMutex sync.RWMutex
- updateCtx context.Context
- updateCancel context.CancelFunc
- updateRunning bool
- localRulesCount int // 本地规则数量
- remoteRulesCount int // 远程规则数量
+ config *config.ShieldConfig
+ domainRules map[string]bool
+ domainExceptions map[string]bool
+ domainRulesIsLocal map[string]bool // 标记域名规则是否为本地规则
+ domainExceptionsIsLocal map[string]bool // 标记域名排除规则是否为本地规则
+ domainRulesSource map[string]string // 标记域名规则来源
+ domainExceptionsSource map[string]string // 标记域名排除规则来源
+ domainRulesOriginal map[string]string // 存储域名规则的原始字符串
+ domainExceptionsOriginal map[string]string // 存储域名排除规则的原始字符串
+ regexRules []regexRule
+ regexExceptions []regexRule
+ hostsMap map[string]string
+ blockedDomainsCount map[string]int
+ resolvedDomainsCount map[string]int
+ rulesMutex sync.RWMutex
+ updateCtx context.Context
+ updateCancel context.CancelFunc
+ updateRunning bool
+ localRulesCount int // 本地规则数量
+ remoteRulesCount int // 远程规则数量
}
// NewShieldManager 创建屏蔽管理器实例
func NewShieldManager(config *config.ShieldConfig) *ShieldManager {
ctx, cancel := context.WithCancel(context.Background())
manager := &ShieldManager{
- config: config,
- domainRules: make(map[string]bool),
- domainExceptions: make(map[string]bool),
- regexRules: []regexRule{},
- regexExceptions: []regexRule{},
- hostsMap: make(map[string]string),
- blockedDomainsCount: make(map[string]int),
- resolvedDomainsCount: make(map[string]int),
- updateCtx: ctx,
- updateCancel: cancel,
- localRulesCount: 0,
- remoteRulesCount: 0,
+ config: config,
+ domainRules: make(map[string]bool),
+ domainExceptions: make(map[string]bool),
+ domainRulesIsLocal: make(map[string]bool),
+ domainExceptionsIsLocal: make(map[string]bool),
+ domainRulesSource: make(map[string]string),
+ domainExceptionsSource: make(map[string]string),
+ domainRulesOriginal: make(map[string]string),
+ domainExceptionsOriginal: make(map[string]string),
+ regexRules: []regexRule{},
+ regexExceptions: []regexRule{},
+ hostsMap: make(map[string]string),
+ blockedDomainsCount: make(map[string]int),
+ resolvedDomainsCount: make(map[string]int),
+ updateCtx: ctx,
+ updateCancel: cancel,
+ localRulesCount: 0,
+ remoteRulesCount: 0,
}
-
+
// 加载已保存的计数数据
manager.loadStatsData()
-
+
return manager
}
@@ -82,6 +96,12 @@ func (m *ShieldManager) LoadRules() error {
// 清空现有规则
m.domainRules = make(map[string]bool)
m.domainExceptions = make(map[string]bool)
+ m.domainRulesIsLocal = make(map[string]bool)
+ m.domainExceptionsIsLocal = make(map[string]bool)
+ m.domainRulesSource = make(map[string]string)
+ m.domainExceptionsSource = make(map[string]string)
+ m.domainRulesOriginal = make(map[string]string)
+ m.domainExceptionsOriginal = make(map[string]string)
m.regexRules = []regexRule{}
m.regexExceptions = []regexRule{}
m.hostsMap = make(map[string]string)
@@ -134,7 +154,7 @@ func (m *ShieldManager) loadLocalRules() error {
if line == "" || strings.HasPrefix(line, "#") {
continue
}
- m.parseRule(line)
+ m.parseRule(line, true, "本地规则") // 本地规则,isLocal=true,来源为"本地规则"
}
// 更新本地规则计数
@@ -187,11 +207,11 @@ func (m *ShieldManager) shouldUpdateCache(cacheFile string) bool {
func (m *ShieldManager) fetchRemoteRules(url string) error {
// 获取缓存文件路径
cacheFile := m.getCacheFilePath(url)
-
+
// 尝试从缓存加载
hasLoadedFromCache := false
if !m.shouldUpdateCache(cacheFile) {
- if err := m.loadCachedRules(cacheFile); err == nil {
+ if err := m.loadCachedRules(cacheFile, url); err == nil {
logger.Info("从缓存加载远程规则", "url", url)
hasLoadedFromCache = true
}
@@ -236,14 +256,14 @@ func (m *ShieldManager) fetchRemoteRules(url string) error {
if line == "" || strings.HasPrefix(line, "#") {
continue
}
- m.parseRule(line)
+ m.parseRule(line, false, url) // 远程规则,isLocal=false,来源为URL
}
return nil
}
// loadCachedRules 从缓存文件加载规则
-func (m *ShieldManager) loadCachedRules(filePath string) error {
+func (m *ShieldManager) loadCachedRules(filePath string, source string) error {
file, err := os.Open(filePath)
if err != nil {
return err
@@ -265,7 +285,7 @@ func (m *ShieldManager) loadCachedRules(filePath string) error {
if line == "" || strings.HasPrefix(line, "#") {
continue
}
- m.parseRule(line)
+ m.parseRule(line, false, source) // 远程规则,isLocal=false,来源为URL
}
// 更新远程规则计数
@@ -318,7 +338,10 @@ func (m *ShieldManager) loadHosts() error {
}
// parseRule 解析规则行
-func (m *ShieldManager) parseRule(line string) {
+func (m *ShieldManager) parseRule(line string, isLocal bool, source string) {
+ // 保存原始规则用于后续使用
+ originalLine := line
+
// 处理注释
if strings.HasPrefix(line, "!") || strings.HasPrefix(line, "#") || line == "" {
return
@@ -343,12 +366,12 @@ func (m *ShieldManager) parseRule(line string) {
case strings.HasPrefix(line, "||") && strings.HasSuffix(line, "^"):
// AdGuardHome域名规则格式: ||example.com^
domain := strings.TrimSuffix(strings.TrimPrefix(line, "||"), "^")
- m.addDomainRule(domain, !isException)
+ m.addDomainRule(domain, !isException, isLocal, source, originalLine)
case strings.HasPrefix(line, "||"):
// 精确域名匹配规则
domain := strings.TrimPrefix(line, "||")
- m.addDomainRule(domain, !isException)
+ m.addDomainRule(domain, !isException, isLocal, source, originalLine)
case strings.HasPrefix(line, "*"):
// 通配符规则,转换为正则表达式
@@ -356,15 +379,17 @@ func (m *ShieldManager) parseRule(line string) {
pattern = "^" + pattern + "$"
if re, err := regexp.Compile(pattern); err == nil {
// 保存原始规则字符串
- m.addRegexRule(re, line, !isException)
+ m.addRegexRule(re, originalLine, !isException, isLocal, source)
}
case strings.HasPrefix(line, "/") && strings.HasSuffix(line, "/"):
- // 正则表达式规则
+ // 正则表达式匹配规则:/regex/ 格式,不区分大小写
pattern := strings.TrimPrefix(strings.TrimSuffix(line, "/"), "/")
- if re, err := regexp.Compile(pattern); err == nil {
+ // 编译为不区分大小写的正则表达式,确保能匹配域名中任意位置
+ // 对于像 /domain/ 这样的规则,应该匹配包含 domain 字符串的任何域名
+ if re, err := regexp.Compile("(?i).*" + regexp.QuoteMeta(pattern) + ".*"); err == nil {
// 保存原始规则字符串
- m.addRegexRule(re, line, !isException)
+ m.addRegexRule(re, originalLine, !isException, isLocal, source)
}
case strings.HasPrefix(line, "|") && strings.HasSuffix(line, "|"):
@@ -373,7 +398,7 @@ func (m *ShieldManager) parseRule(line string) {
// 将URL模式转换为正则表达式
pattern := "^" + regexp.QuoteMeta(urlPattern) + "$"
if re, err := regexp.Compile(pattern); err == nil {
- m.addRegexRule(re, line, !isException)
+ m.addRegexRule(re, originalLine, !isException, isLocal, source)
}
case strings.HasPrefix(line, "|"):
@@ -381,7 +406,7 @@ func (m *ShieldManager) parseRule(line string) {
urlPattern := strings.TrimPrefix(line, "|")
pattern := "^" + regexp.QuoteMeta(urlPattern)
if re, err := regexp.Compile(pattern); err == nil {
- m.addRegexRule(re, line, !isException)
+ m.addRegexRule(re, originalLine, !isException, isLocal, source)
}
case strings.HasSuffix(line, "|"):
@@ -389,12 +414,12 @@ func (m *ShieldManager) parseRule(line string) {
urlPattern := strings.TrimSuffix(line, "|")
pattern := regexp.QuoteMeta(urlPattern) + "$"
if re, err := regexp.Compile(pattern); err == nil {
- m.addRegexRule(re, line, !isException)
+ m.addRegexRule(re, originalLine, !isException, isLocal, source)
}
default:
// 默认作为普通域名规则
- m.addDomainRule(line, !isException)
+ m.addDomainRule(line, !isException, isLocal, source, originalLine)
}
}
@@ -418,42 +443,65 @@ func (m *ShieldManager) parseRuleOptions(optionsStr string) map[string]string {
}
// addDomainRule 添加域名规则,支持是否为阻止规则
-func (m *ShieldManager) addDomainRule(domain string, block bool) {
+func (m *ShieldManager) addDomainRule(domain string, block bool, isLocal bool, source string, original string) {
if block {
- m.domainRules[domain] = true
- // 添加所有子域名的匹配支持
- parts := strings.Split(domain, ".")
- if len(parts) > 1 {
- // 为二级域名和顶级域名添加规则
- for i := 0; i < len(parts)-1; i++ {
- subdomain := strings.Join(parts[i:], ".")
- m.domainRules[subdomain] = true
+ // 如果是远程规则,检查是否已经存在本地规则,如果存在则不覆盖
+ if !isLocal {
+ if _, exists := m.domainRulesIsLocal[domain]; exists && m.domainRulesIsLocal[domain] {
+ // 已经存在本地规则,不覆盖
+ return
}
}
+ m.domainRules[domain] = true
+ m.domainRulesIsLocal[domain] = isLocal
+ m.domainRulesSource[domain] = source
+ m.domainRulesOriginal[domain] = original
} else {
// 添加到排除规则
- m.domainExceptions[domain] = true
- // 为子域名也添加排除规则
- parts := strings.Split(domain, ".")
- if len(parts) > 1 {
- for i := 0; i < len(parts)-1; i++ {
- subdomain := strings.Join(parts[i:], ".")
- m.domainExceptions[subdomain] = true
+ // 如果是远程规则,检查是否已经存在本地规则,如果存在则不覆盖
+ if !isLocal {
+ if _, exists := m.domainExceptionsIsLocal[domain]; exists && m.domainExceptionsIsLocal[domain] {
+ // 已经存在本地规则,不覆盖
+ return
}
}
+ m.domainExceptions[domain] = true
+ m.domainExceptionsIsLocal[domain] = isLocal
+ m.domainExceptionsSource[domain] = source
+ m.domainExceptionsOriginal[domain] = original
}
}
// addRegexRule 添加正则表达式规则,支持是否为阻止规则
-func (m *ShieldManager) addRegexRule(re *regexp.Regexp, original string, block bool) {
+func (m *ShieldManager) addRegexRule(re *regexp.Regexp, original string, block bool, isLocal bool, source string) {
rule := regexRule{
pattern: re,
original: original,
+ isLocal: isLocal,
+ source: source,
}
if block {
+ // 如果是远程规则,检查是否已经存在相同的本地规则,如果存在则不添加
+ if !isLocal {
+ for _, existingRule := range m.regexRules {
+ if existingRule.original == original && existingRule.isLocal {
+ // 已经存在相同的本地规则,不添加
+ return
+ }
+ }
+ }
m.regexRules = append(m.regexRules, rule)
} else {
// 添加到排除规则
+ // 如果是远程规则,检查是否已经存在相同的本地规则,如果存在则不添加
+ if !isLocal {
+ for _, existingRule := range m.regexExceptions {
+ if existingRule.original == original && existingRule.isLocal {
+ // 已经存在相同的本地规则,不添加
+ return
+ }
+ }
+ }
m.regexExceptions = append(m.regexExceptions, rule)
}
}
@@ -471,15 +519,16 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
}
result := map[string]interface{}{
- "domain": domain,
- "blocked": false,
- "blockRule": "",
- "blockRuleType": "",
- "excluded": false,
- "excludeRule": "",
+ "domain": domain,
+ "blocked": false,
+ "blockRule": "",
+ "blockRuleType": "",
+ "blocksource": "",
+ "excluded": false,
+ "excludeRule": "",
"excludeRuleType": "",
- "hasHosts": false,
- "hostsIP": "",
+ "hasHosts": false,
+ "hostsIP": "",
}
// 检查hosts记录
@@ -491,8 +540,9 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
// 检查域名排除规则
if m.domainExceptions[domain] {
result["excluded"] = true
- result["excludeRule"] = domain
+ result["excludeRule"] = m.domainExceptionsOriginal[domain]
result["excludeRuleType"] = "exact_domain"
+ result["blocksource"] = m.domainExceptionsSource[domain]
return result
}
@@ -502,8 +552,9 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
subdomain := strings.Join(parts[i:], ".")
if m.domainExceptions[subdomain] {
result["excluded"] = true
- result["excludeRule"] = subdomain
+ result["excludeRule"] = m.domainExceptionsOriginal[subdomain]
result["excludeRuleType"] = "subdomain"
+ result["blocksource"] = m.domainExceptionsSource[subdomain]
return result
}
}
@@ -514,16 +565,18 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
result["excluded"] = true
result["excludeRule"] = re.original
result["excludeRuleType"] = "regex"
+ result["blocksource"] = re.source
return result
}
}
- // 检查阻止规则
+ // 检查阻止规则 - 先检查精确域名匹配,再检查子域名匹配
// 检查精确域名匹配
if m.domainRules[domain] {
result["blocked"] = true
- result["blockRule"] = domain
+ result["blockRule"] = m.domainRulesOriginal[domain]
result["blockRuleType"] = "exact_domain"
+ result["blocksource"] = m.domainRulesSource[domain]
return result
}
@@ -533,8 +586,9 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
subdomain := strings.Join(parts[i:], ".")
if m.domainRules[subdomain] {
result["blocked"] = true
- result["blockRule"] = subdomain
+ result["blockRule"] = m.domainRulesOriginal[subdomain]
result["blockRuleType"] = "subdomain"
+ result["blocksource"] = m.domainRulesSource[subdomain]
return result
}
}
@@ -545,6 +599,7 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
result["blocked"] = true
result["blockRule"] = re.original
result["blockRuleType"] = "regex"
+ result["blocksource"] = re.source
return result
}
}
@@ -667,13 +722,13 @@ func (m *ShieldManager) GetHostsIP(domain string) (string, bool) {
return ip, exists
}
-// AddRule 添加屏蔽规则
+// AddRule 添加屏蔽规则,用户添加的规则是本地规则
func (m *ShieldManager) AddRule(rule string) error {
m.rulesMutex.Lock()
defer m.rulesMutex.Unlock()
- // 解析并添加规则到内存
- m.parseRule(rule)
+ // 解析并添加规则到内存,isLocal=true表示本地规则,来源为"本地规则"
+ m.parseRule(rule, true, "本地规则")
// 持久化保存规则到文件
if m.config.LocalRulesFile != "" {
@@ -724,6 +779,8 @@ func (m *ShieldManager) RemoveRule(rule string) error {
domain := strings.TrimPrefix(format, "@@||")
if _, exists := m.domainExceptions[domain]; exists {
delete(m.domainExceptions, domain)
+ delete(m.domainExceptionsIsLocal, domain)
+ delete(m.domainExceptionsSource, domain)
removed = true
break
}
@@ -731,19 +788,28 @@ func (m *ShieldManager) RemoveRule(rule string) error {
// 尝试删除域名规则
domain := strings.TrimPrefix(format, "||")
if _, exists := m.domainRules[domain]; exists {
+ // 删除主域名规则
delete(m.domainRules, domain)
+ delete(m.domainRulesIsLocal, domain)
+ delete(m.domainRulesSource, domain)
removed = true
break
}
} else {
// 尝试直接作为域名删除
if _, exists := m.domainRules[format]; exists {
+ // 删除主域名规则
delete(m.domainRules, format)
+ delete(m.domainRulesIsLocal, format)
+ delete(m.domainRulesSource, format)
removed = true
break
}
if _, exists := m.domainExceptions[format]; exists {
+ // 删除主排除规则
delete(m.domainExceptions, format)
+ delete(m.domainExceptionsIsLocal, format)
+ delete(m.domainExceptionsSource, format)
removed = true
break
}
@@ -752,12 +818,10 @@ func (m *ShieldManager) RemoveRule(rule string) error {
// 处理正则表达式规则
if !removed && strings.HasPrefix(cleanRule, "/") && strings.HasSuffix(cleanRule, "/") {
- pattern := strings.TrimPrefix(strings.TrimSuffix(cleanRule, "/"), "/")
-
// 检查是否在正则表达式规则中
newRegexRules := []regexRule{}
for _, re := range m.regexRules {
- if re.pattern.String() != pattern {
+ if re.original != rule && re.original != cleanRule {
newRegexRules = append(newRegexRules, re)
} else {
removed = true
@@ -769,7 +833,7 @@ func (m *ShieldManager) RemoveRule(rule string) error {
if !removed {
newRegexExceptions := []regexRule{}
for _, re := range m.regexExceptions {
- if re.pattern.String() != pattern {
+ if re.original != rule && re.original != cleanRule {
newRegexExceptions = append(newRegexExceptions, re)
} else {
removed = true
@@ -785,6 +849,8 @@ func (m *ShieldManager) RemoveRule(rule string) error {
for domain := range m.domainRules {
if domain == cleanRule || domain == rule {
delete(m.domainRules, domain)
+ delete(m.domainRulesIsLocal, domain)
+ delete(m.domainRulesSource, domain)
removed = true
break
}
@@ -794,6 +860,8 @@ func (m *ShieldManager) RemoveRule(rule string) error {
for domain := range m.domainExceptions {
if domain == cleanRule || domain == rule {
delete(m.domainExceptions, domain)
+ delete(m.domainExceptionsIsLocal, domain)
+ delete(m.domainExceptionsSource, domain)
removed = true
break
}
@@ -801,6 +869,36 @@ func (m *ShieldManager) RemoveRule(rule string) error {
}
}
+ // 如果没有删除任何规则,尝试删除可能的子域名规则
+ if !removed {
+ // 解析原始规则,提取可能的主域名
+ originalRule := cleanRule
+ // 移除可能的前缀
+ originalRule = strings.TrimPrefix(originalRule, "@@||")
+ originalRule = strings.TrimPrefix(originalRule, "||")
+
+ // 检查是否有子域名规则需要删除
+ // 遍历所有域名规则,删除包含原始规则作为后缀的子域名规则
+ for domain := range m.domainRules {
+ if strings.HasSuffix(domain, "."+originalRule) || domain == originalRule {
+ delete(m.domainRules, domain)
+ delete(m.domainRulesIsLocal, domain)
+ delete(m.domainRulesSource, domain)
+ removed = true
+ }
+ }
+
+ // 遍历所有排除规则,删除包含原始规则作为后缀的子域名规则
+ for domain := range m.domainExceptions {
+ if strings.HasSuffix(domain, "."+originalRule) || domain == originalRule {
+ delete(m.domainExceptions, domain)
+ delete(m.domainExceptionsIsLocal, domain)
+ delete(m.domainExceptionsSource, domain)
+ removed = true
+ }
+ }
+ }
+
// 如果有规则被删除,持久化保存更改
if removed && m.config.LocalRulesFile != "" {
if err := m.saveRulesToFile(); err != nil {
@@ -843,7 +941,7 @@ func (m *ShieldManager) StartAutoUpdate() {
}
}
}()
-
+
logger.Info("规则自动更新已启动", "interval", m.config.UpdateInterval)
// 如果是首次启动,先保存一次数据确保目录存在
@@ -859,28 +957,36 @@ func (m *ShieldManager) StopAutoUpdate() {
logger.Info("规则自动更新已停止")
}
-// saveRulesToFile 保存规则到文件
+// saveRulesToFile 保存规则到文件,只保存本地规则
func (m *ShieldManager) saveRulesToFile() error {
var rules []string
- // 添加域名规则
- for domain := range m.domainRules {
- rules = append(rules, "||"+domain)
+ // 添加本地域名规则
+ for domain, isLocal := range m.domainRulesIsLocal {
+ if isLocal {
+ rules = append(rules, "||"+domain)
+ }
}
- // 添加正则表达式规则
+ // 添加本地正则表达式规则
for _, re := range m.regexRules {
- rules = append(rules, re.original)
+ if re.isLocal {
+ rules = append(rules, re.original)
+ }
}
- // 添加排除规则
- for domain := range m.domainExceptions {
- rules = append(rules, "@@||"+domain)
+ // 添加本地排除规则
+ for domain, isLocal := range m.domainExceptionsIsLocal {
+ if isLocal {
+ rules = append(rules, "@@||"+domain)
+ }
}
- // 添加正则表达式排除规则
+ // 添加本地正则表达式排除规则
for _, re := range m.regexExceptions {
- rules = append(rules, re.original)
+ if re.isLocal {
+ rules = append(rules, re.original)
+ }
}
// 写入文件
@@ -1033,18 +1139,18 @@ func (m *ShieldManager) loadStatsData() {
if len(dataSample) > 50 {
dataSample = dataSample[:50] + "..."
}
- logger.Error("解析Shield计数数据失败",
- "file", statsFilePath,
- "error", err,
+ logger.Error("解析Shield计数数据失败",
+ "file", statsFilePath,
+ "error", err,
"data_length", len(data),
"data_sample", dataSample)
-
+
// 重置为默认空数据
m.rulesMutex.Lock()
m.blockedDomainsCount = make(map[string]int)
m.resolvedDomainsCount = make(map[string]int)
m.rulesMutex.Unlock()
-
+
// 尝试保存一个有效的空文件
m.saveStatsData()
return
@@ -1187,6 +1293,131 @@ func (m *ShieldManager) GetHostsCount() int {
return len(m.hostsMap)
}
+// GetLocalRules 获取仅本地规则
+func (m *ShieldManager) GetLocalRules() map[string]interface{} {
+ m.rulesMutex.RLock()
+ defer m.rulesMutex.RUnlock()
+
+ // 转换map和slice为字符串列表,只包含本地规则
+ domainRulesList := make([]string, 0)
+ for domain, isLocal := range m.domainRulesIsLocal {
+ if isLocal && m.domainRules[domain] {
+ domainRulesList = append(domainRulesList, "||"+domain+"^")
+ }
+ }
+
+ domainExceptionsList := make([]string, 0)
+ for domain, isLocal := range m.domainExceptionsIsLocal {
+ if isLocal && m.domainExceptions[domain] {
+ domainExceptionsList = append(domainExceptionsList, "@@||"+domain+"^")
+ }
+ }
+
+ // 获取本地正则规则原始字符串
+ regexRulesList := make([]string, 0)
+ for _, re := range m.regexRules {
+ if re.isLocal {
+ regexRulesList = append(regexRulesList, re.original)
+ }
+ }
+
+ // 获取本地正则排除规则原始字符串
+ regexExceptionsList := make([]string, 0)
+ for _, re := range m.regexExceptions {
+ if re.isLocal {
+ regexExceptionsList = append(regexExceptionsList, re.original)
+ }
+ }
+
+ // 计算本地规则数量
+ localDomainRulesCount := 0
+ for _, isLocal := range m.domainRulesIsLocal {
+ if isLocal {
+ localDomainRulesCount++
+ }
+ }
+ localRegexRulesCount := 0
+ for _, re := range m.regexRules {
+ if re.isLocal {
+ localRegexRulesCount++
+ }
+ }
+ localRulesCount := localDomainRulesCount + localRegexRulesCount
+
+ return map[string]interface{}{
+ "domainRules": domainRulesList,
+ "domainExceptions": domainExceptionsList,
+ "regexRules": regexRulesList,
+ "regexExceptions": regexExceptionsList,
+ "localRulesCount": localRulesCount,
+ "localDomainRulesCount": localDomainRulesCount,
+ "localRegexRulesCount": localRegexRulesCount,
+ }
+}
+
+// GetRemoteRules 获取仅远程规则
+func (m *ShieldManager) GetRemoteRules() map[string]interface{} {
+ m.rulesMutex.RLock()
+ defer m.rulesMutex.RUnlock()
+
+ // 转换map和slice为字符串列表,只包含远程规则
+ domainRulesList := make([]string, 0)
+ for domain, isLocal := range m.domainRulesIsLocal {
+ if !isLocal && m.domainRules[domain] {
+ domainRulesList = append(domainRulesList, "||"+domain+"^")
+ }
+ }
+
+ domainExceptionsList := make([]string, 0)
+ for domain, isLocal := range m.domainExceptionsIsLocal {
+ if !isLocal && m.domainExceptions[domain] {
+ domainExceptionsList = append(domainExceptionsList, "@@||"+domain+"^")
+ }
+ }
+
+ // 获取远程正则规则原始字符串
+ regexRulesList := make([]string, 0)
+ for _, re := range m.regexRules {
+ if !re.isLocal {
+ regexRulesList = append(regexRulesList, re.original)
+ }
+ }
+
+ // 获取远程正则排除规则原始字符串
+ regexExceptionsList := make([]string, 0)
+ for _, re := range m.regexExceptions {
+ if !re.isLocal {
+ regexExceptionsList = append(regexExceptionsList, re.original)
+ }
+ }
+
+ // 计算远程规则数量
+ remoteDomainRulesCount := 0
+ for _, isLocal := range m.domainRulesIsLocal {
+ if !isLocal {
+ remoteDomainRulesCount++
+ }
+ }
+ remoteRegexRulesCount := 0
+ for _, re := range m.regexRules {
+ if !re.isLocal {
+ remoteRegexRulesCount++
+ }
+ }
+ remoteRulesCount := remoteDomainRulesCount + remoteRegexRulesCount
+
+ return map[string]interface{}{
+ "domainRules": domainRulesList,
+ "domainExceptions": domainExceptionsList,
+ "regexRules": regexRulesList,
+ "regexExceptions": regexExceptionsList,
+ "remoteRulesCount": remoteRulesCount,
+ "remoteDomainRulesCount": remoteDomainRulesCount,
+ "remoteRegexRulesCount": remoteRegexRulesCount,
+ "blacklists": m.config.Blacklists,
+ }
+}
+
// GetRules 获取所有规则
func (m *ShieldManager) GetRules() map[string]interface{} {
m.rulesMutex.RLock()
diff --git a/shield/rule_test.go b/shield/rule_test.go
new file mode 100644
index 0000000..ce10eaa
--- /dev/null
+++ b/shield/rule_test.go
@@ -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)
+ }
+ })
+ }
+}
diff --git a/shield_stats.json b/shield_stats.json
new file mode 100644
index 0000000..d99d506
--- /dev/null
+++ b/shield_stats.json
@@ -0,0 +1,5 @@
+{
+ "blockedDomainsCount": {},
+ "resolvedDomainsCount": {},
+ "lastSaved": "2025-11-29T02:08:50.6341349+08:00"
+}
\ No newline at end of file
diff --git a/static/api/css/style.css b/static/api/css/style.css
new file mode 100644
index 0000000..e647096
--- /dev/null
+++ b/static/api/css/style.css
@@ -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;
+ }
+ }
\ No newline at end of file
diff --git a/static/api/index.html b/static/api/index.html
new file mode 100644
index 0000000..ebb375d
--- /dev/null
+++ b/static/api/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+ DNS Server API 文档
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/static/api/js/index.js b/static/api/js/index.js
new file mode 100644
index 0000000..e6e6d1b
--- /dev/null
+++ b/static/api/js/index.js
@@ -0,0 +1,1333 @@
+ // 定义API文档的JSON
+ const swaggerDocument = {
+ "openapi": "3.0.3",
+ "info": {
+ "title": "DNS Server API",
+ "description": "DNS服务器完整API文档,包括统计信息、Shield管理、主机管理等功能。",
+ "version": "1.1.0",
+ "contact": {
+ "name": "DNS Server 支持",
+ "email": "support@dnsserver.com"
+ },
+ "license": {
+ "name": "Apache 2.0",
+ "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
+ }
+ },
+ "servers": [
+ {
+ "url": "http://localhost:8080/api",
+ "description": "本地开发服务器"
+ },
+ {
+ "url": "http://{host}:{port}/api",
+ "description": "自定义服务器",
+ "variables": {
+ "host": {
+ "default": "localhost"
+ },
+ "port": {
+ "default": "8080"
+ }
+ }
+ }
+ ],
+ "paths": {
+ "/stats": {
+ "get": {
+ "summary": "获取系统统计信息",
+ "description": "获取DNS服务器和Shield的详细统计信息,包括查询量、CPU使用率等。",
+ "tags": ["stats"],
+ "responses": {
+ "200": {
+ "description": "成功获取统计信息",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "dns": {
+ "type": "object",
+ "properties": {
+ "Queries": {"type": "integer", "description": "总查询次数"},
+ "Blocked": {"type": "integer", "description": "被阻止的查询次数"},
+ "Allowed": {"type": "integer", "description": "允许的查询次数"},
+ "Errors": {"type": "integer", "description": "错误查询次数"},
+ "LastQuery": {"type": "string", "description": "最近一次查询时间"},
+ "AvgResponseTime": {"type": "number", "description": "平均响应时间(毫秒)"},
+ "TotalResponseTime": {"type": "number", "description": "总响应时间(毫秒)"},
+ "QueryTypes": {"type": "object", "description": "查询类型统计"},
+ "SourceIPs": {"type": "object", "description": "来源IP统计"},
+ "CpuUsage": {"type": "number", "description": "CPU使用率(百分比)"}
+ }
+ },
+ "shield": {"type": "object", "description": "Shield统计信息"},
+ "topQueryType": {"type": "string", "description": "最常见的查询类型"},
+ "activeIPs": {"type": "integer", "description": "活跃IP数量"},
+ "avgResponseTime": {"type": "number", "description": "平均响应时间(毫秒)"},
+ "cpuUsage": {"type": "number", "description": "CPU使用率(百分比)"},
+ "time": {"type": "string", "description": "统计时间"}
+ }
+ },
+ "examples": {
+ "default": {
+ "value": {
+ "dns": {
+ "Queries": 1250,
+ "Blocked": 230,
+ "Allowed": 1020,
+ "Errors": 0,
+ "LastQuery": "2023-07-15T14:30:45Z",
+ "AvgResponseTime": 12.5,
+ "TotalResponseTime": 15625,
+ "QueryTypes": {"A": 850, "AAAA": 250, "CNAME": 150},
+ "SourceIPs": {"192.168.1.100": 500, "192.168.1.101": 750},
+ "CpuUsage": 0.15
+ },
+ "shield": {},
+ "topQueryType": "A",
+ "activeIPs": 2,
+ "avgResponseTime": 12.5,
+ "cpuUsage": 0.15,
+ "time": "2023-07-15T14:30:45Z"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "服务器内部错误",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "error": {"type": "string", "description": "错误信息"}
+ }
+ },
+ "example": {"error": "无法获取统计信息"}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/top-blocked": {
+ "get": {
+ "summary": "获取TOP被阻止域名",
+ "description": "获取被阻止次数最多的域名列表。",
+ "tags": ["stats"],
+ "responses": {
+ "200": {
+ "description": "成功获取TOP被阻止域名",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "domain": {"type": "string", "description": "域名"},
+ "count": {"type": "integer", "description": "被阻止次数"}
+ }
+ }
+ },
+ "example": [
+ {"domain": "ad.example.com", "count": 150},
+ {"domain": "tracker.example.net", "count": 120}
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "/top-resolved": {
+ "get": {
+ "summary": "获取TOP已解析域名",
+ "description": "获取解析次数最多的域名列表。",
+ "tags": ["stats"],
+ "responses": {
+ "200": {
+ "description": "成功获取TOP已解析域名",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "domain": {"type": "string", "description": "域名"},
+ "count": {"type": "integer", "description": "解析次数"}
+ }
+ }
+ },
+ "example": [
+ {"domain": "google.com", "count": 200},
+ {"domain": "facebook.com", "count": 150}
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "/top-clients": {
+ "get": {
+ "summary": "获取TOP客户端",
+ "description": "获取查询量最多的客户端IP列表。",
+ "tags": ["stats"],
+ "responses": {
+ "200": {
+ "description": "成功获取TOP客户端",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "ip": {"type": "string", "description": "客户端IP地址"},
+ "count": {"type": "integer", "description": "查询次数"}
+ }
+ }
+ },
+ "example": [
+ {"ip": "192.168.1.100", "count": 500},
+ {"ip": "192.168.1.101", "count": 750}
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "/top-domains": {
+ "get": {
+ "summary": "获取TOP域名",
+ "description": "获取查询量最多的域名列表(包括被阻止和已解析的域名)。",
+ "tags": ["stats"],
+ "responses": {
+ "200": {
+ "description": "成功获取TOP域名",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "domain": {"type": "string", "description": "域名"},
+ "count": {"type": "integer", "description": "查询次数"}
+ }
+ }
+ },
+ "example": [
+ {"domain": "example.com", "count": 150},
+ {"domain": "google.com", "count": 120}
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "/recent-blocked": {
+ "get": {
+ "summary": "获取最近被阻止的域名",
+ "description": "获取最近被Shield阻止的域名列表。",
+ "tags": ["stats"],
+ "responses": {
+ "200": {
+ "description": "成功获取最近被阻止的域名",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "domain": {"type": "string", "description": "域名"},
+ "timestamp": {"type": "string", "description": "阻止时间"}
+ }
+ }
+ },
+ "example": [
+ {"domain": "ad.example.com", "timestamp": "2023-07-15T14:30:45Z"},
+ {"domain": "tracker.example.net", "timestamp": "2023-07-15T14:29:30Z"}
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "/hourly-stats": {
+ "get": {
+ "summary": "获取小时统计",
+ "description": "获取按小时统计的DNS查询数据。",
+ "tags": ["stats"],
+ "responses": {
+ "200": {
+ "description": "成功获取小时统计",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "hour": {"type": "string", "description": "小时"},
+ "queries": {"type": "integer", "description": "查询次数"},
+ "blocked": {"type": "integer", "description": "被阻止次数"}
+ }
+ }
+ },
+ "example": [
+ {"hour": "00", "queries": 120, "blocked": 20},
+ {"hour": "01", "queries": 90, "blocked": 15}
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "/daily-stats": {
+ "get": {
+ "summary": "获取每日统计",
+ "description": "获取最近7天的DNS查询统计数据。",
+ "tags": ["stats"],
+ "responses": {
+ "200": {
+ "description": "成功获取每日统计",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "date": {"type": "string", "description": "日期"},
+ "queries": {"type": "integer", "description": "查询次数"},
+ "blocked": {"type": "integer", "description": "被阻止次数"}
+ }
+ }
+ },
+ "example": [
+ {"date": "2023-07-09", "queries": 2500, "blocked": 450},
+ {"date": "2023-07-10", "queries": 2700, "blocked": 480}
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "/monthly-stats": {
+ "get": {
+ "summary": "获取每月统计",
+ "description": "获取最近30天的DNS查询统计数据。",
+ "tags": ["stats"],
+ "responses": {
+ "200": {
+ "description": "成功获取每月统计",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "date": {"type": "string", "description": "日期"},
+ "queries": {"type": "integer", "description": "查询次数"},
+ "blocked": {"type": "integer", "description": "被阻止次数"}
+ }
+ }
+ },
+ "example": [
+ {"date": "2023-06-15", "queries": 2500, "blocked": 450},
+ {"date": "2023-06-16", "queries": 2700, "blocked": 480}
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "/query/type": {
+ "get": {
+ "summary": "获取查询类型统计",
+ "description": "获取DNS查询类型的详细统计信息。",
+ "tags": ["stats"],
+ "responses": {
+ "200": {
+ "description": "成功获取查询类型统计",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "type": {"type": "string", "description": "查询类型"},
+ "count": {"type": "integer", "description": "查询次数"}
+ }
+ }
+ },
+ "example": [
+ {"type": "A", "count": 850},
+ {"type": "AAAA", "count": 250},
+ {"type": "CNAME", "count": 150}
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "/shield": {
+ "get": {
+ "summary": "获取Shield配置",
+ "description": "获取Shield的完整配置信息,包括启用状态、规则等。",
+ "tags": ["shield"],
+ "responses": {
+ "200": {
+ "description": "成功获取配置信息",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "enabled": {"type": "boolean", "description": "是否启用Shield"},
+ "rulesCount": {"type": "integer", "description": "规则数量"},
+ "lastUpdate": {"type": "string", "description": "最后更新时间"},
+ "blacklists": {"type": "array", "description": "黑名单列表", "items": {"type": "object"}}
+ }
+ },
+ "example": {
+ "enabled": true,
+ "rulesCount": 5000,
+ "lastUpdate": "2023-07-15T10:00:00Z",
+ "blacklists": []
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "服务器内部错误",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "error": {"type": "string", "description": "错误信息"}
+ }
+ },
+ "example": {"error": "无法获取Shield配置"}
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "summary": "更新Shield配置",
+ "description": "更新Shield的全局配置信息。",
+ "tags": ["shield"],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "enabled": {"type": "boolean", "description": "是否启用Shield"},
+ "updateInterval": {"type": "integer", "description": "更新间隔(秒)"}
+ }
+ },
+ "example": {
+ "enabled": true,
+ "updateInterval": 3600
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "成功更新配置",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {"type": "string", "description": "操作状态"}
+ }
+ },
+ "example": {"status": "success"}
+ }
+ }
+ },
+ "400": {
+ "description": "请求参数错误",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "error": {"type": "string", "description": "错误信息"}
+ }
+ },
+ "example": {"error": "参数格式错误"}
+ }
+ }
+ },
+ "500": {
+ "description": "服务器内部错误",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "error": {"type": "string", "description": "错误信息"}
+ }
+ },
+ "example": {"error": "更新配置失败"}
+ }
+ }
+ }
+ }
+ },
+ "put": {
+ "summary": "重启Shield",
+ "description": "重新加载和应用Shield规则。",
+ "tags": ["shield"],
+ "responses": {
+ "200": {
+ "description": "成功重启Shield",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {"type": "string", "description": "操作状态"}
+ }
+ },
+ "example": {"status": "success"}
+ }
+ }
+ },
+ "500": {
+ "description": "服务器内部错误",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "error": {"type": "string", "description": "错误信息"}
+ }
+ },
+ "example": {"error": "重启失败"}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/shield/blacklists": {
+ "get": {
+ "summary": "获取黑名单列表",
+ "description": "获取所有远程黑名单的列表及详细信息。",
+ "tags": ["shield"],
+ "responses": {
+ "200": {
+ "description": "成功获取黑名单列表",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {"type": "string", "description": "黑名单名称"},
+ "url": {"type": "string", "description": "黑名单URL"},
+ "enabled": {"type": "boolean", "description": "是否启用"},
+ "lastUpdate": {"type": "string", "description": "最后更新时间"},
+ "status": {"type": "string", "description": "状态"},
+ "rulesCount": {"type": "integer", "description": "规则数量"}
+ }
+ }
+ },
+ "example": [
+ {
+ "name": "AdBlock List",
+ "url": "https://example.com/ads.txt",
+ "enabled": true,
+ "lastUpdate": "2023-07-15T10:00:00Z",
+ "status": "active",
+ "rulesCount": 1500
+ }
+ ]
+ }
+ }
+ },
+ "500": {
+ "description": "服务器内部错误",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "error": {"type": "string", "description": "错误信息"}
+ }
+ },
+ "example": {"error": "获取黑名单列表失败"}
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "summary": "添加黑名单",
+ "description": "添加新的远程黑名单URL。",
+ "tags": ["shield"],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": ["name", "url"],
+ "properties": {
+ "name": {"type": "string", "description": "黑名单名称"},
+ "url": {"type": "string", "description": "黑名单URL"},
+ "enabled": {"type": "boolean", "description": "是否启用", "default": true}
+ }
+ },
+ "example": {
+ "name": "AdBlock List",
+ "url": "https://example.com/ads.txt",
+ "enabled": true
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "成功添加黑名单",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {"type": "string", "description": "操作状态"}
+ }
+ },
+ "example": {"status": "success"}
+ }
+ }
+ },
+ "400": {
+ "description": "请求参数错误",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "error": {"type": "string", "description": "错误信息"}
+ }
+ },
+ "example": {"error": "名称和URL为必填项"}
+ }
+ }
+ },
+ "500": {
+ "description": "服务器内部错误",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "error": {"type": "string", "description": "错误信息"}
+ }
+ },
+ "example": {"error": "添加黑名单失败"}
+ }
+ }
+ }
+ }
+ },
+ "put": {
+ "summary": "更新黑名单列表(包括启用/禁用状态)",
+ "description": "更新黑名单列表(包括启用/禁用状态)。",
+ "tags": ["shield"],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "blacklists": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {"type": "string", "description": "黑名单名称"},
+ "url": {"type": "string", "description": "黑名单URL"},
+ "enabled": {"type": "boolean", "description": "是否启用"}
+ }
+ }
+ }
+ }
+ },
+ "example": {
+ "blacklists": [
+ {
+ "name": "AdBlock List",
+ "url": "https://example.com/ads.txt",
+ "enabled": true
+ }
+ ]
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "成功更新黑名单列表",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {"type": "string", "description": "操作状态"}
+ }
+ },
+ "example": {"status": "success"}
+ }
+ }
+ },
+ "400": {
+ "description": "请求参数错误",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "error": {"type": "string", "description": "错误信息"}
+ }
+ },
+ "example": {"error": "无效的请求体"}
+ }
+ }
+ },
+ "500": {
+ "description": "服务器内部错误",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "error": {"type": "string", "description": "错误信息"}
+ }
+ },
+ "example": {"error": "更新黑名单列表失败"}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/shield/localrules": {
+ "get": {
+ "summary": "获取本地规则",
+ "description": "获取Shield的本地规则列表。",
+ "tags": ["shield"],
+ "responses": {
+ "200": {
+ "description": "成功获取本地规则",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {"type": "string", "description": "规则ID"},
+ "pattern": {"type": "string", "description": "规则模式"},
+ "description": {"type": "string", "description": "规则描述"}
+ }
+ }
+ },
+ "example": [
+ {"id": "1", "pattern": ".*malware.*", "description": "恶意软件域名"}
+ ]
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "summary": "添加本地规则",
+ "description": "添加新的本地规则。",
+ "tags": ["shield"],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "pattern": {"type": "string", "description": "规则模式"},
+ "description": {"type": "string", "description": "规则描述"}
+ }
+ },
+ "example": {
+ "pattern": ".*ad\.com$",
+ "description": "广告域名"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "成功添加规则",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {"type": "string", "description": "操作状态"}
+ }
+ },
+ "example": {"status": "success"}
+ }
+ }
+ }
+ }
+ },
+ "delete": {
+ "summary": "删除本地规则",
+ "description": "删除指定ID的本地规则。",
+ "tags": ["shield"],
+ "parameters": [
+ {
+ "name": "id",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "规则ID"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功删除规则",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {"type": "string", "description": "操作状态"}
+ }
+ },
+ "example": {"status": "success"}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/shield/remoterules": {
+ "get": {
+ "summary": "获取远程规则",
+ "description": "获取Shield的远程规则列表。",
+ "tags": ["shield"],
+ "responses": {
+ "200": {
+ "description": "成功获取远程规则",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {"type": "string", "description": "规则ID"},
+ "pattern": {"type": "string", "description": "规则模式"},
+ "source": {"type": "string", "description": "规则来源"}
+ }
+ }
+ },
+ "example": [
+ {"id": "1001", "pattern": ".*phishing.*", "source": "malwarelist"}
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "/shield/hosts": {
+ "get": {
+ "summary": "获取hosts列表",
+ "description": "获取所有hosts记录。",
+ "tags": ["shield"],
+ "responses": {
+ "200": {
+ "description": "成功获取hosts列表",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "ip": {"type": "string", "description": "IP地址"},
+ "domain": {"type": "string", "description": "域名"}
+ }
+ }
+ },
+ "example": [
+ {"ip": "127.0.0.1", "domain": "localhost"}
+ ]
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "summary": "添加hosts记录",
+ "description": "添加新的hosts记录。",
+ "tags": ["shield"],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": ["ip", "domain"],
+ "properties": {
+ "ip": {"type": "string", "description": "IP地址"},
+ "domain": {"type": "string", "description": "域名"}
+ }
+ },
+ "example": {
+ "ip": "127.0.0.1",
+ "domain": "example.com"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "成功添加hosts记录",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {"type": "string", "description": "操作状态"}
+ }
+ },
+ "example": {"status": "success"}
+ }
+ }
+ },
+ "400": {
+ "description": "请求参数错误",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "error": {"type": "string", "description": "错误信息"}
+ }
+ },
+ "example": {"error": "IP和域名是必填项"}
+ }
+ }
+ },
+ "500": {
+ "description": "服务器内部错误",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "error": {"type": "string", "description": "错误信息"}
+ }
+ },
+ "example": {"error": "添加hosts记录失败"}
+ }
+ }
+ }
+ }
+ },
+ "delete": {
+ "summary": "删除hosts记录",
+ "description": "删除指定域名的hosts记录。",
+ "tags": ["shield"],
+ "parameters": [
+ {
+ "name": "domain",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "要删除的域名"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功删除hosts记录",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {"type": "string", "description": "操作状态"}
+ }
+ },
+ "example": {"status": "success"}
+ }
+ }
+ },
+ "400": {
+ "description": "请求参数错误",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "error": {"type": "string", "description": "错误信息"}
+ }
+ },
+ "example": {"error": "域名是必填项"}
+ }
+ }
+ },
+ "500": {
+ "description": "服务器内部错误",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "error": {"type": "string", "description": "错误信息"}
+ }
+ },
+ "example": {"error": "删除hosts记录失败"}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/query": {
+ "get": {
+ "summary": "检查域名是否被屏蔽",
+ "description": "检查指定域名是否被Shield屏蔽,并返回详细的屏蔽信息,包括屏蔽规则、规则类型、来源等。",
+ "tags": ["shield"],
+ "parameters": [
+ {
+ "name": "domain",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "description": "要检查的域名"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功获取域名屏蔽信息",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "domain": {
+ "type": "string",
+ "description": "检查的域名"
+ },
+ "blocked": {
+ "type": "boolean",
+ "description": "是否被屏蔽"
+ },
+ "blockRule": {
+ "type": "string",
+ "description": "屏蔽规则"
+ },
+ "blockRuleType": {
+ "type": "string",
+ "description": "屏蔽规则类型"
+ },
+ "blocksource": {
+ "type": "string",
+ "description": "屏蔽规则来源"
+ },
+ "excluded": {
+ "type": "boolean",
+ "description": "是否被排除"
+ },
+ "excludeRule": {
+ "type": "string",
+ "description": "排除规则"
+ },
+ "excludeRuleType": {
+ "type": "string",
+ "description": "排除规则类型"
+ },
+ "hasHosts": {
+ "type": "boolean",
+ "description": "是否有hosts记录"
+ },
+ "hostsIP": {
+ "type": "string",
+ "description": "hosts记录中的IP"
+ },
+ "timestamp": {
+ "type": "string",
+ "format": "date-time",
+ "description": "查询时间戳"
+ }
+ }
+ },
+ "example": {
+ "domain": "example.com",
+ "blocked": true,
+ "blockRule": "example.com",
+ "blockRuleType": "exact_domain",
+ "blocksource": "本地规则",
+ "excluded": false,
+ "excludeRule": "",
+ "excludeRuleType": "",
+ "hasHosts": false,
+ "hostsIP": "",
+ "timestamp": "2023-07-15T14:30:45Z"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "请求参数错误",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "error": {
+ "type": "string",
+ "description": "错误信息"
+ }
+ }
+ },
+ "example": {
+ "error": "Domain parameter is required"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/status": {
+ "get": {
+ "summary": "获取服务器状态",
+ "description": "获取DNS服务器的状态信息。",
+ "tags": ["server"],
+ "responses": {
+ "200": {
+ "description": "成功获取服务器状态",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {"type": "string", "description": "服务器状态"},
+ "uptime": {"type": "integer", "description": "运行时间(秒)"},
+ "version": {"type": "string", "description": "服务器版本"}
+ }
+ },
+ "example": {
+ "status": "running",
+ "uptime": 3600,
+ "version": "1.0.0"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/config": {
+ "get": {
+ "summary": "获取服务器配置",
+ "description": "获取DNS服务器的配置信息。",
+ "tags": ["server"],
+ "responses": {
+ "200": {
+ "description": "成功获取服务器配置",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "port": {"type": "integer", "description": "服务器端口"},
+ "logLevel": {"type": "string", "description": "日志级别"}
+ }
+ },
+ "example": {
+ "port": 53,
+ "logLevel": "info"
+ }
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "summary": "更新服务器配置",
+ "description": "更新DNS服务器的配置信息。",
+ "tags": ["server"],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "port": {"type": "integer", "description": "服务器端口"},
+ "logLevel": {"type": "string", "description": "日志级别"}
+ }
+ },
+ "example": {
+ "port": 53,
+ "logLevel": "info"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "成功更新服务器配置",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {"type": "string", "description": "操作状态"}
+ }
+ },
+ "example": {"status": "success"}
+ }
+ }
+ },
+ "400": {
+ "description": "请求参数错误",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "error": {"type": "string", "description": "错误信息"}
+ }
+ },
+ "example": {"error": "参数格式错误"}
+ }
+ }
+ },
+ "500": {
+ "description": "服务器内部错误",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "error": {"type": "string", "description": "错误信息"}
+ }
+ },
+ "example": {"error": "更新配置失败"}
+ }
+ }
+ }
+ }
+ }
+ },
+ "/config/restart": {
+ "post": {
+ "summary": "重启服务器",
+ "description": "重启DNS服务器。",
+ "tags": ["server"],
+ "responses": {
+ "200": {
+ "description": "成功重启服务器",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {"type": "string", "description": "操作状态"}
+ }
+ },
+ "example": {"status": "success"}
+ }
+ }
+ },
+ "500": {
+ "description": "服务器内部错误",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "error": {"type": "string", "description": "错误信息"}
+ }
+ },
+ "example": {"error": "重启服务器失败"}
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "tags": [
+ {
+ "name": "stats",
+ "description": "统计相关API"
+ },
+ {
+ "name": "shield",
+ "description": "Shield相关API"
+ },
+ {
+ "name": "server",
+ "description": "服务器相关API"
+ }
+ ]
+ };
+
+ // 初始化Swagger UI
+ window.onload = function() {
+ const ui = SwaggerUIBundle({
+ spec: swaggerDocument,
+ dom_id: '#swagger-ui',
+ deepLinking: true,
+ presets: [
+ SwaggerUIBundle.presets.apis,
+ SwaggerUIStandalonePreset
+ ],
+ plugins: [
+ SwaggerUIBundle.plugins.DownloadUrl
+ ],
+ layout: "StandaloneLayout"
+ });
+
+ window.ui = ui;
+ };
\ No newline at end of file
diff --git a/static/css/animation.css b/static/css/animation.css
new file mode 100644
index 0000000..2319b5b
--- /dev/null
+++ b/static/css/animation.css
@@ -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;
+ }
\ No newline at end of file
diff --git a/static/css/style.css b/static/css/style.css
index 51a0fe6..ce06c0c 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -132,26 +132,7 @@ header p {
/* 响应式布局 - 移动设备 */
@media (max-width: 768px) {
- .sidebar {
- position: fixed;
- left: -var(--sidebar-width);
- top: var(--header-height);
- z-index: 99;
- height: calc(100vh - var(--header-height));
- }
-
- .sidebar.open {
- left: 0;
- width: var(--sidebar-width);
- }
-
- .sidebar.open .nav-item span {
- display: block;
- }
-
- .sidebar.open .nav-item i {
- margin-right: 1rem;
- }
+ /* 这些样式已经通过Tailwind CSS类在HTML中实现,这里移除避免冲突 */
}
.nav-menu {
@@ -1062,18 +1043,6 @@ tr:hover {
font-size: 0.9rem;
}
}
-
-/* 加载动画 */
-.loading {
- display: inline-block;
- width: 20px;
- height: 20px;
- border: 3px solid #f3f3f3;
- border-top: 3px solid #3498db;
- border-radius: 50%;
- animation: spin 1s linear infinite;
-}
-
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
diff --git a/static/index.html b/static/index.html
index 19e7e20..75afaca 100644
--- a/static/index.html
+++ b/static/index.html
@@ -8,170 +8,23 @@
-
-
-
-
+
-
-
-
+
-