package controller import ( "fmt" "net/http" "strconv" "strings" "time" "ezops/middleware" "ezops/model" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // TerminalSessionHandler 终端会话审计处理器 // Logan.Li type TerminalSessionHandler struct { db *gorm.DB killFunc func(sessionID uint) bool // 外部 Kill 回调 } // NewTerminalSessionHandler 创建终端会话审计处理器 func NewTerminalSessionHandler(db *gorm.DB, killFunc func(sessionID uint) bool) *TerminalSessionHandler { return &TerminalSessionHandler{db: db, killFunc: killFunc} } // ListTerminalSessions 终端会话列表 // GET /api/v1/terminal-sessions?server_id=&username=&status=&page=&page_size= func (h *TerminalSessionHandler) ListTerminalSessions(c *gin.Context) { var req model.TerminalSessionQuery if err := c.ShouldBindQuery(&req); err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()}) return } if req.Page <= 0 { req.Page = 1 } if req.PageSize <= 0 { req.PageSize = 20 } query := h.db.Model(&model.TerminalSession{}) if req.ServerID > 0 { query = query.Where("server_id = ?", req.ServerID) } if req.Username != "" { query = query.Where("username LIKE ?", "%"+req.Username+"%") } if req.Status != "" { query = query.Where("status = ?", req.Status) } if kw := strings.TrimSpace(req.Keyword); kw != "" { like := "%" + kw + "%" query = query.Where( "username LIKE ? OR server_name LIKE ? OR server_ip LIKE ? OR client_ip LIKE ?", like, like, like, like, ) } if req.StartDate != "" { if t, err := time.ParseInLocation("2006-01-02", req.StartDate, time.Local); err == nil { query = query.Where("start_time >= ?", t) } } if req.EndDate != "" { if t, err := time.ParseInLocation("2006-01-02", req.EndDate, time.Local); err == nil { query = query.Where("start_time < ?", t.Add(24*time.Hour)) } } if req.HasRecord == "1" || strings.EqualFold(req.HasRecord, "true") { query = query.Where("record_size > 0") } var total int64 if err := query.Count(&total).Error; err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败"}) return } var sessions []model.TerminalSession if err := query.Order("id DESC"). Offset((req.Page - 1) * req.PageSize). Limit(req.PageSize). Find(&sessions).Error; err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败"}) return } c.JSON(http.StatusOK, model.Response{Code: 200, Data: model.ListResponse{ List: sessions, Total: total, }}) } // GetTerminalSession 终端会话详情 // GET /api/v1/terminal-sessions/:id func (h *TerminalSessionHandler) GetTerminalSession(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的会话ID"}) return } var session model.TerminalSession if err := h.db.First(&session, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "会话不存在"}) return } c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败"}) return } c.JSON(http.StatusOK, model.Response{Code: 200, Data: session}) } // GetTerminalRecord 终端会话录制数据(用于回放) // GET /api/v1/terminal-sessions/:id/record func (h *TerminalSessionHandler) GetTerminalRecord(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的会话ID"}) return } var session model.TerminalSession if err := h.db.First(&session, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "会话不存在"}) return } c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败"}) return } c.JSON(http.StatusOK, model.Response{Code: 200, Data: session.RecordData}) } // DeleteTerminalSession 删除终端会话记录 // DELETE /api/v1/terminal-sessions/:id func (h *TerminalSessionHandler) DeleteTerminalSession(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的会话ID"}) return } if err := h.db.Delete(&model.TerminalSession{}, id).Error; err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败"}) return } c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"}) } // BatchDeleteTerminalSessions 批量删除终端会话记录 // POST /api/v1/terminal-sessions/batch-delete func (h *TerminalSessionHandler) BatchDeleteTerminalSessions(c *gin.Context) { var req model.IDsRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的请求参数"}) return } result := h.db.Where("id IN ?", req.IDs).Delete(&model.TerminalSession{}) if result.Error != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败"}) return } c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功", Data: gin.H{"deleted": result.RowsAffected}}) } // KillSession 强制终止终端会话 // POST /api/v1/terminal-sessions/:id/kill func (h *TerminalSessionHandler) KillSession(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的会话ID"}) return } var session model.TerminalSession if err := h.db.First(&session, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "会话不存在"}) return } c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败"}) return } if session.Status == "closed" { c.JSON(http.StatusOK, model.Response{Code: 200, Message: "already closed"}) return } // 强制终止 WebShell 连接;在线连接由 webshell finalize 落盘录制,勿抢先 UPDATE 覆盖 // Logan.Li killed := false if h.killFunc != nil { killed = h.killFunc(session.ID) } if killed { c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已终止"}) return } // 僵尸会话(连接已断开、服务重启等):无录制可落盘,仅关闭记录 now := time.Now() duration := int(now.Sub(session.StartTime).Seconds()) if err := h.db.Model(&session).Updates(map[string]interface{}{ "status": "closed", "end_time": now, "duration": duration, }).Error; err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "终止失败"}) return } c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已终止"}) } // DownloadSession 下载终端会话录制文件 // GET /api/v1/terminal-sessions/:id/download func (h *TerminalSessionHandler) DownloadSession(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的会话ID"}) return } var session model.TerminalSession if err := h.db.First(&session, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "会话不存在"}) return } c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败"}) return } if session.RecordData == "" { c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "无录制数据"}) return } c.Header("Content-Type", "text/plain; charset=utf-8") c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="%d.cast"`, session.ID)) c.Header("Content-Length", fmt.Sprintf("%d", len(session.RecordData))) c.String(http.StatusOK, session.RecordData) } // LogReplayView 记录终端回放查看审计(二次审计留痕) // Logan.Li // POST /api/v1/terminal-sessions/:id/replay-view func (h *TerminalSessionHandler) LogReplayView(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的会话ID"}) return } var session model.TerminalSession if err := h.db.Select("id", "username", "server_name", "server_ip").First(&session, id).Error; err != nil { c.JSON(http.StatusOK, model.Response{Code: 404, Message: "会话不存在"}) return } uid := middleware.GetUserID(c) log := model.AuditLog{ UserID: &uid, Username: middleware.GetUsername(c), IP: c.ClientIP(), UserAgent: c.GetHeader("User-Agent"), Module: "terminal_audit", Action: "replay_view", Resource: "terminal_session", ResourceID: strconv.FormatUint(id, 10), Request: fmt.Sprintf("回放终端会话 #%d(%s → %s/%s)", id, session.Username, session.ServerName, session.ServerIP), Status: 200, } _ = h.db.Create(&log).Error // 落库失败不阻断回放 c.JSON(http.StatusOK, model.Response{Code: 200, Message: "ok"}) } // StreamSession 终端会话实时流(WebSocket 暂未实现) // GET /api/v1/terminal-sessions/:id/stream func (h *TerminalSessionHandler) StreamSession(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的会话ID"}) return } var session model.TerminalSession if err := h.db.First(&session, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "会话不存在"}) return } c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败"}) return } c.JSON(http.StatusOK, gin.H{ "message": "WebSocket streaming not yet implemented", "session_id": id, "status": session.Status, }) }