增加web功能
This commit is contained in:
@@ -596,9 +596,15 @@ func (s *Server) handleTopDomains(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
// 返回屏蔽规则的基本配置信息和统计数据,不返回完整规则列表
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
|
// 检查是否需要返回完整规则列表
|
||||||
|
if r.URL.Query().Get("all") == "true" {
|
||||||
|
// 返回完整规则数据
|
||||||
|
rules := s.shieldManager.GetRules()
|
||||||
|
json.NewEncoder(w).Encode(rules)
|
||||||
|
return
|
||||||
|
}
|
||||||
// 获取规则统计信息
|
// 获取规则统计信息
|
||||||
stats := s.shieldManager.GetStats()
|
stats := s.shieldManager.GetStats()
|
||||||
shieldInfo := map[string]interface{}{
|
shieldInfo := map[string]interface{}{
|
||||||
|
|||||||
@@ -19,12 +19,6 @@ import (
|
|||||||
"dns-server/logger"
|
"dns-server/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// regexRule 正则规则结构,包含编译后的表达式和原始字符串
|
|
||||||
type regexRule struct {
|
|
||||||
pattern *regexp.Regexp
|
|
||||||
original string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShieldStatsData 用于持久化的Shield统计数据
|
// ShieldStatsData 用于持久化的Shield统计数据
|
||||||
type ShieldStatsData struct {
|
type ShieldStatsData struct {
|
||||||
BlockedDomainsCount map[string]int `json:"blockedDomainsCount"`
|
BlockedDomainsCount map[string]int `json:"blockedDomainsCount"`
|
||||||
@@ -32,40 +26,56 @@ type ShieldStatsData struct {
|
|||||||
LastSaved time.Time `json:"lastSaved"`
|
LastSaved time.Time `json:"lastSaved"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// regexRule 正则规则结构,包含编译后的表达式和原始字符串
|
||||||
|
type regexRule struct {
|
||||||
|
pattern *regexp.Regexp
|
||||||
|
original string
|
||||||
|
isLocal bool // 是否为本地规则
|
||||||
|
source string // 规则来源
|
||||||
|
}
|
||||||
|
|
||||||
// ShieldManager 屏蔽管理器
|
// ShieldManager 屏蔽管理器
|
||||||
type ShieldManager struct {
|
type ShieldManager struct {
|
||||||
config *config.ShieldConfig
|
config *config.ShieldConfig
|
||||||
domainRules map[string]bool
|
domainRules map[string]bool
|
||||||
domainExceptions map[string]bool
|
domainExceptions map[string]bool
|
||||||
regexRules []regexRule
|
domainRulesIsLocal map[string]bool // 标记域名规则是否为本地规则
|
||||||
regexExceptions []regexRule
|
domainExceptionsIsLocal map[string]bool // 标记域名排除规则是否为本地规则
|
||||||
hostsMap map[string]string
|
domainRulesSource map[string]string // 标记域名规则来源
|
||||||
blockedDomainsCount map[string]int
|
domainExceptionsSource map[string]string // 标记域名排除规则来源
|
||||||
resolvedDomainsCount map[string]int
|
regexRules []regexRule
|
||||||
rulesMutex sync.RWMutex
|
regexExceptions []regexRule
|
||||||
updateCtx context.Context
|
hostsMap map[string]string
|
||||||
updateCancel context.CancelFunc
|
blockedDomainsCount map[string]int
|
||||||
updateRunning bool
|
resolvedDomainsCount map[string]int
|
||||||
localRulesCount int // 本地规则数量
|
rulesMutex sync.RWMutex
|
||||||
remoteRulesCount int // 远程规则数量
|
updateCtx context.Context
|
||||||
|
updateCancel context.CancelFunc
|
||||||
|
updateRunning bool
|
||||||
|
localRulesCount int // 本地规则数量
|
||||||
|
remoteRulesCount int // 远程规则数量
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewShieldManager 创建屏蔽管理器实例
|
// NewShieldManager 创建屏蔽管理器实例
|
||||||
func NewShieldManager(config *config.ShieldConfig) *ShieldManager {
|
func NewShieldManager(config *config.ShieldConfig) *ShieldManager {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
manager := &ShieldManager{
|
manager := &ShieldManager{
|
||||||
config: config,
|
config: config,
|
||||||
domainRules: make(map[string]bool),
|
domainRules: make(map[string]bool),
|
||||||
domainExceptions: make(map[string]bool),
|
domainExceptions: make(map[string]bool),
|
||||||
regexRules: []regexRule{},
|
domainRulesIsLocal: make(map[string]bool),
|
||||||
regexExceptions: []regexRule{},
|
domainExceptionsIsLocal: make(map[string]bool),
|
||||||
hostsMap: make(map[string]string),
|
domainRulesSource: make(map[string]string),
|
||||||
blockedDomainsCount: make(map[string]int),
|
domainExceptionsSource: make(map[string]string),
|
||||||
resolvedDomainsCount: make(map[string]int),
|
regexRules: []regexRule{},
|
||||||
updateCtx: ctx,
|
regexExceptions: []regexRule{},
|
||||||
updateCancel: cancel,
|
hostsMap: make(map[string]string),
|
||||||
localRulesCount: 0,
|
blockedDomainsCount: make(map[string]int),
|
||||||
remoteRulesCount: 0,
|
resolvedDomainsCount: make(map[string]int),
|
||||||
|
updateCtx: ctx,
|
||||||
|
updateCancel: cancel,
|
||||||
|
localRulesCount: 0,
|
||||||
|
remoteRulesCount: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载已保存的计数数据
|
// 加载已保存的计数数据
|
||||||
@@ -82,6 +92,10 @@ func (m *ShieldManager) LoadRules() error {
|
|||||||
// 清空现有规则
|
// 清空现有规则
|
||||||
m.domainRules = make(map[string]bool)
|
m.domainRules = make(map[string]bool)
|
||||||
m.domainExceptions = make(map[string]bool)
|
m.domainExceptions = make(map[string]bool)
|
||||||
|
m.domainRulesIsLocal = make(map[string]bool)
|
||||||
|
m.domainExceptionsIsLocal = make(map[string]bool)
|
||||||
|
m.domainRulesSource = make(map[string]string)
|
||||||
|
m.domainExceptionsSource = make(map[string]string)
|
||||||
m.regexRules = []regexRule{}
|
m.regexRules = []regexRule{}
|
||||||
m.regexExceptions = []regexRule{}
|
m.regexExceptions = []regexRule{}
|
||||||
m.hostsMap = make(map[string]string)
|
m.hostsMap = make(map[string]string)
|
||||||
@@ -134,7 +148,7 @@ func (m *ShieldManager) loadLocalRules() error {
|
|||||||
if line == "" || strings.HasPrefix(line, "#") {
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
m.parseRule(line)
|
m.parseRule(line, true, "本地规则") // 本地规则,isLocal=true,来源为"本地规则"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新本地规则计数
|
// 更新本地规则计数
|
||||||
@@ -191,7 +205,7 @@ func (m *ShieldManager) fetchRemoteRules(url string) error {
|
|||||||
// 尝试从缓存加载
|
// 尝试从缓存加载
|
||||||
hasLoadedFromCache := false
|
hasLoadedFromCache := false
|
||||||
if !m.shouldUpdateCache(cacheFile) {
|
if !m.shouldUpdateCache(cacheFile) {
|
||||||
if err := m.loadCachedRules(cacheFile); err == nil {
|
if err := m.loadCachedRules(cacheFile, url); err == nil {
|
||||||
logger.Info("从缓存加载远程规则", "url", url)
|
logger.Info("从缓存加载远程规则", "url", url)
|
||||||
hasLoadedFromCache = true
|
hasLoadedFromCache = true
|
||||||
}
|
}
|
||||||
@@ -236,14 +250,14 @@ func (m *ShieldManager) fetchRemoteRules(url string) error {
|
|||||||
if line == "" || strings.HasPrefix(line, "#") {
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
m.parseRule(line)
|
m.parseRule(line, false, url) // 远程规则,isLocal=false,来源为URL
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadCachedRules 从缓存文件加载规则
|
// loadCachedRules 从缓存文件加载规则
|
||||||
func (m *ShieldManager) loadCachedRules(filePath string) error {
|
func (m *ShieldManager) loadCachedRules(filePath string, source string) error {
|
||||||
file, err := os.Open(filePath)
|
file, err := os.Open(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -265,7 +279,7 @@ func (m *ShieldManager) loadCachedRules(filePath string) error {
|
|||||||
if line == "" || strings.HasPrefix(line, "#") {
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
m.parseRule(line)
|
m.parseRule(line, false, source) // 远程规则,isLocal=false,来源为URL
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新远程规则计数
|
// 更新远程规则计数
|
||||||
@@ -318,7 +332,7 @@ func (m *ShieldManager) loadHosts() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parseRule 解析规则行
|
// parseRule 解析规则行
|
||||||
func (m *ShieldManager) parseRule(line string) {
|
func (m *ShieldManager) parseRule(line string, isLocal bool, source string) {
|
||||||
// 处理注释
|
// 处理注释
|
||||||
if strings.HasPrefix(line, "!") || strings.HasPrefix(line, "#") || line == "" {
|
if strings.HasPrefix(line, "!") || strings.HasPrefix(line, "#") || line == "" {
|
||||||
return
|
return
|
||||||
@@ -343,12 +357,12 @@ func (m *ShieldManager) parseRule(line string) {
|
|||||||
case strings.HasPrefix(line, "||") && strings.HasSuffix(line, "^"):
|
case strings.HasPrefix(line, "||") && strings.HasSuffix(line, "^"):
|
||||||
// AdGuardHome域名规则格式: ||example.com^
|
// AdGuardHome域名规则格式: ||example.com^
|
||||||
domain := strings.TrimSuffix(strings.TrimPrefix(line, "||"), "^")
|
domain := strings.TrimSuffix(strings.TrimPrefix(line, "||"), "^")
|
||||||
m.addDomainRule(domain, !isException)
|
m.addDomainRule(domain, !isException, isLocal, source)
|
||||||
|
|
||||||
case strings.HasPrefix(line, "||"):
|
case strings.HasPrefix(line, "||"):
|
||||||
// 精确域名匹配规则
|
// 精确域名匹配规则
|
||||||
domain := strings.TrimPrefix(line, "||")
|
domain := strings.TrimPrefix(line, "||")
|
||||||
m.addDomainRule(domain, !isException)
|
m.addDomainRule(domain, !isException, isLocal, source)
|
||||||
|
|
||||||
case strings.HasPrefix(line, "*"):
|
case strings.HasPrefix(line, "*"):
|
||||||
// 通配符规则,转换为正则表达式
|
// 通配符规则,转换为正则表达式
|
||||||
@@ -356,15 +370,18 @@ func (m *ShieldManager) parseRule(line string) {
|
|||||||
pattern = "^" + pattern + "$"
|
pattern = "^" + pattern + "$"
|
||||||
if re, err := regexp.Compile(pattern); err == nil {
|
if re, err := regexp.Compile(pattern); err == nil {
|
||||||
// 保存原始规则字符串
|
// 保存原始规则字符串
|
||||||
m.addRegexRule(re, line, !isException)
|
m.addRegexRule(re, line, !isException, isLocal, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasPrefix(line, "/") && strings.HasSuffix(line, "/"):
|
case strings.HasPrefix(line, "/") && strings.HasSuffix(line, "/"):
|
||||||
// 正则表达式规则
|
// 关键字匹配规则:/keyword/ 格式,不区分大小写,字面量匹配特殊字符
|
||||||
pattern := strings.TrimPrefix(strings.TrimSuffix(line, "/"), "/")
|
keyword := strings.TrimPrefix(strings.TrimSuffix(line, "/"), "/")
|
||||||
if re, err := regexp.Compile(pattern); err == nil {
|
// 转义特殊字符,确保字面量匹配
|
||||||
|
quotedKeyword := regexp.QuoteMeta(keyword)
|
||||||
|
// 编译为不区分大小写的正则表达式,匹配域名中任意位置
|
||||||
|
if re, err := regexp.Compile("(?i)" + quotedKeyword); err == nil {
|
||||||
// 保存原始规则字符串
|
// 保存原始规则字符串
|
||||||
m.addRegexRule(re, line, !isException)
|
m.addRegexRule(re, line, !isException, isLocal, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasPrefix(line, "|") && strings.HasSuffix(line, "|"):
|
case strings.HasPrefix(line, "|") && strings.HasSuffix(line, "|"):
|
||||||
@@ -373,7 +390,7 @@ func (m *ShieldManager) parseRule(line string) {
|
|||||||
// 将URL模式转换为正则表达式
|
// 将URL模式转换为正则表达式
|
||||||
pattern := "^" + regexp.QuoteMeta(urlPattern) + "$"
|
pattern := "^" + regexp.QuoteMeta(urlPattern) + "$"
|
||||||
if re, err := regexp.Compile(pattern); err == nil {
|
if re, err := regexp.Compile(pattern); err == nil {
|
||||||
m.addRegexRule(re, line, !isException)
|
m.addRegexRule(re, line, !isException, isLocal, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasPrefix(line, "|"):
|
case strings.HasPrefix(line, "|"):
|
||||||
@@ -381,7 +398,7 @@ func (m *ShieldManager) parseRule(line string) {
|
|||||||
urlPattern := strings.TrimPrefix(line, "|")
|
urlPattern := strings.TrimPrefix(line, "|")
|
||||||
pattern := "^" + regexp.QuoteMeta(urlPattern)
|
pattern := "^" + regexp.QuoteMeta(urlPattern)
|
||||||
if re, err := regexp.Compile(pattern); err == nil {
|
if re, err := regexp.Compile(pattern); err == nil {
|
||||||
m.addRegexRule(re, line, !isException)
|
m.addRegexRule(re, line, !isException, isLocal, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasSuffix(line, "|"):
|
case strings.HasSuffix(line, "|"):
|
||||||
@@ -389,12 +406,12 @@ func (m *ShieldManager) parseRule(line string) {
|
|||||||
urlPattern := strings.TrimSuffix(line, "|")
|
urlPattern := strings.TrimSuffix(line, "|")
|
||||||
pattern := regexp.QuoteMeta(urlPattern) + "$"
|
pattern := regexp.QuoteMeta(urlPattern) + "$"
|
||||||
if re, err := regexp.Compile(pattern); err == nil {
|
if re, err := regexp.Compile(pattern); err == nil {
|
||||||
m.addRegexRule(re, line, !isException)
|
m.addRegexRule(re, line, !isException, isLocal, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// 默认作为普通域名规则
|
// 默认作为普通域名规则
|
||||||
m.addDomainRule(line, !isException)
|
m.addDomainRule(line, !isException, isLocal, source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,42 +435,98 @@ func (m *ShieldManager) parseRuleOptions(optionsStr string) map[string]string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// addDomainRule 添加域名规则,支持是否为阻止规则
|
// addDomainRule 添加域名规则,支持是否为阻止规则
|
||||||
func (m *ShieldManager) addDomainRule(domain string, block bool) {
|
func (m *ShieldManager) addDomainRule(domain string, block bool, isLocal bool, source string) {
|
||||||
if block {
|
if block {
|
||||||
|
// 如果是远程规则,检查是否已经存在本地规则,如果存在则不覆盖
|
||||||
|
if !isLocal {
|
||||||
|
if _, exists := m.domainRulesIsLocal[domain]; exists && m.domainRulesIsLocal[domain] {
|
||||||
|
// 已经存在本地规则,不覆盖
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
m.domainRules[domain] = true
|
m.domainRules[domain] = true
|
||||||
|
m.domainRulesIsLocal[domain] = isLocal
|
||||||
|
m.domainRulesSource[domain] = source
|
||||||
// 添加所有子域名的匹配支持
|
// 添加所有子域名的匹配支持
|
||||||
parts := strings.Split(domain, ".")
|
parts := strings.Split(domain, ".")
|
||||||
if len(parts) > 1 {
|
if len(parts) > 1 {
|
||||||
// 为二级域名和顶级域名添加规则
|
// 为二级域名和顶级域名添加规则
|
||||||
for i := 0; i < len(parts)-1; i++ {
|
for i := 0; i < len(parts)-1; i++ {
|
||||||
subdomain := strings.Join(parts[i:], ".")
|
subdomain := strings.Join(parts[i:], ".")
|
||||||
|
// 如果是远程规则,检查是否已经存在本地规则,如果存在则不覆盖
|
||||||
|
if !isLocal {
|
||||||
|
if _, exists := m.domainRulesIsLocal[subdomain]; exists && m.domainRulesIsLocal[subdomain] {
|
||||||
|
// 已经存在本地规则,不覆盖
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
m.domainRules[subdomain] = true
|
m.domainRules[subdomain] = true
|
||||||
|
m.domainRulesIsLocal[subdomain] = isLocal
|
||||||
|
m.domainRulesSource[subdomain] = source
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 添加到排除规则
|
// 添加到排除规则
|
||||||
|
// 如果是远程规则,检查是否已经存在本地规则,如果存在则不覆盖
|
||||||
|
if !isLocal {
|
||||||
|
if _, exists := m.domainExceptionsIsLocal[domain]; exists && m.domainExceptionsIsLocal[domain] {
|
||||||
|
// 已经存在本地规则,不覆盖
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
m.domainExceptions[domain] = true
|
m.domainExceptions[domain] = true
|
||||||
|
m.domainExceptionsIsLocal[domain] = isLocal
|
||||||
|
m.domainExceptionsSource[domain] = source
|
||||||
// 为子域名也添加排除规则
|
// 为子域名也添加排除规则
|
||||||
parts := strings.Split(domain, ".")
|
parts := strings.Split(domain, ".")
|
||||||
if len(parts) > 1 {
|
if len(parts) > 1 {
|
||||||
for i := 0; i < len(parts)-1; i++ {
|
for i := 0; i < len(parts)-1; i++ {
|
||||||
subdomain := strings.Join(parts[i:], ".")
|
subdomain := strings.Join(parts[i:], ".")
|
||||||
|
// 如果是远程规则,检查是否已经存在本地规则,如果存在则不覆盖
|
||||||
|
if !isLocal {
|
||||||
|
if _, exists := m.domainExceptionsIsLocal[subdomain]; exists && m.domainExceptionsIsLocal[subdomain] {
|
||||||
|
// 已经存在本地规则,不覆盖
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
m.domainExceptions[subdomain] = true
|
m.domainExceptions[subdomain] = true
|
||||||
|
m.domainExceptionsIsLocal[subdomain] = isLocal
|
||||||
|
m.domainExceptionsSource[subdomain] = source
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// addRegexRule 添加正则表达式规则,支持是否为阻止规则
|
// addRegexRule 添加正则表达式规则,支持是否为阻止规则
|
||||||
func (m *ShieldManager) addRegexRule(re *regexp.Regexp, original string, block bool) {
|
func (m *ShieldManager) addRegexRule(re *regexp.Regexp, original string, block bool, isLocal bool, source string) {
|
||||||
rule := regexRule{
|
rule := regexRule{
|
||||||
pattern: re,
|
pattern: re,
|
||||||
original: original,
|
original: original,
|
||||||
|
isLocal: isLocal,
|
||||||
|
source: source,
|
||||||
}
|
}
|
||||||
if block {
|
if block {
|
||||||
|
// 如果是远程规则,检查是否已经存在相同的本地规则,如果存在则不添加
|
||||||
|
if !isLocal {
|
||||||
|
for _, existingRule := range m.regexRules {
|
||||||
|
if existingRule.original == original && existingRule.isLocal {
|
||||||
|
// 已经存在相同的本地规则,不添加
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
m.regexRules = append(m.regexRules, rule)
|
m.regexRules = append(m.regexRules, rule)
|
||||||
} else {
|
} else {
|
||||||
// 添加到排除规则
|
// 添加到排除规则
|
||||||
|
// 如果是远程规则,检查是否已经存在相同的本地规则,如果存在则不添加
|
||||||
|
if !isLocal {
|
||||||
|
for _, existingRule := range m.regexExceptions {
|
||||||
|
if existingRule.original == original && existingRule.isLocal {
|
||||||
|
// 已经存在相同的本地规则,不添加
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
m.regexExceptions = append(m.regexExceptions, rule)
|
m.regexExceptions = append(m.regexExceptions, rule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -471,15 +544,16 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
|||||||
}
|
}
|
||||||
|
|
||||||
result := map[string]interface{}{
|
result := map[string]interface{}{
|
||||||
"domain": domain,
|
"domain": domain,
|
||||||
"blocked": false,
|
"blocked": false,
|
||||||
"blockRule": "",
|
"blockRule": "",
|
||||||
"blockRuleType": "",
|
"blockRuleType": "",
|
||||||
"excluded": false,
|
"blocksource": "",
|
||||||
"excludeRule": "",
|
"excluded": false,
|
||||||
|
"excludeRule": "",
|
||||||
"excludeRuleType": "",
|
"excludeRuleType": "",
|
||||||
"hasHosts": false,
|
"hasHosts": false,
|
||||||
"hostsIP": "",
|
"hostsIP": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查hosts记录
|
// 检查hosts记录
|
||||||
@@ -493,6 +567,7 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
|||||||
result["excluded"] = true
|
result["excluded"] = true
|
||||||
result["excludeRule"] = domain
|
result["excludeRule"] = domain
|
||||||
result["excludeRuleType"] = "exact_domain"
|
result["excludeRuleType"] = "exact_domain"
|
||||||
|
result["blocksource"] = m.domainExceptionsSource[domain]
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,6 +579,7 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
|||||||
result["excluded"] = true
|
result["excluded"] = true
|
||||||
result["excludeRule"] = subdomain
|
result["excludeRule"] = subdomain
|
||||||
result["excludeRuleType"] = "subdomain"
|
result["excludeRuleType"] = "subdomain"
|
||||||
|
result["blocksource"] = m.domainExceptionsSource[subdomain]
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -514,6 +590,7 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
|||||||
result["excluded"] = true
|
result["excluded"] = true
|
||||||
result["excludeRule"] = re.original
|
result["excludeRule"] = re.original
|
||||||
result["excludeRuleType"] = "regex"
|
result["excludeRuleType"] = "regex"
|
||||||
|
result["blocksource"] = re.source
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -524,6 +601,7 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
|||||||
result["blocked"] = true
|
result["blocked"] = true
|
||||||
result["blockRule"] = domain
|
result["blockRule"] = domain
|
||||||
result["blockRuleType"] = "exact_domain"
|
result["blockRuleType"] = "exact_domain"
|
||||||
|
result["blocksource"] = m.domainRulesSource[domain]
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -535,6 +613,7 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
|||||||
result["blocked"] = true
|
result["blocked"] = true
|
||||||
result["blockRule"] = subdomain
|
result["blockRule"] = subdomain
|
||||||
result["blockRuleType"] = "subdomain"
|
result["blockRuleType"] = "subdomain"
|
||||||
|
result["blocksource"] = m.domainRulesSource[subdomain]
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -545,6 +624,7 @@ func (m *ShieldManager) CheckDomainBlockDetails(domain string) map[string]interf
|
|||||||
result["blocked"] = true
|
result["blocked"] = true
|
||||||
result["blockRule"] = re.original
|
result["blockRule"] = re.original
|
||||||
result["blockRuleType"] = "regex"
|
result["blockRuleType"] = "regex"
|
||||||
|
result["blocksource"] = re.source
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -667,13 +747,13 @@ func (m *ShieldManager) GetHostsIP(domain string) (string, bool) {
|
|||||||
return ip, exists
|
return ip, exists
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddRule 添加屏蔽规则
|
// AddRule 添加屏蔽规则,用户添加的规则是本地规则
|
||||||
func (m *ShieldManager) AddRule(rule string) error {
|
func (m *ShieldManager) AddRule(rule string) error {
|
||||||
m.rulesMutex.Lock()
|
m.rulesMutex.Lock()
|
||||||
defer m.rulesMutex.Unlock()
|
defer m.rulesMutex.Unlock()
|
||||||
|
|
||||||
// 解析并添加规则到内存
|
// 解析并添加规则到内存,isLocal=true表示本地规则,来源为"本地规则"
|
||||||
m.parseRule(rule)
|
m.parseRule(rule, true, "本地规则")
|
||||||
|
|
||||||
// 持久化保存规则到文件
|
// 持久化保存规则到文件
|
||||||
if m.config.LocalRulesFile != "" {
|
if m.config.LocalRulesFile != "" {
|
||||||
@@ -859,28 +939,36 @@ func (m *ShieldManager) StopAutoUpdate() {
|
|||||||
logger.Info("规则自动更新已停止")
|
logger.Info("规则自动更新已停止")
|
||||||
}
|
}
|
||||||
|
|
||||||
// saveRulesToFile 保存规则到文件
|
// saveRulesToFile 保存规则到文件,只保存本地规则
|
||||||
func (m *ShieldManager) saveRulesToFile() error {
|
func (m *ShieldManager) saveRulesToFile() error {
|
||||||
var rules []string
|
var rules []string
|
||||||
|
|
||||||
// 添加域名规则
|
// 添加本地域名规则
|
||||||
for domain := range m.domainRules {
|
for domain, isLocal := range m.domainRulesIsLocal {
|
||||||
rules = append(rules, "||"+domain)
|
if isLocal {
|
||||||
|
rules = append(rules, "||"+domain)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加正则表达式规则
|
// 添加本地正则表达式规则
|
||||||
for _, re := range m.regexRules {
|
for _, re := range m.regexRules {
|
||||||
rules = append(rules, re.original)
|
if re.isLocal {
|
||||||
|
rules = append(rules, re.original)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加排除规则
|
// 添加本地排除规则
|
||||||
for domain := range m.domainExceptions {
|
for domain, isLocal := range m.domainExceptionsIsLocal {
|
||||||
rules = append(rules, "@@||"+domain)
|
if isLocal {
|
||||||
|
rules = append(rules, "@@||"+domain)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加正则表达式排除规则
|
// 添加本地正则表达式排除规则
|
||||||
for _, re := range m.regexExceptions {
|
for _, re := range m.regexExceptions {
|
||||||
rules = append(rules, re.original)
|
if re.isLocal {
|
||||||
|
rules = append(rules, re.original)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 写入文件
|
// 写入文件
|
||||||
|
|||||||
62
shield/rule_test.go
Normal file
62
shield/rule_test.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package shield
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"dns-server/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRuleParsing(t *testing.T) {
|
||||||
|
// 创建一个简单的配置
|
||||||
|
cfg := &config.ShieldConfig{
|
||||||
|
LocalRulesFile: "",
|
||||||
|
RemoteRulesCacheDir: ".",
|
||||||
|
UpdateInterval: 3600,
|
||||||
|
StatsFile: "",
|
||||||
|
StatsSaveInterval: 300,
|
||||||
|
HostsFile: "",
|
||||||
|
Blacklists: []config.BlacklistEntry{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试规则
|
||||||
|
testCases := []struct {
|
||||||
|
rule string
|
||||||
|
domain string
|
||||||
|
blocked bool
|
||||||
|
desc string
|
||||||
|
}{
|
||||||
|
// 测试关键字匹配规则
|
||||||
|
{"/ad.qq.com/", "ad.qq.com", true, "精确匹配"},
|
||||||
|
{"/ad.qq.com/", "sub.ad.qq.com", true, "子域名包含匹配"},
|
||||||
|
{"/ad/", "ad.example.com", true, "开头匹配"},
|
||||||
|
{"/ad/", "example.ad.com", true, "中间匹配"},
|
||||||
|
{"/ad/", "example.com.ad", true, "结尾匹配"},
|
||||||
|
{"/AD/", "ad.example.com", true, "不区分大小写匹配"},
|
||||||
|
{"/example.com/", "example.com", true, "特殊字符转义匹配"},
|
||||||
|
{"/ad/", "example.com", false, "不包含关键字,不应匹配"},
|
||||||
|
{"/test/", "example.com", false, "不同关键字,不应匹配"},
|
||||||
|
|
||||||
|
// 测试排除规则
|
||||||
|
{"@@/ad/", "ad.example.com", false, "排除规则,不应匹配"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行测试
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
// 为每个测试用例创建一个新的屏蔽管理器实例
|
||||||
|
manager := NewShieldManager(cfg)
|
||||||
|
|
||||||
|
// 添加规则
|
||||||
|
manager.AddRule(tc.rule)
|
||||||
|
|
||||||
|
// 检查域名是否被屏蔽
|
||||||
|
result := manager.CheckDomainBlockDetails(tc.domain)
|
||||||
|
blocked := result["blocked"].(bool)
|
||||||
|
|
||||||
|
// 验证结果
|
||||||
|
if blocked != tc.blocked {
|
||||||
|
t.Errorf("Rule %q: Domain %q expected %t, got %t", tc.rule, tc.domain, tc.blocked, blocked)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -687,23 +687,15 @@
|
|||||||
|
|
||||||
<!-- 本地规则管理 -->
|
<!-- 本地规则管理 -->
|
||||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
<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 mb-6">本地规则管理</h3>
|
||||||
<h3 class="text-lg font-semibold">本地规则管理</h3>
|
|
||||||
<button id="add-rule-btn" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors">
|
|
||||||
<i class="fa fa-plus mr-2"></i>添加规则
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 添加规则表单 -->
|
<!-- 添加规则表单 -->
|
||||||
<div id="add-rule-form" class="mb-6 bg-gray-50 p-4 rounded-lg hidden">
|
<div id="add-rule-form" class="mb-6 bg-gray-50 p-4 rounded-lg">
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
<input type="text" id="new-rule" placeholder="输入规则(例如:example.com 或 regex:/example\.com/)" class="flex-1 px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
<input type="text" id="new-rule" placeholder="输入规则(例如:example.com 或 regex:/example\.com/)" class="flex-1 px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||||
<button id="save-rule-btn" class="px-4 py-2 bg-success text-white rounded-md hover:bg-success/90 transition-colors">
|
<button id="save-rule-btn" class="px-4 py-2 bg-success text-white rounded-md hover:bg-success/90 transition-colors">
|
||||||
保存
|
保存
|
||||||
</button>
|
</button>
|
||||||
<button id="cancel-rule-btn" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors">
|
|
||||||
取消
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -727,15 +719,10 @@
|
|||||||
|
|
||||||
<!-- 远程黑名单管理 -->
|
<!-- 远程黑名单管理 -->
|
||||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
<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 mb-6">远程黑名单管理</h3>
|
||||||
<h3 class="text-lg font-semibold">远程黑名单管理</h3>
|
|
||||||
<button id="add-blacklist-btn" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors">
|
|
||||||
<i class="fa fa-plus mr-2"></i>添加黑名单
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 添加黑名单表单 -->
|
<!-- 添加黑名单表单 -->
|
||||||
<div id="add-blacklist-form" class="mb-6 bg-gray-50 p-4 rounded-lg hidden">
|
<div id="add-blacklist-form" class="mb-6 bg-gray-50 p-4 rounded-lg">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="blacklist-name" class="block text-sm font-medium text-gray-700 mb-1">名称</label>
|
<label for="blacklist-name" class="block text-sm font-medium text-gray-700 mb-1">名称</label>
|
||||||
@@ -745,13 +732,10 @@
|
|||||||
<label for="blacklist-url" class="block text-sm font-medium text-gray-700 mb-1">URL</label>
|
<label for="blacklist-url" class="block text-sm font-medium text-gray-700 mb-1">URL</label>
|
||||||
<input type="text" id="blacklist-url" placeholder="输入黑名单URL" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
<input type="text" id="blacklist-url" placeholder="输入黑名单URL" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-end space-x-4">
|
<div class="flex items-end">
|
||||||
<button id="save-blacklist-btn" class="px-4 py-2 bg-success text-white rounded-md hover:bg-success/90 transition-colors">
|
<button id="save-blacklist-btn" class="px-4 py-2 bg-success text-white rounded-md hover:bg-success/90 transition-colors">
|
||||||
保存
|
保存
|
||||||
</button>
|
</button>
|
||||||
<button id="cancel-blacklist-btn" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors">
|
|
||||||
取消
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -776,26 +760,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hosts条目管理 -->
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="hosts-content" class="hidden space-y-6">
|
||||||
|
<!-- Hosts管理页面内容 -->
|
||||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
<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 mb-6">Hosts条目管理</h3>
|
||||||
<h3 class="text-lg font-semibold">Hosts条目管理</h3>
|
|
||||||
<button id="add-hosts-btn" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors">
|
|
||||||
<i class="fa fa-plus mr-2"></i>添加条目
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 添加hosts条目表单 -->
|
<!-- 添加hosts条目表单 -->
|
||||||
<div id="add-hosts-form" class="mb-6 bg-gray-50 p-4 rounded-lg hidden">
|
<div id="add-hosts-form" class="mb-6 bg-gray-50 p-4 rounded-lg">
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
<input type="text" id="hosts-ip" placeholder="IP地址" class="w-32 px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
<input type="text" id="hosts-ip" placeholder="IP地址" class="w-32 px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||||
<input type="text" id="hosts-domain" placeholder="域名" class="flex-1 px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
<input type="text" id="hosts-domain" placeholder="域名" class="flex-1 px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||||
<button id="save-hosts-btn" class="px-4 py-2 bg-success text-white rounded-md hover:bg-success/90 transition-colors">
|
<button id="save-hosts-btn" class="px-4 py-2 bg-success text-white rounded-md hover:bg-success/90 transition-colors">
|
||||||
保存
|
保存
|
||||||
</button>
|
</button>
|
||||||
<button id="cancel-hosts-btn" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors">
|
|
||||||
取消
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -819,15 +799,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="hosts-content" class="hidden">
|
|
||||||
<!-- Hosts管理页面内容 -->
|
|
||||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
|
||||||
<h3 class="text-lg font-semibold mb-6">Hosts管理</h3>
|
|
||||||
<!-- 这里将添加Hosts管理相关内容 -->
|
|
||||||
<p>Hosts管理页面内容待实现</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="blacklists-content" class="hidden">
|
<div id="blacklists-content" class="hidden">
|
||||||
<!-- 黑名单管理页面内容 -->
|
<!-- 黑名单管理页面内容 -->
|
||||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
<div class="bg-white rounded-lg p-6 card-shadow">
|
||||||
|
|||||||
@@ -2,94 +2,140 @@
|
|||||||
|
|
||||||
// 初始化Hosts管理页面
|
// 初始化Hosts管理页面
|
||||||
function initHostsPage() {
|
function initHostsPage() {
|
||||||
loadHostsContent();
|
// 加载Hosts规则
|
||||||
|
loadHostsRules();
|
||||||
|
// 设置事件监听器
|
||||||
setupHostsEventListeners();
|
setupHostsEventListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载Hosts内容
|
// 加载Hosts规则
|
||||||
async function loadHostsContent() {
|
async function loadHostsRules() {
|
||||||
try {
|
try {
|
||||||
const hostsContent = await api.getHosts();
|
const response = await fetch('/api/shield/hosts');
|
||||||
document.getElementById('hosts-content').value = hostsContent;
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to load hosts rules');
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// 处理API返回的数据格式
|
||||||
|
let hostsRules = [];
|
||||||
|
if (data && Array.isArray(data)) {
|
||||||
|
// 直接是数组格式
|
||||||
|
hostsRules = data;
|
||||||
|
} else if (data && data.hosts) {
|
||||||
|
// 包含在hosts字段中
|
||||||
|
hostsRules = data.hosts;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateHostsTable(hostsRules);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showErrorMessage('加载Hosts文件失败: ' + error.message);
|
console.error('Error loading hosts rules:', error);
|
||||||
|
showErrorMessage('加载Hosts规则失败');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存Hosts内容
|
// 更新Hosts表格
|
||||||
async function handleSaveHosts() {
|
function updateHostsTable(hostsRules) {
|
||||||
const hostsContent = document.getElementById('hosts-content').value;
|
const tbody = document.getElementById('hosts-table-body');
|
||||||
|
|
||||||
try {
|
if (hostsRules.length === 0) {
|
||||||
await api.saveHosts(hostsContent);
|
tbody.innerHTML = '<tr><td colspan="3" class="py-4 text-center text-gray-500">暂无Hosts条目</td></tr>';
|
||||||
showSuccessMessage('Hosts文件保存成功');
|
|
||||||
} catch (error) {
|
|
||||||
showErrorMessage('保存Hosts文件失败: ' + error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 刷新Hosts
|
|
||||||
async function handleRefreshHosts() {
|
|
||||||
try {
|
|
||||||
await api.refreshHosts();
|
|
||||||
showSuccessMessage('Hosts刷新成功');
|
|
||||||
loadHostsContent();
|
|
||||||
} catch (error) {
|
|
||||||
showErrorMessage('刷新Hosts失败: ' + error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加新的Hosts条目
|
|
||||||
function handleAddHostsEntry() {
|
|
||||||
const ipInput = document.getElementById('hosts-ip');
|
|
||||||
const domainInput = document.getElementById('hosts-domain');
|
|
||||||
|
|
||||||
const ip = ipInput.value.trim();
|
|
||||||
const domain = domainInput.value.trim();
|
|
||||||
|
|
||||||
if (!ip || !domain) {
|
|
||||||
showErrorMessage('IP和域名不能为空');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 简单的IP验证
|
tbody.innerHTML = hostsRules.map(rule => {
|
||||||
const ipRegex = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/;
|
// 处理对象格式的规则
|
||||||
if (!ipRegex.test(ip)) {
|
const ip = rule.ip || '';
|
||||||
showErrorMessage('请输入有效的IP地址');
|
const domain = rule.domain || '';
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hostsTextarea = document.getElementById('hosts-content');
|
return `
|
||||||
const newEntry = `\n${ip} ${domain}`;
|
<tr class="border-b border-gray-200">
|
||||||
hostsTextarea.value += newEntry;
|
<td class="py-3 px-4">${ip}</td>
|
||||||
|
<td class="py-3 px-4">${domain}</td>
|
||||||
|
<td class="py-3 px-4 text-right">
|
||||||
|
<button class="delete-hosts-btn px-3 py-1 bg-danger text-white rounded-md hover:bg-danger/90 transition-colors text-sm" data-ip="${ip}" data-domain="${domain}">
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
// 清空输入框
|
// 重新绑定删除事件
|
||||||
ipInput.value = '';
|
document.querySelectorAll('.delete-hosts-btn').forEach(btn => {
|
||||||
domainInput.value = '';
|
btn.addEventListener('click', handleDeleteHostsRule);
|
||||||
|
});
|
||||||
// 滚动到文本区域底部
|
|
||||||
hostsTextarea.scrollTop = hostsTextarea.scrollHeight;
|
|
||||||
|
|
||||||
showSuccessMessage('Hosts条目已添加到编辑器');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置事件监听器
|
// 设置事件监听器
|
||||||
function setupHostsEventListeners() {
|
function setupHostsEventListeners() {
|
||||||
// 保存按钮
|
// 保存Hosts按钮
|
||||||
document.getElementById('save-hosts-btn')?.addEventListener('click', handleSaveHosts);
|
document.getElementById('save-hosts-btn').addEventListener('click', handleAddHostsRule);
|
||||||
|
}
|
||||||
|
|
||||||
// 刷新按钮
|
// 处理添加Hosts规则
|
||||||
document.getElementById('refresh-hosts-btn')?.addEventListener('click', handleRefreshHosts);
|
async function handleAddHostsRule() {
|
||||||
|
const ip = document.getElementById('hosts-ip').value.trim();
|
||||||
|
const domain = document.getElementById('hosts-domain').value.trim();
|
||||||
|
|
||||||
// 添加Hosts条目按钮
|
if (!ip || !domain) {
|
||||||
document.getElementById('add-hosts-entry-btn')?.addEventListener('click', handleAddHostsEntry);
|
showErrorMessage('IP地址和域名不能为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 按回车键添加条目
|
try {
|
||||||
document.getElementById('hosts-domain')?.addEventListener('keypress', (e) => {
|
const response = await fetch('/api/shield/hosts', {
|
||||||
if (e.key === 'Enter') {
|
method: 'POST',
|
||||||
handleAddHostsEntry();
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ ip, domain })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to add hosts rule');
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
showSuccessMessage('Hosts规则添加成功');
|
||||||
|
|
||||||
|
// 清空输入框
|
||||||
|
document.getElementById('hosts-ip').value = '';
|
||||||
|
document.getElementById('hosts-domain').value = '';
|
||||||
|
|
||||||
|
// 重新加载规则
|
||||||
|
loadHostsRules();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding hosts rule:', error);
|
||||||
|
showErrorMessage('添加Hosts规则失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理删除Hosts规则
|
||||||
|
async function handleDeleteHostsRule(e) {
|
||||||
|
const ip = e.target.closest('.delete-hosts-btn').dataset.ip;
|
||||||
|
const domain = e.target.closest('.delete-hosts-btn').dataset.domain;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/shield/hosts', {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ domain })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to delete hosts rule');
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccessMessage('Hosts规则删除成功');
|
||||||
|
|
||||||
|
// 重新加载规则
|
||||||
|
loadHostsRules();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting hosts rule:', error);
|
||||||
|
showErrorMessage('删除Hosts规则失败');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示成功消息
|
// 显示成功消息
|
||||||
@@ -102,6 +148,8 @@ function showErrorMessage(message) {
|
|||||||
showNotification(message, 'error');
|
showNotification(message, 'error');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 显示通知
|
// 显示通知
|
||||||
function showNotification(message, type = 'info') {
|
function showNotification(message, type = 'info') {
|
||||||
// 移除现有通知
|
// 移除现有通知
|
||||||
@@ -112,7 +160,7 @@ function showNotification(message, type = 'info') {
|
|||||||
|
|
||||||
// 创建新通知
|
// 创建新通知
|
||||||
const notification = document.createElement('div');
|
const notification = document.createElement('div');
|
||||||
notification.className = `notification fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform transition-transform duration-300 ease-in-out translate-y-0 opacity-0`;
|
notification.className = `notification fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transform transition-all duration-300 ease-in-out translate-y-0 opacity-0`;
|
||||||
|
|
||||||
// 设置通知样式
|
// 设置通知样式
|
||||||
if (type === 'success') {
|
if (type === 'success') {
|
||||||
@@ -123,18 +171,22 @@ function showNotification(message, type = 'info') {
|
|||||||
notification.classList.add('bg-blue-500', 'text-white');
|
notification.classList.add('bg-blue-500', 'text-white');
|
||||||
}
|
}
|
||||||
|
|
||||||
notification.textContent = message;
|
notification.innerHTML = `
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<i class="fa fa-${type === 'success' ? 'check' : type === 'error' ? 'exclamation' : 'info'}"></i>
|
||||||
|
<span>${message}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
document.body.appendChild(notification);
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
// 显示通知
|
// 显示通知
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
notification.classList.remove('opacity-0');
|
notification.classList.remove('opacity-0');
|
||||||
notification.classList.add('opacity-100');
|
}, 100);
|
||||||
}, 10);
|
|
||||||
|
|
||||||
// 3秒后隐藏通知
|
// 3秒后隐藏通知
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
notification.classList.remove('opacity-100');
|
|
||||||
notification.classList.add('opacity-0');
|
notification.classList.add('opacity-0');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
notification.remove();
|
notification.remove();
|
||||||
|
|||||||
@@ -38,6 +38,13 @@ function setupNavigation() {
|
|||||||
|
|
||||||
// 更新页面标题
|
// 更新页面标题
|
||||||
pageTitle.textContent = item.querySelector('span').textContent;
|
pageTitle.textContent = item.querySelector('span').textContent;
|
||||||
|
|
||||||
|
// 页面特定初始化
|
||||||
|
if (target === 'shield' && typeof initShieldPage === 'function') {
|
||||||
|
initShieldPage();
|
||||||
|
} else if (target === 'hosts' && typeof initHostsPage === 'function') {
|
||||||
|
initHostsPage();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -54,13 +54,60 @@ function displayQueryResult(result, domain) {
|
|||||||
// 解析结果
|
// 解析结果
|
||||||
const status = result.blocked ? '被屏蔽' : '正常';
|
const status = result.blocked ? '被屏蔽' : '正常';
|
||||||
const statusClass = result.blocked ? 'text-danger' : 'text-success';
|
const statusClass = result.blocked ? 'text-danger' : 'text-success';
|
||||||
const blockType = result.blocked ? result.blockType || '未知' : '正常';
|
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();
|
const timestamp = new Date(result.timestamp).toLocaleString();
|
||||||
|
|
||||||
// 更新结果显示
|
// 更新结果显示
|
||||||
document.getElementById('result-domain').textContent = domain;
|
document.getElementById('result-domain').textContent = domain;
|
||||||
document.getElementById('result-status').innerHTML = `<span class="${statusClass}">${status}</span>`;
|
document.getElementById('result-status').innerHTML = `<span class="${statusClass}">${status}</span>`;
|
||||||
document.getElementById('result-type').textContent = blockType;
|
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-time').textContent = timestamp;
|
||||||
document.getElementById('result-details').textContent = JSON.stringify(result, null, 2);
|
document.getElementById('result-details').textContent = JSON.stringify(result, null, 2);
|
||||||
}
|
}
|
||||||
@@ -76,7 +123,9 @@ function saveQueryHistory(domain, result) {
|
|||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
result: {
|
result: {
|
||||||
blocked: result.blocked,
|
blocked: result.blocked,
|
||||||
blockType: result.blockType
|
blockRuleType: result.blockRuleType,
|
||||||
|
blockRule: result.blockRule,
|
||||||
|
blocksource: result.blocksource
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -109,7 +158,9 @@ function loadQueryHistory() {
|
|||||||
const historyHTML = history.map(item => {
|
const historyHTML = history.map(item => {
|
||||||
const statusClass = item.result.blocked ? 'text-danger' : 'text-success';
|
const statusClass = item.result.blocked ? 'text-danger' : 'text-success';
|
||||||
const statusText = item.result.blocked ? '被屏蔽' : '正常';
|
const statusText = item.result.blocked ? '被屏蔽' : '正常';
|
||||||
const blockType = item.result.blocked ? item.result.blockType : '正常';
|
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();
|
const formattedTime = new Date(item.timestamp).toLocaleString();
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@@ -120,6 +171,8 @@ function loadQueryHistory() {
|
|||||||
<span class="${statusClass} text-sm">${statusText}</span>
|
<span class="${statusClass} text-sm">${statusText}</span>
|
||||||
<span class="text-xs text-gray-500">${blockType}</span>
|
<span class="text-xs text-gray-500">${blockType}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">规则: ${blockRule}</div>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">来源: ${blockSource}</div>
|
||||||
<div class="text-xs text-gray-500 mt-1">${formattedTime}</div>
|
<div class="text-xs text-gray-500 mt-1">${formattedTime}</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="mt-2 md:mt-0 px-3 py-1 bg-primary text-white text-sm rounded-md hover:bg-primary/90 transition-colors" onclick="requeryFromHistory('${item.domain}')">
|
<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}')">
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ function initShieldPage() {
|
|||||||
loadLocalRules();
|
loadLocalRules();
|
||||||
// 加载远程黑名单
|
// 加载远程黑名单
|
||||||
loadRemoteBlacklists();
|
loadRemoteBlacklists();
|
||||||
// 加载hosts条目
|
|
||||||
loadHostsEntries();
|
|
||||||
// 设置事件监听器
|
// 设置事件监听器
|
||||||
setupShieldEventListeners();
|
setupShieldEventListeners();
|
||||||
}
|
}
|
||||||
@@ -19,21 +17,33 @@ async function loadShieldStats() {
|
|||||||
showLoading('加载屏蔽规则统计信息...');
|
showLoading('加载屏蔽规则统计信息...');
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/shield');
|
const response = await fetch('/api/shield');
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to load shield stats');
|
throw new Error(`加载失败: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const stats = await response.json();
|
const stats = await response.json();
|
||||||
|
|
||||||
// 更新统计信息
|
// 更新统计信息
|
||||||
document.getElementById('domain-rules-count').textContent = stats.domainRulesCount || 0;
|
const elements = [
|
||||||
document.getElementById('domain-exceptions-count').textContent = stats.domainExceptionsCount || 0;
|
{ id: 'domain-rules-count', value: stats.domainRulesCount },
|
||||||
document.getElementById('regex-rules-count').textContent = stats.regexRulesCount || 0;
|
{ id: 'domain-exceptions-count', value: stats.domainExceptionsCount },
|
||||||
document.getElementById('regex-exceptions-count').textContent = stats.regexExceptionsCount || 0;
|
{ id: 'regex-rules-count', value: stats.regexRulesCount },
|
||||||
document.getElementById('hosts-rules-count').textContent = stats.hostsRulesCount || 0;
|
{ id: 'regex-exceptions-count', value: stats.regexExceptionsCount },
|
||||||
document.getElementById('blacklist-count').textContent = stats.blacklistCount || 0;
|
{ id: 'hosts-rules-count', value: stats.hostsRulesCount },
|
||||||
|
{ id: 'blacklist-count', value: stats.blacklistCount }
|
||||||
|
];
|
||||||
|
|
||||||
|
elements.forEach(item => {
|
||||||
|
const element = document.getElementById(item.id);
|
||||||
|
if (element) {
|
||||||
|
element.textContent = item.value || 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
hideLoading();
|
hideLoading();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading shield stats:', error);
|
console.error('加载屏蔽规则统计信息失败:', error);
|
||||||
showErrorMessage('加载屏蔽规则统计信息失败');
|
showErrorMessage('加载屏蔽规则统计信息失败');
|
||||||
hideLoading();
|
hideLoading();
|
||||||
}
|
}
|
||||||
@@ -43,17 +53,37 @@ async function loadShieldStats() {
|
|||||||
async function loadLocalRules() {
|
async function loadLocalRules() {
|
||||||
showLoading('加载本地规则...');
|
showLoading('加载本地规则...');
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/shield');
|
const response = await fetch('/api/shield?all=true');
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to load local rules');
|
throw new Error(`加载失败: ${response.status}`);
|
||||||
}
|
}
|
||||||
// 注意:当前API不返回完整规则列表,这里只是示例
|
|
||||||
// 实际实现需要后端提供获取本地规则的API
|
const data = await response.json();
|
||||||
const rules = [];
|
|
||||||
|
// 合并所有本地规则
|
||||||
|
let rules = [];
|
||||||
|
// 添加域名规则
|
||||||
|
if (Array.isArray(data.domainRules)) {
|
||||||
|
rules = rules.concat(data.domainRules);
|
||||||
|
}
|
||||||
|
// 添加域名排除规则
|
||||||
|
if (Array.isArray(data.domainExceptions)) {
|
||||||
|
rules = rules.concat(data.domainExceptions);
|
||||||
|
}
|
||||||
|
// 添加正则规则
|
||||||
|
if (Array.isArray(data.regexRules)) {
|
||||||
|
rules = rules.concat(data.regexRules);
|
||||||
|
}
|
||||||
|
// 添加正则排除规则
|
||||||
|
if (Array.isArray(data.regexExceptions)) {
|
||||||
|
rules = rules.concat(data.regexExceptions);
|
||||||
|
}
|
||||||
|
|
||||||
updateRulesTable(rules);
|
updateRulesTable(rules);
|
||||||
hideLoading();
|
hideLoading();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading local rules:', error);
|
console.error('加载本地规则失败:', error);
|
||||||
showErrorMessage('加载本地规则失败');
|
showErrorMessage('加载本地规则失败');
|
||||||
hideLoading();
|
hideLoading();
|
||||||
}
|
}
|
||||||
@@ -63,26 +93,55 @@ async function loadLocalRules() {
|
|||||||
function updateRulesTable(rules) {
|
function updateRulesTable(rules) {
|
||||||
const tbody = document.getElementById('rules-table-body');
|
const tbody = document.getElementById('rules-table-body');
|
||||||
|
|
||||||
|
// 清空表格
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
if (rules.length === 0) {
|
if (rules.length === 0) {
|
||||||
tbody.innerHTML = '<tr><td colspan="2" class="py-4 text-center text-gray-500">暂无规则</td></tr>';
|
const emptyRow = document.createElement('tr');
|
||||||
|
emptyRow.innerHTML = '<td colspan="2" class="py-4 text-center text-gray-500">暂无规则</td>';
|
||||||
|
tbody.appendChild(emptyRow);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody.innerHTML = rules.map(rule => `
|
// 对于大量规则,限制显示数量
|
||||||
<tr class="border-b border-gray-200">
|
const maxRulesToShow = 1000; // 限制最大显示数量
|
||||||
<td class="py-3 px-4">${rule}</td>
|
const rulesToShow = rules.length > maxRulesToShow ? rules.slice(0, maxRulesToShow) : rules;
|
||||||
<td class="py-3 px-4 text-right">
|
|
||||||
<button class="delete-rule-btn px-3 py-1 bg-danger text-white rounded-md hover:bg-danger/90 transition-colors text-sm" data-rule="${rule}">
|
|
||||||
<i class="fa fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
// 重新绑定删除事件
|
// 使用DocumentFragment提高性能
|
||||||
document.querySelectorAll('.delete-rule-btn').forEach(btn => {
|
const fragment = document.createDocumentFragment();
|
||||||
btn.addEventListener('click', handleDeleteRule);
|
|
||||||
|
rulesToShow.forEach(rule => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.className = 'border-b border-gray-200';
|
||||||
|
|
||||||
|
const tdRule = document.createElement('td');
|
||||||
|
tdRule.className = 'py-3 px-4';
|
||||||
|
tdRule.textContent = rule;
|
||||||
|
|
||||||
|
const tdAction = document.createElement('td');
|
||||||
|
tdAction.className = 'py-3 px-4 text-right';
|
||||||
|
|
||||||
|
const deleteBtn = document.createElement('button');
|
||||||
|
deleteBtn.className = 'delete-rule-btn px-3 py-1 bg-danger text-white rounded-md hover:bg-danger/90 transition-colors text-sm';
|
||||||
|
deleteBtn.dataset.rule = rule;
|
||||||
|
deleteBtn.innerHTML = '<i class="fa fa-trash"></i>';
|
||||||
|
deleteBtn.addEventListener('click', handleDeleteRule);
|
||||||
|
|
||||||
|
tdAction.appendChild(deleteBtn);
|
||||||
|
tr.appendChild(tdRule);
|
||||||
|
tr.appendChild(tdAction);
|
||||||
|
fragment.appendChild(tr);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 一次性添加所有行到DOM
|
||||||
|
tbody.appendChild(fragment);
|
||||||
|
|
||||||
|
// 如果有更多规则,添加提示
|
||||||
|
if (rules.length > maxRulesToShow) {
|
||||||
|
const infoRow = document.createElement('tr');
|
||||||
|
infoRow.innerHTML = `<td colspan="2" class="py-4 text-center text-gray-500">显示前 ${maxRulesToShow} 条规则,共 ${rules.length} 条</td>`;
|
||||||
|
tbody.appendChild(infoRow);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理删除规则
|
// 处理删除规则
|
||||||
@@ -140,8 +199,6 @@ async function handleAddRule() {
|
|||||||
showSuccessMessage('规则添加成功');
|
showSuccessMessage('规则添加成功');
|
||||||
// 清空输入框
|
// 清空输入框
|
||||||
document.getElementById('new-rule').value = '';
|
document.getElementById('new-rule').value = '';
|
||||||
// 隐藏表单
|
|
||||||
document.getElementById('add-rule-form').classList.add('hidden');
|
|
||||||
// 重新加载规则
|
// 重新加载规则
|
||||||
loadLocalRules();
|
loadLocalRules();
|
||||||
// 重新加载统计信息
|
// 重新加载统计信息
|
||||||
@@ -159,14 +216,19 @@ async function loadRemoteBlacklists() {
|
|||||||
showLoading('加载远程黑名单...');
|
showLoading('加载远程黑名单...');
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/shield/blacklists');
|
const response = await fetch('/api/shield/blacklists');
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to load remote blacklists');
|
throw new Error(`加载失败: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const blacklists = await response.json();
|
const blacklists = await response.json();
|
||||||
updateBlacklistsTable(blacklists);
|
|
||||||
|
// 确保blacklists是数组
|
||||||
|
const blacklistArray = Array.isArray(blacklists) ? blacklists : [];
|
||||||
|
updateBlacklistsTable(blacklistArray);
|
||||||
hideLoading();
|
hideLoading();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading remote blacklists:', error);
|
console.error('加载远程黑名单失败:', error);
|
||||||
showErrorMessage('加载远程黑名单失败');
|
showErrorMessage('加载远程黑名单失败');
|
||||||
hideLoading();
|
hideLoading();
|
||||||
}
|
}
|
||||||
@@ -176,37 +238,81 @@ async function loadRemoteBlacklists() {
|
|||||||
function updateBlacklistsTable(blacklists) {
|
function updateBlacklistsTable(blacklists) {
|
||||||
const tbody = document.getElementById('blacklists-table-body');
|
const tbody = document.getElementById('blacklists-table-body');
|
||||||
|
|
||||||
|
// 清空表格
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
if (blacklists.length === 0) {
|
if (blacklists.length === 0) {
|
||||||
tbody.innerHTML = '<tr><td colspan="4" class="py-4 text-center text-gray-500">暂无黑名单</td></tr>';
|
const emptyRow = document.createElement('tr');
|
||||||
|
emptyRow.innerHTML = '<tr><td colspan="4" class="py-4 text-center text-gray-500">暂无黑名单</td></tr>';
|
||||||
|
tbody.appendChild(emptyRow);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody.innerHTML = blacklists.map(blacklist => `
|
// 对于大量黑名单,限制显示数量
|
||||||
<tr class="border-b border-gray-200">
|
const maxBlacklistsToShow = 100; // 限制最大显示数量
|
||||||
<td class="py-3 px-4">${blacklist.Name}</td>
|
const blacklistsToShow = blacklists.length > maxBlacklistsToShow ? blacklists.slice(0, maxBlacklistsToShow) : blacklists;
|
||||||
<td class="py-3 px-4 truncate max-w-xs">${blacklist.URL}</td>
|
|
||||||
<td class="py-3 px-4 text-center">
|
|
||||||
<span class="inline-block w-3 h-3 rounded-full ${blacklist.Enabled ? 'bg-success' : 'bg-gray-300'}"></span>
|
|
||||||
</td>
|
|
||||||
<td class="py-3 px-4 text-right space-x-2">
|
|
||||||
<button class="update-blacklist-btn px-3 py-1 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors text-sm" data-url="${blacklist.URL}">
|
|
||||||
<i class="fa fa-refresh"></i>
|
|
||||||
</button>
|
|
||||||
<button class="delete-blacklist-btn px-3 py-1 bg-danger text-white rounded-md hover:bg-danger/90 transition-colors text-sm" data-url="${blacklist.URL}">
|
|
||||||
<i class="fa fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
// 重新绑定事件
|
// 使用DocumentFragment提高性能
|
||||||
document.querySelectorAll('.update-blacklist-btn').forEach(btn => {
|
const fragment = document.createDocumentFragment();
|
||||||
btn.addEventListener('click', handleUpdateBlacklist);
|
|
||||||
|
blacklistsToShow.forEach(blacklist => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.className = 'border-b border-gray-200';
|
||||||
|
|
||||||
|
// 名称单元格
|
||||||
|
const tdName = document.createElement('td');
|
||||||
|
tdName.className = 'py-3 px-4';
|
||||||
|
tdName.textContent = blacklist.Name;
|
||||||
|
|
||||||
|
// URL单元格
|
||||||
|
const tdUrl = document.createElement('td');
|
||||||
|
tdUrl.className = 'py-3 px-4 truncate max-w-xs';
|
||||||
|
tdUrl.textContent = blacklist.URL;
|
||||||
|
|
||||||
|
// 状态单元格
|
||||||
|
const tdStatus = document.createElement('td');
|
||||||
|
tdStatus.className = 'py-3 px-4 text-center';
|
||||||
|
const statusDot = document.createElement('span');
|
||||||
|
statusDot.className = `inline-block w-3 h-3 rounded-full ${blacklist.Enabled ? 'bg-success' : 'bg-gray-300'}`;
|
||||||
|
tdStatus.appendChild(statusDot);
|
||||||
|
|
||||||
|
// 操作单元格
|
||||||
|
const tdActions = document.createElement('td');
|
||||||
|
tdActions.className = 'py-3 px-4 text-right space-x-2';
|
||||||
|
|
||||||
|
// 更新按钮
|
||||||
|
const updateBtn = document.createElement('button');
|
||||||
|
updateBtn.className = 'update-blacklist-btn px-3 py-1 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors text-sm';
|
||||||
|
updateBtn.dataset.url = blacklist.URL;
|
||||||
|
updateBtn.innerHTML = '<i class="fa fa-refresh"></i>';
|
||||||
|
updateBtn.addEventListener('click', handleUpdateBlacklist);
|
||||||
|
|
||||||
|
// 删除按钮
|
||||||
|
const deleteBtn = document.createElement('button');
|
||||||
|
deleteBtn.className = 'delete-blacklist-btn px-3 py-1 bg-danger text-white rounded-md hover:bg-danger/90 transition-colors text-sm';
|
||||||
|
deleteBtn.dataset.url = blacklist.URL;
|
||||||
|
deleteBtn.innerHTML = '<i class="fa fa-trash"></i>';
|
||||||
|
deleteBtn.addEventListener('click', handleDeleteBlacklist);
|
||||||
|
|
||||||
|
tdActions.appendChild(updateBtn);
|
||||||
|
tdActions.appendChild(deleteBtn);
|
||||||
|
|
||||||
|
tr.appendChild(tdName);
|
||||||
|
tr.appendChild(tdUrl);
|
||||||
|
tr.appendChild(tdStatus);
|
||||||
|
tr.appendChild(tdActions);
|
||||||
|
fragment.appendChild(tr);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelectorAll('.delete-blacklist-btn').forEach(btn => {
|
// 一次性添加所有行到DOM
|
||||||
btn.addEventListener('click', handleDeleteBlacklist);
|
tbody.appendChild(fragment);
|
||||||
});
|
|
||||||
|
// 如果有更多黑名单,添加提示
|
||||||
|
if (blacklists.length > maxBlacklistsToShow) {
|
||||||
|
const infoRow = document.createElement('tr');
|
||||||
|
infoRow.innerHTML = `<td colspan="4" class="py-4 text-center text-gray-500">显示前 ${maxBlacklistsToShow} 个黑名单,共 ${blacklists.length} 个</td>`;
|
||||||
|
tbody.appendChild(infoRow);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理更新单个黑名单
|
// 处理更新单个黑名单
|
||||||
@@ -289,8 +395,6 @@ async function handleAddBlacklist() {
|
|||||||
// 清空输入框
|
// 清空输入框
|
||||||
document.getElementById('blacklist-name').value = '';
|
document.getElementById('blacklist-name').value = '';
|
||||||
document.getElementById('blacklist-url').value = '';
|
document.getElementById('blacklist-url').value = '';
|
||||||
// 隐藏表单
|
|
||||||
document.getElementById('add-blacklist-form').classList.add('hidden');
|
|
||||||
// 重新加载黑名单
|
// 重新加载黑名单
|
||||||
loadRemoteBlacklists();
|
loadRemoteBlacklists();
|
||||||
// 重新加载统计信息
|
// 重新加载统计信息
|
||||||
@@ -303,162 +407,15 @@ async function handleAddBlacklist() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载hosts条目
|
|
||||||
async function loadHostsEntries() {
|
|
||||||
showLoading('加载Hosts条目...');
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/shield/hosts');
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to load hosts entries');
|
|
||||||
}
|
|
||||||
const data = await response.json();
|
|
||||||
updateHostsTable(data.hosts || []);
|
|
||||||
hideLoading();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading hosts entries:', error);
|
|
||||||
showErrorMessage('加载Hosts条目失败');
|
|
||||||
hideLoading();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新hosts表格
|
|
||||||
function updateHostsTable(hosts) {
|
|
||||||
const tbody = document.getElementById('hosts-table-body');
|
|
||||||
|
|
||||||
if (hosts.length === 0) {
|
|
||||||
tbody.innerHTML = '<tr><td colspan="3" class="py-4 text-center text-gray-500">暂无Hosts条目</td></tr>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody.innerHTML = hosts.map(entry => `
|
|
||||||
<tr class="border-b border-gray-200">
|
|
||||||
<td class="py-3 px-4">${entry.ip}</td>
|
|
||||||
<td class="py-3 px-4">${entry.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-domain="${entry.domain}">
|
|
||||||
<i class="fa fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
// 重新绑定删除事件
|
|
||||||
document.querySelectorAll('.delete-hosts-btn').forEach(btn => {
|
|
||||||
btn.addEventListener('click', handleDeleteHostsEntry);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理删除hosts条目
|
|
||||||
async function handleDeleteHostsEntry(e) {
|
|
||||||
const domain = e.target.closest('.delete-hosts-btn').dataset.domain;
|
|
||||||
showLoading('删除Hosts条目...');
|
|
||||||
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 entry');
|
|
||||||
}
|
|
||||||
|
|
||||||
showSuccessMessage('Hosts条目删除成功');
|
|
||||||
// 重新加载hosts条目
|
|
||||||
loadHostsEntries();
|
|
||||||
// 重新加载统计信息
|
|
||||||
loadShieldStats();
|
|
||||||
hideLoading();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting hosts entry:', error);
|
|
||||||
showErrorMessage('删除Hosts条目失败');
|
|
||||||
hideLoading();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理添加hosts条目
|
|
||||||
async function handleAddHostsEntry() {
|
|
||||||
const ip = document.getElementById('hosts-ip').value.trim();
|
|
||||||
const domain = document.getElementById('hosts-domain').value.trim();
|
|
||||||
|
|
||||||
if (!ip || !domain) {
|
|
||||||
showErrorMessage('IP地址和域名不能为空');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoading('添加Hosts条目...');
|
|
||||||
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 entry');
|
|
||||||
}
|
|
||||||
|
|
||||||
showSuccessMessage('Hosts条目添加成功');
|
|
||||||
// 清空输入框
|
|
||||||
document.getElementById('hosts-ip').value = '';
|
|
||||||
document.getElementById('hosts-domain').value = '';
|
|
||||||
// 隐藏表单
|
|
||||||
document.getElementById('add-hosts-form').classList.add('hidden');
|
|
||||||
// 重新加载hosts条目
|
|
||||||
loadHostsEntries();
|
|
||||||
// 重新加载统计信息
|
|
||||||
loadShieldStats();
|
|
||||||
hideLoading();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error adding hosts entry:', error);
|
|
||||||
showErrorMessage('添加Hosts条目失败');
|
|
||||||
hideLoading();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置事件监听器
|
// 设置事件监听器
|
||||||
function setupShieldEventListeners() {
|
function setupShieldEventListeners() {
|
||||||
// 本地规则管理事件
|
// 本地规则管理事件
|
||||||
document.getElementById('add-rule-btn').addEventListener('click', () => {
|
|
||||||
document.getElementById('add-rule-form').classList.toggle('hidden');
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('save-rule-btn').addEventListener('click', handleAddRule);
|
document.getElementById('save-rule-btn').addEventListener('click', handleAddRule);
|
||||||
|
|
||||||
document.getElementById('cancel-rule-btn').addEventListener('click', () => {
|
|
||||||
document.getElementById('add-rule-form').classList.add('hidden');
|
|
||||||
document.getElementById('new-rule').value = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
// 远程黑名单管理事件
|
// 远程黑名单管理事件
|
||||||
document.getElementById('add-blacklist-btn').addEventListener('click', () => {
|
|
||||||
document.getElementById('add-blacklist-form').classList.toggle('hidden');
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('save-blacklist-btn').addEventListener('click', handleAddBlacklist);
|
document.getElementById('save-blacklist-btn').addEventListener('click', handleAddBlacklist);
|
||||||
|
|
||||||
document.getElementById('cancel-blacklist-btn').addEventListener('click', () => {
|
|
||||||
document.getElementById('add-blacklist-form').classList.add('hidden');
|
|
||||||
document.getElementById('blacklist-name').value = '';
|
|
||||||
document.getElementById('blacklist-url').value = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Hosts条目管理事件
|
|
||||||
document.getElementById('add-hosts-btn').addEventListener('click', () => {
|
|
||||||
document.getElementById('add-hosts-form').classList.toggle('hidden');
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('save-hosts-btn').addEventListener('click', handleAddHostsEntry);
|
|
||||||
|
|
||||||
document.getElementById('cancel-hosts-btn').addEventListener('click', () => {
|
|
||||||
document.getElementById('add-hosts-form').classList.add('hidden');
|
|
||||||
document.getElementById('hosts-ip').value = '';
|
|
||||||
document.getElementById('hosts-domain').value = '';
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示成功消息
|
// 显示成功消息
|
||||||
|
|||||||
Reference in New Issue
Block a user