package audit import ( "bytes" "database/sql" "encoding/json" "fmt" "io" "net" "net/http" "strings" "time" "udeServer/internal/log" udeModel "udeServer/internal/model" "udeServer/internal/response" ) // 捕获响应 type wrappedResponseWriter struct { http.ResponseWriter statusCode int body *bytes.Buffer } func newWrappedResponseWriter(w http.ResponseWriter) *wrappedResponseWriter { return &wrappedResponseWriter{ ResponseWriter: w, statusCode: http.StatusOK, body: bytes.NewBuffer(nil), } } // Write re-records the response body (buffered for the audit entry). func (w *wrappedResponseWriter) Write(b []byte) (int, error) { w.body.Write(b) return w.ResponseWriter.Write(b) } // WriteHeader re-records the status code. func (w *wrappedResponseWriter) WriteHeader(code int) { w.statusCode = code w.ResponseWriter.WriteHeader(code) } // genericResp parses the unified response envelope. type genericResp struct { Code int `json:"code"` Message string `json:"message"` Data json.RawMessage `json:"data"` } // skipLogPaths silences self-generation noise: the log management endpoints // would otherwise audit every entry they produce, and the archive download // response is a huge binary blob with no audit value. var skipLogPaths = map[string]bool{ "/api/v1/systemLog/list": true, "/api/v1/systemLog/audit": true, "/api/v1/systemLog/link": true, "/api/v1/logRestore/list": true, "/api/v1/logArchive/list": true, "/api/v1/logArchive/download": true, // WebSocket 运维:升级后的连接不经包装 writer,记多了也与 sdf 行为不一致 "/api/v1/ws/ops": true, } // LogMiddleware audits every HTTP request: it captures request/response // bodies, computes digests, uploads the raw bodies to MinIO, builds a // t_system_log record guarded by an HMAC-SM3 signature and stores it // asynchronously. Deleted per sdf-serve build: the entry is built from the // unified response envelope (udeServer answers HTTP 200 always, business // codes live in the body), so ResponseCode records the body code. type LogMiddleware struct { } func NewLogMiddleware() *LogMiddleware { return &LogMiddleware{} } // Handle is the middleware closure, constructed with the system-log and // config models plus the service name of this deployment. func (m *LogMiddleware) Handle(logModel udeModel.TSystemLogModel, configModel udeModel.TConfigModel, serviceName string) func(next http.HandlerFunc) http.HandlerFunc { return func(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if skipLogPaths[r.URL.Path] { next.ServeHTTP(w, r) return } // 1.获取TraceId和SpanId以及请求元数据 ctx, traceID, spanID, userID, userName, _ := EnsureTraceAndSpanID(r.Context()) // 2.读请求体 reqBody, _ := io.ReadAll(r.Body) r.Body = io.NopCloser(bytes.NewBuffer(reqBody)) reqHeader := filterRequestHeader(r.Header) log.RecordDebug(ctx, "请求头:%v", reqHeader) // 3.捕获响应 rw := newWrappedResponseWriter(w) start := time.Now() next.ServeHTTP(rw, r.WithContext(ctx)) duration := time.Since(start).Milliseconds() // 4.解析响应统一封装 respBody := rw.body.Bytes() var resp genericResp httpCode := int64(response.CodeServerError) message := "" parseFailed := false if err := json.Unmarshal(respBody, &resp); err != nil { parseFailed = true log.RecordWarn(ctx, "[LogHandle] 无法解析响应 JSON, Path:%s, Status:%d, Raw:%s", r.URL.Path, rw.statusCode, string(respBody)) } else { httpCode = int64(resp.Code) message = resp.Message if resp.Code != response.CodeOK { log.RecordError(ctx, fmt.Errorf("[Request-Fail] Path=%s Code=%d Msg=%q", r.URL.Path, resp.Code, resp.Message)) } else { log.RecordInfo(ctx, "[Request-Success] Path=%s Code=%d Msg=%s Data:%+v", r.URL.Path, resp.Code, resp.Message, string(resp.Data)) } } // 5. 上传 & 摘要 client, logConfig := GetGlobalConfig(configModel) reqDigestStr, reqUrl, reqErr := UploadAndDigest(reqBody, traceID, spanID, "[Http-Request]", client, r.Header.Get("Content-Type")) if reqErr != nil { log.RecordError(ctx, reqErr) } respDigestStr, respUrl, respErr := UploadAndDigest(respBody, traceID, spanID, "[Http-Response]", client, "application/json") if respErr != nil { log.RecordError(ctx, respErr) } // 6.构造日志对象 entry := &udeModel.TSystemLog{ Iden: NewUUID(), TraceId: traceID, SpanId: spanID, ParentSpanId: sql.NullString{String: "", Valid: false}, //顶层链路父SpanId为空时填写null Type: SystemLog, ServiceName: serviceName, UriPath: sql.NullString{String: r.URL.Path, Valid: true}, HttpMethod: sql.NullString{String: r.Method, Valid: true}, Method: deriveHandlerName(r.URL.Path), Level: ChooseLevelByBusinessCode(httpCode), AppKey: sql.NullString{Valid: false}, UserId: sql.NullString{String: userID, Valid: userID != ""}, UserName: sql.NullString{String: userName, Valid: userName != ""}, ClientIp: sql.NullString{String: GetClientIPPreferPublic(r), Valid: true}, RequestHeader: reqHeader, RequestBodyDigest: reqDigestStr, RequestBodyFileKey: reqUrl, ResponseCode: httpCode, ResponseBodyDigest: respDigestStr, ResponseBodyFileKey: respUrl, DurationMs: duration, CreatedAt: time.Now(), AuditStatus: UnAudited, } // 7.填充ErrMsg switch { case parseFailed: entry.ErrorMessage = sql.NullString{String: "Invalid response format", Valid: true} case resp.Code != response.CodeOK: entry.ErrorMessage = sql.NullString{String: message, Valid: true} default: entry.ErrorMessage = sql.NullString{Valid: false} } // 8.填充Hmac值,复用缓冲区;密钥为空时用空串计算,保持列非空 macBuffer := make([]byte, 0, 32) if mac, hErr := CalculateHMAC(entry, logConfig.HmacSecret, macBuffer); hErr == nil { entry.HMac = mac } else { log.RecordError(ctx, hErr) } // 9.按配置库中 log_record_level 过滤后异步入库 save := SaveToDBByLogLevel(logConfig.LogRecordLevel, entry.Level) if save { go AsyncInsertLog(logModel, entry) } } } } // deriveHandlerName derives the business handler name from the request path // (last path segment + "Handler"). func deriveHandlerName(path string) string { clean := strings.Trim(path, "/") parts := strings.Split(clean, "/") if len(parts) == 0 { return "" } last := parts[len(parts)-1] return last + "Handler" } var keepHeaders = map[string]bool{ "content-type": true, // x-api-key/x-user-id 等 udeServer 还没有鉴权体系,暂无注入来源 } // filterRequestHeader keeps only whitelisted request headers. func filterRequestHeader(header map[string][]string) sql.NullString { filtered := make(map[string][]string) for k, v := range header { if keepHeaders[strings.ToLower(k)] { filtered[k] = v } } if headerBytes, err := json.Marshal(filtered); err == nil { return sql.NullString{ String: string(headerBytes), Valid: true, } } return sql.NullString{Valid: false} } // GetClientIPPreferPublic extracts the client IP, preferring public IPs. func GetClientIPPreferPublic(r *http.Request) string { ip := getClientIP(r, true) if ip == "" { ip = getClientIP(r, false) } return ip } // getClientIP resolves the client IP honouring proxy headers, optionally // hiding private addresses. func getClientIP(r *http.Request, excludePrivate bool) string { // 1. 从 X-Forwarded-For 获取第一个真实IP xff := r.Header.Get("X-Forwarded-For") if xff != "" { parts := strings.Split(xff, ",") for _, ip := range parts { ip = strings.TrimSpace(ip) if ip != "" && (!excludePrivate || isPublicIP(ip)) { return ip } } } // 2. 从 X-Real-IP 获取反向代理地址 if ip := strings.TrimSpace(r.Header.Get("X-Real-IP")); ip != "" { if !excludePrivate || isPublicIP(ip) { return ip } } // 3. 从 RemoteAddr 获取代理地址 ip, _, err := net.SplitHostPort(r.RemoteAddr) if err == nil && (!excludePrivate || isPublicIP(ip)) { return ip } return "" } // isPublicIP reports whether the IP address is public/external. func isPublicIP(ip string) bool { parsedIP := net.ParseIP(ip) if parsedIP == nil { return false } privateBlocks := []*net.IPNet{ // IPv4 私有地址段 {IP: net.IPv4(10, 0, 0, 0), Mask: net.CIDRMask(8, 32)}, {IP: net.IPv4(172, 16, 0, 0), Mask: net.CIDRMask(12, 32)}, {IP: net.IPv4(192, 168, 0, 0), Mask: net.CIDRMask(16, 32)}, // IPv6 loopback {IP: net.ParseIP("::1"), Mask: net.CIDRMask(128, 128)}, } for _, block := range privateBlocks { if block.Contains(parsedIP) { return false } } return true }