package api import ( "context" "crypto/ed25519" "crypto/rand" "encoding/base64" "errors" "fmt" "io" "io/fs" "net/http" "os" "path/filepath" "runtime" "strings" "time" "git.zkcoi.com/zkcoi/Meshray-Manager/internal/api/handler" "git.zkcoi.com/zkcoi/Meshray-Manager/internal/api/middleware" "git.zkcoi.com/zkcoi/Meshray-Manager/internal/config" "git.zkcoi.com/zkcoi/Meshray-Manager/internal/ctr" corehandler "git.zkcoi.com/zkcoi/Meshray-Manager/internal/handler" "git.zkcoi.com/zkcoi/Meshray-Manager/internal/model" "git.zkcoi.com/zkcoi/Meshray-Manager/internal/service" "git.zkcoi.com/zkcoi/Meshray-Manager/internal/store/sqlite" "git.zkcoi.com/zkcoi/Meshray-Manager/web" "github.com/gin-gonic/gin" "github.com/shirou/gopsutil/v4/cpu" "go.uber.org/zap" "gorm.io/gorm" ) // Server API 服务器 type Server struct { engine *gin.Engine config *config.Config logger *zap.Logger store *sqlite.Store ctrClient *ctr.Ctr // 全局 Ctr 实例 } // NewServer 创建 API 服务器 func NewServer(cfg *config.Config, logger *zap.Logger, dbStore *sqlite.Store) (*Server, error) { // 设置 Gin 模式 switch cfg.Server.Mode { case "debug": gin.SetMode(gin.DebugMode) case "release": gin.SetMode(gin.ReleaseMode) default: gin.SetMode(gin.TestMode) } engine := gin.New() server := &Server{ engine: engine, config: cfg, logger: logger, store: dbStore, } // 首次启动初始化管理员账户(已存在则跳过,仅新建时返回随机密码) userService := service.NewUserService(dbStore) if uname, pwd, ierr := userService.InitializeAdmin(); ierr == nil { if pwd != "" { server.logger.Warn("已创建默认管理员账户,请尽快登录并修改密码", zap.String("username", uname), zap.String("password", pwd)) } } else { server.logger.Error("初始化管理员账户失败", zap.Error(ierr)) } // 注册中间件 server.registerMiddleware() // 注册路由 if err := server.registerRoutes(); err != nil { return nil, err } return server, nil } // registerMiddleware 注册全局中间件 func (s *Server) registerMiddleware() { // 恢复中间件 s.engine.Use(gin.Recovery()) // 请求日志中间件 s.engine.Use(middleware.RequestLogger(s.logger)) // CORS 中间件(开发环境) if s.config.Server.Mode != "release" { s.engine.Use(middleware.CORS()) } } // registerRoutes 注册所有路由 func (s *Server) registerRoutes() error { // 健康检查 s.engine.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{"status": "ok"}) }) // 静态文件服务(前端) // 使用 embed 的静态文件 var staticFS fs.FS var useEmbed bool // 直接使用 web 包中的 WebAssets,剥离外层的 static 目录 if embedFS, err := fs.Sub(web.WebAssets, "static"); err == nil { // 检查是否有 index.html if _, statErr := fs.Stat(embedFS, "index.html"); statErr == nil { staticFS = embedFS useEmbed = true s.logger.Info("使用内嵌的静态文件") } else { s.logger.Warn("embed 中找不到 index.html", zap.Error(statErr)) } } else { s.logger.Warn("embed 文件系统创建失败", zap.Error(err)) } // 如果 embed 没有内容,使用外部目录 if !useEmbed && s.config.Server.StaticPath != "" { staticFS = os.DirFS(s.config.Server.StaticPath) s.logger.Info("使用外部静态文件目录", zap.String("path", s.config.Server.StaticPath)) } if staticFS != nil { // ✅ 注册根路径处理 s.engine.GET("/", func(c *gin.Context) { file, err := staticFS.Open("index.html") if err == nil { defer file.Close() content, _ := io.ReadAll(file) c.Data(200, "text/html; charset=utf-8", content) return } c.String(500, "Failed to load index.html") }) // ✅ 简化:只注册 NoRoute 处理 SPA 路由 s.engine.NoRoute(func(c *gin.Context) { path := c.Request.URL.Path // 如果是 API 请求,返回 404 if strings.HasPrefix(path, "/api/") { c.JSON(404, gin.H{"error": "API not found"}) return } // 尝试直接提供文件 // staticFS 已通过 fs.Sub 剥离 "static" 前缀,需先去掉路径中的 "static/" filePath := strings.TrimPrefix(path, "/") filePath = strings.TrimPrefix(filePath, "static/") if filePath == "" { filePath = "index.html" } // 读取并返回文件内容 file, err := staticFS.Open(filePath) if err == nil { defer file.Close() content, _ := io.ReadAll(file) c.Data(200, getContentType(filePath), content) return } // 其他请求都返回 index.html(SPA 路由支持) file, _ = staticFS.Open("index.html") if file != nil { defer file.Close() content, _ := io.ReadAll(file) c.Data(200, "text/html; charset=utf-8", content) } }) } else { s.logger.Warn("未配置静态文件路径,前端将不可用") } // API v1 路由组 v1 := s.engine.Group("/api/v1") { // 公开接口(无需鉴权) public := v1.Group("") { public.POST("/auth/login", middleware.LoginHandler(s.config.JWT.Secret, s.logger, s.store)) public.POST("/auth/refresh", middleware.RefreshTokenHandler(s.config.JWT.Secret, s.logger)) } // ✅ 提前初始化 ctrClient(在路由注册前) var err error s.ctrClient, err = ctr.NewCtr("default", 1, &ctr.CtrConfig{}, s.logger) if err != nil { s.logger.Error("初始化 meshray-ctr 失败", zap.Error(err)) return fmt.Errorf("初始化 ctr 失败:%w", err) } // 需要鉴权的接口 protected := v1.Group("") protected.Use(middleware.JWTAuth(s.config.JWT.Secret)) { // 初始化 Service 层(注入 ctr 客户端) networkService := service.NewNetworkService(s.store, s.ctrClient, s.logger) deviceService := service.NewDeviceService(s.store, s.ctrClient) userService := service.NewUserService(s.store) policyService := service.NewPolicyService(s.store) settingsService := service.NewSettingsService(s.store, s.logger) pendingJoinService := service.NewPendingJoinService(s.store) // 初始化 MeshSeedService(需要 Ed25519 签名密钥) signingKey, err := s.loadSigningKey() if err != nil { s.logger.Error("加载签名密钥失败", zap.Error(err)) return fmt.Errorf("加载签名密钥失败:%w", err) } meshSeedService := service.NewMeshSeedService(s.store, s.logger, signingKey, "node-1") // 初始化 SystemConfigService(系统配置服务) systemConfigService := service.NewSystemConfigService(s.store, s.logger) // 初始化 ExternalService 相关 Handler serviceService := service.NewServiceService(s.store) serviceHandler := handler.NewServiceHandler(serviceService, s.logger) // 初始化 SystemConfigHandler(系统配置处理器) systemConfigHandler := handler.NewSystemConfigHandler(systemConfigService, s.logger) // 初始化 PendingJoinHandler(待审核处理器) pendingJoinHandler := handler.NewPendingJoinHandler(pendingJoinService, s.store, s.logger) // 初始化 handlers(使用 Service 层) networkHandler := handler.NewNetworkHandler(networkService, meshSeedService, s.store, s.logger) deviceHandler := handler.NewDeviceHandler(deviceService, s.logger) adminHandler := handler.NewAdminHandler(userService, s.logger) dashboardHandler := handler.NewDashboardHandler(s.store, s.logger) policyHandler := handler.NewPolicyHandler(policyService, s.logger) settingsHandler := handler.NewSettingsHandler(settingsService, s.logger) wsHandler := handler.NewWSHandler(s.ctrClient, s.store, s.logger) ddnsService, err := service.NewDDNSService(s.store.DB()) if err != nil { s.logger.Error("初始化 DDNSService 失败", zap.Error(err)) return fmt.Errorf("初始化 DDNSService 失败:%w", err) } // 启动后台自动同步协程 go ddnsService.StartAutoSync(context.Background()) ddnsHandler := handler.NewDDNSHandler(ddnsService) // ✅ 新增:DDNS Usage Handler ddnsUsageHandler := handler.NewDDNSUsageHandler(s.store.DB(), s.logger) // ✅ 新增:IP 检测 API ddnsDetectHandler := handler.NewIPDetectHandler() // 管理员管理 protected.GET("/admin/profile", adminHandler.GetProfile) protected.PUT("/admin/profile", adminHandler.UpdateProfile) protected.POST("/admin/change-password", adminHandler.ChangePassword) // ✅ 系统备份恢复 backupHandler := corehandler.NewBackupHandler(s.store.DB(), s.logger) protected.POST("/system/backup", backupHandler.CreateBackup) protected.GET("/system/backups", backupHandler.ListBackups) protected.POST("/system/restore", backupHandler.RestoreBackup) protected.DELETE("/system/backup", backupHandler.DeleteBackup) protected.GET("/system/backup/download", backupHandler.DownloadBackup) // ✅ 系统更新检查 updateHandler := corehandler.NewUpdateHandler(s.config.App.Version) protected.GET("/system/update/check", func(c *gin.Context) { resp, err := updateHandler.CheckUpdate() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "检查更新失败", }) return } c.JSON(http.StatusOK, gin.H{ "data": resp, }) }) // ✅ WebSocket 实时通知推送 notificationSvc := service.NewNotificationService(s.store.DB(), s.logger) notificationHandler := corehandler.NewNotificationHandler(notificationSvc, s.logger, s.store.DB()) protected.GET("/notifications", notificationHandler.GetNotifications) protected.GET("/notifications/unread-count", notificationHandler.GetUnreadCount) protected.POST("/notifications/:id/read", notificationHandler.MarkAsRead) protected.POST("/notifications/read-all", notificationHandler.MarkAllAsRead) protected.DELETE("/notifications/:id", notificationHandler.DeleteNotification) protected.POST("/notifications/test", notificationHandler.TestSendNotification) // DDNS 配置 protected.GET("/ddns/config", ddnsHandler.GetDDNSConfig) protected.PUT("/ddns/config", ddnsHandler.UpdateDDNSConfig) protected.POST("/ddns/test", ddnsHandler.TestDDNSConnectivity) protected.POST("/ddns/sync", ddnsHandler.SyncDDNS) // ✅ IP 检测 API(用于前端自动填充) protected.GET("/ddns/detect-ip", ddnsDetectHandler.DetectIP) // ✅ DDNS 统计 API(用于 Dashboard 监控) ddnsStatsHandler := handler.NewDDNSStatsHandler(s.store.DB()) protected.GET("/ddns/stats", ddnsStatsHandler.GetDDNSStats) // ✅ DDNS Usage 管理 protected.POST("/ddns/usages", ddnsUsageHandler.CreateUsage) protected.GET("/ddns/usages/available", ddnsUsageHandler.GetAvailableUsages) protected.GET("/ddns/check-prefix", ddnsUsageHandler.CheckPrefixOccupied) // Dashboard protected.GET("/dashboard/stats", dashboardHandler.GetStats) protected.GET("/dashboard/logs", dashboardHandler.GetRecentLogs) protected.GET("/dashboard/system-info", dashboardHandler.GetSystemInfo) protected.GET("/dashboard/link-distribution", dashboardHandler.GetLinkDistribution) protected.POST("/dashboard/logs/clear", dashboardHandler.ClearLogs) // 新增:清理日志 // 系统设置 protected.GET("/settings", settingsHandler.GetSettings) protected.PUT("/settings", settingsHandler.UpdateSettings) // 系统配置(WG 模式等) protected.GET("/system-config/wg-mode", systemConfigHandler.GetWGMode) protected.PUT("/system-config/wg-mode", systemConfigHandler.SetWGMode) // 系统运维(P1/P2 功能)- 使用 SystemConfigHandler 统一管理 protected.POST("/system/change-password", systemConfigHandler.ChangePassword) // P1: 修改密码 protected.POST("/system/restart-core", systemConfigHandler.RestartCore) // P1: 重启核心 // 组网管理 protected.GET("/networks", networkHandler.ListNetworks) protected.POST("/networks", networkHandler.CreateNetwork) protected.POST("/networks/preview", networkHandler.PreviewMeshSeed) protected.POST("/networks/join", networkHandler.JoinNetwork) protected.GET("/networks/:id", networkHandler.GetNetwork) protected.PUT("/networks/:id", networkHandler.UpdateNetwork) protected.DELETE("/networks/:id", networkHandler.DeleteNetwork) protected.POST("/networks/:id/start", networkHandler.StartNetwork) protected.POST("/networks/:id/stop", networkHandler.StopNetwork) protected.POST("/networks/:id/switch-mode", networkHandler.SwitchMode) protected.POST("/networks/:id/meshseed", networkHandler.GenerateMeshSeed) // 设备管理(使用独立前缀避免路由冲突) protected.GET("/devices", deviceHandler.ListDevices) protected.POST("/devices", deviceHandler.CreateDevice) protected.GET("/devices/:id", deviceHandler.GetDevice) protected.PUT("/devices/:id", deviceHandler.UpdateDevice) protected.DELETE("/devices/:id", deviceHandler.DeleteDevice) protected.GET("/devices/:id/config", deviceHandler.GenerateDeviceConfig) // 策略管理 protected.GET("/policies", policyHandler.ListPolicies) protected.POST("/policies", policyHandler.CreatePolicy) protected.GET("/policies/:id", policyHandler.GetPolicy) protected.PUT("/policies/:id", policyHandler.UpdatePolicy) protected.DELETE("/policies/:id", policyHandler.DeletePolicy) // ExternalService 管理(用户视角) protected.GET("/services", serviceHandler.ListServices) protected.POST("/services", serviceHandler.CreateService) protected.GET("/services/:id", serviceHandler.GetService) protected.PUT("/services/:id", serviceHandler.UpdateService) protected.DELETE("/services/:id", serviceHandler.DeleteService) protected.POST("/services/:id/test", serviceHandler.TestServiceConnectivity) protected.GET("/services/schema", serviceHandler.GetServiceSchema) protected.GET("/monitor/metrics", s.handleMetrics) // 注册 WebSocket 通道 protected.GET("/ws", wsHandler.ServeWS) // 待审核管理 protected.GET("/pending-joins", pendingJoinHandler.ListPendingJoins) protected.POST("/pending-joins/:id/approve", pendingJoinHandler.ApproveJoin) protected.POST("/pending-joins/:id/reject", pendingJoinHandler.RejectJoin) protected.GET("/pending-joins/count", pendingJoinHandler.CountPending) protected.POST("/pending-joins/cleanup", pendingJoinHandler.DeleteExpired) } } return nil } // handleMetrics 监控指标 API func (s *Server) handleMetrics(c *gin.Context) { // 获取系统信息 var memStats runtime.MemStats runtime.ReadMemStats(&memStats) // 获取 CPU 使用率(简化版本) cpuPercent := 0.0 if cpus, err := cpu.Percent(time.Second, false); err == nil && len(cpus) > 0 { cpuPercent = cpus[0] } // 网络统计(从数据库) var deviceCount, networkCount, onlineCount int64 s.store.DB().Model(&model.Device{}).Count(&deviceCount) s.store.DB().Model(&model.Network{}).Count(&networkCount) s.store.DB().Model(&model.Device{}).Where("status = ?", "online").Count(&onlineCount) // 返回 Prometheus 格式或 JSON 格式 accept := c.GetHeader("Accept") if strings.Contains(accept, "text/plain") { // Prometheus 格式 metrics := fmt.Sprintf(`# HELP meshray_memory_alloc_bytes 当前内存使用量 # TYPE meshray_memory_alloc_bytes gauge meshray_memory_alloc_bytes %d # HELP meshray_cpu_usage_percent CPU 使用率 # TYPE meshray_cpu_usage_percent gauge meshray_cpu_usage_percent %.2f # HELP meshray_device_total 设备总数 # TYPE meshray_device_total gauge meshray_device_total %d # HELP meshray_device_online 在线设备数 # TYPE meshray_device_online gauge meshray_device_online %d # HELP meshray_network_total 网络总数 # TYPE meshray_network_total gauge meshray_network_total %d `, memStats.Alloc, cpuPercent, deviceCount, onlineCount, networkCount) c.Header("Content-Type", "text/plain; version=0.0.4") c.String(200, metrics) } else { // JSON 格式(前端使用) c.JSON(200, gin.H{ "data": gin.H{ "memory": gin.H{ "alloc_bytes": memStats.Alloc, "alloc_mb": float64(memStats.Alloc) / 1024 / 1024, "sys_bytes": memStats.Sys, "num_gc": memStats.NumGC, }, "cpu": gin.H{ "usage_percent": cpuPercent, }, "devices": gin.H{ "total": deviceCount, "online": onlineCount, "offline": deviceCount - onlineCount, }, "networks": gin.H{ "total": networkCount, }, "timestamp": time.Now().Unix(), }, }) } } // getContentType 根据文件扩展名返回 Content-Type func getContentType(filePath string) string { ext := strings.ToLower(filepath.Ext(filePath)) switch ext { case ".html": return "text/html; charset=utf-8" case ".css": return "text/css; charset=utf-8" case ".js": return "application/javascript; charset=utf-8" case ".json": return "application/json; charset=utf-8" case ".png": return "image/png" case ".jpg", ".jpeg": return "image/jpeg" case ".gif": return "image/gif" case ".svg": return "image/svg+xml" case ".ico": return "image/x-icon" default: return "application/octet-stream" } } // Run 启动 API 服务器 func (s *Server) Run() error { addr := fmt.Sprintf(":%d", s.config.Server.Port) s.logger.Info("Starting MeshRay", zap.String("address", addr)) // 打印访问地址 fmt.Println("") fmt.Printf("🌐 MeshRay 启动成功!\n") fmt.Printf("📍 访问地址:http://localhost:%d\n", s.config.Server.Port) fmt.Printf("💡 提示:请在浏览器中打开上述地址访问管理面板\n") fmt.Println("") return s.engine.Run(addr) } // loadSigningKey 加载或生成 Ed25519 签名密钥 func (s *Server) loadSigningKey() (ed25519.PrivateKey, error) { var key model.SecurityKey err := s.store.DB().Where("name = ?", "meshseed_signing").First(&key).Error if err == nil { // 从数据库加载已有密钥 keyBytes, decodeErr := base64.StdEncoding.DecodeString(key.Value) if decodeErr != nil { return nil, fmt.Errorf("解码密钥失败:%w", decodeErr) } return ed25519.PrivateKey(keyBytes), nil } if !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("查询密钥失败:%w", err) } // 密钥不存在,生成新密钥并保存 _, newKey, err := ed25519.GenerateKey(rand.Reader) if err != nil { return nil, fmt.Errorf("生成密钥失败:%w", err) } keyBytes := []byte(newKey) err = s.store.DB().Create(&model.SecurityKey{ Name: "meshseed_signing", Value: base64.StdEncoding.EncodeToString(keyBytes), Algorithm: "ed25519", Purpose: "MeshSeed 数字签名", }).Error if err != nil { return nil, fmt.Errorf("保存密钥失败:%w", err) } s.logger.Info("已生成新的签名密钥") return newKey, nil }