waflog.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. package admin
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. v1 "github.com/go-nunu/nunu-layout-advanced/api/v1"
  7. adminApi "github.com/go-nunu/nunu-layout-advanced/api/v1/admin"
  8. "github.com/go-nunu/nunu-layout-advanced/internal/model"
  9. adminRep "github.com/go-nunu/nunu-layout-advanced/internal/repository/admin"
  10. "github.com/go-nunu/nunu-layout-advanced/internal/repository/api/waf"
  11. "github.com/go-nunu/nunu-layout-advanced/internal/service"
  12. "github.com/go-nunu/nunu-layout-advanced/pkg/excel"
  13. "go.uber.org/zap"
  14. "net/http"
  15. "strings"
  16. "time"
  17. )
  18. // ApiDescriptionMap API描述映射
  19. var ApiDescriptionMap = map[string]string{
  20. "/webForward/add": "添加web",
  21. "/webForward/edit": "修改web",
  22. "/webForward/delete": "删除web",
  23. "/tcpForward/add": "添加tcp",
  24. "/tcpForward/edit": "修改tcp",
  25. "/tcpForward/delete": "删除tcp",
  26. "/udpForward/add": "添加udp",
  27. "/udpForward/edit": "修改udp",
  28. "/udpForward/delete": "删除udp",
  29. "/globalLimit/add": "添加实例",
  30. "/globalLimit/edit": "修改实例",
  31. "/globalLimit/delete": "删除实例",
  32. "/allowAndDeny/add": "添加黑白名单",
  33. "/allowAndDeny/edit": "修改黑白名单",
  34. "/allowAndDeny/delete": "删除黑白名单",
  35. "/cc/editState": "删除CC黑名单",
  36. "/ccIpList/add": "添加CC白名单",
  37. "/ccIpList/edit": "修改CC白名单",
  38. "/ccIpList/delete": "删除CC白名单",
  39. "分配网关组": "分配网关组",
  40. }
  41. type WafLogService interface {
  42. GetWafLog(ctx context.Context, id int64) (*model.WafLog, error)
  43. GetWafLogList(ctx context.Context, req adminApi.SearchWafLogParams) (*v1.PaginatedResponse[model.WafLog], error)
  44. AddWafLog(ctx context.Context, req adminApi.WafLog) error
  45. BatchAddWafLog(ctx context.Context, reqs []*adminApi.WafLog) error
  46. SmartExportWafLog(ctx context.Context, req adminApi.ExportWafLog, w http.ResponseWriter) error
  47. GetApiDescriptions(ctx context.Context) map[string]string
  48. }
  49. func NewWafLogService(
  50. service *service.Service,
  51. wafLogRepository adminRep.WafLogRepository,
  52. globalLimitRepository waf.GlobalLimitRepository,
  53. wafLogDataCleanService WafLogDataCleanService,
  54. ) WafLogService {
  55. return &wafLogService{
  56. Service: service,
  57. wafLogRepository: wafLogRepository,
  58. globalLimitRepository: globalLimitRepository,
  59. wafLogDataCleanService: wafLogDataCleanService,
  60. }
  61. }
  62. type wafLogService struct {
  63. *service.Service
  64. wafLogRepository adminRep.WafLogRepository
  65. globalLimitRepository waf.GlobalLimitRepository
  66. wafLogDataCleanService WafLogDataCleanService
  67. }
  68. func (s *wafLogService) getFirstPathSegment(path string) (segment []string, ok bool) {
  69. trimmedPath := strings.TrimPrefix(path, "/")
  70. if trimmedPath == "" {
  71. return nil, false
  72. }
  73. parts := strings.Split(trimmedPath, "/")
  74. if len(parts) > 0 {
  75. return parts, true
  76. }
  77. return nil, false
  78. }
  79. func (s *wafLogService) GetWafLog(ctx context.Context, id int64) (*model.WafLog, error) {
  80. return s.wafLogRepository.GetWafLog(ctx, id)
  81. }
  82. func (s *wafLogService) GetWafLogList(ctx context.Context,req adminApi.SearchWafLogParams) (*v1.PaginatedResponse[model.WafLog], error) {
  83. return s.wafLogRepository.GetWafLogList(ctx, req)
  84. }
  85. func (s *wafLogService) AddWafLog(ctx context.Context, req adminApi.WafLog) error {
  86. if req.Api != "" {
  87. api := strings.TrimPrefix(req.Api, "/v1")
  88. if _, ok := ApiDescriptionMap[api]; ok {
  89. req.ApiName = ApiDescriptionMap[api]
  90. }
  91. apiType, ok := s.getFirstPathSegment(req.Api)
  92. if ok {
  93. req.ApiType = apiType[len(apiType)-1]
  94. }
  95. }
  96. userInfo, err := s.globalLimitRepository.GetUserInfo(ctx, int64(req.Uid))
  97. if err != nil {
  98. return err
  99. }
  100. req.Name = userInfo.Username
  101. extraData, err := json.Marshal(req.ExtraData)
  102. if err != nil {
  103. return err
  104. }
  105. req.RequestIp = userInfo.LastLoginIp
  106. return s.wafLogRepository.AddWafLog(ctx, &model.WafLog{
  107. Uid: req.Uid,
  108. Name: req.Name,
  109. RequestIp: req.RequestIp,
  110. RuleId: req.RuleId,
  111. HostId: req.HostId,
  112. UserAgent: req.UserAgent,
  113. Api: req.Api,
  114. ApiType: req.ApiType,
  115. ApiName: req.ApiName,
  116. ExtraData: extraData,
  117. })
  118. }
  119. func (s *wafLogService) BatchAddWafLog(ctx context.Context, reqs []*adminApi.WafLog) error {
  120. if len(reqs) == 0 {
  121. return nil
  122. }
  123. wafLogs := make([]*model.WafLog, 0, len(reqs))
  124. for _, req := range reqs {
  125. if req.Api != "" {
  126. api := strings.TrimPrefix(req.Api, "/v1")
  127. if _, ok := ApiDescriptionMap[api]; ok {
  128. req.ApiName = ApiDescriptionMap[api]
  129. }
  130. apiType, ok := s.getFirstPathSegment(req.Api)
  131. if ok {
  132. req.ApiType = apiType[len(apiType)-1]
  133. }
  134. }
  135. userInfo, err := s.globalLimitRepository.GetUserInfo(ctx, int64(req.Uid))
  136. if err != nil {
  137. s.Logger.Error("获取用户信息失败", zap.Error(err), zap.Int("uid", req.Uid))
  138. continue
  139. }
  140. req.Name = userInfo.Username
  141. req.RequestIp = userInfo.LastLoginIp
  142. extraData, err := json.Marshal(req.ExtraData)
  143. if err != nil {
  144. s.Logger.Error("序列化额外数据失败", zap.Error(err))
  145. continue
  146. }
  147. wafLogs = append(wafLogs, &model.WafLog{
  148. Uid: req.Uid,
  149. Name: req.Name,
  150. RequestIp: req.RequestIp,
  151. RuleId: req.RuleId,
  152. HostId: req.HostId,
  153. UserAgent: req.UserAgent,
  154. Api: req.Api,
  155. ApiType: req.ApiType,
  156. ApiName: req.ApiName,
  157. ExtraData: extraData,
  158. })
  159. }
  160. return s.wafLogRepository.BatchAddWafLog(ctx, wafLogs)
  161. }
  162. func (s *wafLogService) ExPortWafLog(ctx context.Context, req adminApi.ExportWafLog) ([]adminApi.ExportWafLogRes, error) {
  163. data, err := s.wafLogRepository.ExportWafLog(ctx, req)
  164. if err != nil {
  165. return nil, err
  166. }
  167. return s.convertRawDataToExportResults(ctx, data)
  168. }
  169. func (s *wafLogService) SmartExportWafLog(ctx context.Context, req adminApi.ExportWafLog, w http.ResponseWriter) error {
  170. count, err := s.wafLogRepository.GetWafLogExportCount(ctx, req)
  171. if err != nil {
  172. return fmt.Errorf("获取导出数据总数失败: %w", err)
  173. }
  174. exportType := excel.SmartExport(count, 200)
  175. headers := []string{"name", "request_ip", "host_id", "rule_id", "api_name", "addr_backend_list", "allow_and_deny_ips", "domain", "custom_host", "expose_addr", "comment", "created_at"}
  176. headerMap := map[string]string{
  177. "name": "用户名",
  178. "request_ip": "请求IP",
  179. "host_id": "实例ID",
  180. "rule_id": "规则ID",
  181. "api_name": "操作名称",
  182. "addr_backend_list": "后端地址",
  183. "allow_and_deny_ips": "黑白名单",
  184. "domain": "域名",
  185. "custom_host": "回源地址",
  186. "expose_addr": "暴露地址",
  187. "comment": "备注",
  188. "created_at": "操作时间",
  189. }
  190. generator := excel.NewExcelGenerator("WAF日志", headers, headerMap)
  191. if err := generator.WriteHeaders(); err != nil {
  192. return fmt.Errorf("写入Excel表头失败: %w", err)
  193. }
  194. switch exportType {
  195. case excel.ExportTypeNormal:
  196. return s.normalExportWafLog(ctx, req, generator, w)
  197. case excel.ExportTypeStream:
  198. return s.streamExportWafLog(ctx, req, generator, w)
  199. case excel.ExportTypeChunk:
  200. return s.chunkExportWafLog(ctx, req, w, count)
  201. default:
  202. return s.normalExportWafLog(ctx, req, generator, w)
  203. }
  204. }
  205. func (s *wafLogService) normalExportWafLog(ctx context.Context, req adminApi.ExportWafLog, generator *excel.ExcelGenerator, w http.ResponseWriter) error {
  206. exportData, err := s.ExPortWafLog(ctx, req)
  207. if err != nil {
  208. return fmt.Errorf("获取导出数据失败: %w", err)
  209. }
  210. data := make([]map[string]interface{}, 0, len(exportData))
  211. for _, item := range exportData {
  212. row := map[string]interface{}{
  213. "name": item.Name,
  214. "request_ip": item.RequestIp,
  215. "host_id": item.HostId,
  216. "rule_id": s.wafLogDataCleanService.FormatBackendList(item.RuleId),
  217. "api_name": item.ApiName,
  218. "addr_backend_list": s.wafLogDataCleanService.FormatBackendList(item.AddrBackendList),
  219. "allow_and_deny_ips": item.AllowAndDenyIps,
  220. "domain": item.Domain,
  221. "comment": item.Comment,
  222. "custom_host": s.wafLogDataCleanService.FormatBackendList(item.CustomHost),
  223. "expose_addr": s.wafLogDataCleanService.FormatBackendList(item.ExposeAddr),
  224. "created_at": item.CreatedAt,
  225. }
  226. data = append(data, row)
  227. }
  228. if err := generator.WriteRows(data); err != nil {
  229. return fmt.Errorf("写入Excel数据失败: %w", err)
  230. }
  231. fileName := fmt.Sprintf("waf_logs_%s.xlsx", time.Now().Format("20060102_150405"))
  232. return excel.NormalExport(generator, w, excel.TransferOption{
  233. FileName: fileName,
  234. ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  235. })
  236. }
  237. func (s *wafLogService) streamExportWafLog(ctx context.Context, req adminApi.ExportWafLog, generator *excel.ExcelGenerator, w http.ResponseWriter) error {
  238. fileName := fmt.Sprintf("waf_logs_%s.xlsx", time.Now().Format("20060102_150405"))
  239. w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
  240. w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fileName))
  241. w.Header().Set("Transfer-Encoding", "chunked")
  242. pageSize := 1000
  243. page := 1
  244. for {
  245. exportData, err := s.wafLogRepository.ExportWafLogWithPagination(ctx, req, page, pageSize)
  246. if err != nil {
  247. return fmt.Errorf("获取第%d页数据失败: %w", page, err)
  248. }
  249. exportResults, err := s.convertRawDataToExportResults(ctx, exportData)
  250. if err != nil {
  251. return fmt.Errorf("转换导出数据失败: %w", err)
  252. }
  253. if len(exportResults) == 0 {
  254. break
  255. }
  256. for _, item := range exportResults {
  257. row := map[string]interface{}{
  258. "name": item.Name,
  259. "request_ip": item.RequestIp,
  260. "host_id": item.HostId,
  261. "rule_id": s.wafLogDataCleanService.FormatBackendList(item.RuleId),
  262. "api_name": item.ApiName,
  263. "addr_backend_list": s.wafLogDataCleanService.FormatBackendList(item.AddrBackendList),
  264. "allow_and_deny_ips": item.AllowAndDenyIps,
  265. "domain": item.Domain,
  266. "comment": item.Comment,
  267. "custom_host": s.wafLogDataCleanService.FormatBackendList(item.CustomHost),
  268. "expose_addr": s.wafLogDataCleanService.FormatBackendList(item.ExposeAddr),
  269. "created_at": item.CreatedAt,
  270. }
  271. if err := generator.WriteRow(row); err != nil {
  272. return fmt.Errorf("写入第%d页数据失败: %w", page, err)
  273. }
  274. }
  275. if len(exportResults) < pageSize {
  276. break
  277. }
  278. page++
  279. }
  280. return excel.StreamExport(generator, w, excel.TransferOption{
  281. FileName: fileName,
  282. ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  283. })
  284. }
  285. func (s *wafLogService) chunkExportWafLog(ctx context.Context, req adminApi.ExportWafLog, w http.ResponseWriter, totalRecords int) error {
  286. fileName := fmt.Sprintf("waf_logs_%s.xlsx", time.Now().Format("20060102_150405"))
  287. pageSize := 5000
  288. excel.ChunkExport(w, excel.TransferOption{
  289. FileName: fileName,
  290. ContentType: "application/json",
  291. }, totalRecords, pageSize)
  292. return nil
  293. }
  294. // convertRawDataToExportResults 将从数据库获取的带有关联IP的数据转换为最终导出格式
  295. func (s *wafLogService) convertRawDataToExportResults(ctx context.Context, rawData []model.WafLogWithGatewayIP) ([]adminApi.ExportWafLogRes, error) {
  296. if len(rawData) == 0 {
  297. return []adminApi.ExportWafLogRes{}, nil
  298. }
  299. var res []adminApi.ExportWafLogRes
  300. for _, v := range rawData {
  301. // --- 数据清洗 ---
  302. cleanedData := s.wafLogDataCleanService.ParseWafLogExtraData(v.ExtraData, v.ApiName)
  303. // --- 网关 IP 处理 ---
  304. var exposeAddr []string
  305. // 直接使用查询结果中附带的 gateway_ip_data
  306. if len(v.GatewayIpData) > 0 && string(v.GatewayIpData) != "null" {
  307. var gateWayIps []string
  308. // 解析从数据库子查询得到的JSON数据
  309. if err := json.Unmarshal(v.GatewayIpData, &gateWayIps); err == nil && len(gateWayIps) > 0 && cleanedData.Port != "" {
  310. for _, ip := range gateWayIps {
  311. exposeAddr = append(exposeAddr, ip+":"+cleanedData.Port)
  312. }
  313. }
  314. }
  315. var ruleIds []int64
  316. if len(cleanedData.RuleID) > 0 {
  317. ruleIds = cleanedData.RuleID
  318. } else {
  319. ruleIds = []int64{int64(v.RuleId)}
  320. }
  321. if cleanedData.IsHttps == 1 {
  322. for i := range exposeAddr {
  323. exposeAddr[i] = "https://" + exposeAddr[i]
  324. }
  325. }
  326. // --- 构造结果 ---
  327. res = append(res, adminApi.ExportWafLogRes{
  328. Name: v.Name,
  329. RequestIp: v.RequestIp,
  330. HostId: v.HostId,
  331. RuleId: ruleIds,
  332. ApiName: v.ApiName,
  333. AddrBackendList: cleanedData.AddrBackendList,
  334. AllowAndDenyIps: cleanedData.AllowAndDenyIps,
  335. Domain: cleanedData.Domain,
  336. Comment: cleanedData.Comment,
  337. CustomHost: cleanedData.CustomHost,
  338. ExposeAddr: exposeAddr,
  339. CreatedAt: v.CreatedAt,
  340. })
  341. }
  342. return res, nil
  343. }
  344. // GetApiDescriptions 获取API描述映射
  345. func (s *wafLogService) GetApiDescriptions(ctx context.Context) map[string]string {
  346. return ApiDescriptionMap
  347. }