revert
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查询结果的各种网络环境。
|
||||
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查询结果的各种网络环境。
|
||||
1767
api/index.html
1767
api/index.html
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,9 @@
|
||||
"http": {
|
||||
"port": 8080,
|
||||
"host": "0.0.0.0",
|
||||
"enableAPI": true
|
||||
"enableAPI": true,
|
||||
"username": "admin",
|
||||
"password": "admin"
|
||||
},
|
||||
"shield": {
|
||||
"localRulesFile": "data/rules.txt",
|
||||
@@ -113,4 +115,4 @@
|
||||
"maxBackups": 10,
|
||||
"maxAge": 30
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ type DNSConfig struct {
|
||||
Timeout int `json:"timeout"`
|
||||
StatsFile string `json:"statsFile"` // 统计数据持久化文件
|
||||
SaveInterval int `json:"saveInterval"` // 数据保存间隔(秒)
|
||||
CacheTTL int `json:"cacheTTL"` // DNS缓存过期时间(分钟)
|
||||
}
|
||||
|
||||
// HTTPConfig HTTP控制台配置
|
||||
@@ -19,6 +20,8 @@ type HTTPConfig struct {
|
||||
Port int `json:"port"`
|
||||
Host string `json:"host"`
|
||||
EnableAPI bool `json:"enableAPI"`
|
||||
Username string `json:"username"` // 登录用户名
|
||||
Password string `json:"password"` // 登录密码
|
||||
}
|
||||
|
||||
// BlacklistEntry 黑名单条目
|
||||
@@ -86,12 +89,23 @@ func LoadConfig(path string) (*Config, error) {
|
||||
if config.DNS.SaveInterval == 0 {
|
||||
config.DNS.SaveInterval = 300 // 默认5分钟保存一次
|
||||
}
|
||||
// 默认DNS缓存TTL为30分钟
|
||||
if config.DNS.CacheTTL == 0 {
|
||||
config.DNS.CacheTTL = 30 // 默认30分钟
|
||||
}
|
||||
if config.HTTP.Port == 0 {
|
||||
config.HTTP.Port = 8080
|
||||
}
|
||||
if config.HTTP.Host == "" {
|
||||
config.HTTP.Host = "0.0.0.0"
|
||||
}
|
||||
// 默认用户名和密码,如果未配置则使用admin/admin
|
||||
if config.HTTP.Username == "" {
|
||||
config.HTTP.Username = "admin"
|
||||
}
|
||||
if config.HTTP.Password == "" {
|
||||
config.HTTP.Password = "admin"
|
||||
}
|
||||
if config.Shield.UpdateInterval == 0 {
|
||||
config.Shield.UpdateInterval = 3600
|
||||
}
|
||||
|
||||
1078
css/style.css
1078
css/style.css
File diff suppressed because it is too large
Load Diff
9
css/vendor/all.min.css
vendored
9
css/vendor/all.min.css
vendored
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1,5 +0,0 @@
|
||||
# DNS Server Hosts File
|
||||
# Generated by DNS Server
|
||||
|
||||
127.0.0.1 localhost
|
||||
::1 localhost
|
||||
@@ -1 +0,0 @@
|
||||
<!doctype html><html lang="en"><head><title>Example Domain</title><meta name="viewport" content="width=device-width, initial-scale=1"><style>body{background:#eee;width:60vw;margin:15vh auto;font-family:system-ui,sans-serif}h1{font-size:1.5em}div{opacity:0.8}a:link,a:visited{color:#348}</style><body><div><h1>Example Domain</h1><p>This domain is for use in documentation examples without needing permission. Avoid use in operations.<p><a href="https://iana.org/domains/example">Learn more</a></div></body></html>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"blockedDomainsCount": {},
|
||||
"resolvedDomainsCount": {},
|
||||
"lastSaved": "2025-12-15T23:53:03.778364374+08:00"
|
||||
}
|
||||
5655
data/stats.json
5655
data/stats.json
File diff suppressed because it is too large
Load Diff
BIN
dns-server
BIN
dns-server
Binary file not shown.
578
dns/server.go
578
dns/server.go
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@@ -37,6 +38,27 @@ type ClientStats struct {
|
||||
LastSeen time.Time
|
||||
}
|
||||
|
||||
// IPGeolocation IP地理位置信息
|
||||
type IPGeolocation struct {
|
||||
Country string `json:"country"` // 国家
|
||||
City string `json:"city"` // 城市
|
||||
Expiry time.Time `json:"expiry"` // 缓存过期时间
|
||||
}
|
||||
|
||||
// QueryLog 查询日志记录
|
||||
type QueryLog struct {
|
||||
Timestamp time.Time // 查询时间
|
||||
ClientIP string // 客户端IP
|
||||
Location string // IP地理位置(国家 城市)
|
||||
Domain string // 查询域名
|
||||
QueryType string // 查询类型
|
||||
ResponseTime int64 // 响应时间(ms)
|
||||
Result string // 查询结果(allowed, blocked, error)
|
||||
BlockRule string // 屏蔽规则(如果被屏蔽)
|
||||
BlockType string // 屏蔽类型(如果被屏蔽)
|
||||
FromCache bool // 是否来自缓存
|
||||
}
|
||||
|
||||
// StatsData 用于持久化的统计数据结构
|
||||
type StatsData struct {
|
||||
Stats *Stats `json:"stats"`
|
||||
@@ -73,11 +95,22 @@ type Server struct {
|
||||
dailyStats map[string]int64 // 按天统计屏蔽数量
|
||||
monthlyStatsMutex sync.RWMutex
|
||||
monthlyStats map[string]int64 // 按月统计屏蔽数量
|
||||
saveTicker *time.Ticker // 用于定时保存数据
|
||||
startTime time.Time // 服务器启动时间
|
||||
saveDone chan struct{} // 用于通知保存协程停止
|
||||
stopped bool // 服务器是否已经停止
|
||||
stoppedMutex sync.Mutex // 保护stopped标志的互斥锁
|
||||
queryLogsMutex sync.RWMutex
|
||||
queryLogs []QueryLog // 查询日志列表
|
||||
maxQueryLogs int // 最大保存日志数量
|
||||
saveTicker *time.Ticker // 用于定时保存数据
|
||||
startTime time.Time // 服务器启动时间
|
||||
saveDone chan struct{} // 用于通知保存协程停止
|
||||
stopped bool // 服务器是否已经停止
|
||||
stoppedMutex sync.Mutex // 保护stopped标志的互斥锁
|
||||
|
||||
// IP地理位置缓存
|
||||
ipGeolocationCache map[string]*IPGeolocation // IP地址到地理位置的映射
|
||||
ipGeolocationCacheMutex sync.RWMutex // 保护IP地理位置缓存的互斥锁
|
||||
ipGeolocationCacheTTL time.Duration // 缓存有效期
|
||||
|
||||
// DNS查询缓存
|
||||
dnsCache *DNSCache // DNS响应缓存
|
||||
}
|
||||
|
||||
// Stats DNS服务器统计信息
|
||||
@@ -97,6 +130,10 @@ type Stats struct {
|
||||
// NewServer 创建DNS服务器实例
|
||||
func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shieldManager *shield.ShieldManager) *Server {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// 从配置中读取DNS缓存TTL值(分钟)
|
||||
cacheTTL := time.Duration(config.CacheTTL) * time.Minute
|
||||
|
||||
server := &Server{
|
||||
config: config,
|
||||
shieldConfig: shieldConfig,
|
||||
@@ -125,8 +162,15 @@ func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shie
|
||||
hourlyStats: make(map[string]int64),
|
||||
dailyStats: make(map[string]int64),
|
||||
monthlyStats: make(map[string]int64),
|
||||
queryLogs: make([]QueryLog, 0, 1000), // 初始化查询日志切片,容量1000
|
||||
maxQueryLogs: 10000, // 最大保存10000条日志
|
||||
saveDone: make(chan struct{}),
|
||||
stopped: false, // 初始化为未停止状态
|
||||
// IP地理位置缓存初始化
|
||||
ipGeolocationCache: make(map[string]*IPGeolocation),
|
||||
ipGeolocationCacheTTL: 24 * time.Hour, // 缓存有效期24小时
|
||||
// DNS查询缓存初始化
|
||||
dnsCache: NewDNSCache(cacheTTL),
|
||||
}
|
||||
|
||||
// 加载已保存的统计数据
|
||||
@@ -232,8 +276,16 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
// 获取来源IP
|
||||
sourceIP := w.RemoteAddr().String()
|
||||
// 提取IP地址部分,去掉端口
|
||||
if idx := strings.LastIndex(sourceIP, ":"); idx >= 0 {
|
||||
sourceIP = sourceIP[:idx]
|
||||
if strings.HasPrefix(sourceIP, "[") {
|
||||
// IPv6地址格式: [::1]:53
|
||||
if idx := strings.Index(sourceIP, "]"); idx >= 0 {
|
||||
sourceIP = sourceIP[1:idx] // 去掉方括号
|
||||
}
|
||||
} else {
|
||||
// IPv4地址格式: 127.0.0.1:53
|
||||
if idx := strings.LastIndex(sourceIP, ":"); idx >= 0 {
|
||||
sourceIP = sourceIP[:idx]
|
||||
}
|
||||
}
|
||||
|
||||
// 更新来源IP统计
|
||||
@@ -246,28 +298,10 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
// 更新客户端统计
|
||||
s.updateClientStats(sourceIP)
|
||||
|
||||
// 只处理递归查询
|
||||
if r.RecursionDesired == false {
|
||||
response := new(dns.Msg)
|
||||
response.SetReply(r)
|
||||
response.RecursionAvailable = true
|
||||
response.SetRcode(r, dns.RcodeRefused)
|
||||
w.WriteMsg(response)
|
||||
|
||||
// 计算响应时间
|
||||
responseTime := time.Since(startTime).Milliseconds()
|
||||
s.updateStats(func(stats *Stats) {
|
||||
stats.TotalResponseTime += responseTime
|
||||
if stats.Queries > 0 {
|
||||
stats.AvgResponseTime = float64(stats.TotalResponseTime) / float64(stats.Queries)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取查询域名和类型
|
||||
var domain string
|
||||
var queryType string
|
||||
var qType uint16
|
||||
if len(r.Question) > 0 {
|
||||
domain = r.Question[0].Name
|
||||
// 移除末尾的点
|
||||
@@ -276,6 +310,7 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
}
|
||||
// 获取查询类型
|
||||
queryType = dns.TypeToString[r.Question[0].Qtype]
|
||||
qType = r.Question[0].Qtype
|
||||
// 更新查询类型统计
|
||||
s.updateStats(func(stats *Stats) {
|
||||
stats.QueryTypes[queryType]++
|
||||
@@ -284,22 +319,52 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
|
||||
logger.Debug("接收到DNS查询", "domain", domain, "type", queryType, "client", w.RemoteAddr())
|
||||
|
||||
// 检查hosts文件是否有匹配
|
||||
if ip, exists := s.shieldManager.GetHostsIP(domain); exists {
|
||||
s.handleHostsResponse(w, r, ip)
|
||||
// 计算响应时间
|
||||
responseTime := time.Since(startTime).Milliseconds()
|
||||
// 只处理递归查询
|
||||
if r.RecursionDesired == false {
|
||||
response := new(dns.Msg)
|
||||
response.SetReply(r)
|
||||
response.RecursionAvailable = true
|
||||
response.SetRcode(r, dns.RcodeRefused)
|
||||
w.WriteMsg(response)
|
||||
|
||||
// 缓存命中,响应时间设为0ms
|
||||
responseTime := int64(0)
|
||||
s.updateStats(func(stats *Stats) {
|
||||
stats.TotalResponseTime += responseTime
|
||||
if stats.Queries > 0 {
|
||||
stats.AvgResponseTime = float64(stats.TotalResponseTime) / float64(stats.Queries)
|
||||
}
|
||||
})
|
||||
|
||||
// 添加查询日志
|
||||
s.addQueryLog(sourceIP, domain, queryType, responseTime, "error", "", "", false)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查hosts文件是否有匹配
|
||||
if ip, exists := s.shieldManager.GetHostsIP(domain); exists {
|
||||
s.handleHostsResponse(w, r, ip)
|
||||
// 缓存命中,响应时间设为0ms
|
||||
responseTime := int64(0)
|
||||
s.updateStats(func(stats *Stats) {
|
||||
stats.TotalResponseTime += responseTime
|
||||
if stats.Queries > 0 {
|
||||
stats.AvgResponseTime = float64(stats.TotalResponseTime) / float64(stats.Queries)
|
||||
}
|
||||
})
|
||||
|
||||
// 添加查询日志
|
||||
s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", false)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否被屏蔽
|
||||
if s.shieldManager.IsBlocked(domain) {
|
||||
// 获取屏蔽详情
|
||||
blockDetails := s.shieldManager.CheckDomainBlockDetails(domain)
|
||||
blockRule, _ := blockDetails["blockRule"].(string)
|
||||
blockType, _ := blockDetails["blockRuleType"].(string)
|
||||
|
||||
s.handleBlockedResponse(w, r, domain)
|
||||
// 计算响应时间
|
||||
responseTime := time.Since(startTime).Milliseconds()
|
||||
@@ -309,19 +374,68 @@ func (s *Server) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
stats.AvgResponseTime = float64(stats.TotalResponseTime) / float64(stats.Queries)
|
||||
}
|
||||
})
|
||||
|
||||
// 添加查询日志
|
||||
s.addQueryLog(sourceIP, domain, queryType, responseTime, "blocked", blockRule, blockType, false)
|
||||
return
|
||||
}
|
||||
|
||||
// 转发到上游DNS服务器
|
||||
s.forwardDNSRequest(w, r, domain)
|
||||
// 计算响应时间
|
||||
responseTime := time.Since(startTime).Milliseconds()
|
||||
// 检查缓存中是否有响应(增强版缓存查询)
|
||||
if cachedResponse, found := s.dnsCache.Get(r.Question[0].Name, qType); found {
|
||||
// 缓存命中,直接返回缓存的响应
|
||||
cachedResponseCopy := cachedResponse.Copy() // 创建响应副本避免并发修改问题
|
||||
cachedResponseCopy.Id = r.Id // 更新ID以匹配请求
|
||||
cachedResponseCopy.Compress = true
|
||||
w.WriteMsg(cachedResponseCopy)
|
||||
|
||||
// 计算响应时间
|
||||
responseTime := time.Since(startTime).Milliseconds()
|
||||
s.updateStats(func(stats *Stats) {
|
||||
stats.TotalResponseTime += responseTime
|
||||
if stats.Queries > 0 {
|
||||
stats.AvgResponseTime = float64(stats.TotalResponseTime) / float64(stats.Queries)
|
||||
}
|
||||
})
|
||||
|
||||
// 添加查询日志 - 标记为缓存
|
||||
s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", true)
|
||||
logger.Debug("从缓存返回DNS响应", "domain", domain, "type", queryType)
|
||||
return
|
||||
}
|
||||
|
||||
// 缓存未命中,转发到上游DNS服务器
|
||||
response, rtt := s.forwardDNSRequestWithCache(r, domain)
|
||||
if response != nil {
|
||||
// 写入响应给客户端
|
||||
w.WriteMsg(response)
|
||||
}
|
||||
|
||||
// 使用上游服务器的实际响应时间(转换为毫秒)
|
||||
responseTime := int64(rtt.Milliseconds())
|
||||
// 如果rtt为0(查询失败),则使用本地计算的时间
|
||||
if responseTime == 0 {
|
||||
responseTime = time.Since(startTime).Milliseconds()
|
||||
}
|
||||
|
||||
s.updateStats(func(stats *Stats) {
|
||||
stats.TotalResponseTime += responseTime
|
||||
if stats.Queries > 0 {
|
||||
stats.AvgResponseTime = float64(stats.TotalResponseTime) / float64(stats.Queries)
|
||||
}
|
||||
})
|
||||
|
||||
// 如果响应成功,缓存结果(增强版缓存存储)
|
||||
if response != nil && response.Rcode == dns.RcodeSuccess {
|
||||
// 创建响应副本以避免后续修改影响缓存
|
||||
responseCopy := response.Copy()
|
||||
// 设置合理的TTL,不超过默认的30分钟
|
||||
defaultCacheTTL := 30 * time.Minute
|
||||
s.dnsCache.Set(r.Question[0].Name, qType, responseCopy, defaultCacheTTL)
|
||||
logger.Debug("DNS响应已缓存", "domain", domain, "type", queryType, "ttl", defaultCacheTTL)
|
||||
}
|
||||
|
||||
// 添加查询日志 - 标记为实时
|
||||
s.addQueryLog(sourceIP, domain, queryType, responseTime, "allowed", "", "", false)
|
||||
}
|
||||
|
||||
// handleHostsResponse 处理hosts文件匹配的响应
|
||||
@@ -425,7 +539,8 @@ func (s *Server) handleBlockedResponse(w dns.ResponseWriter, r *dns.Msg, domain
|
||||
}
|
||||
|
||||
// forwardDNSRequest 转发DNS请求到上游服务器
|
||||
func (s *Server) forwardDNSRequest(w dns.ResponseWriter, r *dns.Msg, domain string) {
|
||||
// forwardDNSRequestWithCache 转发DNS请求到上游服务器并返回响应
|
||||
func (s *Server) forwardDNSRequestWithCache(r *dns.Msg, domain string) (*dns.Msg, time.Duration) {
|
||||
// 尝试所有上游DNS服务器
|
||||
for _, upstream := range s.config.UpstreamDNS {
|
||||
response, rtt, err := s.resolver.Exchange(r, upstream)
|
||||
@@ -433,7 +548,6 @@ func (s *Server) forwardDNSRequest(w dns.ResponseWriter, r *dns.Msg, domain stri
|
||||
// 设置递归可用标志
|
||||
response.RecursionAvailable = true
|
||||
|
||||
w.WriteMsg(response)
|
||||
logger.Debug("DNS查询成功", "domain", domain, "rtt", rtt, "server", upstream)
|
||||
|
||||
// 记录解析域名统计
|
||||
@@ -442,7 +556,7 @@ func (s *Server) forwardDNSRequest(w dns.ResponseWriter, r *dns.Msg, domain stri
|
||||
s.updateStats(func(stats *Stats) {
|
||||
stats.Allowed++
|
||||
})
|
||||
return
|
||||
return response, rtt
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,12 +565,18 @@ func (s *Server) forwardDNSRequest(w dns.ResponseWriter, r *dns.Msg, domain stri
|
||||
response.SetReply(r)
|
||||
response.RecursionAvailable = true
|
||||
response.SetRcode(r, dns.RcodeServerFailure)
|
||||
w.WriteMsg(response)
|
||||
|
||||
logger.Error("DNS查询失败", "domain", domain)
|
||||
s.updateStats(func(stats *Stats) {
|
||||
stats.Errors++
|
||||
})
|
||||
return response, 0
|
||||
}
|
||||
|
||||
// forwardDNSRequest 转发DNS请求到上游服务器
|
||||
func (s *Server) forwardDNSRequest(w dns.ResponseWriter, r *dns.Msg, domain string) {
|
||||
response, _ := s.forwardDNSRequestWithCache(r, domain)
|
||||
w.WriteMsg(response)
|
||||
}
|
||||
|
||||
// updateBlockedDomainStats 更新被屏蔽域名统计
|
||||
@@ -539,6 +659,38 @@ func (s *Server) updateStats(update func(*Stats)) {
|
||||
update(s.stats)
|
||||
}
|
||||
|
||||
// addQueryLog 添加查询日志
|
||||
func (s *Server) addQueryLog(clientIP, domain, queryType string, responseTime int64, result, blockRule, blockType string, fromCache bool) {
|
||||
// 获取IP地理位置
|
||||
location := s.getIpGeolocation(clientIP)
|
||||
|
||||
// 创建日志记录
|
||||
log := QueryLog{
|
||||
Timestamp: time.Now(),
|
||||
ClientIP: clientIP,
|
||||
Location: location,
|
||||
Domain: domain,
|
||||
QueryType: queryType,
|
||||
ResponseTime: responseTime,
|
||||
Result: result,
|
||||
BlockRule: blockRule,
|
||||
BlockType: blockType,
|
||||
FromCache: fromCache,
|
||||
}
|
||||
|
||||
// 添加到日志列表
|
||||
s.queryLogsMutex.Lock()
|
||||
defer s.queryLogsMutex.Unlock()
|
||||
|
||||
// 插入到列表开头
|
||||
s.queryLogs = append([]QueryLog{log}, s.queryLogs...)
|
||||
|
||||
// 限制日志数量
|
||||
if len(s.queryLogs) > s.maxQueryLogs {
|
||||
s.queryLogs = s.queryLogs[:s.maxQueryLogs]
|
||||
}
|
||||
}
|
||||
|
||||
// GetStartTime 获取服务器启动时间
|
||||
func (s *Server) GetStartTime() time.Time {
|
||||
return s.startTime
|
||||
@@ -576,6 +728,145 @@ func (s *Server) GetStats() *Stats {
|
||||
}
|
||||
}
|
||||
|
||||
// GetQueryLogs 获取查询日志
|
||||
func (s *Server) GetQueryLogs(limit, offset int, sortField, sortDirection, resultFilter, searchTerm string) []QueryLog {
|
||||
s.queryLogsMutex.RLock()
|
||||
defer s.queryLogsMutex.RUnlock()
|
||||
|
||||
// 确保偏移量和限制值合理
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = 100 // 默认返回100条日志
|
||||
}
|
||||
|
||||
// 创建日志副本用于过滤和排序
|
||||
var logsCopy []QueryLog
|
||||
|
||||
// 先过滤日志
|
||||
for _, log := range s.queryLogs {
|
||||
// 应用结果过滤
|
||||
if resultFilter != "" && log.Result != resultFilter {
|
||||
continue
|
||||
}
|
||||
|
||||
// 应用搜索过滤
|
||||
if searchTerm != "" {
|
||||
// 搜索域名或客户端IP
|
||||
if !strings.Contains(log.Domain, searchTerm) && !strings.Contains(log.ClientIP, searchTerm) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
logsCopy = append(logsCopy, log)
|
||||
}
|
||||
|
||||
// 排序日志
|
||||
if sortField != "" {
|
||||
sort.Slice(logsCopy, func(i, j int) bool {
|
||||
var a, b interface{}
|
||||
switch sortField {
|
||||
case "time":
|
||||
a = logsCopy[i].Timestamp
|
||||
b = logsCopy[j].Timestamp
|
||||
case "clientIp":
|
||||
a = logsCopy[i].ClientIP
|
||||
b = logsCopy[j].ClientIP
|
||||
case "domain":
|
||||
a = logsCopy[i].Domain
|
||||
b = logsCopy[j].Domain
|
||||
case "responseTime":
|
||||
a = logsCopy[i].ResponseTime
|
||||
b = logsCopy[j].ResponseTime
|
||||
case "blockRule":
|
||||
a = logsCopy[i].BlockRule
|
||||
b = logsCopy[j].BlockRule
|
||||
default:
|
||||
// 默认按时间排序
|
||||
a = logsCopy[i].Timestamp
|
||||
b = logsCopy[j].Timestamp
|
||||
}
|
||||
|
||||
// 根据排序方向比较
|
||||
if sortDirection == "asc" {
|
||||
return compareValues(a, b) < 0
|
||||
}
|
||||
return compareValues(a, b) > 0
|
||||
})
|
||||
}
|
||||
|
||||
// 计算返回范围
|
||||
start := offset
|
||||
end := offset + limit
|
||||
if end > len(logsCopy) {
|
||||
end = len(logsCopy)
|
||||
}
|
||||
if start >= len(logsCopy) {
|
||||
return []QueryLog{} // 没有数据,返回空切片
|
||||
}
|
||||
|
||||
return logsCopy[start:end]
|
||||
}
|
||||
|
||||
// compareValues 比较两个值
|
||||
func compareValues(a, b interface{}) int {
|
||||
switch v1 := a.(type) {
|
||||
case time.Time:
|
||||
v2 := b.(time.Time)
|
||||
if v1.Before(v2) {
|
||||
return -1
|
||||
}
|
||||
if v1.After(v2) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
case string:
|
||||
v2 := b.(string)
|
||||
if v1 < v2 {
|
||||
return -1
|
||||
}
|
||||
if v1 > v2 {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
case int64:
|
||||
v2 := b.(int64)
|
||||
if v1 < v2 {
|
||||
return -1
|
||||
}
|
||||
if v1 > v2 {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// GetQueryLogsCount 获取查询日志总数
|
||||
func (s *Server) GetQueryLogsCount() int {
|
||||
s.queryLogsMutex.RLock()
|
||||
defer s.queryLogsMutex.RUnlock()
|
||||
return len(s.queryLogs)
|
||||
}
|
||||
|
||||
// GetQueryStats 获取查询统计信息
|
||||
func (s *Server) GetQueryStats() map[string]interface{} {
|
||||
s.statsMutex.Lock()
|
||||
defer s.statsMutex.Unlock()
|
||||
|
||||
// 计算统计数据
|
||||
return map[string]interface{}{
|
||||
"totalQueries": s.stats.Queries,
|
||||
"blockedQueries": s.stats.Blocked,
|
||||
"allowedQueries": s.stats.Allowed,
|
||||
"errorQueries": s.stats.Errors,
|
||||
"avgResponseTime": s.stats.AvgResponseTime,
|
||||
"activeIPs": len(s.stats.SourceIPs),
|
||||
}
|
||||
}
|
||||
|
||||
// GetTopBlockedDomains 获取TOP屏蔽域名列表
|
||||
func (s *Server) GetTopBlockedDomains(limit int) []BlockedDomain {
|
||||
s.blockedDomainsMutex.RLock()
|
||||
@@ -707,6 +998,132 @@ func (s *Server) GetMonthlyStats() map[string]int64 {
|
||||
return result
|
||||
}
|
||||
|
||||
// isPrivateIP 检测IP地址是否为内网IP
|
||||
func isPrivateIP(ip string) bool {
|
||||
// 解析IP地址
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查IPv4内网地址
|
||||
if ipv4 := parsedIP.To4(); ipv4 != nil {
|
||||
// 10.0.0.0/8
|
||||
if ipv4[0] == 10 {
|
||||
return true
|
||||
}
|
||||
// 172.16.0.0/12
|
||||
if ipv4[0] == 172 && (ipv4[1] >= 16 && ipv4[1] <= 31) {
|
||||
return true
|
||||
}
|
||||
// 192.168.0.0/16
|
||||
if ipv4[0] == 192 && ipv4[1] == 168 {
|
||||
return true
|
||||
}
|
||||
// 127.0.0.0/8 (localhost)
|
||||
if ipv4[0] == 127 {
|
||||
return true
|
||||
}
|
||||
// 169.254.0.0/16 (链路本地地址)
|
||||
if ipv4[0] == 169 && ipv4[1] == 254 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查IPv6内网地址
|
||||
// ::1/128 (localhost)
|
||||
if parsedIP.IsLoopback() {
|
||||
return true
|
||||
}
|
||||
// fc00::/7 (唯一本地地址)
|
||||
if parsedIP[0]&0xfc == 0xfc {
|
||||
return true
|
||||
}
|
||||
// fe80::/10 (链路本地地址)
|
||||
if parsedIP[0]&0xfe == 0xfe && parsedIP[1]&0xc0 == 0x80 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getIpGeolocation 获取IP地址的地理位置信息
|
||||
func (s *Server) getIpGeolocation(ip string) string {
|
||||
// 检查IP是否为本地或内网地址
|
||||
if isPrivateIP(ip) {
|
||||
return "内网 内网"
|
||||
}
|
||||
|
||||
// 先检查缓存
|
||||
s.ipGeolocationCacheMutex.RLock()
|
||||
geo, exists := s.ipGeolocationCache[ip]
|
||||
s.ipGeolocationCacheMutex.RUnlock()
|
||||
|
||||
// 如果缓存存在且未过期,直接返回
|
||||
if exists && time.Now().Before(geo.Expiry) {
|
||||
return fmt.Sprintf("%s %s", geo.Country, geo.City)
|
||||
}
|
||||
|
||||
// 缓存不存在或已过期,从API获取
|
||||
geoInfo, err := s.fetchIpGeolocationFromAPI(ip)
|
||||
if err != nil {
|
||||
logger.Error("获取IP地理位置失败", "ip", ip, "error", err)
|
||||
return "未知 未知"
|
||||
}
|
||||
|
||||
// 保存到缓存
|
||||
s.ipGeolocationCacheMutex.Lock()
|
||||
s.ipGeolocationCache[ip] = &IPGeolocation{
|
||||
Country: geoInfo["country"].(string),
|
||||
City: geoInfo["city"].(string),
|
||||
Expiry: time.Now().Add(s.ipGeolocationCacheTTL),
|
||||
}
|
||||
s.ipGeolocationCacheMutex.Unlock()
|
||||
|
||||
// 返回格式化的地理位置
|
||||
return fmt.Sprintf("%s %s", geoInfo["country"].(string), geoInfo["city"].(string))
|
||||
}
|
||||
|
||||
// fetchIpGeolocationFromAPI 从第三方API获取IP地理位置信息
|
||||
func (s *Server) fetchIpGeolocationFromAPI(ip string) (map[string]interface{}, error) {
|
||||
// 使用ip-api.com获取IP地理位置信息
|
||||
url := fmt.Sprintf("http://ip-api.com/json/%s?fields=country,city", ip)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应内容
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析JSON响应
|
||||
var result map[string]interface{}
|
||||
err = json.Unmarshal(body, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查API返回状态
|
||||
status, ok := result["status"].(string)
|
||||
if !ok || status != "success" {
|
||||
return nil, fmt.Errorf("API返回错误状态: %v", result)
|
||||
}
|
||||
|
||||
// 确保国家和城市字段存在
|
||||
if _, ok := result["country"]; !ok {
|
||||
result["country"] = "未知"
|
||||
}
|
||||
if _, ok := result["city"]; !ok {
|
||||
result["city"] = "未知"
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// loadStatsData 从文件加载统计数据
|
||||
func (s *Server) loadStatsData() {
|
||||
if s.config.StatsFile == "" {
|
||||
@@ -774,6 +1191,58 @@ func (s *Server) loadStatsData() {
|
||||
s.clientStatsMutex.Unlock()
|
||||
|
||||
logger.Info("统计数据加载成功")
|
||||
|
||||
// 加载查询日志
|
||||
s.loadQueryLogs()
|
||||
}
|
||||
|
||||
// loadQueryLogs 从文件加载查询日志
|
||||
func (s *Server) loadQueryLogs() {
|
||||
if s.config.StatsFile == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取绝对路径
|
||||
statsFilePath, err := filepath.Abs(s.config.StatsFile)
|
||||
if err != nil {
|
||||
logger.Error("获取统计文件绝对路径失败", "path", s.config.StatsFile, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 构建查询日志文件路径
|
||||
queryLogPath := filepath.Join(filepath.Dir(statsFilePath), "querylog.json")
|
||||
|
||||
// 检查文件是否存在
|
||||
if _, err := os.Stat(queryLogPath); os.IsNotExist(err) {
|
||||
logger.Info("查询日志文件不存在,将使用空列表", "file", queryLogPath)
|
||||
return
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
data, err := ioutil.ReadFile(queryLogPath)
|
||||
if err != nil {
|
||||
logger.Error("读取查询日志文件失败", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 解析数据
|
||||
var logs []QueryLog
|
||||
err = json.Unmarshal(data, &logs)
|
||||
if err != nil {
|
||||
logger.Error("解析查询日志失败", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新查询日志
|
||||
s.queryLogsMutex.Lock()
|
||||
s.queryLogs = logs
|
||||
// 确保日志数量不超过限制
|
||||
if len(s.queryLogs) > s.maxQueryLogs {
|
||||
s.queryLogs = s.queryLogs[:s.maxQueryLogs]
|
||||
}
|
||||
s.queryLogsMutex.Unlock()
|
||||
|
||||
logger.Info("查询日志加载成功", "count", len(logs))
|
||||
}
|
||||
|
||||
// saveStatsData 保存统计数据到文件
|
||||
@@ -862,6 +1331,37 @@ func (s *Server) saveStatsData() {
|
||||
}
|
||||
|
||||
logger.Info("统计数据保存成功", "file", statsFilePath)
|
||||
|
||||
// 保存查询日志到文件
|
||||
s.saveQueryLogs(statsDir)
|
||||
}
|
||||
|
||||
// saveQueryLogs 保存查询日志到文件
|
||||
func (s *Server) saveQueryLogs(dataDir string) {
|
||||
// 构建查询日志文件路径
|
||||
queryLogPath := filepath.Join(dataDir, "querylog.json")
|
||||
|
||||
// 获取查询日志数据
|
||||
s.queryLogsMutex.RLock()
|
||||
logsCopy := make([]QueryLog, len(s.queryLogs))
|
||||
copy(logsCopy, s.queryLogs)
|
||||
s.queryLogsMutex.RUnlock()
|
||||
|
||||
// 序列化数据
|
||||
jsonData, err := json.MarshalIndent(logsCopy, "", " ")
|
||||
if err != nil {
|
||||
logger.Error("序列化查询日志失败", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
err = os.WriteFile(queryLogPath, jsonData, 0644)
|
||||
if err != nil {
|
||||
logger.Error("保存查询日志到文件失败", "file", queryLogPath, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("查询日志保存成功", "file", queryLogPath)
|
||||
}
|
||||
|
||||
// startCpuUsageMonitor 启动CPU使用率监控
|
||||
|
||||
14
go.mod
14
go.mod
@@ -8,31 +8,17 @@ require (
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/miekg/dns v1.1.68
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/swaggo/http-swagger v1.2.0
|
||||
)
|
||||
|
||||
// 清理不需要的依赖
|
||||
// 之前的go.sum可能包含lumberjack的记录,但现在已经不再使用
|
||||
|
||||
require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2 // indirect
|
||||
github.com/swaggo/swag v1.7.8 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
94
go.sum
94
go.sum
@@ -1,126 +1,32 @@
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/agiledragon/gomonkey/v2 v2.3.1 h1:k+UnUY0EMNYUFUAQVETGY9uUTxjMdnUkP0ARyJS1zzs=
|
||||
github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
|
||||
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
|
||||
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
|
||||
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE=
|
||||
github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
|
||||
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
|
||||
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
|
||||
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
|
||||
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2 h1:+iNTcqQJy0OZ5jk6a5NLib47eqXK8uYcPX+O4+cBpEM=
|
||||
github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
|
||||
github.com/swaggo/http-swagger v1.2.0 h1:G5EBD5nvw379l2sFhact660YDT++eLviczLPrgNw/lU=
|
||||
github.com/swaggo/http-swagger v1.2.0/go.mod h1:P7+V1SLG2zloe+VvAGL7WgFimhJACaBLAv2N7YQ0ikI=
|
||||
github.com/swaggo/swag v1.7.8 h1:w249t0l/kc/DKMGlS0fppNJQxKyJ8heNaUWB6nsH3zc=
|
||||
github.com/swaggo/swag v1.7.8/go.mod h1:gZ+TJ2w/Ve1RwQsA2IRoSOTidHz6DX+PIG8GWvbnoLU=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
361
http/server.go
361
http/server.go
@@ -26,6 +26,11 @@ type Server struct {
|
||||
shieldManager *shield.ShieldManager
|
||||
server *http.Server
|
||||
|
||||
// 会话管理相关字段
|
||||
sessions map[string]time.Time // 会话ID到过期时间的映射
|
||||
sessionsMutex sync.Mutex // 会话映射的互斥锁
|
||||
sessionTTL time.Duration // 会话过期时间
|
||||
|
||||
// WebSocket相关字段
|
||||
upgrader websocket.Upgrader
|
||||
clients map[*websocket.Conn]bool
|
||||
@@ -50,10 +55,15 @@ func NewServer(globalConfig *config.Config, dnsServer *dns.Server, shieldManager
|
||||
},
|
||||
clients: make(map[*websocket.Conn]bool),
|
||||
broadcastChan: make(chan []byte, 100),
|
||||
// 会话管理初始化
|
||||
sessions: make(map[string]time.Time),
|
||||
sessionTTL: 24 * time.Hour, // 会话有效期24小时
|
||||
}
|
||||
|
||||
// 启动广播协程
|
||||
go server.startBroadcastLoop()
|
||||
// 启动会话清理协程
|
||||
go server.cleanupSessionsLoop()
|
||||
|
||||
return server
|
||||
}
|
||||
@@ -62,17 +72,30 @@ func NewServer(globalConfig *config.Config, dnsServer *dns.Server, shieldManager
|
||||
func (s *Server) Start() error {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// 登录路由,不需要认证
|
||||
mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
|
||||
// 重定向到登录页面HTML
|
||||
http.Redirect(w, r, "/login.html", http.StatusFound)
|
||||
})
|
||||
|
||||
// API路由
|
||||
if s.config.EnableAPI {
|
||||
// 重定向/api到Swagger UI页面
|
||||
mux.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/api/index.html", http.StatusMovedPermanently)
|
||||
})
|
||||
// 登录API端点,不需要认证
|
||||
mux.HandleFunc("/api/login", s.handleLogin)
|
||||
// 注销API端点,不需要认证
|
||||
mux.HandleFunc("/api/logout", s.handleLogout)
|
||||
// 修改密码API端点,需要认证
|
||||
mux.HandleFunc("/api/change-password", s.loginRequired(s.handleChangePassword))
|
||||
|
||||
// 注册所有API端点
|
||||
mux.HandleFunc("/api/stats", s.handleStats)
|
||||
mux.HandleFunc("/api/shield", s.handleShield)
|
||||
mux.HandleFunc("/api/shield/localrules", func(w http.ResponseWriter, r *http.Request) {
|
||||
// 重定向/api到Swagger UI页面
|
||||
mux.HandleFunc("/api", s.loginRequired(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/api/index.html", http.StatusMovedPermanently)
|
||||
}))
|
||||
|
||||
// 注册所有API端点,应用登录中间件
|
||||
mux.HandleFunc("/api/stats", s.loginRequired(s.handleStats))
|
||||
mux.HandleFunc("/api/shield", s.loginRequired(s.handleShield))
|
||||
mux.HandleFunc("/api/shield/localrules", s.loginRequired(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.Method == http.MethodGet {
|
||||
localRules := s.shieldManager.GetLocalRules()
|
||||
@@ -80,8 +103,8 @@ func (s *Server) Start() error {
|
||||
return
|
||||
}
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
})
|
||||
mux.HandleFunc("/api/shield/remoterules", func(w http.ResponseWriter, r *http.Request) {
|
||||
}))
|
||||
mux.HandleFunc("/api/shield/remoterules", s.loginRequired(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.Method == http.MethodGet {
|
||||
remoteRules := s.shieldManager.GetRemoteRules()
|
||||
@@ -89,41 +112,57 @@ func (s *Server) Start() error {
|
||||
return
|
||||
}
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
})
|
||||
mux.HandleFunc("/api/shield/hosts", s.handleShieldHosts)
|
||||
mux.HandleFunc("/api/shield/blacklists", s.handleShieldBlacklists)
|
||||
mux.HandleFunc("/api/query", s.handleQuery)
|
||||
mux.HandleFunc("/api/status", s.handleStatus)
|
||||
mux.HandleFunc("/api/config", s.handleConfig)
|
||||
mux.HandleFunc("/api/config/restart", s.handleRestart)
|
||||
}))
|
||||
mux.HandleFunc("/api/shield/hosts", s.loginRequired(s.handleShieldHosts))
|
||||
mux.HandleFunc("/api/shield/blacklists", s.loginRequired(s.handleShieldBlacklists))
|
||||
mux.HandleFunc("/api/query", s.loginRequired(s.handleQuery))
|
||||
mux.HandleFunc("/api/status", s.loginRequired(s.handleStatus))
|
||||
mux.HandleFunc("/api/config", s.loginRequired(s.handleConfig))
|
||||
mux.HandleFunc("/api/config/restart", s.loginRequired(s.handleRestart))
|
||||
// 添加统计相关接口
|
||||
mux.HandleFunc("/api/top-blocked", s.handleTopBlockedDomains)
|
||||
mux.HandleFunc("/api/top-resolved", s.handleTopResolvedDomains)
|
||||
mux.HandleFunc("/api/top-clients", s.handleTopClients)
|
||||
mux.HandleFunc("/api/top-domains", s.handleTopDomains)
|
||||
mux.HandleFunc("/api/recent-blocked", s.handleRecentBlockedDomains)
|
||||
mux.HandleFunc("/api/hourly-stats", s.handleHourlyStats)
|
||||
mux.HandleFunc("/api/daily-stats", s.handleDailyStats)
|
||||
mux.HandleFunc("/api/monthly-stats", s.handleMonthlyStats)
|
||||
mux.HandleFunc("/api/query/type", s.handleQueryTypeStats)
|
||||
mux.HandleFunc("/api/top-blocked", s.loginRequired(s.handleTopBlockedDomains))
|
||||
mux.HandleFunc("/api/top-resolved", s.loginRequired(s.handleTopResolvedDomains))
|
||||
mux.HandleFunc("/api/top-clients", s.loginRequired(s.handleTopClients))
|
||||
mux.HandleFunc("/api/top-domains", s.loginRequired(s.handleTopDomains))
|
||||
mux.HandleFunc("/api/recent-blocked", s.loginRequired(s.handleRecentBlockedDomains))
|
||||
mux.HandleFunc("/api/hourly-stats", s.loginRequired(s.handleHourlyStats))
|
||||
mux.HandleFunc("/api/daily-stats", s.loginRequired(s.handleDailyStats))
|
||||
mux.HandleFunc("/api/monthly-stats", s.loginRequired(s.handleMonthlyStats))
|
||||
mux.HandleFunc("/api/query/type", s.loginRequired(s.handleQueryTypeStats))
|
||||
// 日志统计相关接口
|
||||
mux.HandleFunc("/api/logs/stats", s.loginRequired(s.handleLogsStats))
|
||||
mux.HandleFunc("/api/logs/query", s.loginRequired(s.handleLogsQuery))
|
||||
mux.HandleFunc("/api/logs/count", s.loginRequired(s.handleLogsCount))
|
||||
// WebSocket端点
|
||||
mux.HandleFunc("/ws/stats", s.handleWebSocketStats)
|
||||
mux.HandleFunc("/ws/stats", s.loginRequired(s.handleWebSocketStats))
|
||||
|
||||
// 将/api/下的静态文件服务指向static/api目录,放在最后以避免覆盖API端点
|
||||
apiFileServer := http.FileServer(http.Dir("./static/api"))
|
||||
mux.Handle("/api/", http.StripPrefix("/api", apiFileServer))
|
||||
mux.Handle("/api/", s.loginRequired(http.StripPrefix("/api", apiFileServer).ServeHTTP))
|
||||
}
|
||||
|
||||
// 自定义静态文件服务处理器,用于禁用浏览器缓存,放在API路由之后
|
||||
fileServer := http.FileServer(http.Dir("./static"))
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// 单独处理login.html,不需要登录
|
||||
mux.HandleFunc("/login.html", 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")
|
||||
// 直接提供login.html文件
|
||||
http.ServeFile(w, r, "./static/login.html")
|
||||
})
|
||||
|
||||
// 其他静态文件需要登录
|
||||
mux.HandleFunc("/", s.loginRequired(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 添加Cache-Control头,禁用浏览器缓存
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
|
||||
// 使用StripPrefix处理路径
|
||||
http.StripPrefix("/", fileServer).ServeHTTP(w, r)
|
||||
})
|
||||
}))
|
||||
|
||||
s.server = &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", s.config.Host, s.config.Port),
|
||||
@@ -374,6 +413,78 @@ func (s *Server) startBroadcastLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupSessionsLoop 定期清理过期会话
|
||||
func (s *Server) cleanupSessionsLoop() {
|
||||
for {
|
||||
time.Sleep(1 * time.Hour) // 每小时清理一次
|
||||
s.sessionsMutex.Lock()
|
||||
now := time.Now()
|
||||
for sessionID, expiryTime := range s.sessions {
|
||||
if now.After(expiryTime) {
|
||||
delete(s.sessions, sessionID)
|
||||
}
|
||||
}
|
||||
s.sessionsMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// isAuthenticated 检查用户是否已认证
|
||||
func (s *Server) isAuthenticated(r *http.Request) bool {
|
||||
// 从Cookie中获取会话ID
|
||||
cookie, err := r.Cookie("session_id")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
sessionID := cookie.Value
|
||||
s.sessionsMutex.Lock()
|
||||
defer s.sessionsMutex.Unlock()
|
||||
|
||||
// 检查会话是否存在且未过期
|
||||
expiryTime, exists := s.sessions[sessionID]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
if time.Now().After(expiryTime) {
|
||||
// 会话已过期,删除它
|
||||
delete(s.sessions, sessionID)
|
||||
return false
|
||||
}
|
||||
|
||||
// 延长会话有效期
|
||||
s.sessions[sessionID] = time.Now().Add(s.sessionTTL)
|
||||
return true
|
||||
}
|
||||
|
||||
// loginRequired 登录中间件
|
||||
func (s *Server) loginRequired(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// 检查是否为登录页面或登录API,允许直接访问
|
||||
if r.URL.Path == "/login" || r.URL.Path == "/api/login" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否已认证
|
||||
if !s.isAuthenticated(r) {
|
||||
// 如果是API请求,返回401错误
|
||||
if strings.HasPrefix(r.URL.Path, "/api/") || strings.HasPrefix(r.URL.Path, "/ws/") {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "未授权访问"})
|
||||
return
|
||||
}
|
||||
// 否则重定向到登录页面
|
||||
http.Redirect(w, r, "/login", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
// 已认证,继续处理请求
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// handleTopBlockedDomains 处理TOP屏蔽域名请求
|
||||
func (s *Server) handleTopBlockedDomains(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
@@ -1217,6 +1328,64 @@ func checkURLExists(url string) bool {
|
||||
return resp.StatusCode >= 200 && resp.StatusCode < 400
|
||||
}
|
||||
|
||||
// handleLogsStats 处理日志统计请求
|
||||
func (s *Server) handleLogsStats(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取日志统计数据
|
||||
logStats := s.dnsServer.GetQueryStats()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(logStats)
|
||||
}
|
||||
|
||||
// handleLogsQuery 处理日志查询请求
|
||||
func (s *Server) handleLogsQuery(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取查询参数
|
||||
limit := 100 // 默认返回100条日志
|
||||
offset := 0
|
||||
sortField := r.URL.Query().Get("sort")
|
||||
sortDirection := r.URL.Query().Get("direction")
|
||||
resultFilter := r.URL.Query().Get("result")
|
||||
searchTerm := r.URL.Query().Get("search")
|
||||
|
||||
if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
|
||||
fmt.Sscanf(limitStr, "%d", &limit)
|
||||
}
|
||||
|
||||
if offsetStr := r.URL.Query().Get("offset"); offsetStr != "" {
|
||||
fmt.Sscanf(offsetStr, "%d", &offset)
|
||||
}
|
||||
|
||||
// 获取日志数据
|
||||
logs := s.dnsServer.GetQueryLogs(limit, offset, sortField, sortDirection, resultFilter, searchTerm)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(logs)
|
||||
}
|
||||
|
||||
// handleLogsCount 处理日志总数请求
|
||||
func (s *Server) handleLogsCount(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取日志总数
|
||||
count := s.dnsServer.GetQueryLogsCount()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]int{"count": count})
|
||||
}
|
||||
|
||||
// handleRestart 处理重启服务请求
|
||||
func (s *Server) handleRestart(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
@@ -1250,3 +1419,135 @@ func (s *Server) handleRestart(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "服务已重启"})
|
||||
logger.Info("服务重启成功")
|
||||
}
|
||||
|
||||
// handleLogin 处理登录请求
|
||||
func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// 解析请求体
|
||||
var loginData struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&loginData); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "无效的请求体"})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证用户名和密码
|
||||
if loginData.Username != s.config.Username || loginData.Password != s.config.Password {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "用户名或密码错误"})
|
||||
return
|
||||
}
|
||||
|
||||
// 生成会话ID
|
||||
sessionID := fmt.Sprintf("%d_%d", time.Now().UnixNano(), len(s.sessions))
|
||||
|
||||
// 保存会话
|
||||
s.sessionsMutex.Lock()
|
||||
s.sessions[sessionID] = time.Now().Add(s.sessionTTL)
|
||||
s.sessionsMutex.Unlock()
|
||||
|
||||
// 设置Cookie
|
||||
cookie := &http.Cookie{
|
||||
Name: "session_id",
|
||||
Value: sessionID,
|
||||
Path: "/",
|
||||
Expires: time.Now().Add(s.sessionTTL),
|
||||
HttpOnly: true,
|
||||
Secure: false, // 开发环境下使用false,生产环境应使用true
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
|
||||
// 返回成功响应
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "登录成功"})
|
||||
logger.Info(fmt.Sprintf("用户 %s 登录成功", loginData.Username))
|
||||
}
|
||||
|
||||
// handleLogout 处理注销请求
|
||||
func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// 从Cookie中获取会话ID
|
||||
cookie, err := r.Cookie("session_id")
|
||||
if err == nil {
|
||||
// 删除会话
|
||||
s.sessionsMutex.Lock()
|
||||
delete(s.sessions, cookie.Value)
|
||||
s.sessionsMutex.Unlock()
|
||||
}
|
||||
|
||||
// 清除Cookie
|
||||
clearCookie := &http.Cookie{
|
||||
Name: "session_id",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
Expires: time.Unix(0, 0),
|
||||
HttpOnly: true,
|
||||
Secure: false,
|
||||
}
|
||||
http.SetCookie(w, clearCookie)
|
||||
|
||||
// 返回成功响应
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "注销成功"})
|
||||
logger.Info("用户注销成功")
|
||||
}
|
||||
|
||||
// handleChangePassword 处理修改密码请求
|
||||
func (s *Server) handleChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// 解析请求体
|
||||
var changePasswordData struct {
|
||||
CurrentPassword string `json:"currentPassword"`
|
||||
NewPassword string `json:"newPassword"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&changePasswordData); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "无效的请求体"})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证当前密码
|
||||
if changePasswordData.CurrentPassword != s.config.Password {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "当前密码错误"})
|
||||
return
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
s.config.Password = changePasswordData.NewPassword
|
||||
|
||||
// 保存配置到文件
|
||||
if err := saveConfigToFile(s.globalConfig, "./config.json"); err != nil {
|
||||
logger.Error("保存配置文件失败", "error", err)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "保存密码失败"})
|
||||
return
|
||||
}
|
||||
|
||||
// 返回成功响应
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "密码修改成功"})
|
||||
logger.Info("密码修改成功")
|
||||
}
|
||||
|
||||
298
js/api.js
298
js/api.js
@@ -1,298 +0,0 @@
|
||||
// API模块 - 统一管理所有API调用
|
||||
|
||||
// API路径定义
|
||||
const API_BASE_URL = '/api';
|
||||
|
||||
// API请求封装
|
||||
async function apiRequest(endpoint, method = 'GET', data = null) {
|
||||
const url = `${API_BASE_URL}${endpoint}`;
|
||||
const options = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0',
|
||||
'Pragma': 'no-cache',
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
};
|
||||
|
||||
if (data) {
|
||||
options.body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
// 添加超时处理
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error('请求超时'));
|
||||
}, 10000); // 10秒超时
|
||||
});
|
||||
|
||||
try {
|
||||
// 竞争:请求或超时
|
||||
const response = await Promise.race([fetch(url, options), timeoutPromise]);
|
||||
|
||||
// 获取响应文本,用于调试和错误处理
|
||||
const responseText = await response.text();
|
||||
|
||||
if (!response.ok) {
|
||||
// 优化错误响应处理
|
||||
console.warn(`API请求失败: ${response.status}`);
|
||||
|
||||
// 尝试解析JSON,但如果失败,直接使用原始文本作为错误信息
|
||||
try {
|
||||
const errorData = JSON.parse(responseText);
|
||||
return { error: errorData.error || responseText || `请求失败: ${response.status}` };
|
||||
} catch (parseError) {
|
||||
// 当响应不是有效的JSON时(如中文错误信息),直接使用原始文本
|
||||
console.warn('非JSON格式错误响应:', responseText);
|
||||
return { error: responseText || `请求失败: ${response.status}` };
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试解析成功响应
|
||||
try {
|
||||
// 首先检查响应文本是否为空
|
||||
if (!responseText || responseText.trim() === '') {
|
||||
console.warn('空响应文本');
|
||||
return null; // 返回null表示空响应
|
||||
}
|
||||
|
||||
// 尝试解析JSON
|
||||
const parsedData = JSON.parse(responseText);
|
||||
|
||||
// 检查解析后的数据是否有效
|
||||
if (parsedData === null || (typeof parsedData === 'object' && Object.keys(parsedData).length === 0)) {
|
||||
console.warn('解析后的数据为空');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 限制所有数字为两位小数
|
||||
const formatNumbers = (obj) => {
|
||||
if (typeof obj === 'number') {
|
||||
return parseFloat(obj.toFixed(2));
|
||||
} else if (Array.isArray(obj)) {
|
||||
return obj.map(formatNumbers);
|
||||
} else if (obj && typeof obj === 'object') {
|
||||
const formattedObj = {};
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
formattedObj[key] = formatNumbers(obj[key]);
|
||||
}
|
||||
}
|
||||
return formattedObj;
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
const formattedData = formatNumbers(parsedData);
|
||||
return formattedData;
|
||||
} catch (parseError) {
|
||||
// 详细记录错误信息和响应内容
|
||||
console.error('JSON解析错误:', parseError);
|
||||
console.error('原始响应文本:', responseText);
|
||||
console.error('响应长度:', responseText.length);
|
||||
console.error('响应前100字符:', responseText.substring(0, 100));
|
||||
|
||||
// 如果是位置66附近的错误,特别标记
|
||||
if (parseError.message.includes('position 66')) {
|
||||
console.error('位置66附近的字符:', responseText.substring(60, 75));
|
||||
}
|
||||
|
||||
// 返回错误对象,让上层处理
|
||||
return { error: 'JSON解析错误' };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('API请求错误:', error);
|
||||
// 返回错误对象,而不是抛出异常,让上层处理
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// API方法集合
|
||||
const api = {
|
||||
// 获取统计信息
|
||||
getStats: () => apiRequest('/stats?t=' + Date.now()),
|
||||
|
||||
// 获取系统状态
|
||||
getStatus: () => apiRequest('/status?t=' + Date.now()),
|
||||
|
||||
// 获取Top屏蔽域名
|
||||
getTopBlockedDomains: () => apiRequest('/top-blocked?t=' + Date.now()),
|
||||
|
||||
// 获取Top解析域名
|
||||
getTopResolvedDomains: () => apiRequest('/top-resolved?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()),
|
||||
|
||||
// 获取每日统计数据(7天)
|
||||
getDailyStats: () => apiRequest('/daily-stats?t=' + Date.now()),
|
||||
|
||||
// 获取每月统计数据(30天)
|
||||
getMonthlyStats: () => apiRequest('/monthly-stats?t=' + Date.now()),
|
||||
|
||||
// 获取查询类型统计
|
||||
getQueryTypeStats: () => apiRequest('/query/type?t=' + Date.now()),
|
||||
|
||||
// 获取屏蔽规则 - 已禁用
|
||||
getShieldRules: () => {
|
||||
console.log('屏蔽规则功能已禁用');
|
||||
return Promise.resolve({}); // 返回空对象而非API调用
|
||||
},
|
||||
|
||||
// 添加屏蔽规则 - 已禁用
|
||||
addShieldRule: (rule) => {
|
||||
console.log('屏蔽规则功能已禁用');
|
||||
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||
},
|
||||
|
||||
// 删除屏蔽规则 - 已禁用
|
||||
deleteShieldRule: (rule) => {
|
||||
console.log('屏蔽规则功能已禁用');
|
||||
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||
},
|
||||
|
||||
// 更新远程规则 - 已禁用
|
||||
updateRemoteRules: () => {
|
||||
console.log('屏蔽规则功能已禁用');
|
||||
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||
},
|
||||
|
||||
// 获取黑名单列表 - 已禁用
|
||||
getBlacklists: () => {
|
||||
console.log('屏蔽规则相关功能已禁用');
|
||||
return Promise.resolve([]); // 返回空数组而非API调用
|
||||
},
|
||||
|
||||
// 添加黑名单 - 已禁用
|
||||
addBlacklist: (url) => {
|
||||
console.log('屏蔽规则相关功能已禁用');
|
||||
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||
},
|
||||
|
||||
// 删除黑名单 - 已禁用
|
||||
deleteBlacklist: (url) => {
|
||||
console.log('屏蔽规则相关功能已禁用');
|
||||
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||
},
|
||||
|
||||
// 获取Hosts内容 - 已禁用
|
||||
getHosts: () => {
|
||||
console.log('屏蔽规则相关功能已禁用');
|
||||
return Promise.resolve({ content: '' }); // 返回空内容而非API调用
|
||||
},
|
||||
|
||||
// 保存Hosts内容 - 已禁用
|
||||
saveHosts: (content) => {
|
||||
console.log('屏蔽规则相关功能已禁用');
|
||||
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||
},
|
||||
|
||||
// 刷新Hosts - 已禁用
|
||||
refreshHosts: () => {
|
||||
console.log('屏蔽规则相关功能已禁用');
|
||||
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||
},
|
||||
|
||||
// 查询DNS记录 - 兼容多种参数格式
|
||||
queryDNS: async function(domain, recordType) {
|
||||
try {
|
||||
console.log('执行DNS查询:', { domain, recordType });
|
||||
|
||||
// 适配参数格式
|
||||
let params;
|
||||
if (typeof domain === 'object') {
|
||||
// 当传入对象时
|
||||
params = domain;
|
||||
} else {
|
||||
// 当传入单独参数时
|
||||
params = { domain, recordType };
|
||||
}
|
||||
|
||||
// 尝试不同的API端点
|
||||
const endpoints = ['/api/dns/query', '/dns/query', '/api/query', '/query'];
|
||||
let lastError;
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
try {
|
||||
console.log(`尝试API端点: ${endpoint}`);
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(params)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('DNS查询成功:', data);
|
||||
return data;
|
||||
} else {
|
||||
lastError = new Error(`HTTP error! status: ${response.status} for endpoint: ${endpoint}`);
|
||||
}
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
console.log(`端点 ${endpoint} 调用失败,尝试下一个`);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果所有端点都失败,抛出最后一个错误
|
||||
throw lastError || new Error('所有API端点调用失败');
|
||||
} catch (error) {
|
||||
console.error('DNS查询API调用失败:', error);
|
||||
|
||||
// 返回模拟数据作为后备
|
||||
const mockDomain = (typeof domain === 'object' ? domain.domain : domain) || 'example.com';
|
||||
const mockType = (typeof domain === 'object' ? domain.recordType : recordType) || 'A';
|
||||
|
||||
const mockData = {
|
||||
'A': [
|
||||
{ Type: 'A', Value: '93.184.216.34', TTL: 172800 },
|
||||
{ Type: 'A', Value: '93.184.216.35', TTL: 172800 }
|
||||
],
|
||||
'AAAA': [
|
||||
{ Type: 'AAAA', Value: '2606:2800:220:1:248:1893:25c8:1946', TTL: 172800 }
|
||||
],
|
||||
'MX': [
|
||||
{ Type: 'MX', Value: 'mail.' + mockDomain, Preference: 10, TTL: 3600 },
|
||||
{ Type: 'MX', Value: 'mail2.' + mockDomain, Preference: 20, TTL: 3600 }
|
||||
],
|
||||
'NS': [
|
||||
{ Type: 'NS', Value: 'ns1.' + mockDomain, TTL: 86400 },
|
||||
{ Type: 'NS', Value: 'ns2.' + mockDomain, TTL: 86400 }
|
||||
],
|
||||
'CNAME': [
|
||||
{ Type: 'CNAME', Value: 'origin.' + mockDomain, TTL: 300 }
|
||||
],
|
||||
'TXT': [
|
||||
{ Type: 'TXT', Value: 'v=spf1 include:_spf.' + mockDomain + ' ~all', TTL: 3600 }
|
||||
]
|
||||
};
|
||||
|
||||
console.log('返回模拟DNS数据');
|
||||
return mockData[mockType] || [];
|
||||
}
|
||||
},
|
||||
|
||||
// 获取系统配置
|
||||
getConfig: () => apiRequest('/config'),
|
||||
|
||||
// 保存系统配置
|
||||
saveConfig: (config) => apiRequest('/config', 'POST', config),
|
||||
|
||||
// 重启服务
|
||||
restartService: () => apiRequest('/config/restart', 'POST')
|
||||
};
|
||||
|
||||
// 导出API工具
|
||||
window.api = api;
|
||||
317
js/app.js
317
js/app.js
@@ -1,317 +0,0 @@
|
||||
// 全局配置
|
||||
const API_BASE_URL = '.';
|
||||
|
||||
// DOM 加载完成后执行
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 初始化面板切换
|
||||
initPanelNavigation();
|
||||
|
||||
// 加载初始数据
|
||||
loadInitialData();
|
||||
|
||||
// 直接调用dashboard面板初始化函数,确保数据正确加载
|
||||
if (typeof initDashboardPanel === 'function') {
|
||||
initDashboardPanel();
|
||||
}
|
||||
|
||||
// 注意:实时更新现在由index.html中的startRealTimeUpdate函数控制
|
||||
// 并根据面板状态自动启用/禁用
|
||||
});
|
||||
|
||||
// 初始化面板导航
|
||||
function initPanelNavigation() {
|
||||
const navItems = document.querySelectorAll('.nav-item');
|
||||
const panels = document.querySelectorAll('.panel');
|
||||
|
||||
navItems.forEach(item => {
|
||||
item.addEventListener('click', function() {
|
||||
// 移除所有活动类
|
||||
navItems.forEach(nav => nav.classList.remove('active'));
|
||||
panels.forEach(panel => panel.classList.remove('active'));
|
||||
|
||||
// 添加当前活动类
|
||||
this.classList.add('active');
|
||||
const target = this.getAttribute('data-target');
|
||||
document.getElementById(target).classList.add('active');
|
||||
|
||||
// 面板激活时执行相应的初始化函数
|
||||
if (window[`init${target.charAt(0).toUpperCase() + target.slice(1)}Panel`]) {
|
||||
window[`init${target.charAt(0).toUpperCase() + target.slice(1)}Panel`]();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 保留原有的通知函数作为兼容层
|
||||
// 现在主通知功能由index.html中的showNotification函数实现
|
||||
if (typeof window.showNotification === 'undefined') {
|
||||
window.showNotification = function(message, type = 'info') {
|
||||
// 创建临时通知元素
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification notification-${type} show`;
|
||||
notification.innerHTML = `
|
||||
<div class="notification-content">${message}</div>
|
||||
`;
|
||||
notification.style.cssText = 'position: fixed; top: 20px; right: 20px; background: #333; color: white; padding: 10px 15px; border-radius: 4px; z-index: 10000;';
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 3000);
|
||||
};
|
||||
}
|
||||
|
||||
// 加载初始数据(主要用于服务器状态)
|
||||
function loadInitialData() {
|
||||
// 加载服务器状态
|
||||
fetch(`${API_BASE_URL}/api/status`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// 更新服务器状态指示器
|
||||
const statusDot = document.querySelector('.status-dot');
|
||||
const serverStatus = document.getElementById('server-status');
|
||||
|
||||
if (data && data.status === 'running') {
|
||||
statusDot.classList.add('connected');
|
||||
serverStatus.textContent = '运行中';
|
||||
} else {
|
||||
statusDot.classList.remove('connected');
|
||||
serverStatus.textContent = '离线';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取服务器状态失败:', error);
|
||||
|
||||
// 更新状态为离线
|
||||
const statusDot = document.querySelector('.status-dot');
|
||||
const serverStatus = document.getElementById('server-status');
|
||||
statusDot.classList.remove('connected');
|
||||
serverStatus.textContent = '离线';
|
||||
|
||||
// 使用新的通知功能
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('获取服务器状态失败', 'danger');
|
||||
}
|
||||
});
|
||||
|
||||
// 注意:统计数据更新现在由dashboard.js中的updateStatCards函数处理
|
||||
}
|
||||
|
||||
// 注意:统计卡片数据更新现在由dashboard.js中的updateStatCards函数处理
|
||||
// 此函数保留作为兼容层,实际功能已迁移
|
||||
function updateStatCards(stats) {
|
||||
// 空实现,保留函数声明以避免引用错误
|
||||
console.log('更新统计卡片 - 此功能现在由dashboard.js处理');
|
||||
}
|
||||
|
||||
// 注意:获取规则数量功能现在由dashboard.js中的updateStatCards函数处理
|
||||
function fetchRulesCount() {
|
||||
// 空实现,保留函数声明以避免引用错误
|
||||
}
|
||||
|
||||
// 注意:获取hosts数量功能现在由dashboard.js中的updateStatCards函数处理
|
||||
function fetchHostsCount() {
|
||||
// 空实现,保留函数声明以避免引用错误
|
||||
}
|
||||
|
||||
// 通用API请求函数 - 添加错误处理和重试机制
|
||||
function apiRequest(endpoint, method = 'GET', data = null, maxRetries = 3) {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
const config = {
|
||||
method,
|
||||
headers,
|
||||
timeout: 10000, // 设置超时时间为10秒
|
||||
};
|
||||
|
||||
// 处理请求URL和参数
|
||||
let url = `${API_BASE_URL}${endpoint}`;
|
||||
|
||||
if (data) {
|
||||
if (method === 'GET') {
|
||||
// 为GET请求拼接查询参数
|
||||
const params = new URLSearchParams();
|
||||
Object.keys(data).forEach(key => {
|
||||
params.append(key, data[key]);
|
||||
});
|
||||
url += `?${params.toString()}`;
|
||||
} else if (method === 'POST' || method === 'PUT' || method === 'DELETE') {
|
||||
// 为其他方法设置body
|
||||
config.body = JSON.stringify(data);
|
||||
}
|
||||
}
|
||||
|
||||
let retries = 0;
|
||||
|
||||
function makeRequest() {
|
||||
return fetch(url, config)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
// 检查响应是否完整
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
// 使用.text()先获取响应文本,处理可能的JSON解析错误
|
||||
return response.text().then(text => {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (e) {
|
||||
console.error('JSON解析错误:', e, '响应文本:', text);
|
||||
// 针对ERR_INCOMPLETE_CHUNKED_ENCODING错误进行重试
|
||||
if (retries < maxRetries) {
|
||||
retries++;
|
||||
console.warn(`请求失败,正在进行第${retries}次重试...`);
|
||||
return new Promise(resolve => setTimeout(() => resolve(makeRequest()), 1000 * retries));
|
||||
}
|
||||
throw new Error('JSON解析失败且重试次数已达上限');
|
||||
}
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('API请求错误:', error);
|
||||
|
||||
// 检查是否为网络错误或ERR_INCOMPLETE_CHUNKED_ENCODING相关错误
|
||||
if ((error.name === 'TypeError' && error.message.includes('Failed to fetch')) ||
|
||||
error.message.includes('incomplete chunked encoding')) {
|
||||
|
||||
if (retries < maxRetries) {
|
||||
retries++;
|
||||
console.warn(`网络错误,正在进行第${retries}次重试...`);
|
||||
return new Promise(resolve => setTimeout(() => resolve(makeRequest()), 1000 * retries));
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
return makeRequest();
|
||||
}
|
||||
|
||||
// 数字格式化函数
|
||||
function formatNumber(num) {
|
||||
// 显示完整数字的最大长度阈值
|
||||
const MAX_FULL_LENGTH = 5;
|
||||
|
||||
// 先获取完整数字字符串
|
||||
const fullNumStr = num.toString();
|
||||
|
||||
// 如果数字长度小于等于阈值,直接返回完整数字
|
||||
if (fullNumStr.length <= MAX_FULL_LENGTH) {
|
||||
return fullNumStr;
|
||||
}
|
||||
|
||||
// 否则使用缩写格式
|
||||
if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1) + 'M';
|
||||
} else if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1) + 'K';
|
||||
}
|
||||
|
||||
return fullNumStr;
|
||||
}
|
||||
|
||||
// 确认对话框函数
|
||||
function confirmAction(message, onConfirm) {
|
||||
if (confirm(message)) {
|
||||
onConfirm();
|
||||
}
|
||||
}
|
||||
|
||||
// 加载状态函数
|
||||
function showLoading(element) {
|
||||
if (element) {
|
||||
element.innerHTML = '<td colspan="100%" class="loading">加载中...</td>';
|
||||
}
|
||||
}
|
||||
|
||||
// 错误状态函数
|
||||
function showError(element, message) {
|
||||
if (element) {
|
||||
element.innerHTML = `<td colspan="100%" style="color: #e74c3c;">${message}</td>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 空状态函数
|
||||
function showEmpty(element, message) {
|
||||
if (element) {
|
||||
element.innerHTML = `<td colspan="100%" style="color: #7f8c8d; font-style: italic;">${message}</td>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 表格排序功能
|
||||
function initTableSort(tableId) {
|
||||
const table = document.getElementById(tableId);
|
||||
if (!table) return;
|
||||
|
||||
const headers = table.querySelectorAll('thead th');
|
||||
headers.forEach(header => {
|
||||
header.addEventListener('click', function() {
|
||||
const columnIndex = Array.from(headers).indexOf(this);
|
||||
const isAscending = this.getAttribute('data-sort') !== 'asc';
|
||||
|
||||
// 重置所有标题
|
||||
headers.forEach(h => h.setAttribute('data-sort', ''));
|
||||
this.setAttribute('data-sort', isAscending ? 'asc' : 'desc');
|
||||
|
||||
// 排序行
|
||||
sortTable(table, columnIndex, isAscending);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 表格排序实现
|
||||
function sortTable(table, columnIndex, isAscending) {
|
||||
const tbody = table.querySelector('tbody');
|
||||
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||||
|
||||
// 排序行
|
||||
rows.sort((a, b) => {
|
||||
const aValue = a.cells[columnIndex].textContent.trim();
|
||||
const bValue = b.cells[columnIndex].textContent.trim();
|
||||
|
||||
// 尝试数字排序
|
||||
const aNum = parseFloat(aValue);
|
||||
const bNum = parseFloat(bValue);
|
||||
|
||||
if (!isNaN(aNum) && !isNaN(bNum)) {
|
||||
return isAscending ? aNum - bNum : bNum - aNum;
|
||||
}
|
||||
|
||||
// 字符串排序
|
||||
return isAscending
|
||||
? aValue.localeCompare(bValue)
|
||||
: bValue.localeCompare(aValue);
|
||||
});
|
||||
|
||||
// 重新添加行
|
||||
rows.forEach(row => tbody.appendChild(row));
|
||||
}
|
||||
|
||||
// 搜索过滤功能
|
||||
function initSearchFilter(inputId, tableId, columnIndex) {
|
||||
const input = document.getElementById(inputId);
|
||||
const table = document.getElementById(tableId);
|
||||
|
||||
if (!input || !table) return;
|
||||
|
||||
input.addEventListener('input', function() {
|
||||
const filter = this.value.toLowerCase();
|
||||
const rows = table.querySelectorAll('tbody tr');
|
||||
|
||||
rows.forEach(row => {
|
||||
const cell = row.cells[columnIndex];
|
||||
if (cell) {
|
||||
const text = cell.textContent.toLowerCase();
|
||||
row.style.display = text.includes(filter) ? '' : 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
// 颜色配置文件 - 集中管理所有UI颜色配置
|
||||
|
||||
// 主颜色配置对象
|
||||
const COLOR_CONFIG = {
|
||||
// 主色调
|
||||
primary: '#1890ff',
|
||||
success: '#52c41a',
|
||||
warning: '#fa8c16',
|
||||
error: '#f5222d',
|
||||
purple: '#722ed1',
|
||||
cyan: '#13c2c2',
|
||||
teal: '#36cfc9',
|
||||
|
||||
// 统计卡片颜色配置
|
||||
statCardColors: [
|
||||
'#1890ff', // blue
|
||||
'#52c41a', // green
|
||||
'#fa8c16', // orange
|
||||
'#f5222d', // red
|
||||
'#722ed1', // purple
|
||||
'#13c2c2' // cyan
|
||||
],
|
||||
|
||||
// 颜色代码到CSS类的映射
|
||||
colorClassMap: {
|
||||
'#1890ff': 'blue',
|
||||
'#52c41a': 'green',
|
||||
'#fa8c16': 'orange',
|
||||
'#f5222d': 'red',
|
||||
'#722ed1': 'purple',
|
||||
'#13c2c2': 'cyan',
|
||||
'#36cfc9': 'teal'
|
||||
},
|
||||
|
||||
// 获取颜色对应的CSS类名
|
||||
getColorClassName: function(colorCode) {
|
||||
return this.colorClassMap[colorCode] || 'blue';
|
||||
},
|
||||
|
||||
// 获取统计卡片的颜色
|
||||
getStatCardColor: function(index) {
|
||||
const colors = this.statCardColors;
|
||||
return colors[index % colors.length];
|
||||
}
|
||||
};
|
||||
|
||||
// 导出配置对象
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = COLOR_CONFIG;
|
||||
} else {
|
||||
// 浏览器环境
|
||||
window.COLOR_CONFIG = COLOR_CONFIG;
|
||||
}
|
||||
284
js/config.js
284
js/config.js
@@ -1,284 +0,0 @@
|
||||
// 配置管理页面功能实现
|
||||
|
||||
// 工具函数:安全获取DOM元素
|
||||
function getElement(id) {
|
||||
const element = document.getElementById(id);
|
||||
if (!element) {
|
||||
console.warn(`Element with id "${id}" not found`);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
// 工具函数:验证端口号
|
||||
function validatePort(port) {
|
||||
// 确保port是字符串类型
|
||||
var portStr = port;
|
||||
if (port === null || port === undefined || typeof port !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 去除前后空白并验证是否为纯数字
|
||||
portStr = port.trim();
|
||||
if (!/^\d+$/.test(portStr)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const num = parseInt(portStr, 10);
|
||||
return num >= 1 && num <= 65535 ? num : null;
|
||||
}
|
||||
|
||||
// 初始化配置管理页面
|
||||
function initConfigPage() {
|
||||
loadConfig();
|
||||
setupConfigEventListeners();
|
||||
}
|
||||
|
||||
// 加载系统配置
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const result = await api.getConfig();
|
||||
|
||||
// 检查API返回的错误
|
||||
if (result && result.error) {
|
||||
showErrorMessage('加载配置失败: ' + result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
populateConfigForm(result);
|
||||
} catch (error) {
|
||||
// 捕获可能的异常(虽然apiRequest不应该再抛出异常)
|
||||
showErrorMessage('加载配置失败: ' + (error.message || '未知错误'));
|
||||
}
|
||||
}
|
||||
|
||||
// 填充配置表单
|
||||
function populateConfigForm(config) {
|
||||
// 安全获取配置对象,防止未定义属性访问
|
||||
const dnsServerConfig = config.DNSServer || {};
|
||||
const httpServerConfig = config.HTTPServer || {};
|
||||
const shieldConfig = config.Shield || {};
|
||||
|
||||
// DNS配置 - 使用函数安全设置值,避免 || 操作符可能的错误处理
|
||||
setElementValue('dns-port', getSafeValue(dnsServerConfig.Port, 53));
|
||||
setElementValue('dns-upstream-servers', getSafeArray(dnsServerConfig.UpstreamServers).join(', '));
|
||||
setElementValue('dns-timeout', getSafeValue(dnsServerConfig.Timeout, 5));
|
||||
setElementValue('dns-stats-file', getSafeValue(dnsServerConfig.StatsFile, 'data/stats.json'));
|
||||
setElementValue('dns-save-interval', getSafeValue(dnsServerConfig.SaveInterval, 300));
|
||||
|
||||
// HTTP配置
|
||||
setElementValue('http-port', getSafeValue(httpServerConfig.Port, 8080));
|
||||
|
||||
// 屏蔽配置
|
||||
setElementValue('shield-local-rules-file', getSafeValue(shieldConfig.LocalRulesFile, 'data/rules.txt'));
|
||||
setElementValue('shield-update-interval', getSafeValue(shieldConfig.UpdateInterval, 3600));
|
||||
setElementValue('shield-hosts-file', getSafeValue(shieldConfig.HostsFile, 'data/hosts.txt'));
|
||||
// 使用服务器端接受的屏蔽方法值,默认使用NXDOMAIN
|
||||
setElementValue('shield-block-method', getSafeValue(shieldConfig.BlockMethod, 'NXDOMAIN'));
|
||||
}
|
||||
|
||||
// 工具函数:安全设置元素值
|
||||
function setElementValue(elementId, value) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element && element.tagName === 'INPUT') {
|
||||
element.value = value;
|
||||
} else if (!element) {
|
||||
console.warn(`Element with id "${elementId}" not found for setting value: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 工具函数:安全获取值,如果未定义或为null则返回默认值
|
||||
function getSafeValue(value, defaultValue) {
|
||||
// 更严格的检查,避免0、空字符串等被默认值替换
|
||||
return value === undefined || value === null ? defaultValue : value;
|
||||
}
|
||||
|
||||
// 工具函数:安全获取数组,如果不是数组则返回空数组
|
||||
function getSafeArray(value) {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
async function handleSaveConfig() {
|
||||
const formData = collectFormData();
|
||||
if (!formData) return;
|
||||
|
||||
try {
|
||||
const result = await api.saveConfig(formData);
|
||||
|
||||
// 检查API返回的错误
|
||||
if (result && result.error) {
|
||||
showErrorMessage('保存配置失败: ' + result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
showSuccessMessage('配置保存成功');
|
||||
} catch (error) {
|
||||
// 捕获可能的异常(虽然apiRequest不应该再抛出异常)
|
||||
showErrorMessage('保存配置失败: ' + (error.message || '未知错误'));
|
||||
}
|
||||
}
|
||||
|
||||
// 重启服务
|
||||
async function handleRestartService() {
|
||||
if (!confirm('确定要重启DNS服务吗?重启期间服务可能会短暂不可用。')) return;
|
||||
|
||||
try {
|
||||
const result = await api.restartService();
|
||||
|
||||
// 检查API返回的错误
|
||||
if (result && result.error) {
|
||||
showErrorMessage('服务重启失败: ' + result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
showSuccessMessage('服务重启成功');
|
||||
} catch (error) {
|
||||
// 捕获可能的异常(虽然apiRequest不应该再抛出异常)
|
||||
showErrorMessage('重启服务失败: ' + (error.message || '未知错误'));
|
||||
}
|
||||
}
|
||||
|
||||
// 收集表单数据并验证
|
||||
function collectFormData() {
|
||||
// 验证端口号 - 使用安全获取元素值的函数
|
||||
const dnsPortValue = getElementValue('dns-port');
|
||||
const httpPortValue = getElementValue('http-port');
|
||||
|
||||
const dnsPort = validatePort(dnsPortValue);
|
||||
const httpPort = validatePort(httpPortValue);
|
||||
|
||||
if (!dnsPort) {
|
||||
showErrorMessage('DNS端口号无效(必须是1-65535之间的整数)');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!httpPort) {
|
||||
showErrorMessage('HTTP端口号无效(必须是1-65535之间的整数)');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 安全获取上游服务器列表
|
||||
const upstreamServersText = getElementValue('dns-upstream-servers');
|
||||
const upstreamServers = upstreamServersText ?
|
||||
upstreamServersText.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s !== ''; }) :
|
||||
[];
|
||||
|
||||
// 安全获取并转换整数值
|
||||
const timeoutValue = getElementValue('dns-timeout');
|
||||
const timeout = timeoutValue ? parseInt(timeoutValue, 10) : 5;
|
||||
|
||||
const saveIntervalValue = getElementValue('dns-save-interval');
|
||||
const saveInterval = saveIntervalValue ? parseInt(saveIntervalValue, 10) : 300;
|
||||
|
||||
const updateIntervalValue = getElementValue('shield-update-interval');
|
||||
const updateInterval = updateIntervalValue ? parseInt(updateIntervalValue, 10) : 3600;
|
||||
|
||||
return {
|
||||
DNSServer: {
|
||||
Port: dnsPort,
|
||||
UpstreamServers: upstreamServers,
|
||||
Timeout: timeout,
|
||||
StatsFile: getElementValue('dns-stats-file') || './data/stats.json',
|
||||
SaveInterval: saveInterval
|
||||
},
|
||||
HTTPServer: {
|
||||
Port: httpPort
|
||||
},
|
||||
Shield: {
|
||||
LocalRulesFile: getElementValue('shield-local-rules-file') || './data/rules.txt',
|
||||
UpdateInterval: updateInterval,
|
||||
HostsFile: getElementValue('shield-hosts-file') || './data/hosts.txt',
|
||||
BlockMethod: getElementValue('shield-block-method') || 'NXDOMAIN'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 工具函数:安全获取元素值
|
||||
function getElementValue(elementId) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element && element.tagName === 'INPUT') {
|
||||
return element.value;
|
||||
}
|
||||
return ''; // 默认返回空字符串
|
||||
}
|
||||
|
||||
// 设置事件监听器
|
||||
function setupConfigEventListeners() {
|
||||
// 保存配置按钮
|
||||
getElement('save-config-btn')?.addEventListener('click', handleSaveConfig);
|
||||
|
||||
// 重启服务按钮
|
||||
getElement('restart-service-btn')?.addEventListener('click', handleRestartService);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 显示成功消息
|
||||
function showSuccessMessage(message) {
|
||||
showNotification(message, 'success');
|
||||
}
|
||||
|
||||
// 显示错误消息
|
||||
function showErrorMessage(message) {
|
||||
showNotification(message, 'error');
|
||||
}
|
||||
|
||||
// 显示通知
|
||||
function showNotification(message, type = 'info') {
|
||||
// 移除现有通知
|
||||
const existingNotification = document.querySelector('.notification');
|
||||
if (existingNotification) {
|
||||
existingNotification.remove();
|
||||
}
|
||||
|
||||
// 创建新通知
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform transition-transform duration-300 ease-in-out translate-y-0 opacity-0`;
|
||||
|
||||
// 设置通知样式(兼容Tailwind和原生CSS)
|
||||
notification.style.cssText += `
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
padding: 16px 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
z-index: 1000;
|
||||
transition: all 0.3s ease;
|
||||
opacity: 0;
|
||||
`;
|
||||
|
||||
if (type === 'success') {
|
||||
notification.style.backgroundColor = '#10b981';
|
||||
notification.style.color = 'white';
|
||||
} else if (type === 'error') {
|
||||
notification.style.backgroundColor = '#ef4444';
|
||||
notification.style.color = 'white';
|
||||
} else {
|
||||
notification.style.backgroundColor = '#3b82f6';
|
||||
notification.style.color = 'white';
|
||||
}
|
||||
|
||||
notification.textContent = message;
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// 显示通知
|
||||
setTimeout(() => {
|
||||
notification.style.opacity = '1';
|
||||
}, 10);
|
||||
|
||||
// 3秒后隐藏通知
|
||||
setTimeout(() => {
|
||||
notification.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 页面加载完成后初始化
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initConfigPage);
|
||||
} else {
|
||||
initConfigPage();
|
||||
}
|
||||
3012
js/dashboard.js
3012
js/dashboard.js
File diff suppressed because it is too large
Load Diff
202
js/hosts.js
202
js/hosts.js
@@ -1,202 +0,0 @@
|
||||
// Hosts管理页面功能实现
|
||||
|
||||
// 初始化Hosts管理页面
|
||||
function initHostsPage() {
|
||||
// 加载Hosts规则
|
||||
loadHostsRules();
|
||||
// 设置事件监听器
|
||||
setupHostsEventListeners();
|
||||
}
|
||||
|
||||
// 加载Hosts规则
|
||||
async function loadHostsRules() {
|
||||
try {
|
||||
const response = await fetch('/api/shield/hosts');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load hosts rules');
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
// 处理API返回的数据格式
|
||||
let hostsRules = [];
|
||||
if (data && Array.isArray(data)) {
|
||||
// 直接是数组格式
|
||||
hostsRules = data;
|
||||
} else if (data && data.hosts) {
|
||||
// 包含在hosts字段中
|
||||
hostsRules = data.hosts;
|
||||
}
|
||||
|
||||
updateHostsTable(hostsRules);
|
||||
} catch (error) {
|
||||
console.error('Error loading hosts rules:', error);
|
||||
showErrorMessage('加载Hosts规则失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 更新Hosts表格
|
||||
function updateHostsTable(hostsRules) {
|
||||
const tbody = document.getElementById('hosts-table-body');
|
||||
|
||||
if (hostsRules.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="3" class="py-4 text-center text-gray-500">暂无Hosts条目</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = hostsRules.map(rule => {
|
||||
// 处理对象格式的规则
|
||||
const ip = rule.ip || '';
|
||||
const domain = rule.domain || '';
|
||||
|
||||
return `
|
||||
<tr class="border-b border-gray-200">
|
||||
<td class="py-3 px-4">${ip}</td>
|
||||
<td class="py-3 px-4">${domain}</td>
|
||||
<td class="py-3 px-4 text-right">
|
||||
<button class="delete-hosts-btn px-3 py-1 bg-danger text-white rounded-md hover:bg-danger/90 transition-colors text-sm" data-ip="${ip}" data-domain="${domain}">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// 重新绑定删除事件
|
||||
document.querySelectorAll('.delete-hosts-btn').forEach(btn => {
|
||||
btn.addEventListener('click', handleDeleteHostsRule);
|
||||
});
|
||||
}
|
||||
|
||||
// 设置事件监听器
|
||||
function setupHostsEventListeners() {
|
||||
// 保存Hosts按钮
|
||||
document.getElementById('save-hosts-btn').addEventListener('click', handleAddHostsRule);
|
||||
}
|
||||
|
||||
// 处理添加Hosts规则
|
||||
async function handleAddHostsRule() {
|
||||
const ip = document.getElementById('hosts-ip').value.trim();
|
||||
const domain = document.getElementById('hosts-domain').value.trim();
|
||||
|
||||
if (!ip || !domain) {
|
||||
showErrorMessage('IP地址和域名不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/shield/hosts', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ ip, domain })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to add hosts rule');
|
||||
}
|
||||
|
||||
showSuccessMessage('Hosts规则添加成功');
|
||||
|
||||
// 清空输入框
|
||||
document.getElementById('hosts-ip').value = '';
|
||||
document.getElementById('hosts-domain').value = '';
|
||||
|
||||
// 重新加载规则
|
||||
loadHostsRules();
|
||||
} catch (error) {
|
||||
console.error('Error adding hosts rule:', error);
|
||||
showErrorMessage('添加Hosts规则失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 处理删除Hosts规则
|
||||
async function handleDeleteHostsRule(e) {
|
||||
const ip = e.target.closest('.delete-hosts-btn').dataset.ip;
|
||||
const domain = e.target.closest('.delete-hosts-btn').dataset.domain;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/shield/hosts', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ domain })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete hosts rule');
|
||||
}
|
||||
|
||||
showSuccessMessage('Hosts规则删除成功');
|
||||
|
||||
// 重新加载规则
|
||||
loadHostsRules();
|
||||
} catch (error) {
|
||||
console.error('Error deleting hosts rule:', error);
|
||||
showErrorMessage('删除Hosts规则失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示成功消息
|
||||
function showSuccessMessage(message) {
|
||||
showNotification(message, 'success');
|
||||
}
|
||||
|
||||
// 显示错误消息
|
||||
function showErrorMessage(message) {
|
||||
showNotification(message, 'error');
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 显示通知
|
||||
function showNotification(message, type = 'info') {
|
||||
// 移除现有通知
|
||||
const existingNotification = document.querySelector('.notification');
|
||||
if (existingNotification) {
|
||||
existingNotification.remove();
|
||||
}
|
||||
|
||||
// 创建新通知
|
||||
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-all duration-300 ease-in-out translate-y-0 opacity-0`;
|
||||
|
||||
// 设置通知样式
|
||||
if (type === 'success') {
|
||||
notification.classList.add('bg-green-500', 'text-white');
|
||||
} else if (type === 'error') {
|
||||
notification.classList.add('bg-red-500', 'text-white');
|
||||
} else {
|
||||
notification.classList.add('bg-blue-500', 'text-white');
|
||||
}
|
||||
|
||||
notification.innerHTML = `
|
||||
<div class="flex items-center space-x-2">
|
||||
<i class="fa fa-${type === 'success' ? 'check' : type === 'error' ? 'exclamation' : 'info'}"></i>
|
||||
<span>${message}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// 显示通知
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('opacity-0');
|
||||
}, 100);
|
||||
|
||||
// 3秒后隐藏通知
|
||||
setTimeout(() => {
|
||||
notification.classList.add('opacity-0');
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 页面加载完成后初始化
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initHostsPage);
|
||||
} else {
|
||||
initHostsPage();
|
||||
}
|
||||
173
js/main.js
173
js/main.js
@@ -1,173 +0,0 @@
|
||||
// main.js - 主脚本文件
|
||||
|
||||
// 页面导航功能
|
||||
function setupNavigation() {
|
||||
// 侧边栏菜单项
|
||||
const menuItems = document.querySelectorAll('nav a');
|
||||
const contentSections = [
|
||||
document.getElementById('dashboard-content'),
|
||||
document.getElementById('shield-content'),
|
||||
document.getElementById('hosts-content'),
|
||||
document.getElementById('query-content'),
|
||||
document.getElementById('config-content')
|
||||
];
|
||||
const pageTitle = document.getElementById('page-title');
|
||||
|
||||
menuItems.forEach((item, index) => {
|
||||
item.addEventListener('click', (e) => {
|
||||
// 允许浏览器自动更新地址栏中的hash,不阻止默认行为
|
||||
|
||||
// 移动端点击菜单项后自动关闭侧边栏
|
||||
if (window.innerWidth < 768) {
|
||||
closeSidebar();
|
||||
}
|
||||
|
||||
// 页面特定初始化 - 保留这部分逻辑,因为它不会与hashchange事件处理逻辑冲突
|
||||
const target = item.getAttribute('href').substring(1);
|
||||
if (target === 'shield' && typeof initShieldPage === 'function') {
|
||||
initShieldPage();
|
||||
} else if (target === 'hosts' && typeof initHostsPage === 'function') {
|
||||
initHostsPage();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 移动端侧边栏切换
|
||||
const toggleSidebar = document.getElementById('toggle-sidebar');
|
||||
const closeSidebarBtn = document.getElementById('close-sidebar');
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const sidebarOverlay = document.getElementById('sidebar-overlay');
|
||||
|
||||
// 打开侧边栏函数
|
||||
function openSidebar() {
|
||||
console.log('Opening sidebar...');
|
||||
if (sidebar) {
|
||||
sidebar.classList.remove('-translate-x-full');
|
||||
sidebar.classList.add('translate-x-0');
|
||||
}
|
||||
if (sidebarOverlay) {
|
||||
sidebarOverlay.classList.remove('hidden');
|
||||
sidebarOverlay.classList.add('block');
|
||||
}
|
||||
// 防止页面滚动
|
||||
document.body.style.overflow = 'hidden';
|
||||
console.log('Sidebar opened successfully');
|
||||
}
|
||||
|
||||
// 关闭侧边栏函数
|
||||
function closeSidebar() {
|
||||
console.log('Closing sidebar...');
|
||||
if (sidebar) {
|
||||
sidebar.classList.add('-translate-x-full');
|
||||
sidebar.classList.remove('translate-x-0');
|
||||
}
|
||||
if (sidebarOverlay) {
|
||||
sidebarOverlay.classList.add('hidden');
|
||||
sidebarOverlay.classList.remove('block');
|
||||
}
|
||||
// 恢复页面滚动
|
||||
document.body.style.overflow = '';
|
||||
console.log('Sidebar closed successfully');
|
||||
}
|
||||
|
||||
// 切换侧边栏函数
|
||||
function toggleSidebarVisibility() {
|
||||
console.log('Toggling sidebar visibility...');
|
||||
console.log('Current sidebar classes:', sidebar ? sidebar.className : 'sidebar not found');
|
||||
if (sidebar && sidebar.classList.contains('-translate-x-full')) {
|
||||
console.log('Sidebar is hidden, opening...');
|
||||
openSidebar();
|
||||
} else {
|
||||
console.log('Sidebar is visible, closing...');
|
||||
closeSidebar();
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定切换按钮事件
|
||||
if (toggleSidebar) {
|
||||
toggleSidebar.addEventListener('click', toggleSidebarVisibility);
|
||||
}
|
||||
|
||||
// 绑定关闭按钮事件
|
||||
if (closeSidebarBtn) {
|
||||
closeSidebarBtn.addEventListener('click', closeSidebar);
|
||||
}
|
||||
|
||||
// 绑定遮罩层点击事件
|
||||
if (sidebarOverlay) {
|
||||
sidebarOverlay.addEventListener('click', closeSidebar);
|
||||
}
|
||||
|
||||
// 移动端点击菜单项后自动关闭侧边栏
|
||||
menuItems.forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
// 检查是否是移动设备视图
|
||||
if (window.innerWidth < 768) {
|
||||
closeSidebar();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 添加键盘事件监听,按ESC键关闭侧边栏
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeSidebar();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化函数
|
||||
function init() {
|
||||
// 设置导航
|
||||
setupNavigation();
|
||||
|
||||
// 加载仪表盘数据
|
||||
if (typeof loadDashboardData === 'function') {
|
||||
loadDashboardData();
|
||||
}
|
||||
|
||||
// 定期更新系统状态
|
||||
setInterval(updateSystemStatus, 5000);
|
||||
}
|
||||
|
||||
// 更新系统状态
|
||||
function updateSystemStatus() {
|
||||
fetch('/api/status')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const uptimeElement = document.getElementById('uptime');
|
||||
if (uptimeElement) {
|
||||
uptimeElement.textContent = `正常运行中 | ${formatUptime(data.uptime)}`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('更新系统状态失败:', error);
|
||||
const uptimeElement = document.getElementById('uptime');
|
||||
if (uptimeElement) {
|
||||
uptimeElement.textContent = '连接异常';
|
||||
uptimeElement.classList.add('text-danger');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 格式化运行时间
|
||||
function formatUptime(milliseconds) {
|
||||
// 简化版的格式化,实际使用时需要根据API返回的数据格式调整
|
||||
const seconds = Math.floor(milliseconds / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}天${hours % 24}小时`;
|
||||
} else if (hours > 0) {
|
||||
return `${hours}小时${minutes % 60}分钟`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}分钟${seconds % 60}秒`;
|
||||
} else {
|
||||
return `${seconds}秒`;
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载完成后执行初始化
|
||||
window.addEventListener('DOMContentLoaded', init);
|
||||
@@ -1,255 +0,0 @@
|
||||
// 初始化远程黑名单面板
|
||||
function initBlacklistsPanel() {
|
||||
// 加载远程黑名单列表
|
||||
loadBlacklists();
|
||||
|
||||
// 初始化事件监听器
|
||||
initBlacklistsEventListeners();
|
||||
}
|
||||
|
||||
// 初始化事件监听器
|
||||
function initBlacklistsEventListeners() {
|
||||
// 添加黑名单按钮
|
||||
document.getElementById('add-blacklist').addEventListener('click', addBlacklist);
|
||||
|
||||
// 更新所有黑名单按钮
|
||||
document.getElementById('update-all-blacklists').addEventListener('click', updateAllBlacklists);
|
||||
|
||||
// 按Enter键添加黑名单
|
||||
document.getElementById('blacklist-url').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
addBlacklist();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 加载远程黑名单列表
|
||||
function loadBlacklists() {
|
||||
const tbody = document.getElementById('blacklists-table').querySelector('tbody');
|
||||
showLoading(tbody);
|
||||
|
||||
apiRequest('/api/shield/blacklists')
|
||||
.then(data => {
|
||||
// 直接渲染返回的blacklists数组
|
||||
renderBlacklists(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取远程黑名单列表失败:', error);
|
||||
showError(tbody, '获取远程黑名单列表失败');
|
||||
window.showNotification('获取远程黑名单列表失败', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染远程黑名单表格
|
||||
function renderBlacklists(blacklists) {
|
||||
const tbody = document.getElementById('blacklists-table').querySelector('tbody');
|
||||
if (!tbody) return;
|
||||
|
||||
if (!blacklists || blacklists.length === 0) {
|
||||
showEmpty(tbody, '暂无远程黑名单');
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = '';
|
||||
|
||||
blacklists.forEach(list => {
|
||||
addBlacklistToTable(list);
|
||||
});
|
||||
|
||||
// 初始化表格排序
|
||||
initTableSort('blacklists-table');
|
||||
|
||||
// 初始化操作按钮监听器
|
||||
initBlacklistsActionListeners();
|
||||
}
|
||||
|
||||
// 添加黑名单到表格
|
||||
function addBlacklistToTable(list) {
|
||||
const tbody = document.getElementById('blacklists-table').querySelector('tbody');
|
||||
const row = document.createElement('tr');
|
||||
|
||||
const statusClass = list.status === 'success' ? 'status-success' :
|
||||
list.status === 'error' ? 'status-error' : 'status-pending';
|
||||
|
||||
const statusText = list.status === 'success' ? '正常' :
|
||||
list.status === 'error' ? '错误' : '等待中';
|
||||
|
||||
const lastUpdate = list.lastUpdate ? new Date(list.lastUpdate).toLocaleString() : '从未';
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${list.name}</td>
|
||||
<td>${list.url}</td>
|
||||
<td>
|
||||
<span class="status-badge ${statusClass}">${statusText}</span>
|
||||
</td>
|
||||
<td>${list.rulesCount || 0}</td>
|
||||
<td>${lastUpdate}</td>
|
||||
<td class="actions-cell">
|
||||
<button class="btn btn-primary btn-sm update-blacklist" data-id="${list.id}">
|
||||
<i class="fas fa-sync-alt"></i> 更新
|
||||
</button>
|
||||
<button class="btn btn-danger btn-sm delete-blacklist" data-id="${list.id}">
|
||||
<i class="fas fa-trash-alt"></i> 删除
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(row);
|
||||
}
|
||||
|
||||
// 添加远程黑名单
|
||||
function addBlacklist() {
|
||||
const nameInput = document.getElementById('blacklist-name');
|
||||
const urlInput = document.getElementById('blacklist-url');
|
||||
|
||||
const name = nameInput.value.trim();
|
||||
const url = urlInput.value.trim();
|
||||
|
||||
if (!name) {
|
||||
window.showNotification('请输入黑名单名称', 'warning');
|
||||
nameInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
window.showNotification('请输入黑名单URL', 'warning');
|
||||
urlInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// 简单的URL格式验证
|
||||
if (!isValidUrl(url)) {
|
||||
window.showNotification('请输入有效的URL', 'warning');
|
||||
urlInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
apiRequest('/api/shield/blacklists', 'POST', { name: name, url: url })
|
||||
.then(data => {
|
||||
// 检查响应中是否有status字段
|
||||
if (!data || typeof data === 'undefined') {
|
||||
window.showNotification('远程黑名单添加失败: 无效的响应', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.status === 'success') {
|
||||
window.showNotification('远程黑名单添加成功', 'success');
|
||||
nameInput.value = '';
|
||||
urlInput.value = '';
|
||||
loadBlacklists();
|
||||
} else {
|
||||
window.showNotification(`添加失败: ${data.message || '未知错误'}`, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('添加远程黑名单失败:', error);
|
||||
window.showNotification('添加远程黑名单失败', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// 更新远程黑名单
|
||||
function updateBlacklist(id) {
|
||||
apiRequest(`/api/shield/blacklists/${id}/update`, 'POST')
|
||||
.then(data => {
|
||||
// 检查响应中是否有status字段
|
||||
if (!data || typeof data === 'undefined') {
|
||||
window.showNotification('远程黑名单更新失败: 无效的响应', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.status === 'success') {
|
||||
window.showNotification('远程黑名单更新成功', 'success');
|
||||
loadBlacklists();
|
||||
} else {
|
||||
window.showNotification(`更新失败: ${data.message || '未知错误'}`, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('更新远程黑名单失败:', error);
|
||||
window.showNotification('更新远程黑名单失败', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// 更新所有远程黑名单
|
||||
function updateAllBlacklists() {
|
||||
confirmAction(
|
||||
'确定要更新所有远程黑名单吗?这可能需要一些时间。',
|
||||
() => {
|
||||
apiRequest('/api/shield/blacklists', 'PUT')
|
||||
.then(data => {
|
||||
// 检查响应中是否有status字段
|
||||
if (!data || typeof data === 'undefined') {
|
||||
window.showNotification('所有远程黑名单更新失败: 无效的响应', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.status === 'success') {
|
||||
window.showNotification('所有远程黑名单更新成功', 'success');
|
||||
loadBlacklists();
|
||||
} else {
|
||||
window.showNotification(`更新失败: ${data.message || '未知错误'}`, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('更新所有远程黑名单失败:', error);
|
||||
window.showNotification('更新所有远程黑名单失败', 'error');
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 删除远程黑名单
|
||||
function deleteBlacklist(id) {
|
||||
apiRequest(`/api/shield/blacklists/${id}`, 'DELETE')
|
||||
.then(data => {
|
||||
// 检查响应中是否有status字段
|
||||
if (!data || typeof data === 'undefined') {
|
||||
window.showNotification('远程黑名单删除失败: 无效的响应', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.status === 'success') {
|
||||
window.showNotification('远程黑名单删除成功', 'success');
|
||||
loadBlacklists();
|
||||
} else {
|
||||
window.showNotification(`删除失败: ${data.message || '未知错误'}`, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('删除远程黑名单失败:', error);
|
||||
window.showNotification('删除远程黑名单失败', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// 为操作按钮添加事件监听器
|
||||
function initBlacklistsActionListeners() {
|
||||
// 更新按钮
|
||||
document.querySelectorAll('.update-blacklist').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const id = this.getAttribute('data-id');
|
||||
updateBlacklist(id);
|
||||
});
|
||||
});
|
||||
|
||||
// 删除按钮
|
||||
document.querySelectorAll('.delete-blacklist').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const id = this.getAttribute('data-id');
|
||||
|
||||
confirmAction(
|
||||
'确定要删除这条远程黑名单吗?',
|
||||
() => deleteBlacklist(id)
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 验证URL格式
|
||||
function isValidUrl(url) {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
// 初始化配置管理面板
|
||||
function initConfigPanel() {
|
||||
// 加载当前配置
|
||||
loadConfig();
|
||||
|
||||
// 初始化事件监听器
|
||||
initConfigEventListeners();
|
||||
}
|
||||
|
||||
// 初始化事件监听器
|
||||
function initConfigEventListeners() {
|
||||
// 保存配置按钮
|
||||
document.getElementById('save-config').addEventListener('click', saveConfig);
|
||||
|
||||
// 屏蔽方法变更
|
||||
document.getElementById('block-method').addEventListener('change', updateCustomBlockIpVisibility);
|
||||
}
|
||||
|
||||
// 加载当前配置
|
||||
function loadConfig() {
|
||||
apiRequest('/config')
|
||||
.then(config => {
|
||||
renderConfig(config);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取配置失败:', error);
|
||||
window.showNotification('获取配置失败', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染配置表单
|
||||
function renderConfig(config) {
|
||||
if (!config) return;
|
||||
|
||||
// 设置屏蔽方法
|
||||
const blockMethodSelect = document.getElementById('block-method');
|
||||
if (config.shield && config.shield.blockMethod) {
|
||||
blockMethodSelect.value = config.shield.blockMethod;
|
||||
}
|
||||
|
||||
// 设置自定义屏蔽IP
|
||||
const customBlockIpInput = document.getElementById('custom-block-ip');
|
||||
if (config.shield && config.shield.customBlockIP) {
|
||||
customBlockIpInput.value = config.shield.customBlockIP;
|
||||
}
|
||||
|
||||
// 设置远程规则更新间隔
|
||||
const updateIntervalInput = document.getElementById('update-interval');
|
||||
if (config.shield && config.shield.updateInterval) {
|
||||
updateIntervalInput.value = config.shield.updateInterval;
|
||||
}
|
||||
|
||||
// 更新自定义屏蔽IP的可见性
|
||||
updateCustomBlockIpVisibility();
|
||||
}
|
||||
|
||||
// 更新自定义屏蔽IP输入框的可见性
|
||||
function updateCustomBlockIpVisibility() {
|
||||
const blockMethod = document.getElementById('block-method').value;
|
||||
const customBlockIpContainer = document.getElementById('custom-block-ip').closest('.form-group');
|
||||
|
||||
if (blockMethod === 'customIP') {
|
||||
customBlockIpContainer.style.display = 'block';
|
||||
} else {
|
||||
customBlockIpContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
function saveConfig() {
|
||||
// 收集表单数据
|
||||
const configData = {
|
||||
shield: {
|
||||
blockMethod: document.getElementById('block-method').value,
|
||||
updateInterval: parseInt(document.getElementById('update-interval').value)
|
||||
}
|
||||
};
|
||||
|
||||
// 如果选择了自定义IP,添加到配置中
|
||||
if (configData.shield.blockMethod === 'customIP') {
|
||||
const customBlockIp = document.getElementById('custom-block-ip').value.trim();
|
||||
|
||||
// 验证自定义IP格式
|
||||
if (!isValidIp(customBlockIp)) {
|
||||
window.showNotification('请输入有效的自定义屏蔽IP', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
configData.shield.customBlockIP = customBlockIp;
|
||||
}
|
||||
|
||||
// 验证更新间隔
|
||||
if (isNaN(configData.shield.updateInterval) || configData.shield.updateInterval < 60) {
|
||||
window.showNotification('更新间隔必须大于等于60秒', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
apiRequest('/config', 'PUT', configData)
|
||||
.then(response => {
|
||||
if (response.success) {
|
||||
window.showNotification('配置保存成功', 'success');
|
||||
|
||||
// 由于服务器没有提供重启API,移除重启提示
|
||||
// 直接提示用户配置已保存
|
||||
} else {
|
||||
window.showNotification(`保存失败: ${response.message || '未知错误'}`, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('保存配置失败:', error);
|
||||
window.showNotification('保存配置失败', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// 服务重启功能已移除,因为服务器没有提供对应的API端点
|
||||
|
||||
// 验证IP地址格式
|
||||
function isValidIp(ip) {
|
||||
// 支持IPv4和IPv6简单验证
|
||||
const ipv4Regex = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/;
|
||||
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}$/;
|
||||
|
||||
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,308 +0,0 @@
|
||||
// 初始化Hosts面板
|
||||
function initHostsPanel() {
|
||||
// 加载Hosts列表
|
||||
loadHosts();
|
||||
|
||||
// 初始化事件监听器
|
||||
initHostsEventListeners();
|
||||
}
|
||||
|
||||
// 初始化事件监听器
|
||||
function initHostsEventListeners() {
|
||||
// 添加Hosts按钮
|
||||
document.getElementById('add-hosts').addEventListener('click', addHostsEntry);
|
||||
|
||||
// Hosts过滤
|
||||
document.getElementById('hosts-filter').addEventListener('input', filterHosts);
|
||||
|
||||
// 按Enter键添加Hosts
|
||||
document.getElementById('hosts-domain').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
addHostsEntry();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 加载Hosts列表
|
||||
function loadHosts() {
|
||||
const tbody = document.getElementById('hosts-table').querySelector('tbody');
|
||||
showLoading(tbody);
|
||||
|
||||
// 更新API路径,使用完整路径
|
||||
apiRequest('/api/shield/hosts', 'GET')
|
||||
.then(data => {
|
||||
// 处理不同格式的响应数据
|
||||
let hostsData;
|
||||
if (Array.isArray(data)) {
|
||||
hostsData = data;
|
||||
} else if (data && data.hosts) {
|
||||
hostsData = data.hosts;
|
||||
} else {
|
||||
hostsData = [];
|
||||
}
|
||||
|
||||
renderHosts(hostsData);
|
||||
|
||||
// 更新Hosts数量统计
|
||||
if (window.updateHostsCount && typeof window.updateHostsCount === 'function') {
|
||||
window.updateHostsCount(hostsData.length);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取Hosts列表失败:', error);
|
||||
|
||||
if (tbody) {
|
||||
tbody.innerHTML = '<tr><td colspan="3" class="text-center py-4">' +
|
||||
'<div class="empty-state">' +
|
||||
'<div class="empty-icon"><i class="fas fa-server text-muted"></i></div>' +
|
||||
'<div class="empty-title text-muted">加载失败</div>' +
|
||||
'<div class="empty-description text-muted">无法获取Hosts列表,请稍后重试</div>' +
|
||||
'</div>' +
|
||||
'</td></tr>';
|
||||
}
|
||||
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('获取Hosts列表失败', 'danger');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染Hosts表格
|
||||
function renderHosts(hosts) {
|
||||
const tbody = document.getElementById('hosts-table').querySelector('tbody');
|
||||
if (!tbody) return;
|
||||
|
||||
if (!hosts || hosts.length === 0) {
|
||||
// 使用更友好的空状态显示
|
||||
tbody.innerHTML = '<tr><td colspan="3" class="text-center py-4">' +
|
||||
'<div class="empty-state">' +
|
||||
'<div class="empty-icon"><i class="fas fa-file-alt text-muted"></i></div>' +
|
||||
'<div class="empty-title text-muted">暂无Hosts条目</div>' +
|
||||
'<div class="empty-description text-muted">添加自定义Hosts条目以控制DNS解析</div>' +
|
||||
'</div>' +
|
||||
'</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = '';
|
||||
|
||||
hosts.forEach(entry => {
|
||||
addHostsToTable(entry.ip, entry.domain);
|
||||
});
|
||||
|
||||
// 初始化删除按钮监听器
|
||||
initDeleteHostsListeners();
|
||||
}
|
||||
|
||||
// 添加Hosts到表格
|
||||
function addHostsToTable(ip, domain) {
|
||||
const tbody = document.getElementById('hosts-table').querySelector('tbody');
|
||||
const row = document.createElement('tr');
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${ip}</td>
|
||||
<td>${domain}</td>
|
||||
<td class="actions-cell">
|
||||
<button class="btn btn-danger btn-sm delete-hosts" data-ip="${ip}" data-domain="${domain}">
|
||||
<i class="fas fa-trash-alt"></i> 删除
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
// 添加行动画效果
|
||||
row.style.opacity = '0';
|
||||
row.style.transform = 'translateY(10px)';
|
||||
tbody.appendChild(row);
|
||||
|
||||
// 使用requestAnimationFrame确保动画平滑
|
||||
requestAnimationFrame(() => {
|
||||
row.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
||||
row.style.opacity = '1';
|
||||
row.style.transform = 'translateY(0)';
|
||||
});
|
||||
}
|
||||
|
||||
// 添加Hosts条目
|
||||
function addHostsEntry() {
|
||||
const ipInput = document.getElementById('hosts-ip');
|
||||
const domainInput = document.getElementById('hosts-domain');
|
||||
|
||||
const ip = ipInput.value.trim();
|
||||
const domain = domainInput.value.trim();
|
||||
|
||||
if (!ip) {
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('请输入IP地址', 'warning');
|
||||
}
|
||||
ipInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!domain) {
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('请输入域名', 'warning');
|
||||
}
|
||||
domainInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// 简单的IP地址格式验证
|
||||
if (!isValidIp(ip)) {
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('请输入有效的IP地址', 'warning');
|
||||
}
|
||||
ipInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// 修复重复API调用问题,只调用一次
|
||||
apiRequest('/api/shield/hosts', 'POST', { ip: ip, domain: domain })
|
||||
.then(data => {
|
||||
// 处理不同的响应格式
|
||||
if (data.success || data.status === 'success') {
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('Hosts条目添加成功', 'success');
|
||||
}
|
||||
|
||||
// 清空输入框并聚焦到域名输入
|
||||
ipInput.value = '';
|
||||
domainInput.value = '';
|
||||
domainInput.focus();
|
||||
|
||||
// 重新加载Hosts列表
|
||||
loadHosts();
|
||||
|
||||
// 触发数据刷新事件
|
||||
if (typeof window.triggerDataRefresh === 'function') {
|
||||
window.triggerDataRefresh('hosts');
|
||||
}
|
||||
} else {
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification(`添加失败: ${data.message || '未知错误'}`, 'danger');
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('添加Hosts条目失败:', error);
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('添加Hosts条目失败', 'danger');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 删除Hosts条目
|
||||
function deleteHostsEntry(ip, domain) {
|
||||
// 找到要删除的行并添加删除动画
|
||||
const rows = document.querySelectorAll('#hosts-table tbody tr');
|
||||
let targetRow = null;
|
||||
|
||||
rows.forEach(row => {
|
||||
if (row.cells[0].textContent === ip && row.cells[1].textContent === domain) {
|
||||
targetRow = row;
|
||||
}
|
||||
});
|
||||
|
||||
if (targetRow) {
|
||||
targetRow.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
||||
targetRow.style.opacity = '0';
|
||||
targetRow.style.transform = 'translateX(-20px)';
|
||||
}
|
||||
|
||||
// 更新API路径
|
||||
apiRequest('/api/shield/hosts', 'DELETE', { ip: ip, domain: domain })
|
||||
.then(data => {
|
||||
// 处理不同的响应格式
|
||||
if (data.success || data.status === 'success') {
|
||||
// 等待动画完成后重新加载列表
|
||||
setTimeout(() => {
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('Hosts条目删除成功', 'success');
|
||||
}
|
||||
loadHosts();
|
||||
|
||||
// 触发数据刷新事件
|
||||
if (typeof window.triggerDataRefresh === 'function') {
|
||||
window.triggerDataRefresh('hosts');
|
||||
}
|
||||
}, 300);
|
||||
} else {
|
||||
// 恢复行样式
|
||||
if (targetRow) {
|
||||
targetRow.style.opacity = '1';
|
||||
targetRow.style.transform = 'translateX(0)';
|
||||
}
|
||||
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification(`删除失败: ${data.message || '未知错误'}`, 'danger');
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// 恢复行样式
|
||||
if (targetRow) {
|
||||
targetRow.style.opacity = '1';
|
||||
targetRow.style.transform = 'translateX(0)';
|
||||
}
|
||||
|
||||
console.error('删除Hosts条目失败:', error);
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('删除Hosts条目失败', 'danger');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 过滤Hosts
|
||||
function filterHosts() {
|
||||
const filterText = document.getElementById('hosts-filter').value.toLowerCase();
|
||||
const rows = document.querySelectorAll('#hosts-table tbody tr');
|
||||
|
||||
rows.forEach(row => {
|
||||
const ip = row.cells[0].textContent.toLowerCase();
|
||||
const domain = row.cells[1].textContent.toLowerCase();
|
||||
|
||||
row.style.display = (ip.includes(filterText) || domain.includes(filterText)) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// 为删除按钮添加事件监听器
|
||||
function initDeleteHostsListeners() {
|
||||
document.querySelectorAll('.delete-hosts').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const ip = this.getAttribute('data-ip');
|
||||
const domain = this.getAttribute('data-domain');
|
||||
|
||||
// 使用标准confirm对话框
|
||||
if (confirm(`确定要删除这条Hosts条目吗?\n${ip} ${domain}`)) {
|
||||
deleteHostsEntry(ip, domain);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 验证IP地址格式
|
||||
function isValidIp(ip) {
|
||||
// 支持IPv4和IPv6简单验证
|
||||
const ipv4Regex = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/;
|
||||
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}$/;
|
||||
|
||||
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
|
||||
}
|
||||
|
||||
// 导出函数,供其他模块调用
|
||||
window.updateHostsCount = function(count) {
|
||||
const hostsCountElement = document.getElementById('hosts-count');
|
||||
if (hostsCountElement) {
|
||||
hostsCountElement.textContent = count;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出初始化函数
|
||||
window.initHostsPanel = initHostsPanel;
|
||||
|
||||
// 注册到面板导航系统
|
||||
if (window.registerPanelModule) {
|
||||
window.registerPanelModule('hosts-panel', {
|
||||
init: initHostsPanel,
|
||||
refresh: loadHosts
|
||||
});
|
||||
}
|
||||
@@ -1,294 +0,0 @@
|
||||
// 初始化DNS查询面板
|
||||
function initQueryPanel() {
|
||||
// 初始化事件监听器
|
||||
initQueryEventListeners();
|
||||
|
||||
// 确保结果容器默认隐藏
|
||||
const resultContainer = document.getElementById('query-result-container');
|
||||
if (resultContainer) {
|
||||
resultContainer.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化事件监听器
|
||||
function initQueryEventListeners() {
|
||||
// 查询按钮
|
||||
document.getElementById('run-query').addEventListener('click', runDnsQuery);
|
||||
|
||||
// 按Enter键执行查询
|
||||
document.getElementById('query-domain').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
runDnsQuery();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 执行DNS查询
|
||||
function runDnsQuery() {
|
||||
const domainInput = document.getElementById('query-domain');
|
||||
const domain = domainInput.value.trim();
|
||||
|
||||
if (!domain) {
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('请输入要查询的域名', 'warning');
|
||||
}
|
||||
domainInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示查询中状态
|
||||
showQueryLoading();
|
||||
|
||||
// 更新API路径,使用完整路径
|
||||
apiRequest('/api/query', 'GET', { domain: domain })
|
||||
.then(data => {
|
||||
// 处理可能的不同响应格式
|
||||
renderQueryResult(data);
|
||||
|
||||
// 触发数据刷新事件
|
||||
if (typeof window.triggerDataRefresh === 'function') {
|
||||
window.triggerDataRefresh('query');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('DNS查询失败:', error);
|
||||
showQueryError('查询失败,请稍后重试');
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('DNS查询失败', 'danger');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 显示查询加载状态
|
||||
function showQueryLoading() {
|
||||
const resultContainer = document.getElementById('query-result-container');
|
||||
if (!resultContainer) return;
|
||||
|
||||
// 添加加载动画类
|
||||
resultContainer.classList.add('loading-animation');
|
||||
resultContainer.classList.remove('hidden', 'error-animation', 'success-animation');
|
||||
|
||||
// 清空之前的结果
|
||||
const resultHeader = resultContainer.querySelector('.result-header h3');
|
||||
const resultContent = resultContainer.querySelector('.result-content');
|
||||
|
||||
if (resultHeader) resultHeader.textContent = '查询中...';
|
||||
if (resultContent) {
|
||||
resultContent.innerHTML = '<div class="loading">' +
|
||||
'<div class="spinner"></div><span>正在查询...</span>' +
|
||||
'</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// 显示查询错误
|
||||
function showQueryError(message) {
|
||||
const resultContainer = document.getElementById('query-result-container');
|
||||
if (!resultContainer) return;
|
||||
|
||||
// 添加错误动画类
|
||||
resultContainer.classList.add('error-animation');
|
||||
resultContainer.classList.remove('hidden', 'loading-animation', 'success-animation');
|
||||
|
||||
const resultHeader = resultContainer.querySelector('.result-header h3');
|
||||
const resultContent = resultContainer.querySelector('.result-content');
|
||||
|
||||
if (resultHeader) resultHeader.textContent = '查询错误';
|
||||
if (resultContent) {
|
||||
resultContent.innerHTML = `<div class="result-item error-message">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<span>${message}</span>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染查询结果
|
||||
function renderQueryResult(result) {
|
||||
const resultContainer = document.getElementById('query-result-container');
|
||||
if (!resultContainer) return;
|
||||
|
||||
// 添加成功动画类
|
||||
resultContainer.classList.add('success-animation');
|
||||
resultContainer.classList.remove('hidden', 'loading-animation', 'error-animation');
|
||||
|
||||
const resultHeader = resultContainer.querySelector('.result-header h3');
|
||||
const resultContent = resultContainer.querySelector('.result-content');
|
||||
|
||||
if (resultHeader) resultHeader.textContent = '查询结果';
|
||||
if (!resultContent) return;
|
||||
|
||||
// 安全的HTML转义函数
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text || '';
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// 根据查询结果构建内容
|
||||
let content = '<div class="result-grid">';
|
||||
|
||||
// 域名
|
||||
const safeDomain = escapeHtml(result.domain || '');
|
||||
content += `<div class="result-item domain-item">
|
||||
<div class="result-label"><i class="fas fa-globe"></i> 域名</div>
|
||||
<div class="result-value" id="result-domain">${safeDomain}</div>
|
||||
</div>`;
|
||||
|
||||
// 状态 - 映射API字段
|
||||
const isBlocked = result.blocked || false;
|
||||
const isExcluded = result.excluded || false;
|
||||
const isAllowed = !isBlocked || isExcluded;
|
||||
|
||||
const statusText = isBlocked ? '被屏蔽' : isAllowed ? '允许访问' : '未知';
|
||||
const statusClass = isBlocked ? 'status-error' : isAllowed ? 'status-success' : '';
|
||||
const statusIcon = isBlocked ? 'fa-ban' : isAllowed ? 'fa-check-circle' : 'fa-question-circle';
|
||||
content += `<div class="result-item status-item">
|
||||
<div class="result-label"><i class="fas fa-shield-alt"></i> 状态</div>
|
||||
<div class="result-value" id="result-status" class="${statusClass}">
|
||||
<i class="fas ${statusIcon}"></i> ${statusText}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// 规则类型 - 映射API字段
|
||||
let ruleType = '';
|
||||
if (isBlocked) {
|
||||
if (result.blockRuleType && result.blockRuleType.toLowerCase().includes('regex')) {
|
||||
ruleType = '正则表达式规则';
|
||||
} else {
|
||||
ruleType = result.blockRuleType || '域名规则';
|
||||
}
|
||||
} else {
|
||||
if (isExcluded) {
|
||||
ruleType = '白名单规则';
|
||||
} else if (result.hasHosts) {
|
||||
ruleType = 'Hosts记录';
|
||||
} else {
|
||||
ruleType = '未匹配任何规则';
|
||||
}
|
||||
}
|
||||
content += `<div class="result-item rule-type-item">
|
||||
<div class="result-label"><i class="fas fa-list-alt"></i> 规则类型</div>
|
||||
<div class="result-value" id="result-rule-type">${escapeHtml(ruleType)}</div>
|
||||
</div>`;
|
||||
|
||||
// 匹配规则 - 映射API字段
|
||||
let matchedRule = '';
|
||||
if (isBlocked) {
|
||||
matchedRule = result.blockRule || '无';
|
||||
} else if (isExcluded) {
|
||||
matchedRule = result.excludeRule || '无';
|
||||
} else {
|
||||
matchedRule = '无';
|
||||
}
|
||||
content += `<div class="result-item matched-rule-item">
|
||||
<div class="result-label"><i class="fas fa-sitemap"></i> 匹配规则</div>
|
||||
<div class="result-value rule-code" id="result-rule">${escapeHtml(matchedRule)}</div>
|
||||
</div>`;
|
||||
|
||||
// Hosts记录 - 映射API字段
|
||||
const hostsRecord = result.hasHosts && result.hostsIP ?
|
||||
escapeHtml(`${result.hostsIP} ${result.domain}`) : '无';
|
||||
content += `<div class="result-item hosts-item">
|
||||
<div class="result-label"><i class="fas fa-file-alt"></i> Hosts记录</div>
|
||||
<div class="result-value" id="result-hosts">${hostsRecord}</div>
|
||||
</div>`;
|
||||
|
||||
// 查询时间 - API没有提供,计算当前时间
|
||||
const queryTime = `${Date.now() % 100} ms`;
|
||||
content += `<div class="result-item time-item">
|
||||
<div class="result-label"><i class="fas fa-clock"></i> 查询时间</div>
|
||||
<div class="result-value" id="result-time">${queryTime}</div>
|
||||
</div>`;
|
||||
|
||||
content += '</div>'; // 结束result-grid
|
||||
|
||||
// DNS响应(如果有)
|
||||
if (result.dnsResponse) {
|
||||
content += '<div class="dns-response-section">';
|
||||
content += '<h4><i class="fas fa-exchange-alt"></i> DNS响应</h4>';
|
||||
|
||||
if (result.dnsResponse.answers && result.dnsResponse.answers.length > 0) {
|
||||
content += '<div class="dns-answers">';
|
||||
result.dnsResponse.answers.forEach((answer, index) => {
|
||||
content += `<div class="dns-answer-item">
|
||||
<span class="answer-index">#${index + 1}</span>
|
||||
<span class="answer-name">${escapeHtml(answer.name)}</span>
|
||||
<span class="answer-type">${escapeHtml(answer.type)}</span>
|
||||
<span class="answer-value">${escapeHtml(answer.value)}</span>
|
||||
</div>`;
|
||||
});
|
||||
content += '</div>';
|
||||
} else {
|
||||
content += '<div class="empty-dns"><i class="fas fa-info-circle"></i> 无DNS响应记录</div>';
|
||||
}
|
||||
content += '</div>';
|
||||
}
|
||||
|
||||
// 添加复制功能
|
||||
content += `<div class="result-actions">
|
||||
<button class="btn btn-sm btn-secondary" onclick="copyQueryResult()">
|
||||
<i class="fas fa-copy"></i> 复制结果
|
||||
</button>
|
||||
</div>`;
|
||||
|
||||
resultContent.innerHTML = content;
|
||||
|
||||
// 通知用户查询成功
|
||||
if (typeof window.showNotification === 'function') {
|
||||
const statusMsg = isBlocked ? '查询完成,该域名被屏蔽' :
|
||||
isAllowed ? '查询完成,该域名允许访问' : '查询完成';
|
||||
window.showNotification(statusMsg, 'info');
|
||||
}
|
||||
}
|
||||
|
||||
// 复制查询结果到剪贴板
|
||||
function copyQueryResult() {
|
||||
const resultContainer = document.getElementById('query-result-container');
|
||||
if (!resultContainer) return;
|
||||
|
||||
// 收集关键信息
|
||||
const domain = document.getElementById('result-domain')?.textContent || '未知域名';
|
||||
const status = document.getElementById('result-status')?.textContent || '未知状态';
|
||||
const ruleType = document.getElementById('result-rule-type')?.textContent || '无规则类型';
|
||||
const matchedRule = document.getElementById('result-rule')?.textContent || '无匹配规则';
|
||||
const queryTime = document.getElementById('result-time')?.textContent || '未知时间';
|
||||
|
||||
// 构建要复制的文本
|
||||
const textToCopy = `DNS查询结果:\n` +
|
||||
`域名: ${domain}\n` +
|
||||
`状态: ${status}\n` +
|
||||
`规则类型: ${ruleType}\n` +
|
||||
`匹配规则: ${matchedRule}\n` +
|
||||
`查询时间: ${queryTime}`;
|
||||
|
||||
// 复制到剪贴板
|
||||
navigator.clipboard.writeText(textToCopy)
|
||||
.then(() => {
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('查询结果已复制到剪贴板', 'success');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('复制失败,请手动复制', 'warning');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 导出函数,供其他模块调用
|
||||
window.initQueryPanel = initQueryPanel;
|
||||
window.runDnsQuery = runDnsQuery;
|
||||
|
||||
// 注册到面板导航系统
|
||||
if (window.registerPanelModule) {
|
||||
window.registerPanelModule('query-panel', {
|
||||
init: initQueryPanel,
|
||||
refresh: function() {
|
||||
// 清除当前查询结果
|
||||
const resultContainer = document.getElementById('query-result-container');
|
||||
if (resultContainer) {
|
||||
resultContainer.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,422 +0,0 @@
|
||||
// 屏蔽规则管理模块
|
||||
|
||||
// 全局变量
|
||||
let rules = [];
|
||||
let currentPage = 1;
|
||||
let itemsPerPage = 50; // 默认每页显示50条规则
|
||||
let filteredRules = [];
|
||||
|
||||
// 初始化屏蔽规则面板
|
||||
function initRulesPanel() {
|
||||
// 加载规则列表
|
||||
loadRules();
|
||||
|
||||
// 绑定添加规则按钮事件
|
||||
document.getElementById('add-rule-btn').addEventListener('click', addNewRule);
|
||||
|
||||
// 绑定刷新规则按钮事件
|
||||
document.getElementById('reload-rules-btn').addEventListener('click', reloadRules);
|
||||
|
||||
// 绑定搜索框事件
|
||||
document.getElementById('rule-search').addEventListener('input', filterRules);
|
||||
|
||||
// 绑定每页显示数量变更事件
|
||||
document.getElementById('items-per-page').addEventListener('change', () => {
|
||||
itemsPerPage = parseInt(document.getElementById('items-per-page').value);
|
||||
currentPage = 1; // 重置为第一页
|
||||
renderRulesList();
|
||||
});
|
||||
|
||||
// 绑定分页按钮事件
|
||||
document.getElementById('prev-page-btn').addEventListener('click', goToPreviousPage);
|
||||
document.getElementById('next-page-btn').addEventListener('click', goToNextPage);
|
||||
document.getElementById('first-page-btn').addEventListener('click', goToFirstPage);
|
||||
document.getElementById('last-page-btn').addEventListener('click', goToLastPage);
|
||||
}
|
||||
|
||||
// 加载规则列表
|
||||
async function loadRules() {
|
||||
try {
|
||||
const rulesPanel = document.getElementById('rules-panel');
|
||||
showLoading(rulesPanel);
|
||||
|
||||
// 更新API路径,使用正确的API路径
|
||||
const data = await apiRequest('/api/shield', 'GET');
|
||||
|
||||
// 处理后端返回的复杂对象数据格式
|
||||
let allRules = [];
|
||||
if (data && typeof data === 'object') {
|
||||
// 合并所有类型的规则到一个数组
|
||||
if (Array.isArray(data.domainRules)) allRules = allRules.concat(data.domainRules);
|
||||
if (Array.isArray(data.domainExceptions)) allRules = allRules.concat(data.domainExceptions);
|
||||
if (Array.isArray(data.regexRules)) allRules = allRules.concat(data.regexRules);
|
||||
if (Array.isArray(data.regexExceptions)) allRules = allRules.concat(data.regexExceptions);
|
||||
}
|
||||
|
||||
rules = allRules;
|
||||
filteredRules = [...rules];
|
||||
currentPage = 1; // 重置为第一页
|
||||
renderRulesList();
|
||||
|
||||
// 更新规则数量统计卡片
|
||||
if (window.updateRulesCount && typeof window.updateRulesCount === 'function') {
|
||||
window.updateRulesCount(rules.length);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载规则失败:', error);
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('加载规则失败', 'danger');
|
||||
}
|
||||
} finally {
|
||||
const rulesPanel = document.getElementById('rules-panel');
|
||||
hideLoading(rulesPanel);
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染规则列表
|
||||
function renderRulesList() {
|
||||
const rulesList = document.getElementById('rules-list');
|
||||
const paginationInfo = document.getElementById('pagination-info');
|
||||
|
||||
// 清空列表
|
||||
rulesList.innerHTML = '';
|
||||
|
||||
if (filteredRules.length === 0) {
|
||||
// 使用更友好的空状态显示
|
||||
rulesList.innerHTML = '<tr><td colspan="4" class="text-center py-4">' +
|
||||
'<div class="empty-state">' +
|
||||
'<div class="empty-icon"><i class="fas fa-shield-alt text-muted"></i></div>' +
|
||||
'<div class="empty-title text-muted">暂无规则</div>' +
|
||||
'<div class="empty-description text-muted">点击添加按钮或刷新规则来获取规则列表</div>' +
|
||||
'</div>' +
|
||||
'</td></tr>';
|
||||
paginationInfo.textContent = '共0条规则';
|
||||
updatePaginationButtons();
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算分页数据
|
||||
const totalPages = Math.ceil(filteredRules.length / itemsPerPage);
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = Math.min(startIndex + itemsPerPage, filteredRules.length);
|
||||
const currentRules = filteredRules.slice(startIndex, endIndex);
|
||||
|
||||
// 渲染当前页的规则
|
||||
currentRules.forEach((rule, index) => {
|
||||
const row = document.createElement('tr');
|
||||
const globalIndex = startIndex + index;
|
||||
|
||||
// 根据规则类型添加不同的样式
|
||||
const ruleTypeClass = getRuleTypeClass(rule);
|
||||
|
||||
row.innerHTML = `
|
||||
<td class="rule-id">${globalIndex + 1}</td>
|
||||
<td class="rule-content ${ruleTypeClass}"><pre>${escapeHtml(rule)}</pre></td>
|
||||
<td class="rule-actions">
|
||||
<button class="btn btn-danger btn-sm delete-rule" data-index="${globalIndex}">
|
||||
<i class="fas fa-trash"></i> 删除
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
// 添加行动画效果
|
||||
row.style.opacity = '0';
|
||||
row.style.transform = 'translateY(10px)';
|
||||
rulesList.appendChild(row);
|
||||
|
||||
// 使用requestAnimationFrame确保动画平滑
|
||||
requestAnimationFrame(() => {
|
||||
row.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
||||
row.style.opacity = '1';
|
||||
row.style.transform = 'translateY(0)';
|
||||
});
|
||||
});
|
||||
|
||||
// 绑定删除按钮事件
|
||||
document.querySelectorAll('.delete-rule').forEach(button => {
|
||||
button.addEventListener('click', (e) => {
|
||||
const index = parseInt(e.currentTarget.dataset.index);
|
||||
deleteRule(index);
|
||||
});
|
||||
});
|
||||
|
||||
// 更新分页信息
|
||||
paginationInfo.textContent = `显示 ${startIndex + 1}-${endIndex} 条,共 ${filteredRules.length} 条规则,第 ${currentPage}/${totalPages} 页`;
|
||||
|
||||
// 更新分页按钮状态
|
||||
updatePaginationButtons();
|
||||
}
|
||||
|
||||
// 更新分页按钮状态
|
||||
function updatePaginationButtons() {
|
||||
const totalPages = Math.ceil(filteredRules.length / itemsPerPage);
|
||||
const prevBtn = document.getElementById('prev-page-btn');
|
||||
const nextBtn = document.getElementById('next-page-btn');
|
||||
const firstBtn = document.getElementById('first-page-btn');
|
||||
const lastBtn = document.getElementById('last-page-btn');
|
||||
|
||||
prevBtn.disabled = currentPage === 1;
|
||||
nextBtn.disabled = currentPage === totalPages || totalPages === 0;
|
||||
firstBtn.disabled = currentPage === 1;
|
||||
lastBtn.disabled = currentPage === totalPages || totalPages === 0;
|
||||
}
|
||||
|
||||
// 上一页
|
||||
function goToPreviousPage() {
|
||||
if (currentPage > 1) {
|
||||
currentPage--;
|
||||
renderRulesList();
|
||||
}
|
||||
}
|
||||
|
||||
// 下一页
|
||||
function goToNextPage() {
|
||||
const totalPages = Math.ceil(filteredRules.length / itemsPerPage);
|
||||
if (currentPage < totalPages) {
|
||||
currentPage++;
|
||||
renderRulesList();
|
||||
}
|
||||
}
|
||||
|
||||
// 第一页
|
||||
function goToFirstPage() {
|
||||
currentPage = 1;
|
||||
renderRulesList();
|
||||
}
|
||||
|
||||
// 最后一页
|
||||
function goToLastPage() {
|
||||
currentPage = Math.ceil(filteredRules.length / itemsPerPage);
|
||||
renderRulesList();
|
||||
}
|
||||
|
||||
// 添加新规则
|
||||
async function addNewRule() {
|
||||
const ruleInput = document.getElementById('rule-input');
|
||||
const rule = ruleInput.value.trim();
|
||||
|
||||
if (!rule) {
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('请输入规则内容', 'warning');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 预处理规则,支持AdGuardHome格式
|
||||
const processedRule = preprocessRule(rule);
|
||||
|
||||
// 使用正确的API路径
|
||||
const response = await apiRequest('/api/shield', 'POST', { rule: processedRule });
|
||||
|
||||
// 处理不同的响应格式
|
||||
if (response.success || response.status === 'success') {
|
||||
rules.push(processedRule);
|
||||
filteredRules = [...rules];
|
||||
ruleInput.value = '';
|
||||
|
||||
// 添加后跳转到最后一页,显示新添加的规则
|
||||
currentPage = Math.ceil(filteredRules.length / itemsPerPage);
|
||||
renderRulesList();
|
||||
|
||||
// 更新规则数量统计
|
||||
if (window.updateRulesCount && typeof window.updateRulesCount === 'function') {
|
||||
window.updateRulesCount(rules.length);
|
||||
}
|
||||
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('规则添加成功', 'success');
|
||||
}
|
||||
} else {
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('规则添加失败:' + (response.message || '未知错误'), 'danger');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('添加规则失败:', error);
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('添加规则失败', 'danger');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除规则
|
||||
async function deleteRule(index) {
|
||||
if (!confirm('确定要删除这条规则吗?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const rule = filteredRules[index];
|
||||
const rowElement = document.querySelectorAll('#rules-list tr')[index];
|
||||
|
||||
// 添加删除动画
|
||||
if (rowElement) {
|
||||
rowElement.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
||||
rowElement.style.opacity = '0';
|
||||
rowElement.style.transform = 'translateX(-20px)';
|
||||
}
|
||||
|
||||
// 使用正确的API路径
|
||||
const response = await apiRequest('/api/shield', 'DELETE', { rule });
|
||||
|
||||
// 处理不同的响应格式
|
||||
if (response.success || response.status === 'success') {
|
||||
// 在原规则列表中找到并删除
|
||||
const originalIndex = rules.indexOf(rule);
|
||||
if (originalIndex !== -1) {
|
||||
rules.splice(originalIndex, 1);
|
||||
}
|
||||
|
||||
// 在过滤后的列表中删除
|
||||
filteredRules.splice(index, 1);
|
||||
|
||||
// 如果当前页没有数据了,回到上一页
|
||||
const totalPages = Math.ceil(filteredRules.length / itemsPerPage);
|
||||
if (currentPage > totalPages && totalPages > 0) {
|
||||
currentPage = totalPages;
|
||||
}
|
||||
|
||||
// 等待动画完成后重新渲染列表
|
||||
setTimeout(() => {
|
||||
renderRulesList();
|
||||
|
||||
// 更新规则数量统计
|
||||
if (window.updateRulesCount && typeof window.updateRulesCount === 'function') {
|
||||
window.updateRulesCount(rules.length);
|
||||
}
|
||||
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('规则删除成功', 'success');
|
||||
}
|
||||
}, 300);
|
||||
} else {
|
||||
// 恢复行样式
|
||||
if (rowElement) {
|
||||
rowElement.style.opacity = '1';
|
||||
rowElement.style.transform = 'translateX(0)';
|
||||
}
|
||||
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('规则删除失败:' + (response.message || '未知错误'), 'danger');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除规则失败:', error);
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('删除规则失败', 'danger');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 重新加载规则
|
||||
async function reloadRules() {
|
||||
if (!confirm('确定要重新加载所有规则吗?这将覆盖当前内存中的规则。')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const rulesPanel = document.getElementById('rules-panel');
|
||||
showLoading(rulesPanel);
|
||||
|
||||
// 使用正确的API路径和方法 - PUT请求到/api/shield
|
||||
await apiRequest('/api/shield', 'PUT');
|
||||
|
||||
// 重新加载规则列表
|
||||
await loadRules();
|
||||
|
||||
// 触发数据刷新事件,通知其他模块数据已更新
|
||||
if (typeof window.triggerDataRefresh === 'function') {
|
||||
window.triggerDataRefresh('rules');
|
||||
}
|
||||
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('规则重新加载成功', 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重新加载规则失败:', error);
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('重新加载规则失败', 'danger');
|
||||
}
|
||||
} finally {
|
||||
const rulesPanel = document.getElementById('rules-panel');
|
||||
hideLoading(rulesPanel);
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤规则
|
||||
function filterRules() {
|
||||
const searchTerm = document.getElementById('rule-search').value.toLowerCase();
|
||||
|
||||
if (searchTerm) {
|
||||
filteredRules = rules.filter(rule => rule.toLowerCase().includes(searchTerm));
|
||||
} else {
|
||||
filteredRules = [...rules];
|
||||
}
|
||||
|
||||
currentPage = 1; // 重置为第一页
|
||||
renderRulesList();
|
||||
}
|
||||
|
||||
// HTML转义,防止XSS攻击
|
||||
function escapeHtml(text) {
|
||||
const map = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return text.replace(/[&<>'"]/g, m => map[m]);
|
||||
}
|
||||
|
||||
// 根据规则类型返回对应的CSS类名
|
||||
function getRuleTypeClass(rule) {
|
||||
// 简单的规则类型判断
|
||||
if (rule.startsWith('||') || rule.startsWith('|http')) {
|
||||
return 'rule-type-url';
|
||||
} else if (rule.startsWith('@@')) {
|
||||
return 'rule-type-exception';
|
||||
} else if (rule.startsWith('#')) {
|
||||
return 'rule-type-comment';
|
||||
} else if (rule.includes('$')) {
|
||||
return 'rule-type-filter';
|
||||
}
|
||||
return 'rule-type-standard';
|
||||
}
|
||||
|
||||
// 预处理规则,支持多种规则格式
|
||||
function preprocessRule(rule) {
|
||||
// 移除首尾空白字符
|
||||
let processed = rule.trim();
|
||||
|
||||
// 处理AdGuardHome格式的规则
|
||||
if (processed.startsWith('0.0.0.0 ') || processed.startsWith('127.0.0.1 ')) {
|
||||
const parts = processed.split(' ');
|
||||
if (parts.length >= 2) {
|
||||
// 转换为AdBlock Plus格式
|
||||
processed = '||' + parts[1] + '^';
|
||||
}
|
||||
}
|
||||
|
||||
return processed;
|
||||
}
|
||||
|
||||
// 导出函数,供其他模块调用
|
||||
window.updateRulesCount = function(count) {
|
||||
const rulesCountElement = document.getElementById('rules-count');
|
||||
if (rulesCountElement) {
|
||||
rulesCountElement.textContent = count;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出初始化函数
|
||||
window.initRulesPanel = initRulesPanel;
|
||||
|
||||
// 注册到面板导航系统
|
||||
if (window.registerPanelModule) {
|
||||
window.registerPanelModule('rules-panel', {
|
||||
init: initRulesPanel,
|
||||
refresh: loadRules
|
||||
});
|
||||
}
|
||||
301
js/query.js
301
js/query.js
@@ -1,301 +0,0 @@
|
||||
// DNS查询页面功能实现
|
||||
|
||||
// 初始化查询页面
|
||||
function initQueryPage() {
|
||||
console.log('初始化DNS查询页面...');
|
||||
setupQueryEventListeners();
|
||||
loadQueryHistory();
|
||||
}
|
||||
|
||||
// 执行DNS查询
|
||||
async function handleDNSQuery() {
|
||||
const domainInput = document.getElementById('dns-query-domain');
|
||||
const resultDiv = document.getElementById('query-result');
|
||||
|
||||
if (!domainInput || !resultDiv) {
|
||||
console.error('找不到必要的DOM元素');
|
||||
return;
|
||||
}
|
||||
|
||||
const domain = domainInput.value.trim();
|
||||
if (!domain) {
|
||||
showErrorMessage('请输入域名');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/query?domain=${encodeURIComponent(domain)}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('查询失败');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
displayQueryResult(result, domain);
|
||||
saveQueryHistory(domain, result);
|
||||
loadQueryHistory();
|
||||
} catch (error) {
|
||||
console.error('DNS查询出错:', error);
|
||||
showErrorMessage('查询失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示查询结果
|
||||
function displayQueryResult(result, domain) {
|
||||
const resultDiv = document.getElementById('query-result');
|
||||
if (!resultDiv) return;
|
||||
|
||||
// 显示结果容器
|
||||
resultDiv.classList.remove('hidden');
|
||||
|
||||
// 解析结果
|
||||
const status = result.blocked ? '被屏蔽' : '正常';
|
||||
const statusClass = result.blocked ? 'text-danger' : 'text-success';
|
||||
const blockType = result.blocked ? result.blockRuleType || '未知' : '正常';
|
||||
const blockRule = result.blocked ? result.blockRule || '未知' : '无';
|
||||
const blockSource = result.blocked ? result.blocksource || '未知' : '无';
|
||||
const timestamp = new Date(result.timestamp).toLocaleString();
|
||||
|
||||
// 更新结果显示
|
||||
document.getElementById('result-domain').textContent = domain;
|
||||
document.getElementById('result-status').innerHTML = `<span class="${statusClass}">${status}</span>`;
|
||||
document.getElementById('result-type').textContent = blockType;
|
||||
|
||||
// 检查是否存在屏蔽规则显示元素,如果不存在则创建
|
||||
let blockRuleElement = document.getElementById('result-block-rule');
|
||||
if (!blockRuleElement) {
|
||||
// 创建屏蔽规则显示区域
|
||||
const grid = resultDiv.querySelector('.grid');
|
||||
if (grid) {
|
||||
const newGridItem = document.createElement('div');
|
||||
newGridItem.className = 'bg-gray-50 p-4 rounded-lg';
|
||||
newGridItem.innerHTML = `
|
||||
<h4 class="text-sm font-medium text-gray-500 mb-2">屏蔽规则</h4>
|
||||
<p class="text-lg font-semibold" id="result-block-rule">-</p>
|
||||
`;
|
||||
grid.appendChild(newGridItem);
|
||||
blockRuleElement = document.getElementById('result-block-rule');
|
||||
}
|
||||
}
|
||||
|
||||
// 更新屏蔽规则显示
|
||||
if (blockRuleElement) {
|
||||
blockRuleElement.textContent = blockRule;
|
||||
}
|
||||
|
||||
// 检查是否存在屏蔽来源显示元素,如果不存在则创建
|
||||
let blockSourceElement = document.getElementById('result-block-source');
|
||||
if (!blockSourceElement) {
|
||||
// 创建屏蔽来源显示区域
|
||||
const grid = resultDiv.querySelector('.grid');
|
||||
if (grid) {
|
||||
const newGridItem = document.createElement('div');
|
||||
newGridItem.className = 'bg-gray-50 p-4 rounded-lg';
|
||||
newGridItem.innerHTML = `
|
||||
<h4 class="text-sm font-medium text-gray-500 mb-2">屏蔽来源</h4>
|
||||
<p class="text-lg font-semibold" id="result-block-source">-</p>
|
||||
`;
|
||||
grid.appendChild(newGridItem);
|
||||
blockSourceElement = document.getElementById('result-block-source');
|
||||
}
|
||||
}
|
||||
|
||||
// 更新屏蔽来源显示
|
||||
if (blockSourceElement) {
|
||||
blockSourceElement.textContent = blockSource;
|
||||
}
|
||||
|
||||
document.getElementById('result-time').textContent = timestamp;
|
||||
document.getElementById('result-details').textContent = JSON.stringify(result, null, 2);
|
||||
}
|
||||
|
||||
// 保存查询历史
|
||||
function saveQueryHistory(domain, result) {
|
||||
// 获取现有历史记录
|
||||
let history = JSON.parse(localStorage.getItem('dnsQueryHistory') || '[]');
|
||||
|
||||
// 创建历史记录项
|
||||
const historyItem = {
|
||||
domain: domain,
|
||||
timestamp: new Date().toISOString(),
|
||||
result: {
|
||||
blocked: result.blocked,
|
||||
blockRuleType: result.blockRuleType,
|
||||
blockRule: result.blockRule,
|
||||
blocksource: result.blocksource
|
||||
}
|
||||
};
|
||||
|
||||
// 添加到历史记录开头
|
||||
history.unshift(historyItem);
|
||||
|
||||
// 限制历史记录数量
|
||||
if (history.length > 20) {
|
||||
history = history.slice(0, 20);
|
||||
}
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('dnsQueryHistory', JSON.stringify(history));
|
||||
}
|
||||
|
||||
// 加载查询历史
|
||||
function loadQueryHistory() {
|
||||
const historyDiv = document.getElementById('query-history');
|
||||
if (!historyDiv) return;
|
||||
|
||||
// 获取历史记录
|
||||
const history = JSON.parse(localStorage.getItem('dnsQueryHistory') || '[]');
|
||||
|
||||
if (history.length === 0) {
|
||||
historyDiv.innerHTML = '<div class="text-center text-gray-500 py-4">暂无查询历史</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成历史记录HTML
|
||||
const historyHTML = history.map(item => {
|
||||
const statusClass = item.result.blocked ? 'text-danger' : 'text-success';
|
||||
const statusText = item.result.blocked ? '被屏蔽' : '正常';
|
||||
const blockType = item.result.blocked ? item.result.blockRuleType : '正常';
|
||||
const blockRule = item.result.blocked ? item.result.blockRule : '无';
|
||||
const blockSource = item.result.blocked ? item.result.blocksource : '无';
|
||||
const formattedTime = new Date(item.timestamp).toLocaleString();
|
||||
|
||||
return `
|
||||
<div class="flex flex-col md:flex-row justify-between items-start md:items-center p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="font-medium">${item.domain}</span>
|
||||
<span class="${statusClass} text-sm">${statusText}</span>
|
||||
<span class="text-xs text-gray-500">${blockType}</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-1">规则: ${blockRule}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">来源: ${blockSource}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">${formattedTime}</div>
|
||||
</div>
|
||||
<button class="mt-2 md:mt-0 px-3 py-1 bg-primary text-white text-sm rounded-md hover:bg-primary/90 transition-colors" onclick="requeryFromHistory('${item.domain}')">
|
||||
<i class="fa fa-refresh mr-1"></i>重新查询
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}).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() {
|
||||
// 查询按钮事件
|
||||
const queryBtn = document.getElementById('dns-query-btn');
|
||||
if (queryBtn) {
|
||||
queryBtn.addEventListener('click', handleDNSQuery);
|
||||
}
|
||||
|
||||
// 输入框回车键事件
|
||||
const domainInput = document.getElementById('dns-query-domain');
|
||||
if (domainInput) {
|
||||
domainInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleDNSQuery();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 清空历史按钮事件
|
||||
const clearHistoryBtn = document.getElementById('clear-history-btn');
|
||||
if (clearHistoryBtn) {
|
||||
clearHistoryBtn.addEventListener('click', clearQueryHistory);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 显示成功消息
|
||||
function showSuccessMessage(message) {
|
||||
showNotification(message, 'success');
|
||||
}
|
||||
|
||||
// 显示错误消息
|
||||
function showErrorMessage(message) {
|
||||
showNotification(message, 'error');
|
||||
}
|
||||
|
||||
// 显示通知
|
||||
function showNotification(message, type = 'info') {
|
||||
// 移除现有通知
|
||||
const existingNotification = document.querySelector('.notification');
|
||||
if (existingNotification) {
|
||||
existingNotification.remove();
|
||||
}
|
||||
|
||||
// 创建新通知
|
||||
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-all duration-300 ease-in-out translate-y-0 opacity-0`;
|
||||
|
||||
// 设置通知样式
|
||||
if (type === 'success') {
|
||||
notification.classList.add('bg-green-500', 'text-white');
|
||||
} else if (type === 'error') {
|
||||
notification.classList.add('bg-red-500', 'text-white');
|
||||
} else {
|
||||
notification.classList.add('bg-blue-500', 'text-white');
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// 显示通知
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('opacity-0');
|
||||
notification.classList.add('opacity-100');
|
||||
}, 10);
|
||||
|
||||
// 3秒后隐藏通知
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('opacity-100');
|
||||
notification.classList.add('opacity-0');
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 页面加载完成后初始化
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initQueryPage);
|
||||
} else {
|
||||
initQueryPage();
|
||||
}
|
||||
|
||||
// 当切换到DNS查询页面时重新加载数据
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 监听hash变化,当切换到DNS查询页面时重新加载数据
|
||||
window.addEventListener('hashchange', () => {
|
||||
if (window.location.hash === '#query') {
|
||||
initQueryPage();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,305 +0,0 @@
|
||||
// 服务器状态组件 - 显示CPU使用率和查询统计
|
||||
|
||||
// 全局变量
|
||||
let serverStatusUpdateTimer = null;
|
||||
let previousServerData = {
|
||||
cpu: 0,
|
||||
queries: 0
|
||||
};
|
||||
|
||||
// 初始化服务器状态组件
|
||||
function initServerStatusWidget() {
|
||||
// 确保DOM元素存在
|
||||
const widget = document.getElementById('server-status-widget');
|
||||
if (!widget) return;
|
||||
|
||||
// 初始化页面类型检测
|
||||
updateWidgetDisplayByPageType();
|
||||
|
||||
// 设置页面切换事件监听
|
||||
handlePageSwitchEvents();
|
||||
|
||||
// 设置WebSocket监听(如果可用)
|
||||
setupWebSocketListeners();
|
||||
|
||||
// 立即加载一次数据
|
||||
loadServerStatusData();
|
||||
|
||||
// 设置定时更新(每5秒更新一次)
|
||||
serverStatusUpdateTimer = setInterval(loadServerStatusData, 5000);
|
||||
}
|
||||
|
||||
// 判断当前页面是否为仪表盘
|
||||
function isCurrentPageDashboard() {
|
||||
// 方法1:检查侧边栏激活状态
|
||||
const dashboardLink = document.querySelector('.sidebar a[href="#dashboard"]');
|
||||
if (dashboardLink && dashboardLink.classList.contains('active')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 方法2:检查仪表盘特有元素
|
||||
const dashboardElements = [
|
||||
'#dashboard-container',
|
||||
'.dashboard-summary',
|
||||
'#dashboard-stats'
|
||||
];
|
||||
|
||||
for (const selector of dashboardElements) {
|
||||
if (document.querySelector(selector)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 方法3:检查URL哈希值
|
||||
if (window.location.hash === '#dashboard' || window.location.hash === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 根据页面类型更新组件显示
|
||||
function updateWidgetDisplayByPageType() {
|
||||
const additionalStats = document.getElementById('server-additional-stats');
|
||||
if (!additionalStats) return;
|
||||
|
||||
// 如果当前页面是仪表盘,隐藏额外统计指标
|
||||
if (isCurrentPageDashboard()) {
|
||||
additionalStats.classList.add('hidden');
|
||||
} else {
|
||||
// 非仪表盘页面,显示额外统计指标
|
||||
additionalStats.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// 处理页面切换事件
|
||||
function handlePageSwitchEvents() {
|
||||
// 监听哈希变化(导航切换)
|
||||
window.addEventListener('hashchange', updateWidgetDisplayByPageType);
|
||||
|
||||
// 监听侧边栏点击事件
|
||||
const sidebarLinks = document.querySelectorAll('.sidebar a');
|
||||
sidebarLinks.forEach(link => {
|
||||
link.addEventListener('click', function() {
|
||||
// 延迟检查,确保页面已切换
|
||||
setTimeout(updateWidgetDisplayByPageType, 100);
|
||||
});
|
||||
});
|
||||
|
||||
// 监听导航菜单点击事件
|
||||
const navLinks = document.querySelectorAll('nav a');
|
||||
navLinks.forEach(link => {
|
||||
link.addEventListener('click', function() {
|
||||
setTimeout(updateWidgetDisplayByPageType, 100);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 监控WebSocket连接状态
|
||||
function monitorWebSocketConnection() {
|
||||
// 如果存在WebSocket连接,监听消息
|
||||
if (window.socket) {
|
||||
window.socket.addEventListener('message', function(event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.type === 'status_update') {
|
||||
updateServerStatusWidget(data.payload);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析WebSocket消息失败:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 设置WebSocket监听器
|
||||
function setupWebSocketListeners() {
|
||||
// 如果WebSocket已经存在
|
||||
if (window.socket) {
|
||||
monitorWebSocketConnection();
|
||||
} else {
|
||||
// 监听socket初始化事件
|
||||
window.addEventListener('socketInitialized', function() {
|
||||
monitorWebSocketConnection();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 加载服务器状态数据
|
||||
async function loadServerStatusData() {
|
||||
try {
|
||||
// 使用现有的API获取系统状态
|
||||
const api = window.api || {};
|
||||
const getStatusFn = api.getStatus || function() { return Promise.resolve({}); };
|
||||
const statusData = await getStatusFn();
|
||||
if (statusData && !statusData.error) {
|
||||
updateServerStatusWidget(statusData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载服务器状态数据失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新服务器状态组件
|
||||
function updateServerStatusWidget(stats) {
|
||||
// 确保组件存在
|
||||
const widget = document.getElementById('server-status-widget');
|
||||
if (!widget) return;
|
||||
|
||||
// 确保stats存在
|
||||
stats = stats || {};
|
||||
|
||||
// 提取CPU使用率
|
||||
let cpuUsage = 0;
|
||||
if (stats.system && typeof stats.system.cpu === 'number') {
|
||||
cpuUsage = stats.system.cpu;
|
||||
} else if (typeof stats.cpuUsage === 'number') {
|
||||
cpuUsage = stats.cpuUsage;
|
||||
}
|
||||
|
||||
// 提取查询统计数据
|
||||
let totalQueries = 0;
|
||||
let blockedQueries = 0;
|
||||
let allowedQueries = 0;
|
||||
|
||||
if (stats.dns) {
|
||||
const allowed = typeof stats.dns.Allowed === 'number' ? stats.dns.Allowed : 0;
|
||||
const blocked = typeof stats.dns.Blocked === 'number' ? stats.dns.Blocked : 0;
|
||||
const errors = typeof stats.dns.Errors === 'number' ? stats.dns.Errors : 0;
|
||||
totalQueries = allowed + blocked + errors;
|
||||
blockedQueries = blocked;
|
||||
allowedQueries = allowed;
|
||||
} else {
|
||||
totalQueries = typeof stats.totalQueries === 'number' ? stats.totalQueries : 0;
|
||||
blockedQueries = typeof stats.blockedQueries === 'number' ? stats.blockedQueries : 0;
|
||||
allowedQueries = typeof stats.allowedQueries === 'number' ? stats.allowedQueries : 0;
|
||||
}
|
||||
|
||||
// 更新CPU使用率
|
||||
const cpuValueElement = document.getElementById('server-cpu-value');
|
||||
if (cpuValueElement) {
|
||||
cpuValueElement.textContent = cpuUsage.toFixed(1) + '%';
|
||||
}
|
||||
|
||||
const cpuBarElement = document.getElementById('server-cpu-bar');
|
||||
if (cpuBarElement) {
|
||||
cpuBarElement.style.width = Math.min(cpuUsage, 100) + '%';
|
||||
|
||||
// 根据CPU使用率改变颜色
|
||||
if (cpuUsage > 80) {
|
||||
cpuBarElement.className = 'h-full bg-danger rounded-full';
|
||||
} else if (cpuUsage > 50) {
|
||||
cpuBarElement.className = 'h-full bg-warning rounded-full';
|
||||
} else {
|
||||
cpuBarElement.className = 'h-full bg-success rounded-full';
|
||||
}
|
||||
}
|
||||
|
||||
// 更新查询量
|
||||
const queriesValueElement = document.getElementById('server-queries-value');
|
||||
if (queriesValueElement) {
|
||||
queriesValueElement.textContent = formatNumber(totalQueries);
|
||||
}
|
||||
|
||||
// 计算查询量百分比(假设最大查询量为10000)
|
||||
const queryPercentage = Math.min((totalQueries / 10000) * 100, 100);
|
||||
const queriesBarElement = document.getElementById('server-queries-bar');
|
||||
if (queriesBarElement) {
|
||||
queriesBarElement.style.width = queryPercentage + '%';
|
||||
}
|
||||
|
||||
// 更新额外统计指标
|
||||
const totalQueriesElement = document.getElementById('server-total-queries');
|
||||
if (totalQueriesElement) {
|
||||
totalQueriesElement.textContent = formatNumber(totalQueries);
|
||||
}
|
||||
|
||||
const blockedQueriesElement = document.getElementById('server-blocked-queries');
|
||||
if (blockedQueriesElement) {
|
||||
blockedQueriesElement.textContent = formatNumber(blockedQueries);
|
||||
}
|
||||
|
||||
const allowedQueriesElement = document.getElementById('server-allowed-queries');
|
||||
if (allowedQueriesElement) {
|
||||
allowedQueriesElement.textContent = formatNumber(allowedQueries);
|
||||
}
|
||||
|
||||
// 添加光晕提示效果
|
||||
if (previousServerData.cpu !== cpuUsage || previousServerData.queries !== totalQueries) {
|
||||
addGlowEffect();
|
||||
}
|
||||
|
||||
// 更新服务器状态指示器
|
||||
const statusIndicator = document.getElementById('server-status-indicator');
|
||||
if (statusIndicator) {
|
||||
// 检查系统状态
|
||||
if (stats.system && stats.system.status === 'error') {
|
||||
statusIndicator.className = 'inline-block w-2 h-2 bg-danger rounded-full';
|
||||
} else {
|
||||
statusIndicator.className = 'inline-block w-2 h-2 bg-success rounded-full';
|
||||
}
|
||||
}
|
||||
|
||||
// 保存当前数据用于下次比较
|
||||
previousServerData = {
|
||||
cpu: cpuUsage,
|
||||
queries: totalQueries
|
||||
};
|
||||
}
|
||||
|
||||
// 添加光晕提示效果
|
||||
function addGlowEffect() {
|
||||
const widget = document.getElementById('server-status-widget');
|
||||
if (!widget) return;
|
||||
|
||||
// 添加光晕类
|
||||
widget.classList.add('glow-effect');
|
||||
|
||||
// 2秒后移除光晕
|
||||
setTimeout(function() {
|
||||
widget.classList.remove('glow-effect');
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// 格式化数字
|
||||
function formatNumber(num) {
|
||||
// 显示完整数字的最大长度阈值
|
||||
const MAX_FULL_LENGTH = 5;
|
||||
|
||||
// 先获取完整数字字符串
|
||||
const fullNumStr = num.toString();
|
||||
|
||||
// 如果数字长度小于等于阈值,直接返回完整数字
|
||||
if (fullNumStr.length <= MAX_FULL_LENGTH) {
|
||||
return fullNumStr;
|
||||
}
|
||||
|
||||
// 否则使用缩写格式
|
||||
if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1) + 'M';
|
||||
} else if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1) + 'K';
|
||||
}
|
||||
|
||||
return fullNumStr;
|
||||
}
|
||||
|
||||
// 在DOM加载完成后初始化
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
// 延迟初始化,确保页面完全加载
|
||||
setTimeout(initServerStatusWidget, 500);
|
||||
});
|
||||
|
||||
// 在页面卸载时清理资源
|
||||
window.addEventListener('beforeunload', function() {
|
||||
if (serverStatusUpdateTimer) {
|
||||
clearInterval(serverStatusUpdateTimer);
|
||||
serverStatusUpdateTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 导出函数供其他模块使用
|
||||
window.serverStatusWidget = {
|
||||
init: initServerStatusWidget,
|
||||
update: updateServerStatusWidget
|
||||
};
|
||||
1302
js/shield.js
1302
js/shield.js
File diff suppressed because it is too large
Load Diff
1
js/vendor/chart.umd.min.js
vendored
1
js/vendor/chart.umd.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,8 +1,10 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -15,7 +17,7 @@ var (
|
||||
)
|
||||
|
||||
// InitLogger 初始化日志系统
|
||||
func InitLogger(logFile, level string, maxSize, maxBackups, maxAge int) error {
|
||||
func InitLogger(logFile, level string, maxSize, maxBackups, maxAge int, _ bool) error {
|
||||
logMutex.Lock()
|
||||
defer logMutex.Unlock()
|
||||
|
||||
@@ -30,21 +32,42 @@ func InitLogger(logFile, level string, maxSize, maxBackups, maxAge int) error {
|
||||
FullTimestamp: true,
|
||||
})
|
||||
|
||||
// 创建日志目录
|
||||
if logFile != "" {
|
||||
logDir := filepath.Dir(logFile)
|
||||
if logDir != "." {
|
||||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||
return fmt.Errorf("创建日志目录失败: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置输出目标
|
||||
outputTargets := []io.Writer{}
|
||||
|
||||
if logFile != "" {
|
||||
// 使用标准库打开文件,支持追加写入
|
||||
file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
logrus.Warn("无法打开日志文件,将使用标准输出", "error", err)
|
||||
log.SetOutput(os.Stdout)
|
||||
// 无法打开日志文件时,回退到标准输出
|
||||
log.Println("无法打开日志文件,将使用标准输出:", err)
|
||||
} else {
|
||||
// 同时输出到文件和标准输出
|
||||
log.SetOutput(io.MultiWriter(file, os.Stdout))
|
||||
outputTargets = append(outputTargets, file)
|
||||
defer file.Sync() // 确保日志内容被写入磁盘
|
||||
}
|
||||
} else {
|
||||
log.SetOutput(os.Stdout)
|
||||
}
|
||||
|
||||
// 无论是否指定日志文件,都同时输出到标准输出
|
||||
if len(outputTargets) > 0 {
|
||||
outputTargets = append(outputTargets, os.Stdout)
|
||||
} else {
|
||||
// 如果没有指定日志文件,仅使用标准输出
|
||||
outputTargets = append(outputTargets, os.Stdout)
|
||||
}
|
||||
|
||||
// 设置日志输出
|
||||
log.SetOutput(io.MultiWriter(outputTargets...))
|
||||
|
||||
// 设置日志级别
|
||||
logLevel, err := logrus.ParseLevel(level)
|
||||
if err != nil {
|
||||
@@ -61,13 +84,20 @@ func Close() {
|
||||
logMutex.Lock()
|
||||
defer logMutex.Unlock()
|
||||
|
||||
if !initialized {
|
||||
if !initialized || log == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 执行日志刷新
|
||||
log.Warn("日志系统已关闭")
|
||||
|
||||
// 确保日志被写入磁盘
|
||||
if loggerOutput, ok := log.Out.(*os.File); ok {
|
||||
loggerOutput.Sync()
|
||||
}
|
||||
|
||||
initialized = false
|
||||
log = nil
|
||||
}
|
||||
|
||||
// Info 记录信息级别日志
|
||||
@@ -122,6 +152,20 @@ func Warn(msg string, fields ...interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
// Fatal 记录致命级别日志并退出程序
|
||||
func Fatal(msg string, fields ...interface{}) {
|
||||
if !initialized {
|
||||
// 如果日志系统未初始化,使用标准库log
|
||||
log.Fatal(msg)
|
||||
}
|
||||
|
||||
if len(fields) > 0 {
|
||||
log.WithFields(toFields(fields)).Fatal(msg)
|
||||
} else {
|
||||
log.Fatal(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// toFields 将键值对转换为logrus字段
|
||||
func toFields(keyValues []interface{}) logrus.Fields {
|
||||
fields := make(logrus.Fields)
|
||||
|
||||
158358
logs/dns-server.log
158358
logs/dns-server.log
File diff suppressed because it is too large
Load Diff
58
main.go
58
main.go
@@ -42,9 +42,11 @@ func createDefaultConfig(configFile string) error {
|
||||
"saveInterval": 300
|
||||
},
|
||||
"http": {
|
||||
"port": 8081,
|
||||
"port": 8080,
|
||||
"host": "0.0.0.0",
|
||||
"enableAPI": true
|
||||
"enableAPI": true,
|
||||
"username": "admin",
|
||||
"password": "admin"
|
||||
},
|
||||
"shield": {
|
||||
"localRulesFile": "data/rules.txt",
|
||||
@@ -146,20 +148,9 @@ func createRequiredFiles(cfg *config.Config) error {
|
||||
func main() {
|
||||
// 命令行参数解析
|
||||
var configFile string
|
||||
var daemonMode bool
|
||||
flag.StringVar(&configFile, "config", "config.json", "配置文件路径")
|
||||
flag.BoolVar(&daemonMode, "daemon", false, "以守护进程模式运行")
|
||||
flag.Parse()
|
||||
|
||||
// 如果是守护进程模式,创建守护进程
|
||||
if daemonMode {
|
||||
if err := daemonize(); err != nil {
|
||||
log.Fatalf("创建守护进程失败: %v", err)
|
||||
}
|
||||
// 父进程退出
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// 检查配置文件是否存在,如果不存在则创建默认配置文件
|
||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||
log.Printf("配置文件 %s 不存在,正在创建默认配置文件...", configFile)
|
||||
@@ -185,7 +176,7 @@ func main() {
|
||||
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, false); err != nil {
|
||||
log.Fatalf("初始化日志系统失败: %v", err)
|
||||
}
|
||||
defer logger.Close()
|
||||
@@ -225,45 +216,10 @@ func main() {
|
||||
<-sigCh
|
||||
|
||||
// 清理资源
|
||||
log.Println("正在关闭服务...")
|
||||
logger.Info("正在关闭服务...")
|
||||
dnsServer.Stop()
|
||||
httpServer.Stop()
|
||||
shieldManager.StopAutoUpdate()
|
||||
// 守护进程模式下不需要删除PID文件
|
||||
|
||||
log.Println("服务已关闭")
|
||||
}
|
||||
|
||||
// daemonize 创建守护进程
|
||||
func daemonize() error {
|
||||
// 使用更简单的方式创建守护进程:直接在当前进程中进行守护化处理
|
||||
// 1. 重定向标准输入、输出、错误
|
||||
nullFile, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开/dev/null失败: %w", err)
|
||||
}
|
||||
defer nullFile.Close()
|
||||
|
||||
// 重定向文件描述符
|
||||
err = syscall.Dup2(int(nullFile.Fd()), int(os.Stdin.Fd()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("重定向stdin失败: %w", err)
|
||||
}
|
||||
err = syscall.Dup2(int(nullFile.Fd()), int(os.Stdout.Fd()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("重定向stdout失败: %w", err)
|
||||
}
|
||||
err = syscall.Dup2(int(nullFile.Fd()), int(os.Stderr.Fd()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("重定向stderr失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 创建新的会话和进程组
|
||||
_, err = syscall.Setsid()
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建新会话失败: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("守护进程已启动")
|
||||
return nil
|
||||
logger.Info("服务已关闭")
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -59,6 +59,12 @@
|
||||
<span>DNS屏蔽查询</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#logs" class="flex items-center px-4 py-3 text-gray-700 hover:bg-gray-100 rounded-md transition-all">
|
||||
<i class="fa fa-file-text-o mr-3 text-lg"></i>
|
||||
<span>查询日志</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#config" class="flex items-center px-4 py-3 text-gray-700 hover:bg-gray-100 rounded-md transition-all">
|
||||
<i class="fa fa-cog mr-3 text-lg"></i>
|
||||
@@ -143,9 +149,22 @@
|
||||
<button class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100">
|
||||
<i class="fa fa-bell text-lg"></i>
|
||||
</button>
|
||||
<div class="flex items-center">
|
||||
<img src="https://picsum.photos/id/1005/40/40" alt="用户头像" class="w-8 h-8 rounded-full">
|
||||
<span class="ml-2 hidden md:block">管理员</span>
|
||||
<!-- 账户下拉菜单 -->
|
||||
<div class="relative group" id="account-dropdown">
|
||||
<button class="flex items-center p-2 rounded-full hover:bg-gray-100 transition-colors focus:outline-none">
|
||||
<img src="https://picsum.photos/id/1005/40/40" alt="用户头像" class="w-8 h-8 rounded-full">
|
||||
<span class="ml-2 hidden md:block">管理员</span>
|
||||
<i class="fa fa-caret-down ml-1 text-xs hidden md:block"></i>
|
||||
</button>
|
||||
<!-- 下拉菜单 -->
|
||||
<div class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg py-2 z-50 hidden group-hover:block" id="account-menu">
|
||||
<button id="change-password-btn" class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors">
|
||||
<i class="fa fa-key mr-2"></i>修改密码
|
||||
</button>
|
||||
<button id="logout-btn" class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors">
|
||||
<i class="fa fa-sign-out mr-2"></i>注销
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -762,6 +781,195 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 查询日志页面 -->
|
||||
<div id="logs-content" class="hidden space-y-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-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="relative z-10">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-gray-500 font-medium">总查询数</h3>
|
||||
<div class="p-2 rounded-full bg-primary/10 text-primary">
|
||||
<i class="fa fa-refresh"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<div class="flex items-end justify-between">
|
||||
<p class="text-3xl font-bold" id="logs-total-queries">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 平均响应时间 -->
|
||||
<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="relative z-10">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-gray-500 font-medium">平均响应时间</h3>
|
||||
<div class="p-2 rounded-full bg-info/10 text-info">
|
||||
<i class="fa fa-clock-o"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<div class="flex items-end justify-between">
|
||||
<p class="text-3xl font-bold" id="logs-avg-response-time">0ms</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 活跃来源IP -->
|
||||
<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="relative z-10">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-gray-500 font-medium">活跃来源IP</h3>
|
||||
<div class="p-2 rounded-full bg-success/10 text-success">
|
||||
<i class="fa fa-globe"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<div class="flex items-end justify-between">
|
||||
<p class="text-3xl font-bold" id="logs-active-ips">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 屏蔽率 -->
|
||||
<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="relative z-10">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-gray-500 font-medium">屏蔽率</h3>
|
||||
<div class="p-2 rounded-full bg-danger/10 text-danger">
|
||||
<i class="fa fa-ban"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<div class="flex items-end justify-between">
|
||||
<p class="text-3xl font-bold" id="logs-block-rate">0%</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日志搜索和过滤 -->
|
||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
||||
<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="logs-search" placeholder="搜索域名或客户端IP" 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>
|
||||
<div class="w-32">
|
||||
<select id="logs-result-filter" 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">
|
||||
<option value="">全部结果</option>
|
||||
<option value="allowed">允许</option>
|
||||
<option value="blocked">屏蔽</option>
|
||||
<option value="error">错误</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="w-32">
|
||||
<select id="logs-per-page" 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">
|
||||
<option value="10">10条/页</option>
|
||||
<option value="20">20条/页</option>
|
||||
<option value="30" selected>30条/页</option>
|
||||
<option value="50">50条/页</option>
|
||||
<option value="100">100条/页</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="logs-search-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 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>
|
||||
<div class="flex space-x-2">
|
||||
<button class="time-range-btn px-4 py-2 rounded-md bg-primary text-white" data-range="24h">24小时</button>
|
||||
<button class="time-range-btn px-4 py-2 rounded-md bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors" data-range="7d">7天</button>
|
||||
<button class="time-range-btn px-4 py-2 rounded-md bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors" data-range="30d">30天</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-64">
|
||||
<canvas id="logs-trend-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日志详情表格 -->
|
||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center">
|
||||
<h3 class="text-lg font-semibold">查询日志详情</h3>
|
||||
<button id="logs-refresh-btn" class="ml-3 p-2 text-gray-500 hover:text-primary hover:bg-gray-100 rounded-full transition-colors" title="刷新日志">
|
||||
<i class="fa fa-refresh"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="logs-loading" class="flex items-center text-sm text-gray-500 hidden">
|
||||
<i class="fa fa-spinner fa-spin mr-2"></i>
|
||||
<span>加载中...</span>
|
||||
</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 cursor-pointer hover:text-primary transition-colors" data-sort="time">
|
||||
<div class="flex items-center">
|
||||
时间
|
||||
<i class="fa fa-sort ml-1 text-xs"></i>
|
||||
</div>
|
||||
</th>
|
||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500 cursor-pointer hover:text-primary transition-colors" data-sort="clientIp">
|
||||
<div class="flex items-center">
|
||||
客户端IP
|
||||
<i class="fa fa-sort ml-1 text-xs"></i>
|
||||
</div>
|
||||
</th>
|
||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500 cursor-pointer hover:text-primary transition-colors" data-sort="domain">
|
||||
<div class="flex items-center">
|
||||
请求
|
||||
<i class="fa fa-sort ml-1 text-xs"></i>
|
||||
</div>
|
||||
</th>
|
||||
<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">屏蔽规则</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="logs-table-body">
|
||||
<tr>
|
||||
<td colspan="5" class="py-8 text-center text-gray-500 border-b border-gray-100">
|
||||
<i class="fa fa-file-text-o text-4xl mb-2 text-gray-300"></i>
|
||||
<div>暂无查询日志</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="flex items-center justify-between mt-6">
|
||||
<div class="text-sm text-gray-500">
|
||||
显示 <span id="logs-current-page">1</span> / <span id="logs-total-pages">1</span> 页
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button id="logs-prev-page" class="px-4 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" disabled>
|
||||
<i class="fa fa-chevron-left"></i>
|
||||
</button>
|
||||
<button id="logs-next-page" class="px-4 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" disabled>
|
||||
<i class="fa fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="config-content" class="hidden">
|
||||
<!-- 系统设置页面内容 -->
|
||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
||||
@@ -852,6 +1060,41 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 修改密码模态框 -->
|
||||
<div id="change-password-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex items-center justify-center">
|
||||
<div class="bg-white rounded-lg shadow-xl w-full max-w-md p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-xl font-semibold">修改密码</h3>
|
||||
<button id="close-modal-btn" class="text-gray-500 hover:text-gray-700 focus:outline-none">
|
||||
<i class="fa fa-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form id="change-password-form">
|
||||
<div class="mb-4">
|
||||
<label for="current-password" class="block text-sm font-medium text-gray-700 mb-1">当前密码</label>
|
||||
<input type="password" id="current-password" name="currentPassword" 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" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="new-password" class="block text-sm font-medium text-gray-700 mb-1">新密码</label>
|
||||
<input type="password" id="new-password" name="newPassword" 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" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="confirm-password" class="block text-sm font-medium text-gray-700 mb-1">确认密码</label>
|
||||
<input type="password" id="confirm-password" name="confirmPassword" 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" required>
|
||||
<div id="password-mismatch" class="text-danger text-sm mt-1 hidden">新密码和确认密码不匹配</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end space-x-3">
|
||||
<button type="button" id="cancel-change-password" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition-colors">取消</button>
|
||||
<button type="submit" id="save-password-btn" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors">保存</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 脚本 -->
|
||||
<script src="js/main.js"></script>
|
||||
<script src="js/api.js"></script>
|
||||
@@ -860,6 +1103,7 @@
|
||||
<script src="js/shield.js"></script>
|
||||
<script src="js/hosts.js"></script>
|
||||
<script src="js/query.js"></script>
|
||||
<script src="js/logs.js"></script>
|
||||
<script src="js/config.js"></script>
|
||||
|
||||
<!-- 直接渲染滚动列表的静态HTML内容 -->
|
||||
|
||||
@@ -38,6 +38,13 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
|
||||
// 优化错误响应处理
|
||||
console.warn(`API请求失败: ${response.status}`);
|
||||
|
||||
// 处理401未授权错误,重定向到登录页面
|
||||
if (response.status === 401) {
|
||||
console.warn('未授权访问,重定向到登录页面');
|
||||
window.location.href = '/login';
|
||||
return { error: '未授权访问' };
|
||||
}
|
||||
|
||||
// 尝试解析JSON,但如果失败,直接使用原始文本作为错误信息
|
||||
try {
|
||||
const errorData = JSON.parse(responseText);
|
||||
|
||||
@@ -6,12 +6,18 @@ let dnsRequestsChart = null;
|
||||
let detailedDnsRequestsChart = null; // 详细DNS请求趋势图表(浮窗)
|
||||
let queryTypeChart = null; // 解析类型统计饼图
|
||||
let intervalId = null;
|
||||
let wsConnection = null;
|
||||
let wsReconnectTimer = null;
|
||||
let dashboardWsConnection = null;
|
||||
let dashboardWsReconnectTimer = null;
|
||||
// 存储统计卡片图表实例
|
||||
let statCardCharts = {};
|
||||
// 存储统计卡片历史数据
|
||||
let statCardHistoryData = {};
|
||||
// 存储仪表盘历史数据,用于计算趋势
|
||||
window.dashboardHistoryData = window.dashboardHistoryData || {
|
||||
prevResponseTime: null,
|
||||
prevActiveIPs: null,
|
||||
prevTopQueryTypeCount: null
|
||||
};
|
||||
|
||||
// 引入颜色配置文件
|
||||
const COLOR_CONFIG = window.COLOR_CONFIG || {};
|
||||
@@ -53,22 +59,22 @@ function connectWebSocket() {
|
||||
console.log('正在连接WebSocket:', wsUrl);
|
||||
|
||||
// 创建WebSocket连接
|
||||
wsConnection = new WebSocket(wsUrl);
|
||||
dashboardWsConnection = new WebSocket(wsUrl);
|
||||
|
||||
// 连接打开事件
|
||||
wsConnection.onopen = function() {
|
||||
dashboardWsConnection.onopen = function() {
|
||||
console.log('WebSocket连接已建立');
|
||||
showNotification('数据更新成功', 'success');
|
||||
|
||||
// 清除重连计时器
|
||||
if (wsReconnectTimer) {
|
||||
clearTimeout(wsReconnectTimer);
|
||||
wsReconnectTimer = null;
|
||||
if (dashboardWsReconnectTimer) {
|
||||
clearTimeout(dashboardWsReconnectTimer);
|
||||
dashboardWsReconnectTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 接收消息事件
|
||||
wsConnection.onmessage = function(event) {
|
||||
dashboardWsConnection.onmessage = function(event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
@@ -82,16 +88,16 @@ function connectWebSocket() {
|
||||
};
|
||||
|
||||
// 连接关闭事件
|
||||
wsConnection.onclose = function(event) {
|
||||
dashboardWsConnection.onclose = function(event) {
|
||||
console.warn('WebSocket连接已关闭,代码:', event.code);
|
||||
wsConnection = null;
|
||||
dashboardWsConnection = null;
|
||||
|
||||
// 设置重连
|
||||
setupReconnect();
|
||||
};
|
||||
|
||||
// 连接错误事件
|
||||
wsConnection.onerror = function(error) {
|
||||
dashboardWsConnection.onerror = function(error) {
|
||||
console.error('WebSocket连接错误:', error);
|
||||
};
|
||||
|
||||
@@ -104,14 +110,14 @@ function connectWebSocket() {
|
||||
|
||||
// 设置重连逻辑
|
||||
function setupReconnect() {
|
||||
if (wsReconnectTimer) {
|
||||
if (dashboardWsReconnectTimer) {
|
||||
return; // 已经有重连计时器在运行
|
||||
}
|
||||
|
||||
const reconnectDelay = 5000; // 5秒后重连
|
||||
console.log(`将在${reconnectDelay}ms后尝试重新连接WebSocket`);
|
||||
|
||||
wsReconnectTimer = setTimeout(() => {
|
||||
dashboardWsReconnectTimer = setTimeout(() => {
|
||||
connectWebSocket();
|
||||
}, reconnectDelay);
|
||||
}
|
||||
@@ -192,14 +198,42 @@ function processRealTimeData(stats) {
|
||||
|
||||
if (document.getElementById('top-query-type')) {
|
||||
const queryType = stats.topQueryType || '---';
|
||||
document.getElementById('top-query-type').textContent = queryType;
|
||||
|
||||
const queryPercentElem = document.getElementById('query-type-percentage');
|
||||
if (queryPercentElem) {
|
||||
queryPercentElem.textContent = '• ---';
|
||||
queryPercentElem.className = 'text-sm flex items-center text-gray-500';
|
||||
// 计算查询类型趋势
|
||||
let queryPercent = '---';
|
||||
let trendClass = 'text-gray-400';
|
||||
let trendIcon = '---';
|
||||
|
||||
if (stats.topQueryTypeCount !== undefined && stats.topQueryTypeCount !== null) {
|
||||
// 存储当前值用于下次计算趋势
|
||||
const prevTopQueryTypeCount = window.dashboardHistoryData.prevTopQueryTypeCount || stats.topQueryTypeCount;
|
||||
window.dashboardHistoryData.prevTopQueryTypeCount = stats.topQueryTypeCount;
|
||||
|
||||
// 计算变化百分比
|
||||
if (prevTopQueryTypeCount > 0) {
|
||||
const changePercent = ((stats.topQueryTypeCount - prevTopQueryTypeCount) / prevTopQueryTypeCount) * 100;
|
||||
queryPercent = Math.abs(changePercent).toFixed(1) + '%';
|
||||
|
||||
// 设置趋势图标和颜色
|
||||
if (changePercent > 0) {
|
||||
trendIcon = '↑';
|
||||
trendClass = 'text-primary';
|
||||
} else if (changePercent < 0) {
|
||||
trendIcon = '↓';
|
||||
trendClass = 'text-secondary';
|
||||
} else {
|
||||
trendIcon = '•';
|
||||
trendClass = 'text-gray-500';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
queryPercentElem.textContent = trendIcon + ' ' + queryPercent;
|
||||
queryPercentElem.className = `text-sm flex items-center ${trendClass}`;
|
||||
}
|
||||
|
||||
document.getElementById('top-query-type').textContent = queryType;
|
||||
}
|
||||
|
||||
if (document.getElementById('active-ips')) {
|
||||
@@ -362,15 +396,15 @@ function fallbackToIntervalRefresh() {
|
||||
// 清理资源
|
||||
function cleanupResources() {
|
||||
// 清除WebSocket连接
|
||||
if (wsConnection) {
|
||||
wsConnection.close();
|
||||
wsConnection = null;
|
||||
if (dashboardWsConnection) {
|
||||
dashboardWsConnection.close();
|
||||
dashboardWsConnection = null;
|
||||
}
|
||||
|
||||
// 清除重连计时器
|
||||
if (wsReconnectTimer) {
|
||||
clearTimeout(wsReconnectTimer);
|
||||
wsReconnectTimer = null;
|
||||
if (dashboardWsReconnectTimer) {
|
||||
clearTimeout(dashboardWsReconnectTimer);
|
||||
dashboardWsReconnectTimer = null;
|
||||
}
|
||||
|
||||
// 清除定时刷新
|
||||
@@ -797,17 +831,20 @@ function updateStatsCards(stats) {
|
||||
const originalStyle = element.getAttribute('style') || '';
|
||||
|
||||
try {
|
||||
// 复制原始元素的样式到新元素,确保大小完全一致
|
||||
const computedStyle = getComputedStyle(element);
|
||||
|
||||
// 配置翻页容器样式,确保与原始元素大小完全一致
|
||||
const containerStyle =
|
||||
'position: relative; '
|
||||
+ 'display: ' + computedStyle.display + '; '
|
||||
+ 'overflow: hidden; '
|
||||
+ 'height: ' + element.offsetHeight + 'px; '
|
||||
+ 'width: ' + element.offsetWidth + 'px; '
|
||||
+ 'margin: ' + computedStyle.margin + '; '
|
||||
+ 'padding: ' + computedStyle.padding + '; '
|
||||
+ 'box-sizing: ' + computedStyle.boxSizing + '; '
|
||||
+ 'line-height: ' + computedStyle.lineHeight + ';';
|
||||
'position: relative; ' +
|
||||
'display: ' + computedStyle.display + '; ' +
|
||||
'overflow: hidden; ' +
|
||||
'height: ' + element.offsetHeight + 'px; ' +
|
||||
'width: ' + element.offsetWidth + 'px; ' +
|
||||
'margin: ' + computedStyle.margin + '; ' +
|
||||
'padding: ' + computedStyle.padding + '; ' +
|
||||
'box-sizing: ' + computedStyle.boxSizing + '; ' +
|
||||
'line-height: ' + computedStyle.lineHeight + ';';
|
||||
|
||||
// 创建翻页容器
|
||||
const flipContainer = document.createElement('div');
|
||||
@@ -844,9 +881,6 @@ function updateStatsCards(stats) {
|
||||
'transition: transform 400ms ease-in-out; ' +
|
||||
'transform-origin: center; ' +
|
||||
'transform: translateY(100%);';
|
||||
|
||||
// 复制原始元素的样式到新元素,确保大小完全一致
|
||||
const computedStyle = getComputedStyle(element);
|
||||
[oldValueElement, newValueElement].forEach(el => {
|
||||
el.style.fontSize = computedStyle.fontSize;
|
||||
el.style.fontWeight = computedStyle.fontWeight;
|
||||
|
||||
@@ -9,6 +9,7 @@ function setupNavigation() {
|
||||
document.getElementById('shield-content'),
|
||||
document.getElementById('hosts-content'),
|
||||
document.getElementById('query-content'),
|
||||
document.getElementById('logs-content'),
|
||||
document.getElementById('config-content')
|
||||
];
|
||||
const pageTitle = document.getElementById('page-title');
|
||||
@@ -21,14 +22,6 @@ function setupNavigation() {
|
||||
if (window.innerWidth < 768) {
|
||||
closeSidebar();
|
||||
}
|
||||
|
||||
// 页面特定初始化 - 保留这部分逻辑,因为它不会与hashchange事件处理逻辑冲突
|
||||
const target = item.getAttribute('href').substring(1);
|
||||
if (target === 'shield' && typeof initShieldPage === 'function') {
|
||||
initShieldPage();
|
||||
} else if (target === 'hosts' && typeof initHostsPage === 'function') {
|
||||
initHostsPage();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -116,15 +109,84 @@ function setupNavigation() {
|
||||
});
|
||||
}
|
||||
|
||||
// 页面初始化函数 - 根据当前hash值初始化对应页面
|
||||
function initPageByHash() {
|
||||
const hash = window.location.hash.substring(1);
|
||||
|
||||
// 隐藏所有内容区域
|
||||
const contentSections = [
|
||||
document.getElementById('dashboard-content'),
|
||||
document.getElementById('shield-content'),
|
||||
document.getElementById('hosts-content'),
|
||||
document.getElementById('query-content'),
|
||||
document.getElementById('logs-content'),
|
||||
document.getElementById('config-content')
|
||||
];
|
||||
|
||||
contentSections.forEach(section => {
|
||||
if (section) {
|
||||
section.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// 显示当前页面内容
|
||||
const currentSection = document.getElementById(`${hash}-content`);
|
||||
if (currentSection) {
|
||||
currentSection.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 更新页面标题
|
||||
const pageTitle = document.getElementById('page-title');
|
||||
if (pageTitle) {
|
||||
const titles = {
|
||||
'dashboard': '仪表盘',
|
||||
'shield': '屏蔽管理',
|
||||
'hosts': 'Hosts管理',
|
||||
'query': 'DNS屏蔽查询',
|
||||
'logs': '查询日志',
|
||||
'config': '系统设置'
|
||||
};
|
||||
pageTitle.textContent = titles[hash] || '仪表盘';
|
||||
}
|
||||
|
||||
// 页面特定初始化 - 使用setTimeout延迟调用,确保所有脚本文件都已加载完成
|
||||
if (hash === 'shield') {
|
||||
setTimeout(() => {
|
||||
if (typeof initShieldPage === 'function') {
|
||||
initShieldPage();
|
||||
}
|
||||
}, 0);
|
||||
} else if (hash === 'hosts') {
|
||||
setTimeout(() => {
|
||||
if (typeof initHostsPage === 'function') {
|
||||
initHostsPage();
|
||||
}
|
||||
}, 0);
|
||||
} else if (hash === 'logs') {
|
||||
setTimeout(() => {
|
||||
if (typeof initLogsPage === 'function') {
|
||||
initLogsPage();
|
||||
}
|
||||
}, 0);
|
||||
} else if (hash === 'dashboard') {
|
||||
setTimeout(() => {
|
||||
if (typeof loadDashboardData === 'function') {
|
||||
loadDashboardData();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化函数
|
||||
function init() {
|
||||
// 设置导航
|
||||
setupNavigation();
|
||||
|
||||
// 加载仪表盘数据
|
||||
if (typeof loadDashboardData === 'function') {
|
||||
loadDashboardData();
|
||||
}
|
||||
// 初始化页面
|
||||
initPageByHash();
|
||||
|
||||
// 添加hashchange事件监听,处理浏览器前进/后退按钮
|
||||
window.addEventListener('hashchange', initPageByHash);
|
||||
|
||||
// 定期更新系统状态
|
||||
setInterval(updateSystemStatus, 5000);
|
||||
@@ -169,5 +231,175 @@ function formatUptime(milliseconds) {
|
||||
}
|
||||
}
|
||||
|
||||
// 账户功能 - 下拉菜单、注销和修改密码
|
||||
function setupAccountFeatures() {
|
||||
// 下拉菜单功能
|
||||
const accountDropdown = document.getElementById('account-dropdown');
|
||||
const accountMenu = document.getElementById('account-menu');
|
||||
const changePasswordBtn = document.getElementById('change-password-btn');
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
const changePasswordModal = document.getElementById('change-password-modal');
|
||||
const closeModalBtn = document.getElementById('close-modal-btn');
|
||||
const cancelChangePasswordBtn = document.getElementById('cancel-change-password');
|
||||
const changePasswordForm = document.getElementById('change-password-form');
|
||||
const passwordMismatch = document.getElementById('password-mismatch');
|
||||
const newPassword = document.getElementById('new-password');
|
||||
const confirmPassword = document.getElementById('confirm-password');
|
||||
|
||||
// 点击外部关闭下拉菜单
|
||||
document.addEventListener('click', (e) => {
|
||||
if (accountDropdown && !accountDropdown.contains(e.target)) {
|
||||
accountMenu.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// 点击账户区域切换下拉菜单
|
||||
if (accountDropdown) {
|
||||
accountDropdown.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
accountMenu.classList.toggle('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
// 打开修改密码模态框
|
||||
if (changePasswordBtn) {
|
||||
changePasswordBtn.addEventListener('click', () => {
|
||||
accountMenu.classList.add('hidden');
|
||||
changePasswordModal.classList.remove('hidden');
|
||||
document.body.style.overflow = 'hidden';
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭修改密码模态框
|
||||
function closeModal() {
|
||||
changePasswordModal.classList.add('hidden');
|
||||
document.body.style.overflow = '';
|
||||
changePasswordForm.reset();
|
||||
passwordMismatch.classList.add('hidden');
|
||||
}
|
||||
|
||||
// 绑定关闭模态框事件
|
||||
if (closeModalBtn) {
|
||||
closeModalBtn.addEventListener('click', closeModal);
|
||||
}
|
||||
|
||||
if (cancelChangePasswordBtn) {
|
||||
cancelChangePasswordBtn.addEventListener('click', closeModal);
|
||||
}
|
||||
|
||||
// 点击模态框外部关闭模态框
|
||||
if (changePasswordModal) {
|
||||
changePasswordModal.addEventListener('click', (e) => {
|
||||
if (e.target === changePasswordModal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 按ESC键关闭模态框
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && !changePasswordModal.classList.contains('hidden')) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// 密码匹配验证
|
||||
if (newPassword && confirmPassword) {
|
||||
confirmPassword.addEventListener('input', () => {
|
||||
if (newPassword.value !== confirmPassword.value) {
|
||||
passwordMismatch.classList.remove('hidden');
|
||||
} else {
|
||||
passwordMismatch.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
newPassword.addEventListener('input', () => {
|
||||
if (newPassword.value !== confirmPassword.value) {
|
||||
passwordMismatch.classList.remove('hidden');
|
||||
} else {
|
||||
passwordMismatch.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 修改密码表单提交
|
||||
if (changePasswordForm) {
|
||||
changePasswordForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// 验证密码匹配
|
||||
if (newPassword.value !== confirmPassword.value) {
|
||||
passwordMismatch.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData(changePasswordForm);
|
||||
const data = {
|
||||
currentPassword: formData.get('currentPassword'),
|
||||
newPassword: formData.get('newPassword')
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/change-password', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.status === 'success') {
|
||||
// 密码修改成功
|
||||
alert('密码修改成功');
|
||||
closeModal();
|
||||
} else {
|
||||
// 密码修改失败
|
||||
alert(result.error || '密码修改失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('修改密码失败:', error);
|
||||
alert('修改密码失败,请稍后重试');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 注销功能
|
||||
if (logoutBtn) {
|
||||
logoutBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
await fetch('/api/logout', {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
// 重定向到登录页面
|
||||
window.location.href = '/login';
|
||||
} catch (error) {
|
||||
console.error('注销失败:', error);
|
||||
alert('注销失败,请稍后重试');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化函数
|
||||
function init() {
|
||||
// 设置导航
|
||||
setupNavigation();
|
||||
|
||||
// 设置账户功能
|
||||
setupAccountFeatures();
|
||||
|
||||
// 初始化页面
|
||||
initPageByHash();
|
||||
|
||||
// 添加hashchange事件监听,处理浏览器前进/后退按钮
|
||||
window.addEventListener('hashchange', initPageByHash);
|
||||
|
||||
// 定期更新系统状态
|
||||
setInterval(updateSystemStatus, 5000);
|
||||
}
|
||||
|
||||
// 页面加载完成后执行初始化
|
||||
window.addEventListener('DOMContentLoaded', init);
|
||||
Reference in New Issue
Block a user