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