diff options
Diffstat (limited to 'weed/s3api/s3_metadata_util.go')
| -rw-r--r-- | weed/s3api/s3_metadata_util.go | 94 |
1 files changed, 94 insertions, 0 deletions
diff --git a/weed/s3api/s3_metadata_util.go b/weed/s3api/s3_metadata_util.go new file mode 100644 index 000000000..37363752a --- /dev/null +++ b/weed/s3api/s3_metadata_util.go @@ -0,0 +1,94 @@ +package s3api + +import ( + "net/http" + "net/url" + "strings" + + "github.com/seaweedfs/seaweedfs/weed/glog" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3err" +) + +// ParseS3Metadata extracts S3-specific metadata from HTTP request headers +// This includes: storage class, tags, user metadata, SSE headers, and ACL headers +// Used by S3 API handlers to prepare metadata before saving to filer +// Returns an S3 error code if tag parsing fails +func ParseS3Metadata(r *http.Request, existing map[string][]byte, isReplace bool) (metadata map[string][]byte, errCode s3err.ErrorCode) { + metadata = make(map[string][]byte) + + // Copy existing metadata unless replacing + if !isReplace { + for k, v := range existing { + metadata[k] = v + } + } + + // Storage class + if sc := r.Header.Get(s3_constants.AmzStorageClass); sc != "" { + metadata[s3_constants.AmzStorageClass] = []byte(sc) + } + + // Content-Encoding (standard HTTP header used by S3) + if ce := r.Header.Get("Content-Encoding"); ce != "" { + metadata["Content-Encoding"] = []byte(ce) + } + + // Object tagging + if tags := r.Header.Get(s3_constants.AmzObjectTagging); tags != "" { + // Use url.ParseQuery for robust parsing and automatic URL decoding + parsedTags, err := url.ParseQuery(tags) + if err != nil { + // Return proper S3 error instead of silently dropping tags + glog.Warningf("Invalid S3 tag format in header '%s': %v", tags, err) + return nil, s3err.ErrInvalidTag + } + + // Validate: S3 spec does not allow duplicate tag keys + for key, values := range parsedTags { + if len(values) > 1 { + glog.Warningf("Duplicate tag key '%s' in header '%s'", key, tags) + return nil, s3err.ErrInvalidTag + } + // Tag value can be an empty string but not nil + value := "" + if len(values) > 0 { + value = values[0] + } + metadata[s3_constants.AmzObjectTagging+"-"+key] = []byte(value) + } + } + + // User-defined metadata (x-amz-meta-* headers) + for header, values := range r.Header { + if strings.HasPrefix(header, s3_constants.AmzUserMetaPrefix) { + // Go's HTTP server canonicalizes headers (e.g., x-amz-meta-foo → X-Amz-Meta-Foo) + // Per HTTP and S3 spec: multiple header values are concatenated with commas + // This ensures no metadata is lost when clients send duplicate header names + metadata[header] = []byte(strings.Join(values, ",")) + } + } + + // SSE-C headers + if algorithm := r.Header.Get(s3_constants.AmzServerSideEncryptionCustomerAlgorithm); algorithm != "" { + metadata[s3_constants.AmzServerSideEncryptionCustomerAlgorithm] = []byte(algorithm) + } + if keyMD5 := r.Header.Get(s3_constants.AmzServerSideEncryptionCustomerKeyMD5); keyMD5 != "" { + // Store as-is; SSE-C MD5 is base64 and case-sensitive + metadata[s3_constants.AmzServerSideEncryptionCustomerKeyMD5] = []byte(keyMD5) + } + + // ACL owner + acpOwner := r.Header.Get(s3_constants.ExtAmzOwnerKey) + if len(acpOwner) > 0 { + metadata[s3_constants.ExtAmzOwnerKey] = []byte(acpOwner) + } + + // ACL grants + acpGrants := r.Header.Get(s3_constants.ExtAmzAclKey) + if len(acpGrants) > 0 { + metadata[s3_constants.ExtAmzAclKey] = []byte(acpGrants) + } + + return metadata, s3err.ErrNone +} |
