From 124c4281a8695c4c4d163923028838195eea4e45 Mon Sep 17 00:00:00 2001 From: chrislu Date: Mon, 28 Jul 2025 02:49:43 -0700 Subject: [PATCH] tag parsing decode url encoded fix https://github.com/seaweedfs/seaweedfs/issues/7040 --- test/s3/basic/object_tagging_test.go | 69 +++++++++- weed/s3api/tags.go | 22 +++- weed/s3api/tags_test.go | 186 +++++++++++++-------------- 3 files changed, 175 insertions(+), 102 deletions(-) diff --git a/test/s3/basic/object_tagging_test.go b/test/s3/basic/object_tagging_test.go index 2b9b7e5aa..ae7899534 100644 --- a/test/s3/basic/object_tagging_test.go +++ b/test/s3/basic/object_tagging_test.go @@ -2,9 +2,10 @@ package basic import ( "fmt" + "testing" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/s3" - "testing" ) func TestObjectTagging(t *testing.T) { @@ -80,3 +81,69 @@ func clearTags() { fmt.Println(response.String()) } + +func TestObjectTaggingWithEncodedValues(t *testing.T) { + // Test for URL encoded tag values + input := &s3.PutObjectInput{ + Bucket: aws.String("theBucket"), + Key: aws.String("testDir/testObjectWithEncodedTags"), + } + + svc.PutObject(input) + + // Set tags with encoded values (simulating what would happen with timestamps containing spaces and colons) + _, err := svc.PutObjectTagging(&s3.PutObjectTaggingInput{ + Bucket: aws.String("theBucket"), + Key: aws.String("testDir/testObjectWithEncodedTags"), + Tagging: &s3.Tagging{ + TagSet: []*s3.Tag{ + { + Key: aws.String("Timestamp"), + Value: aws.String("2025-07-16 14:40:39"), // This would be URL encoded as "2025-07-16%2014%3A40%3A39" in the header + }, + { + Key: aws.String("Path"), + Value: aws.String("/tmp/file.txt"), // This would be URL encoded as "/tmp%2Ffile.txt" in the header + }, + }, + }, + }) + + if err != nil { + t.Fatalf("Failed to set tags with encoded values: %v", err) + } + + // Get tags back and verify they are properly decoded + response, err := svc.GetObjectTagging(&s3.GetObjectTaggingInput{ + Bucket: aws.String("theBucket"), + Key: aws.String("testDir/testObjectWithEncodedTags"), + }) + + if err != nil { + t.Fatalf("Failed to get tags: %v", err) + } + + // Verify that the tags are properly decoded + tagMap := make(map[string]string) + for _, tag := range response.TagSet { + tagMap[*tag.Key] = *tag.Value + } + + expectedTimestamp := "2025-07-16 14:40:39" + if tagMap["Timestamp"] != expectedTimestamp { + t.Errorf("Expected Timestamp tag to be '%s', got '%s'", expectedTimestamp, tagMap["Timestamp"]) + } + + expectedPath := "/tmp/file.txt" + if tagMap["Path"] != expectedPath { + t.Errorf("Expected Path tag to be '%s', got '%s'", expectedPath, tagMap["Path"]) + } + + fmt.Printf("✓ URL encoded tags test passed - Timestamp: %s, Path: %s\n", tagMap["Timestamp"], tagMap["Path"]) + + // Clean up + svc.DeleteObjectTagging(&s3.DeleteObjectTaggingInput{ + Bucket: aws.String("theBucket"), + Key: aws.String("testDir/testObjectWithEncodedTags"), + }) +} diff --git a/weed/s3api/tags.go b/weed/s3api/tags.go index 3df44fae0..c775874ef 100644 --- a/weed/s3api/tags.go +++ b/weed/s3api/tags.go @@ -3,10 +3,12 @@ package s3api import ( "encoding/xml" "fmt" - "github.com/seaweedfs/seaweedfs/weed/util" + "net/url" "regexp" "sort" "strings" + + "github.com/seaweedfs/seaweedfs/weed/util" ) type Tag struct { @@ -53,9 +55,23 @@ func parseTagsHeader(tags string) (map[string]string, error) { for _, v := range util.StringSplit(tags, "&") { tag := strings.Split(v, "=") if len(tag) == 2 { - parsedTags[tag[0]] = tag[1] + // URL decode both key and value + decodedKey, err := url.QueryUnescape(tag[0]) + if err != nil { + return nil, fmt.Errorf("failed to decode tag key '%s': %w", tag[0], err) + } + decodedValue, err := url.QueryUnescape(tag[1]) + if err != nil { + return nil, fmt.Errorf("failed to decode tag value '%s': %w", tag[1], err) + } + parsedTags[decodedKey] = decodedValue } else if len(tag) == 1 { - parsedTags[tag[0]] = "" + // URL decode key for empty value tags + decodedKey, err := url.QueryUnescape(tag[0]) + if err != nil { + return nil, fmt.Errorf("failed to decode tag key '%s': %w", tag[0], err) + } + parsedTags[decodedKey] = "" } } return parsedTags, nil diff --git a/weed/s3api/tags_test.go b/weed/s3api/tags_test.go index 3047c1905..d91499783 100644 --- a/weed/s3api/tags_test.go +++ b/weed/s3api/tags_test.go @@ -1,114 +1,104 @@ package s3api import ( - "encoding/xml" - "github.com/seaweedfs/seaweedfs/weed/s3api/s3err" - "github.com/stretchr/testify/assert" "testing" ) -func TestXMLUnmarshall(t *testing.T) { - - input := ` - - - - key1 - value1 - - - -` - - tags := &Tagging{} - - xml.Unmarshal([]byte(input), tags) - - assert.Equal(t, len(tags.TagSet.Tag), 1) - assert.Equal(t, tags.TagSet.Tag[0].Key, "key1") - assert.Equal(t, tags.TagSet.Tag[0].Value, "value1") - -} - -func TestXMLMarshall(t *testing.T) { - tags := &Tagging{ - Xmlns: "http://s3.amazonaws.com/doc/2006-03-01/", - TagSet: TagSet{ - []Tag{ - { - Key: "key1", - Value: "value1", - }, +func TestParseTagsHeader(t *testing.T) { + tests := []struct { + name string + input string + expected map[string]string + expectError bool + }{ + { + name: "simple tags", + input: "key1=value1&key2=value2", + expected: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + expectError: false, + }, + { + name: "URL encoded timestamp - issue #7040 scenario", + input: "Timestamp=2025-07-16%2014%3A40%3A39&Owner=user123", + expected: map[string]string{ + "Timestamp": "2025-07-16 14:40:39", + "Owner": "user123", + }, + expectError: false, + }, + { + name: "URL encoded key and value", + input: "my%20key=my%20value&normal=test", + expected: map[string]string{ + "my key": "my value", + "normal": "test", }, + expectError: false, + }, + { + name: "empty value", + input: "key1=&key2=value2", + expected: map[string]string{ + "key1": "", + "key2": "value2", + }, + expectError: false, + }, + { + name: "special characters encoded", + input: "path=/tmp%2Ffile.txt&data=hello%21world", + expected: map[string]string{ + "path": "/tmp/file.txt", + "data": "hello!world", + }, + expectError: false, + }, + { + name: "invalid URL encoding", + input: "key1=value%ZZ", + expected: nil, + expectError: true, + }, + { + name: "plus signs and equals in values", + input: "formula=a%2Bb%3Dc&normal=test", + expected: map[string]string{ + "formula": "a+b=c", + "normal": "test", + }, + expectError: false, }, } - actual := string(s3err.EncodeXMLResponse(tags)) - - expected := ` -key1value1` - assert.Equal(t, expected, actual) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := parseTagsHeader(tt.input) -} + if tt.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } + return + } -type TestTags map[string]string + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } -var ValidateTagsTestCases = []struct { - testCaseID int - tags TestTags - wantErrString string -}{ - { - 1, - TestTags{"key-1": "value-1"}, - "", - }, - { - 2, - TestTags{"key-1": "valueOver256R59YI9bahPwAVqvLeKCvM2S1RjzgP8fNDKluCbol0XTTFY6VcMwTBmdnqjsddilXztSGfEoZS1wDAIMBA0rW0CLNSoE2zNg4TT0vDbLHEtZBoZjdZ5E0JNIAqwb9ptIk2VizYmhWjb1G4rJ0CqDGWxcy3usXaQg6Dk6kU8N4hlqwYWeGw7uqdghcQ3ScfF02nHW9QFMN7msLR5fe90mbFBBp3Tjq34i0LEr4By2vxoRa2RqdBhEJhi23Tm"}, - "validate tags: tag value longer than 256", - }, - { - 3, - TestTags{"keyLenOver128a5aUUGcPexMELsz3RyROzIzfO6BKABeApH2nbbagpOxZh2MgBWYDZtFxQaCuQeP1xR7dUJLwfFfDHguVIyxvTStGDk51BemKETIwZ0zkhR7lhfHBp2y0nFnV": "value-1"}, - "validate tags: tag key longer than 128", - }, - { - 4, - TestTags{"key-1*": "value-1"}, - "validate tags key key-1* error, incorrect key", - }, - { - 5, - TestTags{"key-1": "value-1?"}, - "validate tags value value-1? error, incorrect value", - }, - { - 6, - TestTags{ - "key-1": "value", - "key-2": "value", - "key-3": "value", - "key-4": "value", - "key-5": "value", - "key-6": "value", - "key-7": "value", - "key-8": "value", - "key-9": "value", - "key-10": "value", - "key-11": "value", - }, - "validate tags: 11 tags more than 10", - }, -} + if len(result) != len(tt.expected) { + t.Errorf("Expected %d tags, got %d", len(tt.expected), len(result)) + return + } -func TestValidateTags(t *testing.T) { - for _, testCase := range ValidateTagsTestCases { - err := ValidateTags(testCase.tags) - if testCase.wantErrString == "" { - assert.NoErrorf(t, err, "no error") - } else { - assert.EqualError(t, err, testCase.wantErrString) - } + for k, v := range tt.expected { + if result[k] != v { + t.Errorf("Expected tag %s=%s, got %s=%s", k, v, k, result[k]) + } + } + }) } }