From 72aa2846e574076d588da94549d2383e7df67bb2 Mon Sep 17 00:00:00 2001 From: Alex Yang Date: Sun, 30 Nov 2025 11:44:26 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E7=99=BB=E5=BD=95=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.json | 8 +- config/config.go | 9 ++ http/server.go | 222 +++++++++++++++++++++++++++++++++++++++------- main.go | 6 +- server.log | 41 +++++++++ static/js/api.js | 7 ++ static/login.html | 186 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 441 insertions(+), 38 deletions(-) create mode 100644 server.log create mode 100644 static/login.html diff --git a/config.json b/config.json index 13faec7..80a0ff9 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,6 @@ { "dns": { - "port": 53, + "port": 5353, "upstreamDNS": [ "223.5.5.5:53", "223.6.6.6:53" @@ -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", @@ -98,7 +100,7 @@ "enabled": true } ], - "updateInterval": 30, + "updateInterval": 3600, "hostsFile": "data/hosts.txt", "blockMethod": "NXDOMAIN", "customBlockIP": "", diff --git a/config/config.go b/config/config.go index 8994012..33032a0 100644 --- a/config/config.go +++ b/config/config.go @@ -19,6 +19,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 黑名单条目 @@ -92,6 +94,13 @@ func LoadConfig(path string) (*Config, error) { 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 } diff --git a/http/server.go b/http/server.go index dff0967..879a37e 100644 --- a/http/server.go +++ b/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,26 @@ 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/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 +99,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,45 +108,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.handleLogsStats) - mux.HandleFunc("/api/logs/query", s.handleLogsQuery) - mux.HandleFunc("/api/logs/count", s.handleLogsCount) + 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), @@ -378,6 +409,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 { @@ -1312,3 +1415,56 @@ 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)) +} diff --git a/main.go b/main.go index 7c3cf6c..bb5d828 100644 --- a/main.go +++ b/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", diff --git a/server.log b/server.log new file mode 100644 index 0000000..64307d1 --- /dev/null +++ b/server.log @@ -0,0 +1,41 @@ +2025/11/30 11:09:05 正在创建所需的文件和文件夹... +2025/11/30 11:09:05 所需文件和文件夹创建成功 +time="2025-11-30T11:09:05+08:00" level=debug msg="尝试加载Shield统计数据" file=/root/dnsbak/data/shield_stats.json +time="2025-11-30T11:09:05+08:00" level=info msg="Shield计数数据加载成功" blocked_entries=0 resolved_entries=0 +time="2025-11-30T11:09:05+08:00" level=info msg="从缓存加载远程规则" url="https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/filter.txt" +time="2025-11-30T11:09:05+08:00" level=info msg="从缓存加载远程规则" url="https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/adaway.txt" +time="2025-11-30T11:09:06+08:00" level=info msg="从缓存加载远程规则" url="https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/list/easylist.txt" +time="2025-11-30T11:09:06+08:00" level=info msg="从缓存加载远程规则" url="https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/costomize.txt" +time="2025-11-30T11:09:06+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/dsjh.txt" +time="2025-11-30T11:09:06+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hate-and-junk-extended.txt" +time="2025-11-30T11:09:06+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hosts/costomize.txt" +time="2025-11-30T11:09:06+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hosts/anti-remoterequests.txt" +time="2025-11-30T11:09:07+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/url-based-adguard.txt" +time="2025-11-30T11:09:07+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/ads-and-trackers.txt" +time="2025-11-30T11:09:08+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/malware.txt" +time="2025-11-30T11:09:09+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/costomize.txt" +time="2025-11-30T11:09:09+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/rules/AWAvenue-Ads-Rule.txt" +time="2025-11-30T11:09:09+08:00" level=info msg="从缓存加载远程规则" url="https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/cheat.txt" +time="2025-11-30T11:09:10+08:00" level=info msg="规则加载完成,域名规则: 189895, 排除规则: 653, 正则规则: 24094, hosts规则: 0" +time="2025-11-30T11:09:10+08:00" level=info msg="统计数据加载成功" +time="2025-11-30T11:09:10+08:00" level=info msg="查询日志加载成功" count=8608 +time="2025-11-30T11:09:10+08:00" level=info msg="DNS服务器已启动,监听端口: 5353" +time="2025-11-30T11:09:10+08:00" level=info msg="HTTP控制台已启动,监听端口: 8081" +time="2025-11-30T11:09:10+08:00" level=info msg="DNS TCP服务器启动,监听端口: 5353" +time="2025-11-30T11:09:10+08:00" level=info msg="启动Shield计数数据自动保存功能" file=./data/shield_stats.json interval=60 +time="2025-11-30T11:09:10+08:00" level=info msg="HTTP控制台服务器启动,监听地址: 0.0.0.0:8081" +time="2025-11-30T11:09:10+08:00" level=info msg="规则自动更新已启动" interval=3600 +time="2025-11-30T11:09:10+08:00" level=info msg="DNS UDP服务器启动,监听端口: 5353" +time="2025-11-30T11:09:10+08:00" level=info msg="启动统计数据自动保存功能" file=data/stats.json interval=300 +time="2025-11-30T11:09:10+08:00" level=error msg="DNS UDP服务器启动失败" error="listen udp :5353: bind: address already in use" +time="2025-11-30T11:09:10+08:00" level=info msg="Shield计数数据保存成功" blocked_entries=0 file=/root/dnsbak/data/shield_stats.json resolved_entries=0 +2025/11/30 11:09:18 正在关闭服务... +time="2025-11-30T11:09:18+08:00" level=info msg="统计数据保存成功" file=/root/dnsbak/data/stats.json +time="2025-11-30T11:09:18+08:00" level=info msg="查询日志保存成功" file=/root/dnsbak/data/querylog.json +time="2025-11-30T11:09:18+08:00" level=info msg="DNS服务器已停止" +time="2025-11-30T11:09:18+08:00" level=error msg="HTTP控制台服务器启动失败" error="http: Server closed" +time="2025-11-30T11:09:18+08:00" level=info msg="HTTP控制台服务器已停止" +time="2025-11-30T11:09:18+08:00" level=info msg="Shield计数数据保存成功" blocked_entries=0 file=/root/dnsbak/data/shield_stats.json resolved_entries=0 +time="2025-11-30T11:09:18+08:00" level=info msg="规则自动更新已停止" +2025/11/30 11:09:18 服务已关闭 +time="2025-11-30T11:09:18+08:00" level=warning msg="日志系统已关闭" diff --git a/static/js/api.js b/static/js/api.js index 9ae67f9..d8f5bbd 100644 --- a/static/js/api.js +++ b/static/js/api.js @@ -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); diff --git a/static/login.html b/static/login.html new file mode 100644 index 0000000..c0ffa10 --- /dev/null +++ b/static/login.html @@ -0,0 +1,186 @@ + + + + + + DNS服务器控制台 - 登录 + + + +
+ + +
+ +
+
+ + +
+ +
+ + +
+ + +
+
+ + + + \ No newline at end of file