digest.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. package audit
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/hex"
  6. "encoding/json"
  7. "fmt"
  8. "io"
  9. "mime"
  10. "mime/multipart"
  11. "os"
  12. "strings"
  13. "time"
  14. "github.com/tjfoc/gmsm/sm3"
  15. )
  16. // UploadAndDigest computes the SM3 digest of the payload and uploads the
  17. // raw bytes to MinIO under logs/{traceID}/{spanID}/. When the MinIO client
  18. // is not loaded (no/invalid file config) the upload is skipped and only the
  19. // digest is returned with an empty file key, so the audit chain keeps
  20. // working without MinIO.
  21. // Supported content types: application/json and multipart/form-data.
  22. func UploadAndDigest(data []byte, traceID, spanID, label string, minioClient *MinioClient, contentType string) (string, string, error) {
  23. // 1.计算摘要
  24. h := sm3.New()
  25. h.Write(data)
  26. digest := hex.EncodeToString(h.Sum(nil))
  27. // 2.构造标签、文件名、文件上传路径
  28. safeLabel := strings.Trim(label, "[]")
  29. baseName := fmt.Sprintf("%s_%d", safeLabel, time.Now().UnixNano())
  30. objectDir := fmt.Sprintf("%s/%s/%s", ObjectDirLogs, traceID, spanID)
  31. // MinIO 未就绪时只返回摘要
  32. if minioClient == nil {
  33. return digest, "", nil
  34. }
  35. // 空 body(典型如 GET 请求)无需上传原文
  36. if len(data) == 0 {
  37. return digest, "", nil
  38. }
  39. // 3.判断请求头类型
  40. switch {
  41. case strings.HasPrefix(contentType, "application/json"):
  42. // application/json
  43. fileName := baseName + ".json"
  44. objectKey := fmt.Sprintf("%s/%s", objectDir, fileName)
  45. fileKey, err := writeAndUploadTempFile(minioClient, data, fileName, objectKey)
  46. if err != nil {
  47. return "", "", fmt.Errorf("minio upload file failed: %w", err)
  48. }
  49. return digest, fileKey, nil
  50. case strings.HasPrefix(contentType, "multipart/form-data"):
  51. // multipart/form-data
  52. boundary := extractBoundary(contentType)
  53. if boundary == "" {
  54. return "", "", fmt.Errorf("invalid multipart content-type: %s", contentType)
  55. }
  56. reader := multipart.NewReader(bytes.NewReader(data), boundary)
  57. fields := make(map[string]string)
  58. fileKeys := make(map[string]string)
  59. for {
  60. part, err := reader.NextPart()
  61. if err == io.EOF {
  62. break
  63. }
  64. if err != nil {
  65. return "", "", fmt.Errorf("read multipart part failed: %w", err)
  66. }
  67. partData, err := io.ReadAll(part)
  68. if err != nil {
  69. return "", "", fmt.Errorf("read part data failed: %w", err)
  70. }
  71. if part.FileName() == "" {
  72. // 普通字段
  73. fields[part.FormName()] = string(partData)
  74. continue
  75. }
  76. // 文件字段
  77. objectKey := fmt.Sprintf("%s/%s_%s", objectDir, baseName, part.FileName())
  78. fileKey, err := writeAndUploadTempFile(minioClient, partData, part.FileName(), objectKey)
  79. if err != nil {
  80. return "", "", fmt.Errorf("upload file part failed: %w", err)
  81. }
  82. fileKeys[part.FormName()] = fileKey
  83. }
  84. // 保存字段 JSON
  85. fieldsFile := baseName + "_fields.json"
  86. fieldsKey := fmt.Sprintf("%s/%s", objectDir, fieldsFile)
  87. fieldBytes, err := json.MarshalIndent(fields, "", " ")
  88. if err != nil {
  89. return "", "", fmt.Errorf("marshal fields failed: %w", err)
  90. }
  91. if _, err := writeAndUploadTempFile(minioClient, fieldBytes, fieldsFile, fieldsKey); err != nil {
  92. return "", "", fmt.Errorf("upload fields failed: %w", err)
  93. }
  94. // 保存元数据 JSON
  95. metaJson := map[string]interface{}{
  96. "digest": digest,
  97. "trace_id": traceID,
  98. "span_id": spanID,
  99. "files": fileKeys,
  100. "fields": fieldsKey,
  101. }
  102. metaFile := baseName + "_meta.json"
  103. metaKey := fmt.Sprintf("%s/%s", objectDir, metaFile)
  104. metaBytes, err := json.MarshalIndent(metaJson, "", " ")
  105. if err != nil {
  106. return "", "", fmt.Errorf("marshal meta failed: %w", err)
  107. }
  108. if _, err := writeAndUploadTempFile(minioClient, metaBytes, metaFile, metaKey); err != nil {
  109. return "", "", fmt.Errorf("upload meta failed: %w", err)
  110. }
  111. return digest, metaKey, nil
  112. default:
  113. return "", "", fmt.Errorf("unsupported content type: %s", contentType)
  114. }
  115. }
  116. // extractBoundary extracts the boundary from a Content-Type header value.
  117. func extractBoundary(contentType string) string {
  118. _, params, err := mime.ParseMediaType(contentType)
  119. if err != nil {
  120. return ""
  121. }
  122. return params["boundary"]
  123. }
  124. // writeAndUploadTempFile writes data to a temp file, uploads it, and
  125. // removes the temp file afterwards.
  126. func writeAndUploadTempFile(minioClient *MinioClient, data []byte, tmpName, objectKey string) (string, error) {
  127. tmpFile, err := os.CreateTemp("", tmpName)
  128. if err != nil {
  129. return "", fmt.Errorf("create temp file failed: %w", err)
  130. }
  131. defer func() {
  132. _ = tmpFile.Close()
  133. _ = os.Remove(tmpFile.Name())
  134. }()
  135. if _, err := tmpFile.Write(data); err != nil {
  136. return "", fmt.Errorf("write temp file failed: %w", err)
  137. }
  138. if err := tmpFile.Close(); err != nil {
  139. return "", fmt.Errorf("close temp file failed: %w", err)
  140. }
  141. fileKey, err := minioClient.UploadFile(context.Background(), tmpFile.Name(), objectKey)
  142. if err != nil {
  143. return "", fmt.Errorf("minio upload failed: %w", err)
  144. }
  145. return fileKey, nil
  146. }