package handler import ( "context" "database/sql" "encoding/base64" "encoding/json" "errors" "fmt" "io" "log" "net/http" "net/url" "sort" "strconv" "strings" "sync" "time" "go-backend/internal/auth" "go-backend/internal/health" "go-backend/internal/http/middleware" "go-backend/internal/http/response" "go-backend/internal/metrics" "go-backend/internal/monitoring" runtimenft "go-backend/internal/runtime/nftables" "go-backend/internal/security" "go-backend/internal/store/repo" "go-backend/internal/ws" "github.com/google/uuid" ) type Handler struct { repo *repo.Repository jwtSecret string wsServer *ws.Server metrics *metrics.IngestionService healthCheck *health.Checker nftablesManager nftablesRuntimeManager captchaMu sync.Mutex captchaTokens map[string]int64 jobsMu sync.Mutex jobsCancel context.CancelFunc jobsStarted bool jobsWG sync.WaitGroup fingerprintMu sync.Mutex licenseValidationMu sync.Mutex upgradeMu sync.Mutex systemUpgradeMu sync.Mutex pendingUpgradeRedeploy map[int64]struct{} nodeOnlineRedeployAt map[int64]time.Time nodeOnlineRedeployQueued map[int64]struct{} nodeOnlineRedeploying map[int64]struct{} qualityProber *tunnelQualityProber bestExit *bestExitManager } const monitorTunnelQualityEnabledConfigKey = "monitor_tunnel_quality_enabled" const allowLocalRemoteAddrConfigKey = "allow_local_remote_addr" type loginRequest struct { Username string `json:"username"` Password string `json:"password"` CaptchaID string `json:"captchaId"` } type captchaVerifyRequest struct { ID string `json:"id"` Data string `json:"data"` } type nameRequest struct { Name string `json:"name"` } type configSingleRequest struct { Name string `json:"name"` Value string `json:"value"` } type licenseActivateRequest struct { LicenseKey string `json:"license_key"` } type changePasswordRequest struct { NewUsername string `json:"newUsername"` CurrentPassword string `json:"currentPassword"` NewPassword string `json:"newPassword"` ConfirmPassword string `json:"confirmPassword"` } type flowItem struct { N string `json:"n"` U int64 `json:"u"` D int64 `json:"d"` } const ( pngDataURLPrefix = "data:image/png;base64," maxBrandAssetDataURLBytes = 1024 * 1024 ) func New(repo *repo.Repository, jwtSecret string) *Handler { h := &Handler{ repo: repo, jwtSecret: jwtSecret, wsServer: ws.NewServer(repo, jwtSecret), metrics: metrics.NewIngestionService(repo), healthCheck: nil, nftablesManager: runtimenft.NewManager(nil), captchaTokens: make(map[string]int64), pendingUpgradeRedeploy: make(map[int64]struct{}), nodeOnlineRedeployAt: make(map[int64]time.Time), nodeOnlineRedeployQueued: make(map[int64]struct{}), nodeOnlineRedeploying: make(map[int64]struct{}), bestExit: newBestExitManager(), } h.healthCheck = health.NewChecker(repo, h.wsServer) h.qualityProber = newTunnelQualityProber(h) h.wsServer.SetNodeOnlineHook(h.onNodeOnline) h.wsServer.SetNodeMetricHook(func(nodeID int64, info ws.SystemInfo) { metricInfo := metrics.SystemInfo{ Uptime: info.Uptime, BytesReceived: info.BytesReceived, BytesTransmitted: info.BytesTransmitted, CPUUsage: info.CPUUsage, MemoryUsage: info.MemoryUsage, DiskUsage: info.DiskUsage, Load1: info.Load1, Load5: info.Load5, Load15: info.Load15, TCPConns: info.TCPConns, UDPConns: info.UDPConns, NetInSpeed: info.NetInSpeed, NetOutSpeed: info.NetOutSpeed, } h.metrics.RecordNodeMetric(nodeID, metricInfo) }) h.wsServer.SetUserAuthStateLookup(h.GetUserAuthState) return h } func (h *Handler) WebSocketHandler() http.Handler { return h.wsServer } func (h *Handler) GetUserAuthState(userID int64) (*auth.UserAuthState, error) { return h.repo.GetUserAuthState(userID) } func (h *Handler) Register(mux *http.ServeMux) { mux.HandleFunc("/api/v1/user/login", h.login) mux.HandleFunc("/api/v1/user/list", h.userList) mux.HandleFunc("/api/v1/user/create", h.userCreate) mux.HandleFunc("/api/v1/user/update", h.userUpdate) mux.HandleFunc("/api/v1/user/delete", h.userDelete) mux.HandleFunc("/api/v1/user/reset", h.userResetFlow) mux.HandleFunc("/api/v1/user/quota/reset", h.userQuotaReset) mux.HandleFunc("/api/v1/user/groups", h.userGroups) mux.HandleFunc("/api/v1/public/config/get", h.getPublicConfigByName) mux.HandleFunc("/api/v1/config/get", h.getConfigByName) mux.HandleFunc("/api/v1/config/list", h.getConfigs) mux.HandleFunc("/api/v1/config/update", h.updateConfigs) mux.HandleFunc("/api/v1/config/update-single", h.updateSingleConfig) mux.HandleFunc("/api/v1/system/storage", h.storageSummary) mux.HandleFunc("/api/v1/system/version", h.systemVersion) mux.HandleFunc("/api/v1/system/check-updates", h.systemCheckUpdates) mux.HandleFunc("/api/v1/system/upgrade", h.systemUpgrade) mux.HandleFunc("/api/v1/license/activate", h.licenseActivate) mux.HandleFunc("/api/v1/backup/export", h.backupExport) mux.HandleFunc("/api/v1/backup/import", h.backupImport) mux.HandleFunc("/api/v1/backup/restore", h.backupImport) mux.HandleFunc("/api/v1/api/v1/backup/export", h.backupExport) mux.HandleFunc("/api/v1/api/v1/backup/import", h.backupImport) mux.HandleFunc("/api/v1/api/v1/backup/restore", h.backupImport) mux.HandleFunc("/api/v1/captcha/check", h.checkCaptcha) mux.HandleFunc("/api/v1/captcha/verify", h.captchaVerify) mux.HandleFunc("/api/v1/user/package", h.userPackage) mux.HandleFunc("/api/v1/user/updatePassword", h.updatePassword) mux.HandleFunc("/api/v1/node/list", h.nodeList) mux.HandleFunc("/api/v1/node/create", h.nodeCreate) mux.HandleFunc("/api/v1/node/update", h.nodeUpdate) mux.HandleFunc("/api/v1/node/delete", h.nodeDelete) mux.HandleFunc("/api/v1/node/install", h.nodeInstall) mux.HandleFunc("/api/v1/node/update-order", h.nodeUpdateOrder) mux.HandleFunc("/api/v1/node/dismiss-expiry-reminder", h.nodeDismissExpiryReminder) mux.HandleFunc("/api/v1/node/batch-delete", h.nodeBatchDelete) mux.HandleFunc("/api/v1/node/check-status", h.nodeCheckStatus) mux.HandleFunc("/api/v1/node/upgrade", h.nodeUpgrade) mux.HandleFunc("/api/v1/node/batch-upgrade", h.nodeBatchUpgrade) mux.HandleFunc("/api/v1/node/rollback", h.nodeRollback) mux.HandleFunc("/api/v1/node/releases", h.listReleases) mux.HandleFunc("/api/v1/node/nftables/test", h.nodeNftablesTest) mux.HandleFunc("/api/v1/node/nftables/reconcile", h.nodeNftablesReconcile) mux.HandleFunc("/api/v1/node/nftables/clear", h.nodeNftablesClear) mux.HandleFunc("/api/v1/tunnel/list", h.tunnelList) mux.HandleFunc("/api/v1/tunnel/create", h.tunnelCreate) mux.HandleFunc("/api/v1/tunnel/get", h.tunnelGet) mux.HandleFunc("/api/v1/tunnel/update", h.tunnelUpdate) mux.HandleFunc("/api/v1/tunnel/delete", h.tunnelDelete) mux.HandleFunc("/api/v1/tunnel/delete-preview", h.tunnelDeletePreview) mux.HandleFunc("/api/v1/tunnel/delete-with-forwards", h.tunnelDeleteWithForwards) mux.HandleFunc("/api/v1/tunnel/batch-delete-preview", h.tunnelBatchDeletePreview) mux.HandleFunc("/api/v1/tunnel/batch-delete-with-forwards", h.tunnelBatchDeleteWithForwards) mux.HandleFunc("/api/v1/tunnel/diagnose", h.tunnelDiagnose) mux.HandleFunc("/api/v1/tunnel/diagnose/stream", h.tunnelDiagnoseStream) mux.HandleFunc("/api/v1/tunnel/update-order", h.tunnelUpdateOrder) mux.HandleFunc("/api/v1/tunnel/batch-delete", h.tunnelBatchDelete) mux.HandleFunc("/api/v1/tunnel/batch-redeploy", h.tunnelBatchRedeploy) mux.HandleFunc("/api/v1/tunnel/user/assign", h.userTunnelAssign) mux.HandleFunc("/api/v1/tunnel/user/batch-assign", h.userTunnelBatchAssign) mux.HandleFunc("/api/v1/tunnel/user/remove", h.userTunnelRemove) mux.HandleFunc("/api/v1/tunnel/user/update", h.userTunnelUpdate) mux.HandleFunc("/api/v1/forward/list", h.forwardList) mux.HandleFunc("/api/v1/forward/create", h.forwardCreate) mux.HandleFunc("/api/v1/forward/update", h.forwardUpdate) mux.HandleFunc("/api/v1/forward/delete", h.forwardDelete) mux.HandleFunc("/api/v1/forward/force-delete", h.forwardForceDelete) mux.HandleFunc("/api/v1/forward/pause", h.forwardPause) mux.HandleFunc("/api/v1/forward/resume", h.forwardResume) mux.HandleFunc("/api/v1/forward/reset-flow", h.forwardResetFlow) mux.HandleFunc("/api/v1/forward/diagnose", h.forwardDiagnose) mux.HandleFunc("/api/v1/forward/diagnose/stream", h.forwardDiagnoseStream) mux.HandleFunc("/api/v1/forward/update-order", h.forwardUpdateOrder) mux.HandleFunc("/api/v1/forward/batch-delete", h.forwardBatchDelete) mux.HandleFunc("/api/v1/forward/batch-pause", h.forwardBatchPause) mux.HandleFunc("/api/v1/forward/batch-resume", h.forwardBatchResume) mux.HandleFunc("/api/v1/forward/batch-redeploy", h.forwardBatchRedeploy) mux.HandleFunc("/api/v1/forward/batch-change-tunnel", h.forwardBatchChangeTunnel) mux.HandleFunc("/api/v1/speed-limit/list", h.speedLimitList) mux.HandleFunc("/api/v1/speed-limit/create", h.speedLimitCreate) mux.HandleFunc("/api/v1/speed-limit/update", h.speedLimitUpdate) mux.HandleFunc("/api/v1/speed-limit/delete", h.speedLimitDelete) mux.HandleFunc("/api/v1/tunnel/user/tunnel", h.userTunnelVisibleList) mux.HandleFunc("/api/v1/tunnel/user/list", h.userTunnelList) mux.HandleFunc("/api/v1/group/tunnel/list", h.tunnelGroupList) mux.HandleFunc("/api/v1/group/tunnel/create", h.groupTunnelCreate) mux.HandleFunc("/api/v1/group/tunnel/update", h.groupTunnelUpdate) mux.HandleFunc("/api/v1/group/tunnel/delete", h.groupTunnelDelete) mux.HandleFunc("/api/v1/group/tunnel/assign", h.groupTunnelAssign) mux.HandleFunc("/api/v1/group/user/list", h.userGroupList) mux.HandleFunc("/api/v1/group/user/create", h.groupUserCreate) mux.HandleFunc("/api/v1/group/user/update", h.groupUserUpdate) mux.HandleFunc("/api/v1/group/user/delete", h.groupUserDelete) mux.HandleFunc("/api/v1/group/user/assign", h.groupUserAssign) mux.HandleFunc("/api/v1/group/permission/list", h.groupPermissionList) mux.HandleFunc("/api/v1/group/permission/assign", h.groupPermissionAssign) mux.HandleFunc("/api/v1/group/permission/remove", h.groupPermissionRemove) mux.HandleFunc("/api/v1/open_api/sub_store", h.openAPISubStore) mux.HandleFunc("/api/v1/federation/share/list", h.federationShareList) mux.HandleFunc("/api/v1/federation/share/create", h.federationShareCreate) mux.HandleFunc("/api/v1/federation/share/update", h.federationShareUpdate) mux.HandleFunc("/api/v1/federation/share/delete", h.federationShareDelete) mux.HandleFunc("/api/v1/federation/share/reset-flow", h.federationShareResetFlow) mux.HandleFunc("/api/v1/federation/share/remote-usage/list", h.federationRemoteUsageList) mux.HandleFunc("/api/v1/federation/connect", h.authPeer(h.federationConnect)) mux.HandleFunc("/api/v1/federation/tunnel/create", h.authPeer(h.federationTunnelCreate)) mux.HandleFunc("/api/v1/federation/runtime/reserve-port", h.authPeer(h.federationRuntimeReservePort)) mux.HandleFunc("/api/v1/federation/runtime/apply-role", h.authPeer(h.federationRuntimeApplyRole)) mux.HandleFunc("/api/v1/federation/runtime/release-role", h.authPeer(h.federationRuntimeReleaseRole)) mux.HandleFunc("/api/v1/federation/runtime/diagnose", h.authPeer(h.federationRuntimeDiagnose)) mux.HandleFunc("/api/v1/federation/runtime/command", h.authPeer(h.federationRuntimeCommand)) mux.HandleFunc("/api/v1/federation/node/import", h.nodeImport) mux.HandleFunc("/api/v1/announcement/get", h.getAnnouncement) mux.HandleFunc("/api/v1/announcement/update", h.updateAnnouncement) mux.HandleFunc("/api/v1/monitor/access", h.monitorAccessHandler) mux.HandleFunc("/api/v1/monitor/nodes/", h.monitorNodeMetricsHandler) mux.HandleFunc("/api/v1/monitor/nodes", h.monitorNodeListHandler) mux.HandleFunc("/api/v1/monitor/tunnels", h.monitorTunnelListHandler) mux.HandleFunc("/api/v1/monitor/tunnels/quality", h.monitorTunnelQualityHandler) mux.HandleFunc("/api/v1/monitor/tunnels/", h.monitorTunnelMetrics) mux.HandleFunc("/api/v1/monitor/services", h.monitorServiceListHandler) mux.HandleFunc("/api/v1/monitor/services/create", h.monitorServiceCreate) mux.HandleFunc("/api/v1/monitor/services/update", h.monitorServiceUpdate) mux.HandleFunc("/api/v1/monitor/services/delete", h.monitorServiceDelete) mux.HandleFunc("/api/v1/monitor/services/run", h.monitorServiceRun) mux.HandleFunc("/api/v1/monitor/services/latest-results", h.monitorServiceLatestResultsHandler) mux.HandleFunc("/api/v1/monitor/services/limits", h.monitorServiceLimitsHandler) mux.HandleFunc("/api/v1/monitor/services/", h.monitorServiceResultsHandler) mux.HandleFunc("/api/v1/monitor/permission/list", h.monitorPermissionList) mux.HandleFunc("/api/v1/monitor/permission/assign", h.monitorPermissionAssign) mux.HandleFunc("/api/v1/monitor/permission/remove", h.monitorPermissionRemove) mux.HandleFunc("/flow/test", h.flowTest) mux.HandleFunc("/flow/config", h.flowConfig) mux.HandleFunc("/flow/upload", h.flowUpload) mux.HandleFunc("/error", h.errorPage) } func (h *Handler) login(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } var req loginRequest if err := decodeJSON(r.Body, &req); err != nil { response.WriteJSON(w, response.Err(500, "请求参数错误")) return } if strings.TrimSpace(req.Username) == "" { response.WriteJSON(w, response.Err(500, "用户名不能为空")) return } if strings.TrimSpace(req.Password) == "" { response.WriteJSON(w, response.Err(500, "密码不能为空")) return } captchaEnabled, err := h.captchaEnabled() if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } if captchaEnabled && !h.apiClientCaptchaBypassEnabled(r) { captchaID := strings.TrimSpace(req.CaptchaID) if captchaID == "" { response.WriteJSON(w, response.ErrDefault("验证码校验失败")) return } if !h.consumeCaptchaToken(captchaID) { secretCfg, err := h.repo.GetConfigByName("cloudflare_secret_key") if err != nil || secretCfg == nil || strings.TrimSpace(secretCfg.Value) == "" { response.WriteJSON(w, response.ErrDefault("验证码校验失败")) return } if !h.verifyCloudflareTurnstile(captchaID, strings.TrimSpace(secretCfg.Value)) { response.WriteJSON(w, response.ErrDefault("验证码校验失败")) return } } } user, err := h.repo.GetUserByUsername(req.Username) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } if user == nil { response.WriteJSON(w, response.ErrDefault("账号或密码错误")) return } passwordMatched, passwordWasLegacy := security.VerifyPassword(user.Pwd, req.Password) if !passwordMatched { response.WriteJSON(w, response.ErrDefault("账号或密码错误")) return } if user.Status == 0 { response.WriteJSON(w, response.ErrDefault("账号被停用")) return } issueAt := time.Now() if passwordWasLegacy { updatedAt := time.Now().UnixMilli() hashedPassword, err := security.HashPassword(req.Password) if err != nil { log.Printf("legacy password rehash skipped user_id=%d path=login err=%v", user.ID, err) } else if err := h.repo.UpdateUserPassword(user.ID, hashedPassword, updatedAt); err != nil { log.Printf("legacy password rehash update skipped user_id=%d path=login err=%v", user.ID, err) } else { issueAt = time.UnixMilli(updatedAt + 1) } } token, err := auth.GenerateTokenAt(user.ID, user.User, user.RoleID, h.jwtSecret, issueAt) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } requirePasswordChange := req.Username == "admin_user" || req.Password == "admin_user" response.WriteJSON(w, response.OK(map[string]interface{}{ "token": token, "name": user.User, "role_id": user.RoleID, "requirePasswordChange": requirePasswordChange, })) } func (h *Handler) getConfigByName(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } var req nameRequest if err := decodeJSON(r.Body, &req); err != nil { response.WriteJSON(w, response.ErrDefault("配置名称不能为空")) return } if strings.TrimSpace(req.Name) == "" { response.WriteJSON(w, response.ErrDefault("配置名称不能为空")) return } configName := strings.ToLower(strings.TrimSpace(req.Name)) if configName == "license_key" || configName == "license_machine_id" || configName == "machine_fingerprint" { response.WriteJSON(w, response.Err(403, "禁止访问系统授权凭据")) return } if repo.IsSensitiveConfigKey(configName) && !isAdminRequest(r) { response.WriteJSON(w, response.Err(403, "禁止访问敏感配置")) return } if _, ok := r.Context().Value(middleware.ClaimsContextKey).(auth.Claims); !ok && !repo.IsPublicConfigKey(configName) { response.WriteJSON(w, response.Err(401, "未登录或token已过期")) return } cfg, err := h.repo.GetConfigByName(configName) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } if cfg == nil { response.WriteJSON(w, response.ErrDefault("配置不存在")) return } response.WriteJSON(w, response.OK(cfg)) } func (h *Handler) getConfigs(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } cfgMap, err := h.repo.ListConfigs() if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } ctxClaims := r.Context().Value(middleware.ClaimsContextKey) claims, isAdmin := ctxClaims.(auth.Claims) if !isAdmin || claims.RoleID != 0 { cfgMap = repo.FilterSensitiveConfigs(cfgMap) } delete(cfgMap, "license_key") delete(cfgMap, "license_machine_id") delete(cfgMap, "machine_fingerprint") response.WriteJSON(w, response.OK(cfgMap)) } func (h *Handler) userList(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } var req struct { Current int `json:"current"` Size int `json:"size"` Keyword string `json:"keyword"` } if err := decodeJSON(r.Body, &req); err != nil && err != io.EOF { response.WriteJSON(w, response.ErrDefault("请求参数错误")) return } users, err := h.repo.ListUsers() if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } keyword := strings.ToLower(strings.TrimSpace(req.Keyword)) if keyword != "" { filtered := make([]map[string]interface{}, 0, len(users)) for _, item := range users { username := strings.ToLower(strings.TrimSpace(fmt.Sprint(item["user"]))) displayName := strings.ToLower(strings.TrimSpace(fmt.Sprint(item["name"]))) if strings.Contains(username, keyword) || strings.Contains(displayName, keyword) { filtered = append(filtered, item) } } users = filtered } response.WriteJSON(w, response.OK(users)) } func (h *Handler) nodeList(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } items, err := h.repo.ListNodes() if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } h.syncRemoteNodeStatuses(items) response.WriteJSON(w, response.OK(items)) } func (h *Handler) tunnelList(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } items, err := h.repo.ListTunnels() if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } h.attachBestExitStates(items) response.WriteJSON(w, response.OK(items)) } func (h *Handler) forwardList(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } userID, roleID, err := userRoleFromRequest(r) if err != nil { response.WriteJSON(w, response.Err(401, "无效的token或token已过期")) return } items, err := h.repo.ListForwards() if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } if roleID != 0 { filtered := make([]map[string]interface{}, 0, len(items)) for _, item := range items { if asInt64(item["userId"], 0) == userID { filtered = append(filtered, item) } } items = filtered } response.WriteJSON(w, response.OK(items)) } func (h *Handler) speedLimitList(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } items, err := h.repo.ListSpeedLimits() if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } response.WriteJSON(w, response.OK(items)) } func (h *Handler) openAPISubStore(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { response.WriteJSON(w, response.ErrDefault("请求失败")) return } if h == nil || h.repo == nil { response.WriteJSON(w, response.Err(-2, "database unavailable")) return } username := strings.TrimSpace(r.URL.Query().Get("user")) password := strings.TrimSpace(r.URL.Query().Get("pwd")) tunnel := strings.TrimSpace(r.URL.Query().Get("tunnel")) if tunnel == "" { tunnel = "-1" } if username == "" { response.WriteJSON(w, response.ErrDefault("用户不能为空")) return } if password == "" { response.WriteJSON(w, response.ErrDefault("密码不能为空")) return } user, err := h.repo.GetUserByUsername(username) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } if user == nil { response.WriteJSON(w, response.ErrDefault("鉴权失败")) return } passwordMatched, passwordWasLegacy := security.VerifyPassword(user.Pwd, password) if !passwordMatched { response.WriteJSON(w, response.ErrDefault("鉴权失败")) return } if user.Status == 0 { response.WriteJSON(w, response.ErrDefault("账号被停用")) return } if passwordWasLegacy { hashedPassword, err := security.HashPassword(password) if err != nil { log.Printf("legacy password rehash skipped user_id=%d path=sub_store err=%v", user.ID, err) } else if err := h.repo.UpdateUserPassword(user.ID, hashedPassword, time.Now().UnixMilli()); err != nil { log.Printf("legacy password rehash update skipped user_id=%d path=sub_store err=%v", user.ID, err) } } const giga = int64(1024 * 1024 * 1024) headerValue := "" if tunnel == "-1" { headerValue = buildSubscriptionHeader(user.OutFlow, user.InFlow, user.Flow*giga, user.ExpTime/1000) } else { tunnelID, parseErr := strconv.ParseInt(tunnel, 10, 64) if parseErr != nil || tunnelID <= 0 { response.WriteJSON(w, response.ErrDefault("隧道不存在")) return } ut, err := h.repo.GetUserTunnelByID(tunnelID) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } if ut == nil { response.WriteJSON(w, response.ErrDefault("隧道不存在")) return } if ut.UserID != user.ID { response.WriteJSON(w, response.ErrDefault("隧道不存在")) return } headerValue = buildSubscriptionHeader(ut.OutFlow, ut.InFlow, ut.Flow*giga, ut.ExpTime/1000) } w.Header().Set("subscription-userinfo", headerValue) w.Header().Set("Content-Type", "text/plain; charset=utf-8") _, _ = w.Write([]byte(headerValue)) } func (h *Handler) errorPage(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/html; charset=UTF-8") w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte("错误 404
404
你推开了后端的大门,却发现里面只有寂寞。
")) } func buildSubscriptionHeader(upload, download, total, expire int64) string { return fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", download, upload, total, expire) } func (h *Handler) userTunnelVisibleList(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } userID, roleID, err := userRoleFromRequest(r) if err != nil { response.WriteJSON(w, response.Err(401, "无效的token或token已过期")) return } items := make([]map[string]interface{}, 0) if roleID == 0 { items, err = h.repo.ListEnabledTunnelSummaries() } else { items, err = h.repo.ListUserAccessibleTunnels(userID) } if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } response.WriteJSON(w, response.OK(items)) } func (h *Handler) userTunnelList(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } var req struct { UserID int64 `json:"userId"` } if err := decodeJSON(r.Body, &req); err != nil { response.WriteJSON(w, response.ErrDefault("请求参数错误")) return } if req.UserID <= 0 { response.WriteJSON(w, response.OK([]interface{}{})) return } tunnels, err := h.repo.GetUserPackageTunnels(req.UserID) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } out := make([]map[string]interface{}, 0, len(tunnels)) for _, t := range tunnels { item := map[string]interface{}{ "id": t.ID, "userId": t.UserID, "tunnelId": t.TunnelID, "tunnelName": t.TunnelName, "status": t.Status, "flow": t.Flow, "num": t.Num, "expTime": t.ExpTime, "flowResetTime": t.FlowResetTime, "inFlow": t.InFlow, "outFlow": t.OutFlow, "tunnelFlow": t.TunnelFlow, "speedId": nil, "speedLimitName": nil, } if t.SpeedID.Valid { item["speedId"] = t.SpeedID.Int64 } if t.SpeedLimit.Valid { item["speedLimitName"] = t.SpeedLimit.String } out = append(out, item) } response.WriteJSON(w, response.OK(out)) } func (h *Handler) tunnelGroupList(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } items, err := h.repo.ListTunnelGroups() if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } response.WriteJSON(w, response.OK(items)) } func (h *Handler) userGroupList(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } items, err := h.repo.ListUserGroups() if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } response.WriteJSON(w, response.OK(items)) } func (h *Handler) groupPermissionList(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } items, err := h.repo.ListGroupPermissions() if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } response.WriteJSON(w, response.OK(items)) } func (h *Handler) checkCaptcha(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } enabled, err := h.captchaEnabled() if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } if enabled { response.WriteJSON(w, response.OK(1)) return } response.WriteJSON(w, response.OK(0)) } func (h *Handler) captchaVerify(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } var req captchaVerifyRequest if err := decodeJSON(r.Body, &req); err != nil { h.writeCaptchaVerifyResult(w, false, "") return } id := strings.TrimSpace(req.ID) data := strings.TrimSpace(req.Data) if id == "" || data == "" { h.writeCaptchaVerifyResult(w, false, "") return } verified := false secretCfg, err := h.repo.GetConfigByName("cloudflare_secret_key") if err == nil && secretCfg != nil && strings.TrimSpace(secretCfg.Value) != "" { verified = h.verifyCloudflareTurnstile(data, strings.TrimSpace(secretCfg.Value)) } else { verified = data == "ok" } if !verified { h.writeCaptchaVerifyResult(w, false, "") return } h.markCaptchaToken(id) h.writeCaptchaVerifyResult(w, true, id) } func (h *Handler) flowTest(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") _, _ = w.Write([]byte("test")) } func (h *Handler) flowConfig(w http.ResponseWriter, r *http.Request) { secret := r.URL.Query().Get("secret") node, err := h.repo.GetNodeBySecret(secret) if err != nil || node == nil { w.Header().Set("Content-Type", "text/plain; charset=utf-8") _, _ = w.Write([]byte("ok")) return } rawData, err := readAndDecryptFlowBody(r.Body, secret) if err == nil && strings.TrimSpace(rawData) != "" { h.cleanNodeConfigs(node.ID, rawData) } w.Header().Set("Content-Type", "text/plain; charset=utf-8") _, _ = w.Write([]byte("ok")) } func (h *Handler) flowUpload(w http.ResponseWriter, r *http.Request) { secret := r.URL.Query().Get("secret") node, _ := h.repo.GetNodeBySecret(secret) if node == nil { w.Header().Set("Content-Type", "text/plain; charset=utf-8") _, _ = w.Write([]byte("ok")) return } raw, err := readAndDecryptFlowBody(r.Body, secret) if err == nil && strings.TrimSpace(raw) != "" { var items []flowItem if json.Unmarshal([]byte(raw), &items) == nil { now := time.Now() forwardIDs := collectFlowUploadForwardIDs(items) metas, metaErr := h.repo.GetFlowUploadForwardMetas(forwardIDs) if metaErr != nil { log.Printf("flow upload metadata lookup failed node_id=%d err=%v", node.ID, metaErr) metas = map[int64]repo.FlowUploadForwardMeta{} } batch := h.buildFlowUploadBatch(items, metas) h.recordTunnelMetricsFromForwardBatch(node.ID, batch.forwardTraffic, metas, now.UnixMilli()) h.applyFlowUploadBatch(node.ID, batch, now) } } w.Header().Set("Content-Type", "text/plain; charset=utf-8") _, _ = w.Write([]byte("ok")) } func (h *Handler) getOrCreateMachineFingerprint() (string, error) { h.fingerprintMu.Lock() defer h.fingerprintMu.Unlock() fp, err := h.repo.GetViteConfigValue("machine_fingerprint") if fp != "" { return fp, nil } if err != nil && !errors.Is(err, sql.ErrNoRows) { return "", err } newFp := uuid.New().String() now := time.Now().UnixMilli() if err := h.repo.UpsertConfig("machine_fingerprint", newFp, now); err != nil { return "", err } return newFp, nil } func (h *Handler) licenseActivate(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } var req licenseActivateRequest if err := decodeJSON(r.Body, &req); err != nil { response.WriteJSON(w, response.ErrDefault("授权码不能为空")) return } key := strings.TrimSpace(req.LicenseKey) if key == "" { response.WriteJSON(w, response.ErrDefault("授权码不能为空")) return } h.licenseValidationMu.Lock() defer h.licenseValidationMu.Unlock() valResp, err := h.validateLicenseForMachine(key) if err != nil { log.Printf("license activation failed: %v", err) response.WriteJSON(w, response.ErrDefault(licenseValidationErrorMessage(err))) return } if !valResp.Meta.Valid { response.WriteJSON(w, response.ErrDefault("授权码无效或已过期 (Code: "+valResp.Meta.Code+")")) return } now := time.Now().UnixMilli() expiry := valResp.Data.Attributes.Expiry if expiry == "" { expiry = "never" } licenseState := map[string]string{ "license_key": key, "is_commercial": "true", "license_expiry": expiry, } if valResp.MachineID != "" { licenseState["license_machine_id"] = valResp.MachineID } if err := h.repo.UpsertConfigs(licenseState, now); err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } response.WriteJSON(w, response.OKEmpty()) } func (h *Handler) updateConfigs(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } var payload map[string]string if err := decodeJSON(r.Body, &payload); err != nil { response.WriteJSON(w, response.ErrDefault("配置数据不能为空")) return } if len(payload) == 0 { response.WriteJSON(w, response.ErrDefault("配置数据不能为空")) return } isCommercial, _ := h.repo.GetViteConfigValue("is_commercial") protectedKeys := map[string]bool{ "app_name": true, "app_logo": true, "app_favicon": true, "hide_footer_brand": true, } now := time.Now().UnixMilli() for k, v := range payload { key := strings.TrimSpace(k) if key == "" { continue } if repo.IsSensitiveConfigKey(key) && !isAdminRequest(r) { response.WriteJSON(w, response.Err(403, "禁止访问敏感配置")) return } if repo.IsSystemManagedConfigKey(key) { response.WriteJSON(w, response.ErrDefault("该配置由系统管理")) return } if protectedKeys[key] && isCommercial != "true" { response.WriteJSON(w, response.ErrDefault("需要商业版授权")) return } value, err := normalizeAndValidateConfigValue(key, v) if err != nil { response.WriteJSON(w, response.ErrDefault(err.Error())) return } if err := h.repo.UpsertConfig(key, value, now); err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } h.notifyTunnelQualityConfigChanged(key) } response.WriteJSON(w, response.OKEmpty()) } func (h *Handler) updateSingleConfig(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } var req configSingleRequest if err := decodeJSON(r.Body, &req); err != nil { response.WriteJSON(w, response.ErrDefault("配置名称不能为空")) return } name := strings.TrimSpace(req.Name) if name == "" { response.WriteJSON(w, response.ErrDefault("配置名称不能为空")) return } if repo.IsSensitiveConfigKey(name) && !isAdminRequest(r) { response.WriteJSON(w, response.Err(403, "禁止访问敏感配置")) return } if repo.IsSystemManagedConfigKey(name) { response.WriteJSON(w, response.ErrDefault("该配置由系统管理")) return } isCommercial, _ := h.repo.GetViteConfigValue("is_commercial") if (name == "app_name" || name == "app_logo" || name == "app_favicon" || name == "hide_footer_brand") && isCommercial != "true" { response.WriteJSON(w, response.ErrDefault("需要商业版授权")) return } value, err := normalizeAndValidateConfigValue(name, req.Value) if err != nil { response.WriteJSON(w, response.ErrDefault(err.Error())) return } if value == "" && name != "app_logo" && name != "app_favicon" { response.WriteJSON(w, response.ErrDefault("配置值不能为空")) return } if err := h.repo.UpsertConfig(name, value, time.Now().UnixMilli()); err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } h.notifyTunnelQualityConfigChanged(name) response.WriteJSON(w, response.OKEmpty()) } func isAdminRequest(r *http.Request) bool { if r == nil { return false } claims, ok := r.Context().Value(middleware.ClaimsContextKey).(auth.Claims) return ok && claims.RoleID == 0 } func normalizeAndValidateConfigValue(key, value string) (string, error) { switch strings.TrimSpace(key) { case "app_logo", "app_favicon": normalized := strings.TrimSpace(value) if normalized == "" { return "", nil } if !strings.HasPrefix(normalized, pngDataURLPrefix) { return "", fmt.Errorf("品牌图片必须通过上传生成 PNG 数据") } if len(normalized) > maxBrandAssetDataURLBytes { return "", fmt.Errorf("品牌图片过大,请上传更小图片") } payload := strings.TrimSpace(strings.TrimPrefix(normalized, pngDataURLPrefix)) if payload == "" { return "", fmt.Errorf("品牌图片数据不能为空") } if _, err := base64.StdEncoding.DecodeString(payload); err != nil { return "", fmt.Errorf("品牌图片数据格式无效") } return pngDataURLPrefix + payload, nil case monitorTunnelQualityEnabledConfigKey: normalized := strings.TrimSpace(strings.ToLower(value)) switch normalized { case "true", "false": return normalized, nil default: return "", fmt.Errorf("隧道质量检测开关配置值无效") } case monitoring.ConfigMonitorRetentionDays: return monitoring.NormalizeMonitoringRetentionDays(value) case monitoring.ConfigTunnelQualityProbeIntervalSec: return monitoring.NormalizeTunnelQualityProbeIntervalSeconds(value) default: return value, nil } } func (h *Handler) notifyTunnelQualityConfigChanged(key string) { if h == nil || h.qualityProber == nil { return } switch strings.TrimSpace(key) { case monitorTunnelQualityEnabledConfigKey, monitoring.ConfigTunnelQualityProbeIntervalSec: h.qualityProber.NotifyConfigChanged() } } func (h *Handler) isTunnelQualityMonitoringEnabled() bool { if h == nil || h.repo == nil { return true } cfg, err := h.repo.GetConfigByName(monitorTunnelQualityEnabledConfigKey) if err != nil || cfg == nil { return true } return strings.TrimSpace(strings.ToLower(cfg.Value)) != "false" } func (h *Handler) allowLocalRemoteAddr() bool { if h == nil || h.repo == nil { return false } cfg, err := h.repo.GetConfigByName(allowLocalRemoteAddrConfigKey) if err != nil || cfg == nil { return false } return strings.TrimSpace(strings.ToLower(cfg.Value)) == "true" } func (h *Handler) userPackage(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } claims, ok := r.Context().Value(middleware.ClaimsContextKey).(auth.Claims) if !ok { response.WriteJSON(w, response.Err(401, "无效的token或token已过期")) return } userID, err := parseUserID(claims.Sub) if err != nil { response.WriteJSON(w, response.Err(401, "无效的token或token已过期")) return } user, err := h.repo.GetUserByID(userID) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } if user == nil { response.WriteJSON(w, response.ErrDefault("用户不存在")) return } tunnels, err := h.repo.GetUserPackageTunnels(userID) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } forwards, err := h.repo.GetUserPackageForwards(userID) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } stats, err := h.repo.GetStatisticsFlows(userID, 24) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } sort.Slice(stats, func(i, j int) bool { return stats[i].ID < stats[j].ID }) tunnelOut := make([]map[string]interface{}, 0, len(tunnels)) for _, t := range tunnels { item := map[string]interface{}{ "id": t.ID, "userId": t.UserID, "tunnelId": t.TunnelID, "tunnelName": t.TunnelName, "tunnelFlow": t.TunnelFlow, "flow": t.Flow, "inFlow": t.InFlow, "outFlow": t.OutFlow, "num": t.Num, "flowResetTime": t.FlowResetTime, "expTime": t.ExpTime, "speedId": nil, "speedLimitName": nil, "speed": nil, } if t.SpeedID.Valid { item["speedId"] = t.SpeedID.Int64 } if t.SpeedLimit.Valid { item["speedLimitName"] = t.SpeedLimit.String } if t.Speed.Valid { item["speed"] = t.Speed.Int64 } tunnelOut = append(tunnelOut, item) } forwardOut := make([]map[string]interface{}, 0, len(forwards)) for _, f := range forwards { item := map[string]interface{}{ "id": f.ID, "name": f.Name, "tunnelId": f.TunnelID, "tunnelName": f.TunnelName, "inIp": f.InIP, "inPort": nil, "remoteAddr": f.RemoteAddr, "inFlow": f.InFlow, "outFlow": f.OutFlow, "status": f.Status, "createdTime": f.CreatedAt, } if f.InPort.Valid { item["inPort"] = f.InPort.Int64 } forwardOut = append(forwardOut, item) } payload := map[string]interface{}{ "userInfo": map[string]interface{}{ "id": user.ID, "name": user.User, "user": user.User, "status": user.Status, "flow": user.Flow, "inFlow": user.InFlow, "outFlow": user.OutFlow, "num": user.Num, "expTime": user.ExpTime, "flowResetTime": user.FlowResetTime, "createdTime": user.CreatedTime, "updatedTime": nullableNullInt64(user.UpdatedTime), }, "tunnelPermissions": tunnelOut, "forwards": forwardOut, "statisticsFlows": stats, } response.WriteJSON(w, response.OK(payload)) } func (h *Handler) updatePassword(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } claims, ok := r.Context().Value(middleware.ClaimsContextKey).(auth.Claims) if !ok { response.WriteJSON(w, response.Err(401, "无效的token或token已过期")) return } userID, err := parseUserID(claims.Sub) if err != nil { response.WriteJSON(w, response.Err(401, "无效的token或token已过期")) return } var req changePasswordRequest if err := decodeJSON(r.Body, &req); err != nil { response.WriteJSON(w, response.ErrDefault("修改账号密码时发生错误")) return } if strings.TrimSpace(req.NewUsername) == "" { response.WriteJSON(w, response.ErrDefault("新用户名不能为空")) return } if strings.TrimSpace(req.CurrentPassword) == "" { response.WriteJSON(w, response.ErrDefault("当前密码不能为空")) return } if strings.TrimSpace(req.NewPassword) == "" { response.WriteJSON(w, response.ErrDefault("新密码不能为空")) return } if strings.TrimSpace(req.ConfirmPassword) == "" { response.WriteJSON(w, response.ErrDefault("确认密码不能为空")) return } if req.NewPassword != req.ConfirmPassword { response.WriteJSON(w, response.ErrDefault("新密码和确认密码不匹配")) return } user, err := h.repo.GetUserByID(userID) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } if user == nil { response.WriteJSON(w, response.ErrDefault("用户不存在")) return } passwordMatched, _ := security.VerifyPassword(user.Pwd, req.CurrentPassword) if !passwordMatched { response.WriteJSON(w, response.ErrDefault("当前密码错误")) return } exists, err := h.repo.UsernameExistsExceptID(req.NewUsername, userID) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } if exists { response.WriteJSON(w, response.ErrDefault("用户名已存在")) return } hashedPassword, err := security.HashPassword(req.NewPassword) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } if err := h.repo.UpdateUserNameAndPassword(userID, req.NewUsername, hashedPassword, time.Now().UnixMilli()); err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } response.WriteJSON(w, response.OKEmpty()) } func (h *Handler) captchaEnabled() (bool, error) { cfg, err := h.repo.GetConfigByName("captcha_enabled") if err != nil { return false, err } if cfg == nil || !strings.EqualFold(strings.TrimSpace(cfg.Value), "true") { return false, nil } siteCfg, err := h.repo.GetConfigByName("cloudflare_site_key") if err != nil { return false, err } if siteCfg == nil || strings.TrimSpace(siteCfg.Value) == "" { return false, nil } secretCfg, err := h.repo.GetConfigByName("cloudflare_secret_key") if err != nil { return false, err } if secretCfg == nil || strings.TrimSpace(secretCfg.Value) == "" { return false, nil } return true, nil } func (h *Handler) apiClientCaptchaBypassEnabled(r *http.Request) bool { if r == nil { return false } client := strings.ToLower(strings.TrimSpace(r.Header.Get("X-FLVX-API-Client"))) switch client { case "whmcs", "whmcs-module": return true default: return false } } func (h *Handler) markCaptchaToken(token string) { if h == nil { return } token = strings.TrimSpace(token) if token == "" { return } now := time.Now().UnixMilli() exp := now + int64(5*time.Minute/time.Millisecond) h.captchaMu.Lock() defer h.captchaMu.Unlock() if h.captchaTokens == nil { h.captchaTokens = make(map[string]int64) } for k, v := range h.captchaTokens { if v <= now { delete(h.captchaTokens, k) } } h.captchaTokens[token] = exp } func (h *Handler) consumeCaptchaToken(token string) bool { if h == nil { return false } token = strings.TrimSpace(token) if token == "" { return false } now := time.Now().UnixMilli() h.captchaMu.Lock() defer h.captchaMu.Unlock() if h.captchaTokens == nil { return false } for k, v := range h.captchaTokens { if v <= now { delete(h.captchaTokens, k) } } exp, ok := h.captchaTokens[token] if !ok { return false } delete(h.captchaTokens, token) return exp > now } func (h *Handler) writeCaptchaVerifyResult(w http.ResponseWriter, success bool, token string) { w.Header().Set("Content-Type", "application/json; charset=utf-8") payload := map[string]interface{}{ "success": success, "data": map[string]interface{}{ "validToken": token, }, } _ = json.NewEncoder(w).Encode(payload) } func decodeJSON(body io.ReadCloser, out interface{}) error { defer body.Close() decoder := json.NewDecoder(body) decoder.DisallowUnknownFields() return decoder.Decode(out) } func parseUserID(sub string) (int64, error) { id, err := strconv.ParseInt(sub, 10, 64) if err != nil || id <= 0 { return 0, strconv.ErrSyntax } return id, nil } func userIDFromRequest(r *http.Request) (int64, error) { claims, ok := r.Context().Value(middleware.ClaimsContextKey).(auth.Claims) if !ok { return 0, strconv.ErrSyntax } return parseUserID(claims.Sub) } func userRoleFromRequest(r *http.Request) (int64, int, error) { claims, ok := r.Context().Value(middleware.ClaimsContextKey).(auth.Claims) if !ok { return 0, 0, strconv.ErrSyntax } userID, err := parseUserID(claims.Sub) if err != nil { return 0, 0, err } return userID, claims.RoleID, nil } func nullableNullInt64(v sql.NullInt64) interface{} { if v.Valid { return v.Int64 } return nil } // flowCryptoCache caches AES crypto instances by secret to avoid per-request SHA256+GCM init. var flowCryptoCache sync.Map func getOrCreateFlowCrypto(secret string) *security.AESCrypto { if v, ok := flowCryptoCache.Load(secret); ok { return v.(*security.AESCrypto) } c, err := security.NewAESCrypto(secret) if err != nil { return nil } flowCryptoCache.Store(secret, c) return c } func readAndDecryptFlowBody(body io.ReadCloser, secret string) (string, error) { defer body.Close() raw, err := io.ReadAll(body) if err != nil { return "", err } text := strings.TrimSpace(string(raw)) if text == "" { return "", nil } var wrap struct { Encrypted bool `json:"encrypted"` Data string `json:"data"` Timestamp int64 `json:"timestamp"` } if err := json.Unmarshal(raw, &wrap); err != nil || !wrap.Encrypted || strings.TrimSpace(wrap.Data) == "" { return text, nil } crypto := getOrCreateFlowCrypto(secret) if crypto == nil { return text, nil } plain, err := crypto.Decrypt(wrap.Data) if err != nil { return text, nil } return string(plain), nil } func (h *Handler) verifyCloudflareTurnstile(token, secretKey string) bool { if token == "" || secretKey == "" { return false } resp, err := http.PostForm("https://challenges.cloudflare.com/turnstile/v0/siteverify", url.Values{ "secret": {secretKey}, "response": {token}, }) if err != nil { return false } defer resp.Body.Close() var body struct { Success bool `json:"success"` } if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { return false } return body.Success } type backupExportRequest struct { Types []string `json:"types"` } func (h *Handler) backupExport(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } var req backupExportRequest if err := decodeJSON(r.Body, &req); err != nil { response.WriteJSON(w, response.Err(500, "请求参数错误")) return } var backup interface{} var err error if len(req.Types) == 0 { backup, err = h.repo.ExportAll() } else { backup, err = h.repo.ExportPartial(req.Types) } if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } w.Header().Set("Content-Disposition", "attachment; filename=backup.json") w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(backup); err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } } type backupImportRequest struct { Types []string `json:"types"` repo.BackupData } func (h *Handler) backupImport(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } var req backupImportRequest if err := decodeJSON(r.Body, &req); err != nil { response.WriteJSON(w, response.Err(500, "请求参数错误")) return } if len(req.Types) == 0 { response.WriteJSON(w, response.Err(500, "请选择要导入的数据类型")) return } autoBackup, err := h.repo.ExportAll() if err != nil { response.WriteJSON(w, response.Err(-2, fmt.Sprintf("导入前自动备份失败: %v", err))) return } if req.BackupData.Version == "" { response.WriteJSON(w, response.Err(500, "备份数据格式错误")) return } result, err := h.repo.Import(&req.BackupData, req.Types) if err != nil { response.WriteJSON(w, response.Err(-2, fmt.Sprintf("导入失败: %v", err))) return } result.AutoBackup = autoBackup response.WriteJSON(w, response.OK(result)) } func (h *Handler) getAnnouncement(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { response.WriteJSON(w, response.ErrDefault("请求失败")) return } ann, err := h.repo.GetAnnouncement() if err != nil { response.WriteJSON(w, response.Err(-1, fmt.Sprintf("获取公告失败: %v", err))) return } if ann == nil { response.WriteJSON(w, response.OK(map[string]interface{}{ "content": "", "enabled": 0, "update_time": 0, })) return } updateTime := ann.CreatedTime if ann.UpdatedTime.Valid { updateTime = ann.UpdatedTime.Int64 } response.WriteJSON(w, response.OK(map[string]interface{}{ "content": ann.Content, "enabled": ann.Enabled, "update_time": updateTime, })) } func (h *Handler) updateAnnouncement(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } var req struct { Content string `json:"content"` Enabled int `json:"enabled"` } if err := decodeJSON(r.Body, &req); err != nil { response.WriteJSON(w, response.Err(500, "请求参数错误")) return } now := time.Now().UnixMilli() if err := h.repo.UpsertAnnouncement(req.Content, req.Enabled, now); err != nil { response.WriteJSON(w, response.Err(-1, fmt.Sprintf("更新公告失败: %v", err))) return } response.WriteJSON(w, response.OKEmpty()) }