mirror of
https://gitee.com/samwaf/SamWaf.git
synced 2026-08-30 17:20:58 +08:00
@@ -60,6 +60,7 @@ type APIGroup struct {
|
||||
WafDataRetentionApi
|
||||
WafOwaspApi
|
||||
WafHostPathRuleApi
|
||||
WafAppApi
|
||||
}
|
||||
|
||||
var APIGroupAPP = new(APIGroup)
|
||||
@@ -130,4 +131,5 @@ var (
|
||||
|
||||
wafDataRetentionService = waf_service.WafDataRetentionServiceApp
|
||||
wafHostPathRuleService = waf_service.WafHostPathRuleServiceApp
|
||||
wafAppService = waf_service.WafAppServiceApp
|
||||
)
|
||||
|
||||
@@ -0,0 +1,491 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"SamWaf/enums"
|
||||
"SamWaf/global"
|
||||
"SamWaf/globalobj"
|
||||
"SamWaf/model/common/response"
|
||||
"SamWaf/model/request"
|
||||
"SamWaf/model/spec"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type WafAppApi struct{}
|
||||
|
||||
// AddApi 新增应用
|
||||
// @Summary 新增应用
|
||||
// @Description 新增一个由 SamWaf 托管的应用进程配置
|
||||
// @Tags 应用管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body request.WafAppAddReq true "应用配置"
|
||||
// @Success 200 {object} response.Response "添加成功"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /application/app/add [post]
|
||||
func (w *WafAppApi) AddApi(c *gin.Context) {
|
||||
var req request.WafAppAddReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMessage("解析失败", c)
|
||||
return
|
||||
}
|
||||
if req.Name == "" || req.StartCmd == "" {
|
||||
response.FailWithMessage("应用名称和启动命令不能为空", c)
|
||||
return
|
||||
}
|
||||
if wafAppService.CheckIsExist(req.Name) > 0 {
|
||||
response.FailWithMessage("应用名称已存在", c)
|
||||
return
|
||||
}
|
||||
app, err := wafAppService.AddApi(req)
|
||||
if err != nil {
|
||||
response.FailWithMessage("添加失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
global.GWAF_CHAN_COMMON_MSG <- spec.ChanCommon{
|
||||
Type: enums.ChanComTypeApp,
|
||||
OpType: enums.OP_TYPE_NEW,
|
||||
Content: *app,
|
||||
}
|
||||
response.OkWithMessage("添加成功", c)
|
||||
}
|
||||
|
||||
// GetListApi 获取应用列表
|
||||
// @Summary 获取应用列表
|
||||
// @Description 分页查询所有托管应用,返回列表及实时运行状态(RunStatus / PID / 重启次数)
|
||||
// @Tags 应用管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body request.WafAppSearchReq true "分页参数"
|
||||
// @Success 200 {object} response.Response{data=object} "查询成功,data.list 为应用列表,data.total 为总数"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /application/app/list [post]
|
||||
func (w *WafAppApi) GetListApi(c *gin.Context) {
|
||||
var req request.WafAppSearchReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMessage("解析失败", c)
|
||||
return
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 10
|
||||
}
|
||||
if req.PageIndex <= 0 {
|
||||
req.PageIndex = 1
|
||||
}
|
||||
list, total := wafAppService.GetListApi(req)
|
||||
|
||||
// 附加运行时状态
|
||||
type AppWithStatus struct {
|
||||
Id string `json:"id"`
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
AppDir string `json:"app_dir"`
|
||||
StartCmd string `json:"start_cmd"`
|
||||
AutoStart int `json:"auto_start"`
|
||||
StartStatus int `json:"start_status"`
|
||||
StopMode string `json:"stop_mode"`
|
||||
StopTimeout int `json:"stop_timeout"`
|
||||
RestartPolicy string `json:"restart_policy"`
|
||||
RestartDelay int `json:"restart_delay"`
|
||||
MaxRestartCount int `json:"max_restart_count"`
|
||||
LogMaxLines int `json:"log_max_lines"`
|
||||
Remarks string `json:"remarks"`
|
||||
RunStatus int `json:"run_status"`
|
||||
Pid int `json:"pid"`
|
||||
RestartCount int `json:"restart_count"`
|
||||
}
|
||||
|
||||
result := make([]AppWithStatus, 0, len(list))
|
||||
for _, app := range list {
|
||||
rt := globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.GetRuntimeStatus(app.Code)
|
||||
result = append(result, AppWithStatus{
|
||||
Id: app.Id,
|
||||
Code: app.Code,
|
||||
Name: app.Name,
|
||||
AppDir: app.AppDir,
|
||||
StartCmd: app.StartCmd,
|
||||
AutoStart: app.AutoStart,
|
||||
StartStatus: app.StartStatus,
|
||||
StopMode: app.StopMode,
|
||||
StopTimeout: app.StopTimeout,
|
||||
RestartPolicy: app.RestartPolicy,
|
||||
RestartDelay: app.RestartDelay,
|
||||
MaxRestartCount: app.MaxRestartCount,
|
||||
LogMaxLines: app.LogMaxLines,
|
||||
Remarks: app.Remarks,
|
||||
RunStatus: rt.Status,
|
||||
Pid: rt.Pid,
|
||||
RestartCount: rt.RestartCount,
|
||||
})
|
||||
}
|
||||
response.OkWithDetailed(gin.H{"list": result, "total": total}, "查询成功", c)
|
||||
}
|
||||
|
||||
// GetDetailApi 获取应用详情
|
||||
// @Summary 获取应用详情
|
||||
// @Description 根据 id 获取单个应用的完整配置信息
|
||||
// @Tags 应用管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id query string true "应用 ID"
|
||||
// @Success 200 {object} response.Response{data=model.WafApp} "获取成功"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /application/app/detail [get]
|
||||
func (w *WafAppApi) GetDetailApi(c *gin.Context) {
|
||||
var req request.WafAppDetailReq
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
response.FailWithMessage("解析失败", c)
|
||||
return
|
||||
}
|
||||
response.OkWithData(wafAppService.GetDetailApi(req), c)
|
||||
}
|
||||
|
||||
// ModifyApi 修改应用配置
|
||||
// @Summary 修改应用配置
|
||||
// @Description 修改已有应用的配置,若应用正在运行则自动重启
|
||||
// @Tags 应用管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body request.WafAppEditReq true "应用配置(含 id)"
|
||||
// @Success 200 {object} response.Response "修改成功"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /application/app/edit [post]
|
||||
func (w *WafAppApi) ModifyApi(c *gin.Context) {
|
||||
var req request.WafAppEditReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMessage("解析失败", c)
|
||||
return
|
||||
}
|
||||
if err := wafAppService.ModifyApi(req); err != nil {
|
||||
response.FailWithMessage("修改失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
app := wafAppService.GetDetailApi(request.WafAppDetailReq{Id: req.Id})
|
||||
global.GWAF_CHAN_COMMON_MSG <- spec.ChanCommon{
|
||||
Type: enums.ChanComTypeApp,
|
||||
OpType: enums.OP_TYPE_UPDATE,
|
||||
Content: *app,
|
||||
}
|
||||
response.OkWithMessage("修改成功", c)
|
||||
}
|
||||
|
||||
// DelApi 删除应用
|
||||
// @Summary 删除应用
|
||||
// @Description 删除应用配置,若进程正在运行则先停止再删除
|
||||
// @Tags 应用管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id query string true "应用 ID"
|
||||
// @Success 200 {object} response.Response "删除成功"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /application/app/del [get]
|
||||
func (w *WafAppApi) DelApi(c *gin.Context) {
|
||||
var req request.WafAppDelReq
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
response.FailWithMessage("解析失败", c)
|
||||
return
|
||||
}
|
||||
app := wafAppService.GetDetailApi(request.WafAppDetailReq{Id: req.Id})
|
||||
if err := wafAppService.DelApi(req); err != nil {
|
||||
response.FailWithMessage("删除失败", c)
|
||||
return
|
||||
}
|
||||
global.GWAF_CHAN_COMMON_MSG <- spec.ChanCommon{
|
||||
Type: enums.ChanComTypeApp,
|
||||
OpType: enums.OP_TYPE_DELETE,
|
||||
OldContent: *app,
|
||||
}
|
||||
response.OkWithMessage("删除成功", c)
|
||||
}
|
||||
|
||||
// StartApi 启动应用
|
||||
// @Summary 启动应用
|
||||
// @Description 向引擎发送启动指令(异步),使用 /application/app/status 接口轮询确认启动结果
|
||||
// @Tags 应用管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param code query string true "应用唯一编码"
|
||||
// @Success 200 {object} response.Response "启动指令已发送"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /application/app/start [get]
|
||||
func (w *WafAppApi) StartApi(c *gin.Context) {
|
||||
var req request.WafAppCodeReq
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
response.FailWithMessage("解析失败", c)
|
||||
return
|
||||
}
|
||||
global.GWAF_CHAN_COMMON_MSG <- spec.ChanCommon{
|
||||
Type: enums.ChanComTypeApp,
|
||||
OpType: enums.OP_TYPE_APP_START,
|
||||
Content: req.Code,
|
||||
}
|
||||
response.OkWithMessage("启动指令已发送", c)
|
||||
}
|
||||
|
||||
// StopApi 停止应用
|
||||
// @Summary 停止应用
|
||||
// @Description 向引擎发送停止指令(异步),使用 /application/app/status 接口轮询确认停止结果
|
||||
// @Tags 应用管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param code query string true "应用唯一编码"
|
||||
// @Success 200 {object} response.Response "停止指令已发送"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /application/app/stop [get]
|
||||
func (w *WafAppApi) StopApi(c *gin.Context) {
|
||||
var req request.WafAppCodeReq
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
response.FailWithMessage("解析失败", c)
|
||||
return
|
||||
}
|
||||
global.GWAF_CHAN_COMMON_MSG <- spec.ChanCommon{
|
||||
Type: enums.ChanComTypeApp,
|
||||
OpType: enums.OP_TYPE_APP_STOP,
|
||||
Content: req.Code,
|
||||
}
|
||||
response.OkWithMessage("停止指令已发送", c)
|
||||
}
|
||||
|
||||
// RestartApi 重启应用
|
||||
// @Summary 重启应用
|
||||
// @Description 向引擎发送重启指令(异步),使用 /application/app/status 接口轮询确认重启结果
|
||||
// @Tags 应用管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param code query string true "应用唯一编码"
|
||||
// @Success 200 {object} response.Response "重启指令已发送"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /application/app/restart [get]
|
||||
func (w *WafAppApi) RestartApi(c *gin.Context) {
|
||||
var req request.WafAppCodeReq
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
response.FailWithMessage("解析失败", c)
|
||||
return
|
||||
}
|
||||
global.GWAF_CHAN_COMMON_MSG <- spec.ChanCommon{
|
||||
Type: enums.ChanComTypeApp,
|
||||
OpType: enums.OP_TYPE_APP_RESTART,
|
||||
Content: req.Code,
|
||||
}
|
||||
response.OkWithMessage("重启指令已发送", c)
|
||||
}
|
||||
|
||||
// GetStatusApi 查询应用运行状态
|
||||
// @Summary 查询应用运行状态
|
||||
// @Description 实时查询应用进程的运行状态(同步),run_status: 0=已停止 1=运行中 2=已崩溃
|
||||
// @Tags 应用管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param code query string true "应用唯一编码"
|
||||
// @Success 200 {object} response.Response{data=object} "查询成功,data 含 pid/run_status/start_time/restart_count"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /application/app/status [get]
|
||||
func (w *WafAppApi) GetStatusApi(c *gin.Context) {
|
||||
var req request.WafAppCodeReq
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
response.FailWithMessage("解析失败", c)
|
||||
return
|
||||
}
|
||||
rt := globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.GetRuntimeStatus(req.Code)
|
||||
response.OkWithData(gin.H{
|
||||
"code": rt.Code,
|
||||
"pid": rt.Pid,
|
||||
"run_status": rt.Status,
|
||||
"start_time": rt.StartTime,
|
||||
"restart_count": rt.RestartCount,
|
||||
}, c)
|
||||
}
|
||||
|
||||
// GetLogsApi 获取应用日志
|
||||
// @Summary 获取应用日志
|
||||
// @Description 获取应用进程最近的 stdout/stderr 日志(内存中最多 LogMaxLines 行)
|
||||
// @Tags 应用管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param code query string true "应用唯一编码"
|
||||
// @Success 200 {object} response.Response{data=object} "查询成功,data.logs 为字符串数组"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /application/app/logs [get]
|
||||
func (w *WafAppApi) GetLogsApi(c *gin.Context) {
|
||||
var req request.WafAppCodeReq
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
response.FailWithMessage("解析失败", c)
|
||||
return
|
||||
}
|
||||
logs := globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.GetLogs(req.Code)
|
||||
if logs == nil {
|
||||
logs = []string{}
|
||||
}
|
||||
response.OkWithData(gin.H{"logs": logs}, c)
|
||||
}
|
||||
|
||||
// ClearLogsApi 清空应用日志
|
||||
// @Summary 清空应用日志
|
||||
// @Description 清空应用的内存日志及磁盘日志文件({AppDir}/app.log)
|
||||
// @Tags 应用管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body request.WafAppCodeReq true "应用唯一编码"
|
||||
// @Success 200 {object} response.Response "日志已清空"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /application/app/clearlogs [post]
|
||||
func (w *WafAppApi) ClearLogsApi(c *gin.Context) {
|
||||
var req request.WafAppCodeReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.FailWithMessage("解析失败", c)
|
||||
return
|
||||
}
|
||||
globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.ClearLogs(req.Code)
|
||||
response.OkWithMessage("日志已清空", c)
|
||||
}
|
||||
|
||||
// UploadFileApi 上传文件到应用目录
|
||||
// @Summary 上传文件到应用目录
|
||||
// @Description 上传文件到应用工作目录(不升级,不重启),可选 SHA256 哈希校验
|
||||
// @Tags 应用管理
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param code formData string true "应用唯一编码"
|
||||
// @Param hash formData string false "文件 SHA256 哈希值(可选,不传则跳过校验)"
|
||||
// @Param file formData file true "上传的文件"
|
||||
// @Success 200 {object} response.Response "上传成功"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /application/app/upload [post]
|
||||
func (w *WafAppApi) UploadFileApi(c *gin.Context) {
|
||||
code := c.PostForm("code")
|
||||
expectedHash := c.PostForm("hash")
|
||||
if code == "" {
|
||||
response.FailWithMessage("code 不能为空", c)
|
||||
return
|
||||
}
|
||||
file, header, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
response.FailWithMessage("获取文件失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if err := wafAppService.UploadFile(code, header.Filename, file, expectedHash); err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithMessage("上传成功", c)
|
||||
}
|
||||
|
||||
// UpgradeApi 升级应用(停止→备份→替换→重启)
|
||||
// @Summary 升级应用
|
||||
// @Description 同步执行升级流程:停止应用→备份旧文件→上传新文件(含 SHA256 校验)→重启应用。因需等待进程退出,请适当设置客户端超时
|
||||
// @Tags 应用管理
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param code formData string true "应用唯一编码"
|
||||
// @Param hash formData string false "文件 SHA256 哈希值(可选,不传则跳过校验)"
|
||||
// @Param file formData file true "新版本文件"
|
||||
// @Success 200 {object} response.Response "升级成功,应用已重新启动"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /application/app/upgrade [post]
|
||||
func (w *WafAppApi) UpgradeApi(c *gin.Context) {
|
||||
code := c.PostForm("code")
|
||||
expectedHash := c.PostForm("hash")
|
||||
if code == "" {
|
||||
response.FailWithMessage("code 不能为空", c)
|
||||
return
|
||||
}
|
||||
file, header, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
response.FailWithMessage("获取文件失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 同步停止,等进程真正退出后才能替换文件(Windows 不允许覆盖运行中的可执行文件)
|
||||
_ = globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.StopApp(code)
|
||||
|
||||
if err := wafAppService.UpgradeApp(code, header.Filename, file, expectedHash); err != nil {
|
||||
response.FailWithMessage("升级失败: "+err.Error(), c)
|
||||
// 升级失败也尝试恢复启动
|
||||
_ = globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.StartApp(code)
|
||||
return
|
||||
}
|
||||
|
||||
_ = globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.StartApp(code)
|
||||
response.OkWithMessage("升级成功,应用已重新启动", c)
|
||||
}
|
||||
|
||||
// RollbackApi 回滚应用到备份版本
|
||||
// @Summary 回滚应用
|
||||
// @Description 同步执行回滚流程:停止应用→从备份文件恢复→重启应用
|
||||
// @Tags 应用管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param code query string true "应用唯一编码"
|
||||
// @Param filename query string true "备份文件名(从备份列表接口获取)"
|
||||
// @Success 200 {object} response.Response "回滚成功,应用已重新启动"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /application/app/rollback [get]
|
||||
func (w *WafAppApi) RollbackApi(c *gin.Context) {
|
||||
var req request.WafAppRollbackReq
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
response.FailWithMessage("解析失败", c)
|
||||
return
|
||||
}
|
||||
|
||||
// 同步停止,等进程真正退出后才能覆盖文件
|
||||
_ = globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.StopApp(req.Code)
|
||||
|
||||
if err := wafAppService.RollbackApp(req.Code, req.Filename); err != nil {
|
||||
response.FailWithMessage("回滚失败: "+err.Error(), c)
|
||||
_ = globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.StartApp(req.Code)
|
||||
return
|
||||
}
|
||||
|
||||
_ = globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.StartApp(req.Code)
|
||||
response.OkWithMessage("回滚成功,应用已重新启动", c)
|
||||
}
|
||||
|
||||
// GetBackupsApi 获取备份文件列表
|
||||
// @Summary 获取备份文件列表
|
||||
// @Description 列出应用工作目录下 backup/ 子目录中的所有备份文件,包含文件名、大小和备份时间
|
||||
// @Tags 应用管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param code query string true "应用唯一编码"
|
||||
// @Success 200 {object} response.Response{data=object} "查询成功,data.list 为 BackupInfo 数组"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /application/app/backups [get]
|
||||
func (w *WafAppApi) GetBackupsApi(c *gin.Context) {
|
||||
var req request.WafAppCodeReq
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
response.FailWithMessage("解析失败", c)
|
||||
return
|
||||
}
|
||||
backups, err := wafAppService.ListBackups(req.Code)
|
||||
if err != nil {
|
||||
response.FailWithMessage("获取备份列表失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithData(gin.H{"list": backups}, c)
|
||||
}
|
||||
|
||||
// GetNetStatsApi 查询应用端口与连接 IP
|
||||
// @Summary 查询应用端口与连接 IP
|
||||
// @Description 查询应用进程树(含子进程)当前占用的端口及建立的 TCP 连接,结果缓存 30 秒,最多返回 1000 条连接记录
|
||||
// @Tags 应用管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param code query string true "应用唯一编码"
|
||||
// @Success 200 {object} response.Response{data=wafappmodel.NetStatsResult} "查询成功"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /application/app/network [get]
|
||||
func (w *WafAppApi) GetNetStatsApi(c *gin.Context) {
|
||||
var req request.WafAppCodeReq
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
response.FailWithMessage("解析失败", c)
|
||||
return
|
||||
}
|
||||
result, err := globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.GetNetStats(req.Code)
|
||||
if err != nil {
|
||||
response.FailWithMessage("查询失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
response.OkWithData(result, c)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"SamWaf/model/wafenginmodel"
|
||||
"SamWaf/plugin"
|
||||
"SamWaf/utils"
|
||||
"SamWaf/wafappengine"
|
||||
"SamWaf/wafconfig"
|
||||
"SamWaf/wafdb"
|
||||
"SamWaf/wafenginecore"
|
||||
@@ -439,6 +440,9 @@ func (m *wafSystenService) run() {
|
||||
//启动隧道
|
||||
globalobj.GWAF_RUNTIME_OBJ_TUNNEL_ENGINE = waftunnelengine.NewWafTunnelEngine()
|
||||
globalobj.GWAF_RUNTIME_OBJ_TUNNEL_ENGINE.StartTunnel()
|
||||
//启动应用管理引擎
|
||||
globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE = wafappengine.NewWafAppEngine()
|
||||
globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.StartApps()
|
||||
//启动管理界面
|
||||
webmanager = &wafmangeweb.WafWebManager{LogName: "WebManager"}
|
||||
go func() {
|
||||
@@ -734,6 +738,41 @@ func (m *wafSystenService) run() {
|
||||
globalobj.GWAF_RUNTIME_OBJ_TUNNEL_ENGINE.RemoveTunnel(tunnelDelete)
|
||||
break
|
||||
}
|
||||
} else if common.Type == enums.ChanComTypeApp {
|
||||
// 应用管理类型
|
||||
if globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE != nil {
|
||||
switch common.OpType {
|
||||
case enums.OP_TYPE_NEW:
|
||||
// 新增:如果 AutoStart=1,则启动
|
||||
appNew := common.Content.(model.WafApp)
|
||||
if appNew.AutoStart == 1 && appNew.StartStatus == 1 {
|
||||
if err := globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.StartApp(appNew.Code); err != nil {
|
||||
zlog.Error("自动启动应用失败", "code", appNew.Code, "error", err.Error())
|
||||
}
|
||||
}
|
||||
case enums.OP_TYPE_UPDATE:
|
||||
appNew := common.Content.(model.WafApp)
|
||||
globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.LoadApp(appNew)
|
||||
case enums.OP_TYPE_DELETE:
|
||||
appDel := common.OldContent.(model.WafApp)
|
||||
globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.RemoveApp(appDel.Code)
|
||||
case enums.OP_TYPE_APP_START:
|
||||
code := common.Content.(string)
|
||||
if err := globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.StartApp(code); err != nil {
|
||||
zlog.Error("启动应用失败", "code", code, "error", err.Error())
|
||||
}
|
||||
case enums.OP_TYPE_APP_STOP:
|
||||
code := common.Content.(string)
|
||||
if err := globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.StopApp(code); err != nil {
|
||||
zlog.Error("停止应用失败", "code", code, "error", err.Error())
|
||||
}
|
||||
case enums.OP_TYPE_APP_RESTART:
|
||||
code := common.Content.(string)
|
||||
if err := globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.RestartApp(code); err != nil {
|
||||
zlog.Error("重启应用失败", "code", code, "error", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case engineStatus := <-global.GWAF_CHAN_ENGINE:
|
||||
if engineStatus == 1 {
|
||||
@@ -856,6 +895,14 @@ func (m *wafSystenService) stopSamWaf() {
|
||||
zlog.Warn("Tunnel Engine is nil, skipping shutdown")
|
||||
}
|
||||
|
||||
zlog.Info("Shutdown SamWaf App Engine...")
|
||||
if globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE != nil {
|
||||
globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.StopApps()
|
||||
zlog.Info("Shutdown SamWaf App Engine finished")
|
||||
} else {
|
||||
zlog.Warn("App Engine is nil, skipping shutdown")
|
||||
}
|
||||
|
||||
zlog.Info("Shutdown SamWaf Queue Processors...")
|
||||
// 关闭信号通道,通知所有队列处理协程退出
|
||||
close(global.GWAF_QUEUE_SHUTDOWN_SIGNAL)
|
||||
|
||||
@@ -3,4 +3,6 @@ package enums
|
||||
const (
|
||||
//隧道
|
||||
ChanComTypeTunnel = iota
|
||||
//应用管理
|
||||
ChanComTypeApp
|
||||
)
|
||||
|
||||
@@ -4,4 +4,7 @@ const (
|
||||
OP_TYPE_NEW = iota
|
||||
OP_TYPE_UPDATE
|
||||
OP_TYPE_DELETE
|
||||
OP_TYPE_APP_START
|
||||
OP_TYPE_APP_STOP
|
||||
OP_TYPE_APP_RESTART
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ package globalobj
|
||||
|
||||
import (
|
||||
"SamWaf/plugin/manager"
|
||||
"SamWaf/wafappengine"
|
||||
"SamWaf/wafenginecore"
|
||||
"SamWaf/waftask"
|
||||
"SamWaf/waftunnelengine"
|
||||
@@ -16,4 +17,5 @@ var (
|
||||
GWAF_RUNTIME_OBJ_WAF_TaskRegistry *waftask.TaskRegistry // 任务执行器
|
||||
GWAF_RUNTIME_OBJ_WAF_TaskScheduler *waftask.TaskScheduler // 任务计划
|
||||
GWAF_RUNTIME_OBJ_PLUGIN_MANAGER *manager.PluginManager // 插件管理器
|
||||
GWAF_RUNTIME_OBJ_APP_ENGINE *wafappengine.WafAppEngine // 应用管理引擎
|
||||
)
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package request
|
||||
|
||||
import "SamWaf/model/common/request"
|
||||
|
||||
type WafAppAddReq struct {
|
||||
Code string `json:"code" form:"code"`
|
||||
Name string `json:"name" form:"name"`
|
||||
AppDir string `json:"app_dir" form:"app_dir"`
|
||||
StartCmd string `json:"start_cmd" form:"start_cmd"`
|
||||
Env string `json:"env" form:"env"`
|
||||
AutoStart int `json:"auto_start" form:"auto_start"`
|
||||
StartStatus int `json:"start_status" form:"start_status"`
|
||||
StopMode string `json:"stop_mode" form:"stop_mode"`
|
||||
StopCmd string `json:"stop_cmd" form:"stop_cmd"`
|
||||
StopTimeout int `json:"stop_timeout" form:"stop_timeout"`
|
||||
RestartPolicy string `json:"restart_policy" form:"restart_policy"`
|
||||
RestartDelay int `json:"restart_delay" form:"restart_delay"`
|
||||
MaxRestartCount int `json:"max_restart_count" form:"max_restart_count"`
|
||||
LogMaxLines int `json:"log_max_lines" form:"log_max_lines"`
|
||||
Remarks string `json:"remarks" form:"remarks"`
|
||||
}
|
||||
|
||||
type WafAppEditReq struct {
|
||||
Id string `json:"id"`
|
||||
WafAppAddReq
|
||||
}
|
||||
|
||||
type WafAppDetailReq struct {
|
||||
Id string `json:"id" form:"id"`
|
||||
}
|
||||
|
||||
type WafAppDelReq struct {
|
||||
Id string `json:"id" form:"id"`
|
||||
}
|
||||
|
||||
type WafAppSearchReq struct {
|
||||
request.PageInfo
|
||||
}
|
||||
|
||||
type WafAppCodeReq struct {
|
||||
Code string `json:"code" form:"code"`
|
||||
}
|
||||
|
||||
type WafAppRollbackReq struct {
|
||||
Code string `json:"code" form:"code"`
|
||||
Filename string `json:"filename" form:"filename"`
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package model
|
||||
|
||||
import "SamWaf/model/baseorm"
|
||||
|
||||
type WafApp struct {
|
||||
baseorm.BaseOrm
|
||||
Code string `gorm:"size:64;uniqueIndex" json:"code"`
|
||||
Name string `gorm:"size:128" json:"name"`
|
||||
AppDir string `gorm:"size:512" json:"app_dir"`
|
||||
StartCmd string `gorm:"size:1024" json:"start_cmd"`
|
||||
Env string `gorm:"size:2048" json:"env"`
|
||||
AutoStart int `json:"auto_start"`
|
||||
StartStatus int `json:"start_status"`
|
||||
StopMode string `gorm:"size:16" json:"stop_mode"`
|
||||
StopCmd string `gorm:"size:1024" json:"stop_cmd"`
|
||||
StopTimeout int `json:"stop_timeout"`
|
||||
RestartPolicy string `gorm:"size:16" json:"restart_policy"`
|
||||
RestartDelay int `json:"restart_delay"`
|
||||
MaxRestartCount int `json:"max_restart_count"`
|
||||
LogMaxLines int `json:"log_max_lines"`
|
||||
Remarks string `gorm:"size:512" json:"remarks"`
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package wafappmodel
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 保留 time 供 StartTime 字段使用
|
||||
|
||||
const (
|
||||
AppStatusStopped = 0
|
||||
AppStatusRunning = 1
|
||||
AppStatusCrashed = 2
|
||||
)
|
||||
|
||||
type AppRuntime struct {
|
||||
Code string
|
||||
Pid int
|
||||
Status int
|
||||
StartTime time.Time
|
||||
RestartCount int
|
||||
LogLines []string
|
||||
LogMu sync.Mutex
|
||||
Cmd *exec.Cmd
|
||||
StopChan chan struct{} // 关闭后阻止 monitorApp 自动重启
|
||||
Done chan struct{} // monitorApp 退出时关闭,供 StopApp 等待
|
||||
}
|
||||
|
||||
type BackupInfo struct {
|
||||
Filename string `json:"filename"`
|
||||
Size int64 `json:"size"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type PortInfo struct {
|
||||
Protocol string `json:"protocol"`
|
||||
LocalAddr string `json:"local_addr"`
|
||||
Port int `json:"port"`
|
||||
State string `json:"state"`
|
||||
Pid int `json:"pid"`
|
||||
}
|
||||
|
||||
type ConnInfo struct {
|
||||
Protocol string `json:"protocol"`
|
||||
LocalAddr string `json:"local_addr"`
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
RemoteIP string `json:"remote_ip"`
|
||||
State string `json:"state"`
|
||||
Pid int `json:"pid"`
|
||||
}
|
||||
|
||||
type NetStatsResult struct {
|
||||
Ports []PortInfo `json:"ports"`
|
||||
Connections []ConnInfo `json:"connections"`
|
||||
CachedAt string `json:"cached_at"`
|
||||
Pid int `json:"pid"`
|
||||
Pids []int `json:"pids"`
|
||||
}
|
||||
@@ -58,6 +58,7 @@ type ApiGroup struct {
|
||||
WafDataRetentionRouter
|
||||
WafOwaspRouter
|
||||
WafHostPathRuleRouter
|
||||
WafAppRouter
|
||||
}
|
||||
type PublicApiGroup struct {
|
||||
LoginRouter
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"SamWaf/api"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type WafAppRouter struct{}
|
||||
|
||||
func (r *WafAppRouter) InitWafAppRouter(group *gin.RouterGroup) {
|
||||
appApi := api.APIGroupAPP.WafAppApi
|
||||
group.POST("/api/v1/application/app/add", appApi.AddApi)
|
||||
group.POST("/api/v1/application/app/list", appApi.GetListApi)
|
||||
group.GET("/api/v1/application/app/detail", appApi.GetDetailApi)
|
||||
group.POST("/api/v1/application/app/edit", appApi.ModifyApi)
|
||||
group.GET("/api/v1/application/app/del", appApi.DelApi)
|
||||
group.GET("/api/v1/application/app/start", appApi.StartApi)
|
||||
group.GET("/api/v1/application/app/stop", appApi.StopApi)
|
||||
group.GET("/api/v1/application/app/restart", appApi.RestartApi)
|
||||
group.GET("/api/v1/application/app/status", appApi.GetStatusApi)
|
||||
group.GET("/api/v1/application/app/logs", appApi.GetLogsApi)
|
||||
group.POST("/api/v1/application/app/clearlogs", appApi.ClearLogsApi)
|
||||
group.POST("/api/v1/application/app/upload", appApi.UploadFileApi)
|
||||
group.POST("/api/v1/application/app/upgrade", appApi.UpgradeApi)
|
||||
group.GET("/api/v1/application/app/rollback", appApi.RollbackApi)
|
||||
group.GET("/api/v1/application/app/backups", appApi.GetBackupsApi)
|
||||
group.GET("/api/v1/application/app/network", appApi.GetNetStatsApi)
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
package waf_service
|
||||
|
||||
import (
|
||||
"SamWaf/common/uuid"
|
||||
"SamWaf/common/zlog"
|
||||
"SamWaf/customtype"
|
||||
"SamWaf/global"
|
||||
"SamWaf/model"
|
||||
"SamWaf/model/baseorm"
|
||||
"SamWaf/model/request"
|
||||
"SamWaf/model/wafappmodel"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type WafAppService struct{}
|
||||
|
||||
var WafAppServiceApp = new(WafAppService)
|
||||
|
||||
func (s *WafAppService) AddApi(req request.WafAppAddReq) (*model.WafApp, error) {
|
||||
if req.Name == "" {
|
||||
return nil, errors.New("应用名称不能为空")
|
||||
}
|
||||
if req.StartCmd == "" {
|
||||
return nil, errors.New("启动命令不能为空")
|
||||
}
|
||||
|
||||
bean := &model.WafApp{
|
||||
BaseOrm: baseorm.BaseOrm{
|
||||
Id: uuid.GenUUID(),
|
||||
USER_CODE: global.GWAF_USER_CODE,
|
||||
Tenant_ID: global.GWAF_TENANT_ID,
|
||||
CREATE_TIME: customtype.JsonTime(time.Now()),
|
||||
UPDATE_TIME: customtype.JsonTime(time.Now()),
|
||||
},
|
||||
Code: req.Code,
|
||||
Name: req.Name,
|
||||
AppDir: req.AppDir,
|
||||
StartCmd: req.StartCmd,
|
||||
Env: req.Env,
|
||||
AutoStart: req.AutoStart,
|
||||
StartStatus: req.StartStatus,
|
||||
StopMode: req.StopMode,
|
||||
StopCmd: req.StopCmd,
|
||||
StopTimeout: req.StopTimeout,
|
||||
RestartPolicy: req.RestartPolicy,
|
||||
RestartDelay: req.RestartDelay,
|
||||
MaxRestartCount: req.MaxRestartCount,
|
||||
LogMaxLines: req.LogMaxLines,
|
||||
Remarks: req.Remarks,
|
||||
}
|
||||
if bean.Code == "" {
|
||||
bean.Code = bean.Id
|
||||
}
|
||||
if bean.StopMode == "" {
|
||||
bean.StopMode = "signal"
|
||||
}
|
||||
if bean.RestartPolicy == "" {
|
||||
bean.RestartPolicy = "no"
|
||||
}
|
||||
if bean.StopTimeout == 0 {
|
||||
bean.StopTimeout = 30
|
||||
}
|
||||
if bean.LogMaxLines == 0 {
|
||||
bean.LogMaxLines = 1000
|
||||
}
|
||||
if bean.AppDir == "" {
|
||||
bean.AppDir = "data/applications/" + bean.Code
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(bean.AppDir, 0755); err != nil {
|
||||
zlog.Warn("创建应用目录失败", "dir", bean.AppDir, "err", err.Error())
|
||||
}
|
||||
|
||||
if err := global.GWAF_LOCAL_DB.Create(bean).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bean, nil
|
||||
}
|
||||
|
||||
func (s *WafAppService) CheckIsExist(name string) int {
|
||||
var total int64
|
||||
global.GWAF_LOCAL_DB.Model(&model.WafApp{}).Where("name = ?", name).Count(&total)
|
||||
return int(total)
|
||||
}
|
||||
|
||||
func (s *WafAppService) ModifyApi(req request.WafAppEditReq) error {
|
||||
var total int64
|
||||
global.GWAF_LOCAL_DB.Model(&model.WafApp{}).Where("name = ? AND id != ?", req.Name, req.Id).Count(&total)
|
||||
if total > 0 {
|
||||
return errors.New("应用名称已存在")
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{
|
||||
"name": req.Name,
|
||||
"app_dir": req.AppDir,
|
||||
"start_cmd": req.StartCmd,
|
||||
"env": req.Env,
|
||||
"auto_start": req.AutoStart,
|
||||
"start_status": req.StartStatus,
|
||||
"stop_mode": req.StopMode,
|
||||
"stop_cmd": req.StopCmd,
|
||||
"stop_timeout": req.StopTimeout,
|
||||
"restart_policy": req.RestartPolicy,
|
||||
"restart_delay": req.RestartDelay,
|
||||
"max_restart_count": req.MaxRestartCount,
|
||||
"log_max_lines": req.LogMaxLines,
|
||||
"remarks": req.Remarks,
|
||||
"update_time": customtype.JsonTime(time.Now()),
|
||||
}
|
||||
return global.GWAF_LOCAL_DB.Model(&model.WafApp{}).Where("id = ?", req.Id).Updates(updates).Error
|
||||
}
|
||||
|
||||
func (s *WafAppService) DelApi(req request.WafAppDelReq) error {
|
||||
return global.GWAF_LOCAL_DB.Where("id = ?", req.Id).Delete(&model.WafApp{}).Error
|
||||
}
|
||||
|
||||
func (s *WafAppService) GetDetailApi(req request.WafAppDetailReq) *model.WafApp {
|
||||
var bean model.WafApp
|
||||
global.GWAF_LOCAL_DB.Where("id = ?", req.Id).First(&bean)
|
||||
return &bean
|
||||
}
|
||||
|
||||
func (s *WafAppService) GetDetailByCodeApi(code string) *model.WafApp {
|
||||
var bean model.WafApp
|
||||
global.GWAF_LOCAL_DB.Where("code = ?", code).First(&bean)
|
||||
return &bean
|
||||
}
|
||||
|
||||
func (s *WafAppService) GetListApi(req request.WafAppSearchReq) ([]model.WafApp, int64) {
|
||||
var list []model.WafApp
|
||||
var total int64
|
||||
global.GWAF_LOCAL_DB.Model(&model.WafApp{}).Count(&total)
|
||||
pageSize := req.PageSize
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
pageIndex := req.PageIndex
|
||||
if pageIndex <= 0 {
|
||||
pageIndex = 1
|
||||
}
|
||||
global.GWAF_LOCAL_DB.Limit(pageSize).Offset((pageIndex - 1) * pageSize).Find(&list)
|
||||
return list, total
|
||||
}
|
||||
|
||||
// UploadFile 上传文件并校验 SHA256
|
||||
func (s *WafAppService) UploadFile(code string, filename string, src io.Reader, expectedHash string) error {
|
||||
var app model.WafApp
|
||||
if err := global.GWAF_LOCAL_DB.Where("code = ?", code).First(&app).Error; err != nil {
|
||||
return fmt.Errorf("应用不存在: %s", code)
|
||||
}
|
||||
appDir := app.AppDir
|
||||
if appDir == "" {
|
||||
appDir = "data/applications/" + code
|
||||
}
|
||||
if err := os.MkdirAll(appDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 安全文件名,防止路径穿越
|
||||
safeFilename := filepath.Base(filename)
|
||||
destPath := filepath.Join(appDir, safeFilename)
|
||||
|
||||
tmpPath := destPath + ".tmp"
|
||||
f, err := os.Create(tmpPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建临时文件失败: %w", err)
|
||||
}
|
||||
defer func() { os.Remove(tmpPath) }()
|
||||
|
||||
h := sha256.New()
|
||||
writer := io.MultiWriter(f, h)
|
||||
if _, err := io.Copy(writer, src); err != nil {
|
||||
f.Close()
|
||||
return fmt.Errorf("写入文件失败: %w", err)
|
||||
}
|
||||
f.Close()
|
||||
|
||||
actualHash := hex.EncodeToString(h.Sum(nil))
|
||||
if expectedHash != "" && !equalIgnoreCase(actualHash, expectedHash) {
|
||||
return fmt.Errorf("文件哈希校验失败,期望: %s,实际: %s", expectedHash, actualHash)
|
||||
}
|
||||
|
||||
if err := os.Rename(tmpPath, destPath); err != nil {
|
||||
return fmt.Errorf("保存文件失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpgradeApp 升级应用:备份当前文件后替换
|
||||
func (s *WafAppService) UpgradeApp(code string, filename string, src io.Reader, expectedHash string) error {
|
||||
var app model.WafApp
|
||||
if err := global.GWAF_LOCAL_DB.Where("code = ?", code).First(&app).Error; err != nil {
|
||||
return fmt.Errorf("应用不存在: %s", code)
|
||||
}
|
||||
appDir := app.AppDir
|
||||
if appDir == "" {
|
||||
appDir = "data/applications/" + code
|
||||
}
|
||||
safeFilename := filepath.Base(filename)
|
||||
destPath := filepath.Join(appDir, safeFilename)
|
||||
|
||||
// 备份已有文件
|
||||
if _, err := os.Stat(destPath); err == nil {
|
||||
backupDir := filepath.Join(appDir, "backup")
|
||||
if err2 := os.MkdirAll(backupDir, 0755); err2 == nil {
|
||||
backupName := safeFilename + "." + time.Now().Format("20060102150405")
|
||||
backupPath := filepath.Join(backupDir, backupName)
|
||||
if copyErr := copyFile(destPath, backupPath); copyErr != nil {
|
||||
// 备份失败时移除不完整的空文件,不阻断升级
|
||||
os.Remove(backupPath)
|
||||
zlog.Warn("备份文件失败,继续升级", "file", destPath, "error", copyErr.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return s.UploadFile(code, filename, src, expectedHash)
|
||||
}
|
||||
|
||||
// RollbackApp 回滚到备份文件
|
||||
func (s *WafAppService) RollbackApp(code string, backupFilename string) error {
|
||||
var app model.WafApp
|
||||
if err := global.GWAF_LOCAL_DB.Where("code = ?", code).First(&app).Error; err != nil {
|
||||
return fmt.Errorf("应用不存在: %s", code)
|
||||
}
|
||||
appDir := app.AppDir
|
||||
if appDir == "" {
|
||||
appDir = "data/applications/" + code
|
||||
}
|
||||
safeBackup := filepath.Base(backupFilename)
|
||||
backupPath := filepath.Join(appDir, "backup", safeBackup)
|
||||
|
||||
if _, err := os.Stat(backupPath); err != nil {
|
||||
return fmt.Errorf("备份文件不存在: %s", safeBackup)
|
||||
}
|
||||
|
||||
// 从备份文件名推断原文件名(去掉时间戳后缀 .20060102150405)
|
||||
origName := safeBackup
|
||||
if len(safeBackup) > 15 {
|
||||
origName = safeBackup[:len(safeBackup)-15]
|
||||
}
|
||||
destPath := filepath.Join(appDir, origName)
|
||||
|
||||
return copyFile(backupPath, destPath)
|
||||
}
|
||||
|
||||
// ListBackups 列出备份文件
|
||||
func (s *WafAppService) ListBackups(code string) ([]wafappmodel.BackupInfo, error) {
|
||||
var app model.WafApp
|
||||
if err := global.GWAF_LOCAL_DB.Where("code = ?", code).First(&app).Error; err != nil {
|
||||
return nil, fmt.Errorf("应用不存在: %s", code)
|
||||
}
|
||||
appDir := app.AppDir
|
||||
if appDir == "" {
|
||||
appDir = "data/applications/" + code
|
||||
}
|
||||
backupDir := filepath.Join(appDir, "backup")
|
||||
entries, err := os.ReadDir(backupDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return []wafappmodel.BackupInfo{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []wafappmodel.BackupInfo
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
fullPath := filepath.Join(backupDir, entry.Name())
|
||||
info, err2 := os.Stat(fullPath)
|
||||
if err2 != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, wafappmodel.BackupInfo{
|
||||
Filename: entry.Name(),
|
||||
Size: info.Size(),
|
||||
CreatedAt: info.ModTime().Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func copyFile(src, dst string) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
_, err = io.Copy(out, in)
|
||||
return err
|
||||
}
|
||||
|
||||
func equalIgnoreCase(a, b string) bool {
|
||||
return len(a) == len(b) && (a == b ||
|
||||
len(a) > 0 && string([]byte(a)) == string([]byte(b)))
|
||||
}
|
||||
@@ -0,0 +1,843 @@
|
||||
package wafappengine
|
||||
|
||||
import (
|
||||
"SamWaf/common/zlog"
|
||||
"SamWaf/global"
|
||||
"SamWaf/model"
|
||||
"SamWaf/model/wafappmodel"
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
)
|
||||
|
||||
type netCacheEntry struct {
|
||||
result *wafappmodel.NetStatsResult
|
||||
fetchedAt time.Time
|
||||
}
|
||||
|
||||
const netCacheTTL = 30 * time.Second
|
||||
|
||||
type WafAppEngine struct {
|
||||
runtimes map[string]*wafappmodel.AppRuntime
|
||||
mu sync.RWMutex
|
||||
netCache map[string]*netCacheEntry
|
||||
cacheMu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewWafAppEngine() *WafAppEngine {
|
||||
return &WafAppEngine{
|
||||
runtimes: make(map[string]*wafappmodel.AppRuntime),
|
||||
netCache: make(map[string]*netCacheEntry),
|
||||
}
|
||||
}
|
||||
|
||||
// StartApps 启动所有 AutoStart=1 且 StartStatus=1 的应用
|
||||
func (e *WafAppEngine) StartApps() {
|
||||
var apps []model.WafApp
|
||||
global.GWAF_LOCAL_DB.Where("auto_start = 1 AND start_status = 1").Find(&apps)
|
||||
for _, app := range apps {
|
||||
if err := e.StartApp(app.Code); err != nil {
|
||||
zlog.Error("自动启动应用失败", "code", app.Code, "name", app.Name, "error", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// StopApps 优雅关闭所有运行中的应用
|
||||
func (e *WafAppEngine) StopApps() {
|
||||
e.mu.RLock()
|
||||
codes := make([]string, 0, len(e.runtimes))
|
||||
for code := range e.runtimes {
|
||||
codes = append(codes, code)
|
||||
}
|
||||
e.mu.RUnlock()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, code := range codes {
|
||||
wg.Add(1)
|
||||
go func(c string) {
|
||||
defer wg.Done()
|
||||
if err := e.StopApp(c); err != nil {
|
||||
zlog.Error("关闭应用失败", "code", c, "error", err.Error())
|
||||
}
|
||||
}(code)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// StartApp 启动单个应用
|
||||
func (e *WafAppEngine) StartApp(code string) error {
|
||||
var app model.WafApp
|
||||
if err := global.GWAF_LOCAL_DB.Where("code = ?", code).First(&app).Error; err != nil {
|
||||
return fmt.Errorf("应用不存在: %s", code)
|
||||
}
|
||||
if app.StartStatus == 0 {
|
||||
return fmt.Errorf("应用已停用,不允许启动")
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
rt, exists := e.runtimes[code]
|
||||
if exists && rt.Status == wafappmodel.AppStatusRunning {
|
||||
e.mu.Unlock()
|
||||
return fmt.Errorf("应用已在运行中")
|
||||
}
|
||||
stopChan := make(chan struct{})
|
||||
rt = &wafappmodel.AppRuntime{
|
||||
Code: code,
|
||||
Status: wafappmodel.AppStatusStopped,
|
||||
StopChan: stopChan,
|
||||
}
|
||||
if app.LogMaxLines <= 0 {
|
||||
app.LogMaxLines = 1000
|
||||
}
|
||||
e.runtimes[code] = rt
|
||||
e.mu.Unlock()
|
||||
|
||||
return e.startProcess(app, rt)
|
||||
}
|
||||
|
||||
func (e *WafAppEngine) startProcess(app model.WafApp, rt *wafappmodel.AppRuntime) error {
|
||||
appDir := app.AppDir
|
||||
if appDir == "" {
|
||||
appDir = "data/applications/" + app.Code
|
||||
}
|
||||
if err := os.MkdirAll(appDir, 0755); err != nil {
|
||||
return fmt.Errorf("创建工作目录失败: %w", err)
|
||||
}
|
||||
|
||||
var cmd *exec.Cmd
|
||||
if runtime.GOOS == "windows" {
|
||||
cmd = exec.Command("cmd", "/C", app.StartCmd)
|
||||
} else {
|
||||
cmd = exec.Command("/bin/sh", "-c", app.StartCmd)
|
||||
}
|
||||
cmd.Dir = appDir
|
||||
|
||||
envs := append(os.Environ(), "")
|
||||
if app.Env != "" {
|
||||
for _, pair := range strings.Split(app.Env, ",") {
|
||||
pair = strings.TrimSpace(pair)
|
||||
if pair != "" {
|
||||
envs = append(envs, pair)
|
||||
}
|
||||
}
|
||||
}
|
||||
cmd.Env = envs
|
||||
|
||||
stdoutPipe, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取stdout失败: %w", err)
|
||||
}
|
||||
stderrPipe, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取stderr失败: %w", err)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("启动进程失败: %w", err)
|
||||
}
|
||||
|
||||
rt.Cmd = cmd
|
||||
rt.Pid = cmd.Process.Pid
|
||||
rt.Status = wafappmodel.AppStatusRunning
|
||||
rt.StartTime = time.Now()
|
||||
|
||||
logFile, _ := os.OpenFile(appDir+"/app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
|
||||
logMaxLines := app.LogMaxLines
|
||||
if logMaxLines <= 0 {
|
||||
logMaxLines = 1000
|
||||
}
|
||||
|
||||
rt.Done = make(chan struct{})
|
||||
go e.pipeReader(stdoutPipe, rt, logFile, logMaxLines)
|
||||
go e.pipeReader(stderrPipe, rt, logFile, logMaxLines)
|
||||
go e.monitorApp(app, rt)
|
||||
|
||||
zlog.Info("应用已启动", "code", app.Code, "name", app.Name, "pid", rt.Pid)
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeOutputLine(raw []byte) string {
|
||||
if utf8.Valid(raw) {
|
||||
return string(raw)
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
if decoded, err := simplifiedchinese.GBK.NewDecoder().Bytes(raw); err == nil {
|
||||
return string(decoded)
|
||||
}
|
||||
}
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
func (e *WafAppEngine) pipeReader(r io.Reader, rt *wafappmodel.AppRuntime, logFile *os.File, maxLines int) {
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := time.Now().Format("2006-01-02 15:04:05") + " " + decodeOutputLine(scanner.Bytes())
|
||||
rt.LogMu.Lock()
|
||||
rt.LogLines = append(rt.LogLines, line)
|
||||
if len(rt.LogLines) > maxLines {
|
||||
rt.LogLines = rt.LogLines[len(rt.LogLines)-maxLines:]
|
||||
}
|
||||
rt.LogMu.Unlock()
|
||||
if logFile != nil {
|
||||
logFile.WriteString(line + "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *WafAppEngine) monitorApp(app model.WafApp, rt *wafappmodel.AppRuntime) {
|
||||
defer close(rt.Done)
|
||||
for {
|
||||
err := rt.Cmd.Wait()
|
||||
|
||||
select {
|
||||
case <-rt.StopChan:
|
||||
e.mu.Lock()
|
||||
rt.Status = wafappmodel.AppStatusStopped
|
||||
rt.Pid = 0
|
||||
e.mu.Unlock()
|
||||
zlog.Info("应用已主动停止", "code", app.Code, "name", app.Name)
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
rt.Status = wafappmodel.AppStatusCrashed
|
||||
rt.Pid = 0
|
||||
e.mu.Unlock()
|
||||
zlog.Warn("应用进程退出", "code", app.Code, "name", app.Name, "error", err)
|
||||
|
||||
// 重新从数据库读取最新配置(可能已被更改)
|
||||
var latestApp model.WafApp
|
||||
if dbErr := global.GWAF_LOCAL_DB.Where("code = ?", app.Code).First(&latestApp).Error; dbErr != nil {
|
||||
zlog.Error("读取应用配置失败,停止监控", "code", app.Code, "name", app.Name)
|
||||
return
|
||||
}
|
||||
app = latestApp
|
||||
|
||||
if app.StartStatus == 0 {
|
||||
return
|
||||
}
|
||||
if app.RestartPolicy == "no" || app.RestartPolicy == "" {
|
||||
return
|
||||
}
|
||||
if app.RestartPolicy == "on-failure" && err == nil {
|
||||
return
|
||||
}
|
||||
if app.MaxRestartCount > 0 && rt.RestartCount >= app.MaxRestartCount {
|
||||
zlog.Warn("已达最大重启次数,停止重启", "code", app.Code, "name", app.Name, "max", app.MaxRestartCount)
|
||||
return
|
||||
}
|
||||
|
||||
delay := app.RestartDelay
|
||||
if delay <= 0 {
|
||||
delay = 5
|
||||
}
|
||||
zlog.Info("等待后重启应用", "code", app.Code, "name", app.Name, "delay_sec", delay)
|
||||
|
||||
select {
|
||||
case <-rt.StopChan:
|
||||
e.mu.Lock()
|
||||
rt.Status = wafappmodel.AppStatusStopped
|
||||
e.mu.Unlock()
|
||||
return
|
||||
case <-time.After(time.Duration(delay) * time.Second):
|
||||
}
|
||||
|
||||
// 重新检查 StopChan(等待期间可能已被关闭)
|
||||
select {
|
||||
case <-rt.StopChan:
|
||||
e.mu.Lock()
|
||||
rt.Status = wafappmodel.AppStatusStopped
|
||||
e.mu.Unlock()
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
rt.RestartCount++
|
||||
zlog.Info("正在重启应用", "code", app.Code, "name", app.Name, "attempt", rt.RestartCount)
|
||||
|
||||
appDir := app.AppDir
|
||||
if appDir == "" {
|
||||
appDir = "data/applications/" + app.Code
|
||||
}
|
||||
var newCmd *exec.Cmd
|
||||
if runtime.GOOS == "windows" {
|
||||
newCmd = exec.Command("cmd", "/C", app.StartCmd)
|
||||
} else {
|
||||
newCmd = exec.Command("/bin/sh", "-c", app.StartCmd)
|
||||
}
|
||||
newCmd.Dir = appDir
|
||||
envs := append(os.Environ(), "")
|
||||
if app.Env != "" {
|
||||
for _, pair := range strings.Split(app.Env, ",") {
|
||||
pair = strings.TrimSpace(pair)
|
||||
if pair != "" {
|
||||
envs = append(envs, pair)
|
||||
}
|
||||
}
|
||||
}
|
||||
newCmd.Env = envs
|
||||
|
||||
stdoutPipe, _ := newCmd.StdoutPipe()
|
||||
stderrPipe, _ := newCmd.StderrPipe()
|
||||
if startErr := newCmd.Start(); startErr != nil {
|
||||
zlog.Error("重启应用失败", "code", app.Code, "name", app.Name, "error", startErr.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
logMaxLines := app.LogMaxLines
|
||||
if logMaxLines <= 0 {
|
||||
logMaxLines = 1000
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
rt.Cmd = newCmd
|
||||
rt.Pid = newCmd.Process.Pid
|
||||
rt.Status = wafappmodel.AppStatusRunning
|
||||
rt.StartTime = time.Now()
|
||||
e.mu.Unlock()
|
||||
|
||||
logFile, _ := os.OpenFile(appDir+"/app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
go e.pipeReader(stdoutPipe, rt, logFile, logMaxLines)
|
||||
go e.pipeReader(stderrPipe, rt, logFile, logMaxLines)
|
||||
zlog.Info("应用重启成功", "code", app.Code, "name", app.Name, "pid", rt.Pid)
|
||||
}
|
||||
}
|
||||
|
||||
// StopApp 停止单个应用
|
||||
func (e *WafAppEngine) StopApp(code string) error {
|
||||
e.mu.RLock()
|
||||
rt, exists := e.runtimes[code]
|
||||
e.mu.RUnlock()
|
||||
if !exists || rt.Cmd == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var app model.WafApp
|
||||
global.GWAF_LOCAL_DB.Where("code = ?", code).First(&app)
|
||||
|
||||
// 1. 关闭 StopChan,阻止 monitorApp 重启循环
|
||||
select {
|
||||
case <-rt.StopChan:
|
||||
default:
|
||||
close(rt.StopChan)
|
||||
}
|
||||
|
||||
// 2. 进程尚未启动,直接更新状态
|
||||
if rt.Cmd.Process == nil {
|
||||
e.mu.Lock()
|
||||
rt.Status = wafappmodel.AppStatusStopped
|
||||
rt.Pid = 0
|
||||
e.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
timeout := app.StopTimeout
|
||||
if timeout <= 0 {
|
||||
timeout = 30
|
||||
}
|
||||
|
||||
// 3. 发送优雅关闭信号(不在此调用 Wait(),由 monitorApp 负责)
|
||||
if app.StopMode == "cmd" && app.StopCmd != "" {
|
||||
var stopCmd *exec.Cmd
|
||||
if runtime.GOOS == "windows" {
|
||||
stopCmd = exec.Command("cmd", "/C", app.StopCmd)
|
||||
} else {
|
||||
stopCmd = exec.Command("/bin/sh", "-c", app.StopCmd)
|
||||
}
|
||||
stopCmd.Dir = app.AppDir
|
||||
_ = stopCmd.Run()
|
||||
} else {
|
||||
if runtime.GOOS == "windows" {
|
||||
// 发送 WM_CLOSE 给进程,给其机会优雅退出(不带 /F)
|
||||
exec.Command("taskkill", "/PID", strconv.Itoa(rt.Pid)).Run()
|
||||
} else {
|
||||
rt.Cmd.Process.Signal(os.Interrupt)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 等待 monitorApp 检测到进程退出并关闭 rt.Done;超时则强制终止整棵进程树
|
||||
select {
|
||||
case <-rt.Done:
|
||||
zlog.Info("应用已优雅停止", "code", code, "name", app.Name)
|
||||
case <-time.After(time.Duration(timeout) * time.Second):
|
||||
if runtime.GOOS == "windows" {
|
||||
// /F 强制 + /T 终止整棵子进程树,解决 cmd.exe 子进程孤儿问题
|
||||
exec.Command("taskkill", "/F", "/T", "/PID", strconv.Itoa(rt.Pid)).Run()
|
||||
} else {
|
||||
if rt.Cmd.Process != nil {
|
||||
rt.Cmd.Process.Kill()
|
||||
}
|
||||
}
|
||||
zlog.Warn("应用强制终止", "code", code, "name", app.Name)
|
||||
// 等待 monitorApp 确认退出(最多再等 5s)
|
||||
select {
|
||||
case <-rt.Done:
|
||||
case <-time.After(5 * time.Second):
|
||||
}
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
rt.Status = wafappmodel.AppStatusStopped
|
||||
rt.Pid = 0
|
||||
e.mu.Unlock()
|
||||
|
||||
e.cacheMu.Lock()
|
||||
delete(e.netCache, code)
|
||||
e.cacheMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestartApp 重启单个应用
|
||||
func (e *WafAppEngine) RestartApp(code string) error {
|
||||
_ = e.StopApp(code)
|
||||
// 重置 runtime,让 StartApp 重新创建
|
||||
e.mu.Lock()
|
||||
delete(e.runtimes, code)
|
||||
e.mu.Unlock()
|
||||
return e.StartApp(code)
|
||||
}
|
||||
|
||||
// GetRuntimeStatus 获取运行时状态(线程安全的快照)
|
||||
func (e *WafAppEngine) GetRuntimeStatus(code string) wafappmodel.AppRuntime {
|
||||
e.mu.RLock()
|
||||
defer e.mu.RUnlock()
|
||||
rt, exists := e.runtimes[code]
|
||||
if !exists {
|
||||
return wafappmodel.AppRuntime{Code: code, Status: wafappmodel.AppStatusStopped}
|
||||
}
|
||||
return wafappmodel.AppRuntime{
|
||||
Code: rt.Code,
|
||||
Pid: rt.Pid,
|
||||
Status: rt.Status,
|
||||
StartTime: rt.StartTime,
|
||||
RestartCount: rt.RestartCount,
|
||||
}
|
||||
}
|
||||
|
||||
// GetLogs 获取最近日志行
|
||||
func (e *WafAppEngine) GetLogs(code string) []string {
|
||||
e.mu.RLock()
|
||||
rt, exists := e.runtimes[code]
|
||||
e.mu.RUnlock()
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
rt.LogMu.Lock()
|
||||
defer rt.LogMu.Unlock()
|
||||
result := make([]string, len(rt.LogLines))
|
||||
copy(result, rt.LogLines)
|
||||
return result
|
||||
}
|
||||
|
||||
// ClearLogs 清空内存和文件日志
|
||||
func (e *WafAppEngine) ClearLogs(code string) {
|
||||
e.mu.RLock()
|
||||
rt, exists := e.runtimes[code]
|
||||
e.mu.RUnlock()
|
||||
if exists {
|
||||
rt.LogMu.Lock()
|
||||
rt.LogLines = nil
|
||||
rt.LogMu.Unlock()
|
||||
}
|
||||
|
||||
var app model.WafApp
|
||||
if global.GWAF_LOCAL_DB.Where("code = ?", code).First(&app).Error == nil {
|
||||
appDir := app.AppDir
|
||||
if appDir == "" {
|
||||
appDir = "data/applications/" + code
|
||||
}
|
||||
os.Truncate(appDir+"/app.log", 0)
|
||||
}
|
||||
}
|
||||
|
||||
// LoadApp 热加载(新增/修改应用时调用)
|
||||
func (e *WafAppEngine) LoadApp(app model.WafApp) {
|
||||
// 如果当前在运行,重启;否则忽略(等待手动启动)
|
||||
e.mu.RLock()
|
||||
rt, exists := e.runtimes[app.Code]
|
||||
e.mu.RUnlock()
|
||||
if exists && rt.Status == wafappmodel.AppStatusRunning {
|
||||
_ = e.RestartApp(app.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveApp 移除应用(删除时调用)
|
||||
func (e *WafAppEngine) RemoveApp(code string) {
|
||||
_ = e.StopApp(code)
|
||||
e.mu.Lock()
|
||||
delete(e.runtimes, code)
|
||||
e.mu.Unlock()
|
||||
}
|
||||
|
||||
// GetNetStats 返回应用占用的端口及连接 IP,结果缓存 30s
|
||||
func (e *WafAppEngine) GetNetStats(code string) (*wafappmodel.NetStatsResult, error) {
|
||||
empty := &wafappmodel.NetStatsResult{
|
||||
Ports: []wafappmodel.PortInfo{},
|
||||
Connections: []wafappmodel.ConnInfo{},
|
||||
}
|
||||
|
||||
e.mu.RLock()
|
||||
rt, exists := e.runtimes[code]
|
||||
e.mu.RUnlock()
|
||||
if !exists || rt.Pid == 0 || rt.Status != wafappmodel.AppStatusRunning {
|
||||
return empty, nil
|
||||
}
|
||||
pid := rt.Pid
|
||||
|
||||
e.cacheMu.RLock()
|
||||
cached := e.netCache[code]
|
||||
e.cacheMu.RUnlock()
|
||||
if cached != nil && time.Since(cached.fetchedAt) < netCacheTTL {
|
||||
return cached.result, nil
|
||||
}
|
||||
|
||||
result, err := fetchNetStats(pid)
|
||||
if err != nil {
|
||||
return empty, err
|
||||
}
|
||||
result.CachedAt = time.Now().Format("2006-01-02 15:04:05")
|
||||
result.Pid = pid
|
||||
|
||||
e.cacheMu.Lock()
|
||||
e.netCache[code] = &netCacheEntry{result: result, fetchedAt: time.Now()}
|
||||
e.cacheMu.Unlock()
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// fetchNetStats 查询进程树的端口和连接信息
|
||||
func fetchNetStats(rootPid int) (*wafappmodel.NetStatsResult, error) {
|
||||
pids := getDescendantPIDs(rootPid)
|
||||
pidSet := make(map[int]bool, len(pids))
|
||||
for _, p := range pids {
|
||||
pidSet[p] = true
|
||||
}
|
||||
var result *wafappmodel.NetStatsResult
|
||||
var err error
|
||||
if runtime.GOOS == "windows" {
|
||||
result, err = fetchNetStatsWindows(pidSet)
|
||||
} else {
|
||||
result, err = fetchNetStatsLinux(pidSet)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.Pids = pids
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// getDescendantPIDs BFS 遍历进程树,返回所有子孙 PID(含自身)
|
||||
func getDescendantPIDs(rootPid int) []int {
|
||||
childMap := buildChildMap()
|
||||
var result []int
|
||||
queue := []int{rootPid}
|
||||
for len(queue) > 0 {
|
||||
pid := queue[0]
|
||||
queue = queue[1:]
|
||||
result = append(result, pid)
|
||||
queue = append(queue, childMap[pid]...)
|
||||
}
|
||||
if len(result) == 0 {
|
||||
result = []int{rootPid}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// buildChildMap 构建 parentPID→[]childPID 映射
|
||||
func buildChildMap() map[int][]int {
|
||||
m := make(map[int][]int)
|
||||
if runtime.GOOS == "windows" {
|
||||
buildChildMapWindows(m)
|
||||
} else {
|
||||
buildChildMapLinux(m)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func buildChildMapWindows(m map[int][]int) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
// 列名字母序:ParentProcessId 排在 ProcessId 前面
|
||||
out, err := exec.CommandContext(ctx, "wmic", "process", "get", "ParentProcessId,ProcessId").Output()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
lines := strings.Split(strings.ReplaceAll(string(out), "\r", ""), "\n")
|
||||
for i, line := range lines {
|
||||
if i == 0 {
|
||||
continue // 跳过表头
|
||||
}
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
ppid, e1 := strconv.Atoi(fields[0])
|
||||
pid, e2 := strconv.Atoi(fields[1])
|
||||
if e1 != nil || e2 != nil || pid == ppid || ppid == 0 {
|
||||
continue
|
||||
}
|
||||
m[ppid] = append(m[ppid], pid)
|
||||
}
|
||||
}
|
||||
|
||||
func buildChildMapLinux(m map[int][]int) {
|
||||
entries, err := os.ReadDir("/proc")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
pid, err := strconv.Atoi(entry.Name())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, statusLine := range strings.Split(string(data), "\n") {
|
||||
if strings.HasPrefix(statusLine, "PPid:") {
|
||||
ppid, _ := strconv.Atoi(strings.TrimSpace(statusLine[5:]))
|
||||
if ppid > 0 {
|
||||
m[ppid] = append(m[ppid], pid)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchNetStatsWindows(pidSet map[int]bool) (*wafappmodel.NetStatsResult, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
out, err := exec.CommandContext(ctx, "netstat", "-ano").Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("netstat failed: %w", err)
|
||||
}
|
||||
|
||||
var ports []wafappmodel.PortInfo
|
||||
var conns []wafappmodel.ConnInfo
|
||||
|
||||
for _, line := range strings.Split(strings.ReplaceAll(string(out), "\r", ""), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 4 {
|
||||
continue
|
||||
}
|
||||
proto := strings.ToUpper(fields[0])
|
||||
localAddr := fields[1]
|
||||
var state string
|
||||
var pid int
|
||||
|
||||
switch proto {
|
||||
case "TCP":
|
||||
if len(fields) < 5 {
|
||||
continue
|
||||
}
|
||||
state = strings.ToUpper(fields[3])
|
||||
pid, _ = strconv.Atoi(fields[4])
|
||||
case "UDP":
|
||||
state = "LISTEN"
|
||||
pid, _ = strconv.Atoi(fields[3])
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
if pid == 0 || !pidSet[pid] {
|
||||
continue
|
||||
}
|
||||
port := parseNetPort(localAddr)
|
||||
|
||||
switch state {
|
||||
case "LISTENING", "LISTEN":
|
||||
ports = append(ports, wafappmodel.PortInfo{
|
||||
Protocol: proto, LocalAddr: localAddr, Port: port, State: "LISTEN", Pid: pid,
|
||||
})
|
||||
case "ESTABLISHED":
|
||||
remoteAddr := fields[2]
|
||||
conns = append(conns, wafappmodel.ConnInfo{
|
||||
Protocol: proto, LocalAddr: localAddr, RemoteAddr: remoteAddr,
|
||||
RemoteIP: parseNetIP(remoteAddr), State: state, Pid: pid,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if ports == nil {
|
||||
ports = []wafappmodel.PortInfo{}
|
||||
}
|
||||
if conns == nil {
|
||||
conns = []wafappmodel.ConnInfo{}
|
||||
}
|
||||
return &wafappmodel.NetStatsResult{Ports: ports, Connections: conns}, nil
|
||||
}
|
||||
|
||||
func fetchNetStatsLinux(pidSet map[int]bool) (*wafappmodel.NetStatsResult, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
// ss -tanp: TCP all-states, numeric, with-process
|
||||
out, err := exec.CommandContext(ctx, "ss", "-tanp").Output()
|
||||
if err != nil {
|
||||
// 降级到 netstat
|
||||
return fetchNetStatsLinuxNetstat(pidSet)
|
||||
}
|
||||
return parseSSOutput(string(out), pidSet), nil
|
||||
}
|
||||
|
||||
func parseSSOutput(data string, pidSet map[int]bool) *wafappmodel.NetStatsResult {
|
||||
pidRe := regexp.MustCompile(`pid=(\d+)`)
|
||||
var ports []wafappmodel.PortInfo
|
||||
var conns []wafappmodel.ConnInfo
|
||||
|
||||
lines := strings.Split(data, "\n")
|
||||
for i, line := range lines {
|
||||
if i == 0 {
|
||||
continue // 跳过表头
|
||||
}
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 5 {
|
||||
continue
|
||||
}
|
||||
state := strings.ToUpper(fields[0])
|
||||
localAddr := fields[3]
|
||||
peerAddr := fields[4]
|
||||
|
||||
var pid int
|
||||
for j := 5; j < len(fields); j++ {
|
||||
if m := pidRe.FindStringSubmatch(fields[j]); m != nil {
|
||||
pid, _ = strconv.Atoi(m[1])
|
||||
break
|
||||
}
|
||||
}
|
||||
if pid == 0 || !pidSet[pid] {
|
||||
continue
|
||||
}
|
||||
port := parseNetPort(localAddr)
|
||||
|
||||
switch state {
|
||||
case "LISTEN":
|
||||
ports = append(ports, wafappmodel.PortInfo{
|
||||
Protocol: "TCP", LocalAddr: localAddr, Port: port, State: "LISTEN", Pid: pid,
|
||||
})
|
||||
case "ESTAB", "ESTABLISHED":
|
||||
conns = append(conns, wafappmodel.ConnInfo{
|
||||
Protocol: "TCP", LocalAddr: localAddr, RemoteAddr: peerAddr,
|
||||
RemoteIP: parseNetIP(peerAddr), State: "ESTABLISHED", Pid: pid,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if ports == nil {
|
||||
ports = []wafappmodel.PortInfo{}
|
||||
}
|
||||
if conns == nil {
|
||||
conns = []wafappmodel.ConnInfo{}
|
||||
}
|
||||
return &wafappmodel.NetStatsResult{Ports: ports, Connections: conns}
|
||||
}
|
||||
|
||||
func fetchNetStatsLinuxNetstat(pidSet map[int]bool) (*wafappmodel.NetStatsResult, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
// netstat -tnap: TCP numeric all programs
|
||||
out, err := exec.CommandContext(ctx, "netstat", "-tnap").Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("netstat failed: %w", err)
|
||||
}
|
||||
|
||||
var ports []wafappmodel.PortInfo
|
||||
var conns []wafappmodel.ConnInfo
|
||||
|
||||
lines := strings.Split(string(out), "\n")
|
||||
for i, line := range lines {
|
||||
if i <= 1 {
|
||||
continue // 跳过两行表头
|
||||
}
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
// Proto Recv-Q Send-Q Local Foreign State PID/prog
|
||||
if len(fields) < 7 {
|
||||
continue
|
||||
}
|
||||
proto := strings.ToLower(fields[0])
|
||||
if proto != "tcp" && proto != "tcp6" {
|
||||
continue
|
||||
}
|
||||
localAddr := fields[3]
|
||||
peerAddr := fields[4]
|
||||
state := strings.ToUpper(fields[5])
|
||||
pidProc := fields[6] // "1234/myapp" or "-"
|
||||
|
||||
var pid int
|
||||
if parts := strings.SplitN(pidProc, "/", 2); len(parts) >= 1 {
|
||||
pid, _ = strconv.Atoi(parts[0])
|
||||
}
|
||||
if pid == 0 || !pidSet[pid] {
|
||||
continue
|
||||
}
|
||||
port := parseNetPort(localAddr)
|
||||
|
||||
switch state {
|
||||
case "LISTEN":
|
||||
ports = append(ports, wafappmodel.PortInfo{
|
||||
Protocol: "TCP", LocalAddr: localAddr, Port: port, State: "LISTEN", Pid: pid,
|
||||
})
|
||||
case "ESTABLISHED":
|
||||
conns = append(conns, wafappmodel.ConnInfo{
|
||||
Protocol: "TCP", LocalAddr: localAddr, RemoteAddr: peerAddr,
|
||||
RemoteIP: parseNetIP(peerAddr), State: state, Pid: pid,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if ports == nil {
|
||||
ports = []wafappmodel.PortInfo{}
|
||||
}
|
||||
if conns == nil {
|
||||
conns = []wafappmodel.ConnInfo{}
|
||||
}
|
||||
return &wafappmodel.NetStatsResult{Ports: ports, Connections: conns}, nil
|
||||
}
|
||||
|
||||
func parseNetPort(addr string) int {
|
||||
_, portStr, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
return port
|
||||
}
|
||||
|
||||
func parseNetIP(addr string) string {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return addr
|
||||
}
|
||||
return host
|
||||
}
|
||||
@@ -957,6 +957,22 @@ func RunCoreDBMigrations(db *gorm.DB) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
// 迁移24: 创建应用管理表
|
||||
{
|
||||
ID: "202606080002_add_waf_apps_table",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
zlog.Info("迁移 202606080002: 创建 waf_apps 表")
|
||||
if err := tx.AutoMigrate(&model.WafApp{}); err != nil {
|
||||
return fmt.Errorf("创建 waf_apps 表失败: %w", err)
|
||||
}
|
||||
zlog.Info("waf_apps 表创建成功")
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
zlog.Info("回滚 202606080002: 删除 waf_apps 表")
|
||||
return tx.Migrator().DropTable(&model.WafApp{})
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// 执行迁移
|
||||
|
||||
@@ -155,6 +155,7 @@ func (web *WafWebManager) initRouter(r *gin.Engine) {
|
||||
router.ApiGroupApp.InitWafDataRetentionRouter(RouterGroup)
|
||||
router.ApiGroupApp.InitWafOwaspRouter(RouterGroup)
|
||||
router.ApiGroupApp.InitWafHostPathRuleRouter(RouterGroup)
|
||||
router.ApiGroupApp.InitWafAppRouter(RouterGroup)
|
||||
}
|
||||
|
||||
// 仅允许后台 Token 登录访问,拒绝 API Key 访问(安全敏感接口)
|
||||
|
||||
Reference in New Issue
Block a user