Initial commit

This commit is contained in:
2026-06-30 15:14:37 +08:00
commit 15dab96872
311 changed files with 95639 additions and 0 deletions
+532
View File
@@ -0,0 +1,532 @@
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/internal/api/handler"
"git.zkcoi.com/zkcoi/meshray/internal/api/middleware"
"git.zkcoi.com/zkcoi/meshray/internal/config"
"git.zkcoi.com/zkcoi/meshray/internal/ctr"
corehandler "git.zkcoi.com/zkcoi/meshray/internal/handler"
"git.zkcoi.com/zkcoi/meshray/internal/model"
"git.zkcoi.com/zkcoi/meshray/internal/service"
"git.zkcoi.com/zkcoi/meshray/internal/store/sqlite"
"git.zkcoi.com/zkcoi/meshray/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,
}
// 注册中间件
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
}
// 尝试直接提供文件
filePath := strings.TrimPrefix(path, "/")
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.htmlSPA 路由支持)
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
}