Files
pika/internal/app.go
T
dushixiang 6203b87e58 - 修复探针过期时间无法取消的问题
- 增加注册探针时服务端地址的检测与提示
- 修复指标图表数据不跟随鼠标的问题
- 修复服务监控提示框被遮挡的问题
- 首页展示全部的温度数据
2025-12-18 18:40:29 +08:00

501 lines
17 KiB
Go

package internal
import (
"bytes"
"context"
"html/template"
"log"
"net/http"
"strings"
"time"
"github.com/dushixiang/pika/internal/config"
"github.com/dushixiang/pika/internal/handler"
"github.com/dushixiang/pika/internal/models"
"github.com/dushixiang/pika/internal/scheduler"
"github.com/dushixiang/pika/pkg/replace"
"github.com/dushixiang/pika/pkg/version"
"github.com/dushixiang/pika/web"
"github.com/google/uuid"
"github.com/spf13/afero/mem"
"github.com/go-errors/errors"
"github.com/go-orz/orz"
"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"go.uber.org/zap"
"gorm.io/gorm"
)
func Run(configPath string) {
err := orz.Quick(configPath, setup)
if err != nil {
log.Fatal(err)
}
}
func setup(app *orz.App) error {
// 数据库迁移
if err := autoMigrate(app.GetDatabase()); err != nil {
return err
}
// 读取应用配置
var appConfig config.AppConfig
_config := app.GetConfig()
if _config != nil {
if err := _config.App.Unmarshal(&appConfig); err != nil {
app.Logger().Error("读取配置失败", zap.Error(err))
return err
}
}
// 设置默认值
if appConfig.JWT.Secret == "" {
appConfig.JWT.Secret = uuid.NewString()
app.Logger().Warn("未配置JWT密钥,使用随机UUID")
}
if appConfig.JWT.ExpiresHours == 0 {
appConfig.JWT.ExpiresHours = 168 // 7天
}
// 初始化应用组件
components, err := InitializeApp(app.Logger(), app.GetDatabase(), &appConfig)
if err != nil {
return err
}
// 初始化默认属性配置
ctx := context.Background()
if err := initDefaultProperties(ctx, components, app.Logger()); err != nil {
app.Logger().Error("初始化默认属性配置失败", zap.Error(err))
// 不返回错误,继续启动
}
// 初始化探针的状态全部为离线
if err := components.AgentService.InitStatus(ctx); err != nil {
app.Logger().Error("初始化探针状态失败", zap.Error(err))
// 不返回错误,继续启动
}
// 启动WebSocket管理器
go components.WSManager.Run(ctx)
// 启动指标监控任务(用于告警检测)
go startMetricsMonitoring(ctx, components, app.Logger())
// 启动服务监控任务调度器
monitorScheduler := scheduler.NewMonitorScheduler(components.MonitorService, app.Logger())
// 将调度器注入到 MonitorService(避免循环依赖)
components.MonitorService.SetScheduler(monitorScheduler)
monitorScheduler.Start(ctx)
// 启动流量重置检查任务(每小时检查一次)
go startTrafficResetCheck(ctx, components, app.Logger())
// 启动 DDNS 定时任务
go components.DDNSService.Run(ctx)
// 设置API
setupApi(app, components)
return nil
}
func setupApi(app *orz.App, components *AppComponents) {
logger := app.Logger()
e := app.GetEcho()
e.Use(middleware.Recover())
e.Use(ErrorHandler(logger))
indexTemplate, err := template.New("index").Parse(web.IndexHtml())
if err != nil {
logger.Fatal("failed to parse index.html", zap.Error(err))
}
// 静态文件服务
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
Skipper: func(c echo.Context) bool {
// 不处理接口
if strings.HasPrefix(c.Request().RequestURI, "/api") {
return true
}
// 不处理WebSocket
if strings.HasPrefix(c.Request().RequestURI, "/ws") {
return true
}
return false
},
Index: "index.html",
HTML5: true,
Browse: false,
IgnoreBase: false,
Filesystem: replace.FS(http.FS(web.Assets()), func(name string, file http.File) (http.File, error) {
if name == "index.html" {
fileData := mem.CreateFile(name)
fileHandle := mem.NewFileHandle(fileData)
systemConfig, err := components.PropertyService.GetSystemConfig(context.Background())
if err != nil {
return file, nil
}
var buf bytes.Buffer
err = indexTemplate.Execute(&buf, systemConfig)
if err != nil {
return file, err
}
if _, err := fileHandle.Write(buf.Bytes()); err != nil {
return nil, err
}
return fileHandle, nil
}
return file, nil
}),
}))
customValidator := CustomValidator{Validator: validator.New()}
if err := customValidator.TransInit(); err != nil {
logger.Fatal("failed to init custom validator", zap.Error(err))
}
e.Validator = &customValidator
// 公开接口(无需认证)
publicApi := e.Group("/api")
{
// 认证相关
publicApi.POST("/login", components.AccountHandler.Login)
publicApi.GET("/auth/config", components.AccountHandler.GetAuthConfig)
publicApi.GET("/auth/oidc/url", components.AccountHandler.GetOIDCAuthURL)
publicApi.GET("/auth/github/url", components.AccountHandler.GetGitHubAuthURL)
// Agent 版本和下载(完全公开,无需任何认证)
publicApi.GET("/agent/version", components.AgentHandler.GetAgentVersion)
publicApi.GET("/agent/downloads/:filename", components.AgentHandler.DownloadAgent)
publicApi.GET("/agent/install.sh", components.AgentHandler.GetInstallScript)
}
// 公开接口(支持可选认证)- 已登录返回全部数据,未登录只返回公开数据
publicApiWithOptionalAuth := e.Group("/api")
publicApiWithOptionalAuth.Use(OptionalJWTAuthMiddleware(components.AccountHandler))
{
// 探针信息(公开访问,支持可选认证)- 用于公共展示页面
publicApiWithOptionalAuth.GET("/agents", components.AgentHandler.GetAgents)
publicApiWithOptionalAuth.GET("/agents/tags", components.AgentHandler.GetTags)
publicApiWithOptionalAuth.GET("/agents/:id", components.AgentHandler.Get)
publicApiWithOptionalAuth.GET("/agents/:id/metrics", components.AgentHandler.GetMetrics)
publicApiWithOptionalAuth.GET("/agents/:id/metrics/latest", components.AgentHandler.GetLatestMetrics)
publicApiWithOptionalAuth.GET("/agents/:id/network-interfaces", components.AgentHandler.GetAvailableNetworkInterfaces)
publicApiWithOptionalAuth.GET("/agents/:id/traffic", components.AgentHandler.GetTrafficStats)
// 监控统计数据(公开访问,支持可选认证)- 用于公共展示页面
publicApiWithOptionalAuth.GET("/monitors", components.MonitorHandler.GetMonitors)
publicApiWithOptionalAuth.GET("/monitors/:id/stats", components.MonitorHandler.GetStatsByID)
publicApiWithOptionalAuth.GET("/monitors/:id/agents", components.MonitorHandler.GetAgentStatsByID)
publicApiWithOptionalAuth.GET("/monitors/:id/history", components.MonitorHandler.GetHistoryByID)
// Logo(公开访问)- 用于公共页面只获取 Logo
publicApiWithOptionalAuth.GET("/logo", components.PropertyHandler.GetLogo)
}
// WebSocket 路由(探针连接)
e.GET("/ws/agent", components.AgentHandler.HandleWebSocket)
// 管理员 API 路由(需要认证)
adminApi := e.Group("/api/admin")
adminApi.Use(JWTAuthMiddleware(components.AccountHandler))
{
adminApi.GET("/version", func(c echo.Context) error {
return c.JSON(http.StatusOK, orz.Map{
"version": version.GetVersion(),
"agentVersion": version.GetAgentVersion(),
})
})
// 账户相关
adminApi.GET("/account/info", components.AccountHandler.GetCurrentUser)
adminApi.POST("/logout", components.AccountHandler.Logout)
// API密钥管理
adminApi.GET("/api-keys", components.ApiKeyHandler.Paging)
adminApi.POST("/api-keys", components.ApiKeyHandler.Create)
adminApi.GET("/api-keys/:id", components.ApiKeyHandler.Get)
adminApi.PUT("/api-keys/:id", components.ApiKeyHandler.Update)
adminApi.DELETE("/api-keys/:id", components.ApiKeyHandler.Delete)
adminApi.POST("/api-keys/:id/enable", components.ApiKeyHandler.Enable)
adminApi.POST("/api-keys/:id/disable", components.ApiKeyHandler.Disable)
// 探针管理(管理员功能)
adminApi.POST("/server-url", components.AgentHandler.GetServerUrl)
adminApi.GET("/agents", components.AgentHandler.Paging)
adminApi.GET("/agents/statistics", components.AgentHandler.GetStatistics)
adminApi.GET("/agents/tags", components.AgentHandler.GetTags)
adminApi.GET("/agents/:id", components.AgentHandler.GetForAdmin)
adminApi.PUT("/agents/:id", components.AgentHandler.UpdateInfo)
adminApi.POST("/agents/batch/tags", components.AgentHandler.BatchUpdateTags)
adminApi.DELETE("/agents/:id", components.AgentHandler.Delete)
adminApi.POST("/agents/:id/command", components.AgentHandler.SendCommand)
// 流量管理(管理员访问)
adminApi.PUT("/agents/:id/traffic-config", components.AgentHandler.UpdateTrafficConfig)
adminApi.POST("/agents/:id/traffic-reset", components.AgentHandler.ResetAgentTraffic)
// VPS审计结果(管理员访问)
adminApi.GET("/agents/:id/audit/result", components.AgentHandler.GetAuditResult)
adminApi.GET("/agents/:id/audit/results", components.AgentHandler.ListAuditResults)
// 防篡改管理(管理员功能)
adminApi.GET("/agents/:id/tamper/config", components.TamperHandler.GetTamperConfig)
adminApi.PUT("/agents/:id/tamper/config", components.TamperHandler.UpdateTamperConfig)
adminApi.GET("/agents/:id/tamper/events", components.TamperHandler.GetTamperEvents)
adminApi.GET("/agents/:id/tamper/alerts", components.TamperHandler.GetTamperAlerts)
// 通用属性管理
adminApi.GET("/properties/:id", components.PropertyHandler.GetProperty)
adminApi.PUT("/properties/:id", components.PropertyHandler.SetProperty)
// 通知渠道测试(从数据库读取配置测试)
adminApi.POST("/notification-channels/:type/test", components.PropertyHandler.TestNotificationChannel)
// 告警记录查询
adminApi.GET("/alert-records", components.AlertHandler.ListAlertRecords)
adminApi.DELETE("/alert-records", components.AlertHandler.ClearAlertRecords)
// 服务监控配置
adminApi.GET("/monitors", components.MonitorHandler.List)
adminApi.POST("/monitors", components.MonitorHandler.Create)
adminApi.GET("/monitors/:id", components.MonitorHandler.Get)
adminApi.PUT("/monitors/:id", components.MonitorHandler.Update)
adminApi.DELETE("/monitors/:id", components.MonitorHandler.Delete)
// DNS Provider 管理
adminApi.GET("/dns-providers", components.DNSProviderHandler.GetAll)
adminApi.POST("/dns-providers", components.DNSProviderHandler.Upsert)
adminApi.DELETE("/dns-providers/:provider", components.DNSProviderHandler.Delete)
// DDNS 配置管理
adminApi.GET("/ddns", components.DDNSHandler.Paging)
adminApi.POST("/ddns", components.DDNSHandler.Create)
adminApi.GET("/ddns/:id", components.DDNSHandler.Get)
adminApi.PUT("/ddns/:id", components.DDNSHandler.Update)
adminApi.DELETE("/ddns/:id", components.DDNSHandler.Delete)
adminApi.POST("/ddns/:id/enable", components.DDNSHandler.Enable)
adminApi.POST("/ddns/:id/disable", components.DDNSHandler.Disable)
adminApi.GET("/ddns/:id/records", components.DDNSHandler.GetRecords)
}
// OIDC 认证路由(如果启用)
publicApi.POST("/auth/oidc/callback", components.AccountHandler.OIDCLogin)
// GitHub 认证路由(如果启用)
publicApi.POST("/auth/github/callback", components.AccountHandler.GitHubLogin)
}
func autoMigrate(database *gorm.DB) error {
// 自动迁移数据库表
return database.AutoMigrate(
&models.Agent{}, // 探针
&models.ApiKey{}, // ApiKey
&models.HostMetric{}, // 保留主机静态信息表
&models.AuditResult{}, // 审计历史
&models.Property{}, // 系统属性
&models.AlertRecord{}, // 告警记录
&models.AlertState{}, // 告警状态
&models.MonitorTask{}, // 服务监控
&models.TamperProtectConfig{}, // 防篡改配置
&models.TamperEvent{}, // 防篡改事件
&models.TamperAlert{}, // 防篡改告警
&models.DDNSConfig{}, // DDNS 配置
&models.DDNSRecord{}, // DDNS 记录
)
}
// initDefaultProperties 初始化默认属性配置
func initDefaultProperties(ctx context.Context, components *AppComponents, logger *zap.Logger) error {
// 使用 PropertyService 的初始化方法
return components.PropertyService.InitializeDefaultConfigs(ctx)
}
func ErrorHandler(logger *zap.Logger) func(next echo.HandlerFunc) echo.HandlerFunc {
var a = func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if err := next(c); err != nil {
var he *echo.HTTPError
if errors.As(err, &he) {
return c.JSON(he.Code, orz.Map{
"code": he.Code,
"message": err.Error(),
})
}
var oe *orz.Error
if errors.As(err, &oe) {
return c.JSON(400, orz.Map{
"code": oe.Code,
"message": err.Error(),
})
}
logger.Sugar().Errorf("[ERROR] %s", err.Error())
return c.JSON(500, orz.Map{
"code": 500,
"message": "Internal Server Error",
})
}
return nil
}
}
return a
}
// startMetricsMonitoring 启动指标监控任务(用于告警检测)
func startMetricsMonitoring(ctx context.Context, components *AppComponents, logger *zap.Logger) {
logger.Info("启动指标监控任务")
ticker := time.NewTicker(30 * time.Second) // 每30秒检查一次
defer ticker.Stop()
for {
select {
case <-ctx.Done():
logger.Info("指标监控任务已停止")
return
case <-ticker.C:
// 检查所有在线探针的最新指标
agents, err := components.AgentService.ListOnlineAgents(ctx)
if err != nil {
logger.Error("获取在线探针失败", zap.Error(err))
continue
}
for _, agent := range agents {
// 获取最新指标
latest, ok := components.MetricService.GetLatestMetrics(agent.ID)
if !ok {
logger.Debug("获取探针最新指标失败", zap.String("agentId", agent.ID))
continue
}
if latest == nil {
logger.Debug("探针最新指标为空", zap.String("agentId", agent.ID))
continue
}
// 提取 CPU、内存、磁盘使用率、网速
var cpuUsage, memoryUsage, diskUsage, networkSpeed float64
if latest.CPU != nil {
cpuUsage = latest.CPU.UsagePercent
}
if latest.Memory != nil {
memoryUsage = latest.Memory.UsagePercent
}
if latest.Disk != nil {
diskUsage = latest.Disk.UsagePercent
}
if latest.Network != nil {
// 网速 = (发送速率 + 接收速率) / 1024 / 1024 (转换为 MB/s)
networkSpeed = float64(latest.Network.TotalBytesSentRate+latest.Network.TotalBytesRecvRate) / 1024 / 1024
}
// 检查告警规则
if err := components.AlertService.CheckMetrics(ctx, agent.ID, cpuUsage, memoryUsage, diskUsage, networkSpeed); err != nil {
logger.Error("检查告警规则失败", zap.String("agentId", agent.ID), zap.Error(err))
}
}
// 检查监控相关告警(证书和服务下线)
if err := components.AlertService.CheckMonitorAlerts(ctx); err != nil {
logger.Error("检查监控告警失败", zap.Error(err))
}
}
}
}
// startTrafficResetCheck 启动流量重置检查定时任务
func startTrafficResetCheck(ctx context.Context, components *AppComponents, logger *zap.Logger) {
logger.Info("启动流量重置检查任务")
ticker := time.NewTicker(1 * time.Hour) // 每小时检查一次
defer ticker.Stop()
for {
select {
case <-ctx.Done():
logger.Info("流量重置检查任务已停止")
return
case <-ticker.C:
if err := components.AgentService.CheckAndResetTraffic(ctx); err != nil {
logger.Error("流量重置检查失败", zap.Error(err))
}
}
}
}
// JWTAuthMiddleware JWT 认证中间件(必须登录)
func JWTAuthMiddleware(accountHandler *handler.AccountHandler) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// 从 Authorization header 获取 token
authHeader := c.Request().Header.Get("Authorization")
if authHeader == "" {
return echo.NewHTTPError(http.StatusUnauthorized, "未提供认证令牌")
}
// 检查 Bearer 前缀
const bearerPrefix = "Bearer "
if len(authHeader) < len(bearerPrefix) || authHeader[:len(bearerPrefix)] != bearerPrefix {
return echo.NewHTTPError(http.StatusUnauthorized, "认证令牌格式错误")
}
tokenString := authHeader[len(bearerPrefix):]
// 验证 token
claims, err := accountHandler.ValidateToken(tokenString)
if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "认证令牌无效: "+err.Error())
}
// 将用户信息存入 context
c.Set("userID", claims.UserID)
c.Set("username", claims.Username)
c.Set("authenticated", true)
return next(c)
}
}
}
// OptionalJWTAuthMiddleware 可选 JWT 认证中间件(尝试解析 token,但不强制要求)
func OptionalJWTAuthMiddleware(accountHandler *handler.AccountHandler) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// 从 Authorization header 获取 token
authHeader := c.Request().Header.Get("Authorization")
if authHeader != "" {
// 检查 Bearer 前缀
const bearerPrefix = "Bearer "
if len(authHeader) >= len(bearerPrefix) && authHeader[:len(bearerPrefix)] == bearerPrefix {
tokenString := authHeader[len(bearerPrefix):]
// 尝试验证 token
claims, err := accountHandler.ValidateToken(tokenString)
if err == nil {
// token 有效,将用户信息存入 context
c.Set("userID", claims.UserID)
c.Set("username", claims.Username)
c.Set("authenticated", true)
}
}
}
// 无论 token 是否有效,都继续处理请求
return next(c)
}
}
}
// APIKeyAuthMiddleware 使用 API Key 进行认证