aboutsummaryrefslogtreecommitdiff
path: root/weed/s3api/s3_metadata_util.go
blob: 37363752a1f9ec7b06d37a821c1f75f73d400517 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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
}