This commit is contained in:
Alex Yang
2025-11-23 19:35:02 +08:00
parent d870f6bfcc
commit 5a1d44c3b3
10 changed files with 4783 additions and 17 deletions

View File

@@ -14,7 +14,7 @@
"remoteRules": [ "remoteRules": [
"https://example.com/rules.txt" "https://example.com/rules.txt"
], ],
"updateInterval": 3600, "updateInterval": 60,
"hostsFile": "data/hosts.txt" "hostsFile": "data/hosts.txt"
}, },
"log": { "log": {

View File

@@ -31,6 +31,7 @@ type ShieldConfig struct {
CustomBlockIP string `json:"customBlockIP"` // 自定义屏蔽IP当BlockMethod为"customIP"时使用 CustomBlockIP string `json:"customBlockIP"` // 自定义屏蔽IP当BlockMethod为"customIP"时使用
StatsFile string `json:"statsFile"` // 计数数据持久化文件 StatsFile string `json:"statsFile"` // 计数数据持久化文件
StatsSaveInterval int `json:"statsSaveInterval"` // 计数数据保存间隔(秒) StatsSaveInterval int `json:"statsSaveInterval"` // 计数数据保存间隔(秒)
RemoteRulesCacheDir string `json:"remoteRulesCacheDir"` // 远程规则缓存目录
} }
// LogConfig 日志配置 // LogConfig 日志配置
@@ -94,6 +95,9 @@ func LoadConfig(path string) (*Config, error) {
if config.Shield.StatsSaveInterval == 0 { if config.Shield.StatsSaveInterval == 0 {
config.Shield.StatsSaveInterval = 300 // 默认5分钟保存一次 config.Shield.StatsSaveInterval = 300 // 默认5分钟保存一次
} }
if config.Shield.RemoteRulesCacheDir == "" {
config.Shield.RemoteRulesCacheDir = "./data/remote_rules" // 默认远程规则缓存目录
}
if config.Log.Level == "" { if config.Log.Level == "" {
config.Log.Level = "info" config.Log.Level = "info"
} }

View File

@@ -0,0 +1,5 @@
{
"blockedDomainsCount": {},
"resolvedDomainsCount": {},
"lastSaved": "2025-11-23T19:30:41.791142096+08:00"
}

View File

@@ -1,10 +1,10 @@
{ {
"stats": { "stats": {
"Queries": 33, "Queries": 34,
"Blocked": 24, "Blocked": 24,
"Allowed": 19, "Allowed": 20,
"Errors": 2, "Errors": 2,
"LastQuery": "2025-11-23T19:06:10.694259822+08:00" "LastQuery": "2025-11-23T19:28:57.615407202+08:00"
}, },
"blockedDomains": { "blockedDomains": {
"ad.qq.com": { "ad.qq.com": {
@@ -29,6 +29,11 @@
"Count": 8, "Count": 8,
"LastSeen": "2025-11-23T19:05:54.356216418+08:00" "LastSeen": "2025-11-23T19:05:54.356216418+08:00"
}, },
"apd-pcdnwxlogin.teg.tencent-cloud.net": {
"Domain": "apd-pcdnwxlogin.teg.tencent-cloud.net",
"Count": 1,
"LastSeen": "2025-11-23T19:28:57.621471084+08:00"
},
"apd-pcdnwxstat.teg.tencent-cloud.net": { "apd-pcdnwxstat.teg.tencent-cloud.net": {
"Domain": "apd-pcdnwxstat.teg.tencent-cloud.net", "Domain": "apd-pcdnwxstat.teg.tencent-cloud.net",
"Count": 1, "Count": 1,
@@ -38,5 +43,5 @@
"hourlyStats": { "hourlyStats": {
"2025-11-23-19": 12 "2025-11-23-19": 12
}, },
"lastSaved": "2025-11-23T19:06:21.194988376+08:00" "lastSaved": "2025-11-23T19:30:24.828634638+08:00"
} }

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -373,11 +373,13 @@ func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
switch r.Method { switch r.Method {
case http.MethodGet: case http.MethodGet:
// 返回当前配置(只返回必要的部分 // 返回当前配置(包括远程规则相关配置
config := map[string]interface{}{ config := map[string]interface{}{
"shield": map[string]string{ "shield": map[string]interface{}{
"blockMethod": s.globalConfig.Shield.BlockMethod, "blockMethod": s.globalConfig.Shield.BlockMethod,
"customBlockIP": s.globalConfig.Shield.CustomBlockIP, "customBlockIP": s.globalConfig.Shield.CustomBlockIP,
"remoteRules": s.globalConfig.Shield.RemoteRules,
"updateInterval": s.globalConfig.Shield.UpdateInterval,
}, },
} }
json.NewEncoder(w).Encode(config) json.NewEncoder(w).Encode(config)
@@ -388,6 +390,8 @@ func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
Shield struct { Shield struct {
BlockMethod string `json:"blockMethod"` BlockMethod string `json:"blockMethod"`
CustomBlockIP string `json:"customBlockIP"` CustomBlockIP string `json:"customBlockIP"`
RemoteRules []string `json:"remoteRules"`
UpdateInterval int `json:"updateInterval"`
} `json:"shield"` } `json:"shield"`
} }
@@ -431,6 +435,23 @@ func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
s.globalConfig.Shield.CustomBlockIP = req.Shield.CustomBlockIP s.globalConfig.Shield.CustomBlockIP = req.Shield.CustomBlockIP
} }
// 更新远程规则列表
if req.Shield.RemoteRules != nil {
s.globalConfig.Shield.RemoteRules = req.Shield.RemoteRules
// 重新加载规则
if err := s.shieldManager.LoadRules(); err != nil {
logger.Error("重新加载规则失败", "error", err)
}
}
// 更新更新间隔
if req.Shield.UpdateInterval > 0 {
s.globalConfig.Shield.UpdateInterval = req.Shield.UpdateInterval
// 重新启动自动更新
s.shieldManager.StopAutoUpdate()
s.shieldManager.StartAutoUpdate()
}
// 返回成功响应 // 返回成功响应
json.NewEncoder(w).Encode(map[string]interface{}{ json.NewEncoder(w).Encode(map[string]interface{}{
"success": true, "success": true,

View File

@@ -141,15 +141,65 @@ func (m *ShieldManager) loadRemoteRules() error {
return nil return nil
} }
// getCacheFilePath 根据URL生成缓存文件路径
func (m *ShieldManager) getCacheFilePath(url string) string {
// 使用URL的哈希值作为文件名避免文件名冲突
hash := fmt.Sprintf("%x", url)
// 简单处理,移除特殊字符,确保文件名合法
hash = strings.ReplaceAll(hash, "/", "_")
hash = strings.ReplaceAll(hash, "\\", "_")
return filepath.Join(m.config.RemoteRulesCacheDir, hash+".rules")
}
// shouldUpdateCache 检查缓存是否需要更新
func (m *ShieldManager) shouldUpdateCache(cacheFile string) bool {
// 检查文件是否存在
if _, err := os.Stat(cacheFile); os.IsNotExist(err) {
return true
}
// 检查文件修改时间
fileInfo, err := os.Stat(cacheFile)
if err != nil {
return true
}
// 如果缓存文件超过更新间隔时间,则需要更新
return time.Since(fileInfo.ModTime()) > time.Duration(m.config.UpdateInterval)*time.Second
}
// fetchRemoteRules 从远程URL获取规则 // fetchRemoteRules 从远程URL获取规则
func (m *ShieldManager) fetchRemoteRules(url string) error { func (m *ShieldManager) fetchRemoteRules(url string) error {
// 获取缓存文件路径
cacheFile := m.getCacheFilePath(url)
// 尝试从缓存加载
hasLoadedFromCache := false
if !m.shouldUpdateCache(cacheFile) {
if err := m.loadCachedRules(cacheFile); err == nil {
logger.Info("从缓存加载远程规则", "url", url)
hasLoadedFromCache = true
}
}
// 从远程获取规则
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { if err != nil {
// 如果从远程获取失败但已经从缓存加载成功则返回nil
if hasLoadedFromCache {
logger.Warn("远程规则更新失败,使用缓存版本", "url", url, "error", err)
return nil
}
return err return err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
// 如果状态码不正确但已经从缓存加载成功则返回nil
if hasLoadedFromCache {
logger.Warn("远程规则更新失败,使用缓存版本", "url", url, "statusCode", resp.StatusCode)
return nil
}
return fmt.Errorf("远程服务器返回错误状态码: %d", resp.StatusCode) return fmt.Errorf("远程服务器返回错误状态码: %d", resp.StatusCode)
} }
@@ -158,6 +208,38 @@ func (m *ShieldManager) fetchRemoteRules(url string) error {
return err return err
} }
// 保存规则到缓存
if err := m.saveRemoteRulesToCache(cacheFile, body); err != nil {
logger.Warn("保存远程规则缓存失败", "url", url, "error", err)
// 继续处理,不返回错误
}
// 解析并加载规则
lines := strings.Split(string(body), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
m.parseRule(line)
}
return nil
}
// loadCachedRules 从缓存文件加载规则
func (m *ShieldManager) loadCachedRules(filePath string) error {
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
body, err := ioutil.ReadAll(file)
if err != nil {
return err
}
lines := strings.Split(string(body), "\n") lines := strings.Split(string(body), "\n")
for _, line := range lines { for _, line := range lines {
line = strings.TrimSpace(line) line = strings.TrimSpace(line)
@@ -170,6 +252,17 @@ func (m *ShieldManager) fetchRemoteRules(url string) error {
return nil return nil
} }
// saveRemoteRulesToCache 保存远程规则到缓存文件
func (m *ShieldManager) saveRemoteRulesToCache(filePath string, data []byte) error {
// 确保缓存目录存在
if err := os.MkdirAll(m.config.RemoteRulesCacheDir, 0755); err != nil {
return err
}
// 写入文件
return ioutil.WriteFile(filePath, data, 0644)
}
// loadHosts 加载hosts文件 // loadHosts 加载hosts文件
func (m *ShieldManager) loadHosts() error { func (m *ShieldManager) loadHosts() error {
if m.config.HostsFile == "" { if m.config.HostsFile == "" {

View File

@@ -312,16 +312,29 @@
background-color: #c0392b; background-color: #c0392b;
} }
.btn-outline { .btn-outline { background-color: transparent; color: var(--primary-color); border: 1px solid var(--primary-color); }
background-color: transparent;
color: var(--primary-color);
border: 1px solid var(--primary-color);
}
.btn-outline:hover { .btn-outline:hover { background-color: var(--primary-color); color: white; }
background-color: var(--primary-color);
color: white; .btn-secondary { background-color: var(--secondary-color); color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; }
}
.btn-secondary:hover { background-color: #1a252f; transform: translateY(-1px); box-shadow: var(--shadow-md); }
.btn-sm { padding: 4px 8px; font-size: 0.875rem; }
.form-label { display: block; margin-bottom: 0.5rem; font-weight: 500; }
.form-text { display: block; margin-top: 0.25rem; font-size: 0.875rem; color: var(--gray-600); }
.mb-3 { margin-bottom: 1rem; }
.mt-3 { margin-top: 1rem; }
.rule-items { display: flex; flex-direction: column; gap: 0.5rem; }
.rule-item { display: flex; align-items: center; justify-content: space-between; padding: 0.75rem; background-color: var(--gray-50); border: 1px solid var(--gray-200); border-radius: 4px; }
.rule-text { flex: 1; word-break: break-all; margin-right: 0.5rem; }
/* 悬浮通知样式 */ /* 悬浮通知样式 */
.notification { .notification {
@@ -800,6 +813,39 @@
</small> </small>
</div> </div>
</div> </div>
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-globe"></i> 远程规则管理</h3>
</div>
<div class="card-body">
<div class="mb-3">
<label for="update-interval" class="form-label">更新间隔 (秒)</label>
<input type="number" id="update-interval" min="60" max="86400" placeholder="3600">
<small class="form-text">远程规则自动更新的时间间隔建议至少60秒</small>
</div>
<div class="mb-3">
<div class="input-group">
<input type="text" id="remote-rule-url" placeholder="输入远程规则URL">
<button id="add-remote-rule" class="btn-secondary">
<i class="fas fa-plus"></i> 添加规则
</button>
</div>
</div>
<div class="list-container" id="remote-rules-list">
<div class="empty-state">
<i class="fas fa-info-circle"></i>
<p>加载中...</p>
</div>
</div>
<button id="save-remote-settings" class="btn-primary mt-3">
<i class="fas fa-save"></i> 保存远程设置
</button>
</div>
</div>
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h3 class="card-title"><i class="fas fa-plus-circle"></i> 添加屏蔽规则</h3> <h3 class="card-title"><i class="fas fa-plus-circle"></i> 添加屏蔽规则</h3>
@@ -1719,6 +1765,13 @@ function loadRules() {
// 加载当前屏蔽设置 // 加载当前屏蔽设置
loadBlockSettings(); loadBlockSettings();
// 远程规则相关事件监听
document.getElementById('add-remote-rule').addEventListener('click', addRemoteRule);
document.getElementById('save-remote-settings').addEventListener('click', saveRemoteSettings);
// 加载远程规则设置
loadRemoteSettings();
}; };
// 加载当前屏蔽设置 // 加载当前屏蔽设置
@@ -1782,6 +1835,169 @@ function loadRules() {
}); });
} }
// 加载远程规则设置
function loadRemoteSettings() {
fetch('/api/config', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.shield) {
// 设置更新间隔
document.getElementById('update-interval').value = data.shield.updateInterval || 3600;
// 加载远程规则列表
renderRemoteRulesList(data.shield.remoteRules || []);
}
})
.catch(error => {
console.error('加载远程规则设置失败:', error);
showNotification('danger', '加载远程规则设置失败');
renderRemoteRulesList([]);
});
}
// 渲染远程规则列表
function renderRemoteRulesList(rules) {
const listContainer = document.getElementById('remote-rules-list');
if (rules.length === 0) {
listContainer.innerHTML = `
<div class="empty-state">
<i class="fas fa-info-circle"></i>
<p>暂无远程规则</p>
</div>
`;
return;
}
let html = '<div class="list-container">';
rules.forEach((rule, index) => {
html += `
<div class="list-item">
<div class="list-content">
<div class="list-title">远程规则 ${index + 1}</div>
<div class="list-description">${rule}</div>
</div>
<div class="list-actions">
<span class="badge badge-primary">远程</span>
<button class="btn-danger btn-sm delete-rule" data-index="${index}">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</div>
`;
});
html += '</div>';
listContainer.innerHTML = html;
// 添加删除按钮事件监听
document.querySelectorAll('.delete-rule').forEach(btn => {
btn.addEventListener('click', function() {
const index = parseInt(this.getAttribute('data-index'));
deleteRemoteRule(index);
});
});
}
// 添加远程规则
function addRemoteRule() {
const urlInput = document.getElementById('remote-rule-url');
const url = urlInput.value.trim();
if (!url) {
showNotification('warning', '请输入有效的URL');
return;
}
// 简单的URL验证
try {
new URL(url);
} catch (e) {
showNotification('warning', '请输入有效的URL格式');
return;
}
// 获取当前列表中的规则
const ruleItems = document.querySelectorAll('.list-item');
const rules = Array.from(ruleItems).map(item =>
item.querySelector('.list-description').textContent
);
// 检查是否已存在
if (rules.includes(url)) {
showNotification('warning', '该规则已存在');
return;
}
// 添加新规则
rules.push(url);
renderRemoteRulesList(rules);
urlInput.value = '';
showNotification('success', '规则已添加');
}
// 删除远程规则
function deleteRemoteRule(index) {
const ruleItems = document.querySelectorAll('.list-item');
const rules = Array.from(ruleItems).map(item =>
item.querySelector('.list-description').textContent
);
// 移除指定索引的规则
rules.splice(index, 1);
renderRemoteRulesList(rules);
showNotification('success', '规则已删除');
}
// 保存远程规则设置
function saveRemoteSettings() {
const updateInterval = parseInt(document.getElementById('update-interval').value);
// 验证更新间隔
if (isNaN(updateInterval) || updateInterval < 60) {
showNotification('warning', '更新间隔必须大于等于60秒');
return;
}
// 获取当前列表中的规则
const ruleItems = document.querySelectorAll('.list-item');
const remoteRules = Array.from(ruleItems).map(item =>
item.querySelector('.list-description').textContent
);
fetch('/api/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
shield: {
remoteRules: remoteRules,
updateInterval: updateInterval
}
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('success', '远程规则设置已保存');
} else {
showNotification('danger', '保存失败: ' + (data.error || '未知错误'));
}
})
.catch(error => {
console.error('保存远程规则设置失败:', error);
showNotification('danger', '保存远程规则设置失败: ' + error.message);
});
}
// 显示悬浮通知 // 显示悬浮通知
function showNotification(type, message) { function showNotification(type, message) {
// 创建通知元素 // 创建通知元素