update once

This commit is contained in:
XOF
2026-01-06 02:25:24 +08:00
commit 7bf4f27be3
25 changed files with 4587 additions and 0 deletions

205
internal/web/handler.go Normal file
View File

@@ -0,0 +1,205 @@
package web
import (
"crypto/subtle"
"embed"
"encoding/json"
"io/fs"
"net/http"
"godns/internal/stats"
"godns/pkg/logger"
)
//go:embed static/*
var staticFiles embed.FS
// Handler Web服务处理器
type Handler struct {
stats stats.StatsRecorder
version string
checkUpdateCh chan<- struct{}
logger logger.Logger
username string
password string
}
// NewHandler 创建Web处理器
func NewHandler(s stats.StatsRecorder, ver string, checkCh chan<- struct{}, log logger.Logger, username, password string) *Handler {
return &Handler{
stats: s,
version: ver,
checkUpdateCh: checkCh,
logger: log,
username: username,
password: password,
}
}
// basicAuth 中间件
func (h *Handler) basicAuth(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 如果未配置鉴权,直接放行
if h.username == "" || h.password == "" {
next(w, r)
return
}
user, pass, ok := r.BasicAuth()
if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(h.username)) != 1 ||
subtle.ConstantTimeCompare([]byte(pass), []byte(h.password)) != 1 {
w.Header().Set("WWW-Authenticate", `Basic realm="NBDNS Monitor"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next(w, r)
}
}
// RegisterRoutes 注册路由
func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
// API路由
mux.HandleFunc("/api/stats", h.basicAuth(h.handleStats))
mux.HandleFunc("/api/version", h.basicAuth(h.handleVersion))
mux.HandleFunc("/api/check-update", h.basicAuth(h.handleCheckUpdate))
mux.HandleFunc("/api/stats/reset", h.basicAuth(h.handleStatsReset))
// 静态文件服务
staticFS, err := fs.Sub(staticFiles, "static")
if err != nil {
h.logger.Printf("Failed to load static files: %v", err)
return
}
mux.Handle("/", h.basicAuth(func(w http.ResponseWriter, r *http.Request) {
http.FileServer(http.FS(staticFS)).ServeHTTP(w, r)
}))
}
// handleStats 处理统计信息请求
func (h *Handler) handleStats(w http.ResponseWriter, r *http.Request) {
// 只允许GET请求
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 获取统计快照
snapshot := h.stats.GetSnapshot()
// 设置响应头
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
// 编码JSON并返回
if err := json.NewEncoder(w).Encode(snapshot); err != nil {
h.logger.Printf("Error encoding stats JSON: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
}
// ResetResponse 重置响应
type ResetResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
}
// handleStatsReset 处理统计数据重置请求
func (h *Handler) handleStatsReset(w http.ResponseWriter, r *http.Request) {
// 只允许POST请求
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 重置统计数据
h.stats.Reset()
h.logger.Printf("Statistics reset by user request")
// 设置响应头
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
// 返回成功响应
if err := json.NewEncoder(w).Encode(ResetResponse{
Success: true,
Message: "统计数据已重置",
}); err != nil {
h.logger.Printf("Error encoding reset response JSON: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
}
// VersionResponse 版本信息响应
type VersionResponse struct {
Version string `json:"version"`
}
// handleVersion 处理版本查询请求
func (h *Handler) handleVersion(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
ver := h.version
if ver == "" {
ver = "0.0.0"
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
if err := json.NewEncoder(w).Encode(VersionResponse{Version: ver}); err != nil {
h.logger.Printf("Error encoding version JSON: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
}
// UpdateCheckResponse 更新检查响应
type UpdateCheckResponse struct {
HasUpdate bool `json:"has_update"`
CurrentVersion string `json:"current_version"`
LatestVersion string `json:"latest_version"`
Message string `json:"message"`
}
// handleCheckUpdate 处理检查更新请求生产者2用户手动触发
func (h *Handler) handleCheckUpdate(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
ver := h.version
if ver == "" {
ver = "0.0.0"
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
// 触发后台检查更新(非阻塞)
select {
case h.checkUpdateCh <- struct{}{}:
h.logger.Printf("Update check triggered by user")
json.NewEncoder(w).Encode(UpdateCheckResponse{
HasUpdate: false,
CurrentVersion: ver,
LatestVersion: ver,
Message: "已触发更新检查,请查看服务器日志",
})
default:
// 如果通道已满,说明已经在检查中
json.NewEncoder(w).Encode(UpdateCheckResponse{
HasUpdate: false,
CurrentVersion: ver,
LatestVersion: ver,
Message: "更新检查正在进行中",
})
}
}