Files
aiops/backend/controller/terminal_session.go
T
attacker cd30af5bd6 feat(quality): Q5 后端错误处理治理扩展 — RDP/API Token/AIOps 模块 8 处 _ = DB 写入改为 log.Printf
Q4 修复了 controller/ 层 8 处 DB 写入的 _ = 静默忽略,但后续合并的 RDP
远程桌面、API Token、AIOps 告警分析等模块引入了新的 _ = DB 写入忽略。
本次将 controller/middleware 层所有 DB 写入操作(Create/Update/Updates)
的 _ = 静默忽略改为带 log.Printf 错误日志(不阻断业务流程)。

修改文件 (6):
- middleware/auth.go: API 密钥最后使用时间更新
- controller/terminal_session.go: 终端回放审计日志 + 修复 log 变量遮蔽
- controller/user_api_token.go: API 密钥默认值补全
- controller/rdp.go: RDP 会话结束状态 + 命令记录
- controller/rdp_session.go: 会话终止 + 回放审计 + 修复 log 变量遮蔽
- controller/aiops_alert_analysis.go: AI 告警分析结果保存

修复 2 处 Go 变量名遮蔽陷阱(log := model.AuditLog{} 遮蔽 "log" 包 → auditLog)
保留 4 处可接受忽略(1 DB 读取 + 3 WebSocket 帧写入)

验证: go build=0, 17/17 集成测试通过, Docker healthy
2026-07-06 07:16:39 +08:00

308 lines
9.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
})
}