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服务器控制台 - - - - - - - - - - - - - - - - - -
- - - - - - - -
- -
-
- -

仪表盘

-
- -
- -
-
-
- CPU - 0% -
-
-
-
-
-
-
-
- 查询 - 0 -
-
-
-
-
- - -
- -
-
- - -
- 用户头像 - -
-
-
- - -
- -
- -
- -
- -
-
-
-

查询总量

-
- -
-
-
-
-

0

- - - 0% - -
-
-
-
- - -
- -
-
-
-

屏蔽数量

-
- -
-
-
-
-

0

- - - 0% - -
-
-
-
- - -
- -
-
-
-

正常解析

-
- -
-
-
-
-

0

- - - 0% - -
-
-
-
- - -
- -
-
-
-

错误数量

-
- -
-
-
-
-

0

- - - 0% - -
-
-
-
- - -
- -
-
-
-

平均响应时间

-
- -
-
-
-
-

0ms

- - - 0% - -
-
-
-
- - -
- -
-
-
-

最常用查询类型

-
- -
-
-
-

A

- - - 0% - -
-
-
- - -
- -
-
-
-

活跃来源IP

-
- -
-
-
-
-

0

- - - 0% - -
-
-
-
- - -
- - -
- -
-

解析与屏蔽比例

-
- -
-
- -
-

解析类型统计

-
- -
-
- -
-
-

DNS请求趋势

- - -
-
- -
-
-
- - - - - -
- -
-

被拦截域名排行

-
-
-
-
-
- 1 - example1.com -
-
- 150 -
-
-
-
- 2 - example2.com -
-
- 130 -
-
-
-
- 3 - example3.com -
-
- 120 -
-
-
-
- 4 - example4.com -
-
- 110 -
-
-
-
- 5 - example5.com -
-
- 100 -
-
-
-
- - -
-

请求域名排行

-
-
-
-
-
- 1 - example.com -
-
- 50 -
-
-
-
-
- - -
- -
-
-

客户端排行

-
- - 加载中... -
- -
-
-
-
-
-
- 1 - 192.168.1.1 -
-
- 500 -
-
-
-
- 2 - 192.168.1.2 -
-
- 450 -
-
-
-
- 3 - 192.168.1.3 -
-
- 400 -
-
-
-
- 4 - 192.168.1.4 -
-
- 350 -
-
-
-
- 5 - 192.168.1.5 -
-
- 300 -
-
-
-
-
-
- - - - - - - - - - - -
-
-
- - - - - - - - - - - - - - \ 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 @@ - - - - + - - - +
-