123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225 |
- package admin
- import (
- "encoding/json"
- "fmt"
- "github.com/go-nunu/nunu-layout-advanced/internal/service"
- "github.com/tidwall/gjson"
- "go.uber.org/zap"
- "strconv"
- "strings"
- )
- // --- 1. 集中化字段路径配置 ---
- // FieldPathConfig 定义了提取一个特定字段所需的所有信息
- type FieldPathConfig struct {
- // Paths 是一个优先级列表,解析器会从前到后尝试这些路径
- Paths []string
- // FieldType 指示字段的预期类型,用于特殊处理(如'array_object', 'array_string', 'bool')
- FieldType string
- }
- // apiFieldMappings 是驱动整个解析逻辑的核心配置
- // Key: API名称的关键字 (e.g., "web", "tcp", "allowAndDeny")
- // Value: 一个映射,定义了该API类型下需要提取的各个字段及其查找路径
- var apiFieldMappings = map[string]map[string]FieldPathConfig{
- "web": {
- "Comment": {Paths: []string{"comment", "data.comment", "desc"}},
- "Port": {Paths: []string{"port", "data.port"}},
- "Domain": {Paths: []string{"domain", "data.domain", "host"}},
- "IsHttps": {Paths: []string{"isHttps", "data.isHttps"}},
- "RuleID": {Paths: []string{"ruleId", "data.ruleId", "ids", "data.ids"}, FieldType: "array_int"},
- "BackendList": {Paths: []string{"backendList", "data.backendList", "backends"}, FieldType: "array_object"},
- },
- "tcp": {
- "Comment": {Paths: []string{"comment", "data.comment", "desc"}},
- "Port": {Paths: []string{"port", "data.port"}},
- "RuleID": {Paths: []string{"ruleId", "data.ruleId", "ids", "data.ids"}, FieldType: "array_int"},
- "AddrBackendList": {Paths: []string{"addrBackendList", "data.addrBackendList"}, FieldType: "array_string"},
- },
- "udp": { // UDP 和 TCP 结构类似
- "Comment": {Paths: []string{"comment", "data.comment", "desc"}},
- "Port": {Paths: []string{"port", "data.port"}},
- "RuleID": {Paths: []string{"ruleId", "data.ruleId", "ids", "data.ids"}, FieldType: "array_int"},
- "AddrBackendList": {Paths: []string{"addrBackendList", "data.addrBackendList"}, FieldType: "array_string"},
- },
- "globalLimit": {
- "Comment": {Paths: []string{"comment", "data.comment", "desc"}},
- "RuleID": {Paths: []string{"ruleId", "data.ruleId", "ids", "data.ids"}, FieldType: "array_int"},
- },
- "allowAndDeny": {
- "AllowAndDenyIps": {Paths: []string{"ip", "ips"}, FieldType: "array_string"},
- "RuleID": {Paths: []string{"ruleId", "data.ruleId", "ids", "data.ids"}, FieldType: "array_int"},
- },
- "ccIpList": {
- "AllowAndDenyIps": {Paths: []string{"ip", "ips","newIp"}}, // 精确指定 ccIpList 只查找 "ip"
- "RuleID": {Paths: []string{"ruleId", "data.ruleId", "ids", "data.ids"}, FieldType: "array_int"},
- },
- // "分配网关组" 的日志通常不包含用户层面的业务数据,所以这里不定义
- }
- // --- 2. 清洗后的统一数据结构 ---
- type CleanedExtraData struct {
- Port string
- Domain string
- Comment string
- IsHttps int
- RuleID []int64
- AddrBackendList []string
- CustomHost []string
- AllowAndDenyIps string
- }
- // --- 3. 服务实现 ---
- type WafLogDataCleanService interface {
- ParseWafLogExtraData(extraDataBytes json.RawMessage, apiName string) CleanedExtraData
- FormatBackendList(backendList interface{}) string
- }
- func NewWafLogDataCleanService(
- service *service.Service,
- ) WafLogDataCleanService {
- return &wafLogDataCleanService{
- Service: service,
- }
- }
- type wafLogDataCleanService struct {
- *service.Service
- }
- // ParseWafLogExtraData 使用配置驱动的 gjson 解析,兼具灵活性和可维护性
- func (s *wafLogDataCleanService) ParseWafLogExtraData(extraDataBytes json.RawMessage, apiName string) CleanedExtraData {
- var cleaned CleanedExtraData
- if len(extraDataBytes) == 0 || !gjson.Valid(string(extraDataBytes)) {
- if len(extraDataBytes) > 0 {
- s.Logger.Warn("ExtraData 不是有效的JSON", zap.String("raw_data", string(extraDataBytes)))
- }
- return cleaned
- }
- jsonStr := string(extraDataBytes)
- // 根据 apiName 找到对应的字段映射配置
- var fieldConfig map[string]FieldPathConfig
- for keyword, config := range apiFieldMappings {
- if strings.Contains(strings.ToLower(apiName), keyword) {
- fieldConfig = config
- break
- }
- }
- // 如果没有找到配置,直接返回空结构
- if fieldConfig == nil {
- return cleaned
- }
- // 通用、循环地提取字段
- for fieldName, config := range fieldConfig {
- s.extractField(jsonStr, fieldName, config, &cleaned)
- }
- return cleaned
- }
- // extractField 是一个通用的字段提取辅助函数
- func (s *wafLogDataCleanService) extractField(jsonStr, fieldName string, config FieldPathConfig, cleaned *CleanedExtraData) {
- // 找到第一个有效的路径和其结果
- var validPathResult gjson.Result
- for _, path := range config.Paths {
- result := gjson.Get(jsonStr, path)
- if result.Exists() {
- validPathResult = result
- break
- }
- }
- if !validPathResult.Exists() {
- return // 如果所有路径都找不到,直接返回
- }
- // 根据字段名称和类型将结果赋值给 CleanedExtraData
- switch fieldName {
- case "Comment":
- cleaned.Comment = validPathResult.String()
- case "Port":
- cleaned.Port = validPathResult.String()
- case "Domain":
- cleaned.Domain = validPathResult.String()
- case "IsHttps":
- cleaned.IsHttps = int(validPathResult.Int())
- case "RuleID":
- if validPathResult.IsArray() {
- validPathResult.ForEach(func(_, value gjson.Result) bool {
- cleaned.RuleID = append(cleaned.RuleID, value.Int())
- return true
- })
- } else {
- cleaned.RuleID = append(cleaned.RuleID, validPathResult.Int())
- }
- case "BackendList": // 特殊处理对象数组
- if validPathResult.IsArray() {
- validPathResult.ForEach(func(_, value gjson.Result) bool {
- if value.IsObject() {
- addr := gjson.Get(value.Raw, "addr").String()
- customHost := gjson.Get(value.Raw, "customHost").String()
- isHttps := gjson.Get(value.Raw, "isHttps").Int()
- if isHttps == 1 {
- addr = "https://" + addr
- } else {
- addr = "http://" + addr
- }
- cleaned.AddrBackendList = append(cleaned.AddrBackendList, addr)
- cleaned.CustomHost = append(cleaned.CustomHost, customHost)
- }
- return true
- })
- }
- case "AddrBackendList": // 处理字符串数组
- if validPathResult.IsArray() {
- validPathResult.ForEach(func(_, value gjson.Result) bool {
- cleaned.AddrBackendList = append(cleaned.AddrBackendList, value.String())
- return true
- })
- }
- case "AllowAndDenyIps":
- if validPathResult.IsArray() {
- var ips []string
- validPathResult.ForEach(func(_, value gjson.Result) bool {
- ips = append(ips, value.String())
- return true
- })
- cleaned.AllowAndDenyIps = strings.Join(ips, ", ")
- } else {
- cleaned.AllowAndDenyIps = validPathResult.String()
- }
- }
- }
- // FormatBackendList 格式化后端地址列表(已简化)
- func (s *wafLogDataCleanService) FormatBackendList(backendList interface{}) string {
- if backendList == nil {
- return ""
- }
- switch v := backendList.(type) {
- case []string:
- return strings.Join(v, ", ")
- case []int64:
- if len(v) == 0 {
- return ""
- }
- var strList []string
- for _, id := range v {
- strList = append(strList, strconv.FormatInt(id, 10))
- }
- return strings.Join(strList, ", ")
- case string:
- return v
- default:
- // 其他类型直接转为字符串,作为最后的兼容手段
- return fmt.Sprintf("%v", v)
- }
- }
|