实现登录功能
This commit is contained in:
@@ -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": "",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
224
http/server.go
224
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()
|
||||
|
||||
// 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)
|
||||
// 登录路由,不需要认证
|
||||
mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
|
||||
// 重定向到登录页面HTML
|
||||
http.Redirect(w, r, "/login.html", http.StatusFound)
|
||||
})
|
||||
|
||||
// 注册所有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路由
|
||||
if s.config.EnableAPI {
|
||||
// 登录API端点,不需要认证
|
||||
mux.HandleFunc("/api/login", s.handleLogin)
|
||||
|
||||
// 重定向/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))
|
||||
}
|
||||
|
||||
6
main.go
6
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",
|
||||
|
||||
41
server.log
Normal file
41
server.log
Normal file
@@ -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="日志系统已关闭"
|
||||
@@ -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);
|
||||
|
||||
186
static/login.html
Normal file
186
static/login.html
Normal file
@@ -0,0 +1,186 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DNS服务器控制台 - 登录</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: #f5f7fa;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
padding: 40px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
font-size: 24px;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
color: #7f8c8d;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #e1e5e9;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #3498db;
|
||||
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.1);
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: #2980b9;
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background-color: #fee;
|
||||
color: #c00;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loading {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-header">
|
||||
<h1>DNS服务器控制台</h1>
|
||||
<p>请输入您的登录凭据</p>
|
||||
</div>
|
||||
|
||||
<div class="error-message" id="errorMessage"></div>
|
||||
|
||||
<form id="loginForm">
|
||||
<div class="form-group">
|
||||
<label for="username">用户名</label>
|
||||
<input type="text" id="username" name="username" placeholder="请输入用户名" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<input type="password" id="password" name="password" placeholder="请输入密码" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn" id="loginBtn">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('loginForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
const loginBtn = document.getElementById('loginBtn');
|
||||
const errorMessage = document.getElementById('errorMessage');
|
||||
|
||||
// 显示加载状态
|
||||
loginBtn.textContent = '登录中...';
|
||||
loginBtn.classList.add('loading');
|
||||
errorMessage.style.display = 'none';
|
||||
|
||||
// 发送登录请求
|
||||
fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
password: password
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('登录失败');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
// 登录成功,重定向到主页
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
throw new Error(data.error || '登录失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// 显示错误信息
|
||||
errorMessage.textContent = error.message;
|
||||
errorMessage.style.display = 'block';
|
||||
loginBtn.textContent = '登录';
|
||||
loginBtn.classList.remove('loading');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user