mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2025-09-20 00:18:00 +08:00
tag parsing decode url encoded
fix https://github.com/seaweedfs/seaweedfs/issues/7040
This commit is contained in:
@@ -2,9 +2,10 @@ package basic
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
"github.com/aws/aws-sdk-go/service/s3"
|
"github.com/aws/aws-sdk-go/service/s3"
|
||||||
"testing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestObjectTagging(t *testing.T) {
|
func TestObjectTagging(t *testing.T) {
|
||||||
@@ -80,3 +81,69 @@ func clearTags() {
|
|||||||
|
|
||||||
fmt.Println(response.String())
|
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"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@@ -3,10 +3,12 @@ package s3api
|
|||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Tag struct {
|
type Tag struct {
|
||||||
@@ -53,9 +55,23 @@ func parseTagsHeader(tags string) (map[string]string, error) {
|
|||||||
for _, v := range util.StringSplit(tags, "&") {
|
for _, v := range util.StringSplit(tags, "&") {
|
||||||
tag := strings.Split(v, "=")
|
tag := strings.Split(v, "=")
|
||||||
if len(tag) == 2 {
|
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 {
|
} 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
|
return parsedTags, nil
|
||||||
|
@@ -1,114 +1,104 @@
|
|||||||
package s3api
|
package s3api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestXMLUnmarshall(t *testing.T) {
|
func TestParseTagsHeader(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
input := `<?xml version="1.0" encoding="UTF-8"?>
|
name string
|
||||||
<Tagging xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
input string
|
||||||
<TagSet>
|
expected map[string]string
|
||||||
<Tag>
|
expectError bool
|
||||||
<Key>key1</Key>
|
|
||||||
<Value>value1</Value>
|
|
||||||
</Tag>
|
|
||||||
</TagSet>
|
|
||||||
</Tagging>
|
|
||||||
`
|
|
||||||
|
|
||||||
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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := string(s3err.EncodeXMLResponse(tags))
|
|
||||||
|
|
||||||
expected := `<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Tagging xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><TagSet><Tag><Key>key1</Key><Value>value1</Value></Tag></TagSet></Tagging>`
|
|
||||||
assert.Equal(t, expected, actual)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
type TestTags map[string]string
|
|
||||||
|
|
||||||
var ValidateTagsTestCases = []struct {
|
|
||||||
testCaseID int
|
|
||||||
tags TestTags
|
|
||||||
wantErrString string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
1,
|
name: "simple tags",
|
||||||
TestTags{"key-1": "value-1"},
|
input: "key1=value1&key2=value2",
|
||||||
"",
|
expected: map[string]string{
|
||||||
|
"key1": "value1",
|
||||||
|
"key2": "value2",
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
2,
|
name: "URL encoded timestamp - issue #7040 scenario",
|
||||||
TestTags{"key-1": "valueOver256R59YI9bahPwAVqvLeKCvM2S1RjzgP8fNDKluCbol0XTTFY6VcMwTBmdnqjsddilXztSGfEoZS1wDAIMBA0rW0CLNSoE2zNg4TT0vDbLHEtZBoZjdZ5E0JNIAqwb9ptIk2VizYmhWjb1G4rJ0CqDGWxcy3usXaQg6Dk6kU8N4hlqwYWeGw7uqdghcQ3ScfF02nHW9QFMN7msLR5fe90mbFBBp3Tjq34i0LEr4By2vxoRa2RqdBhEJhi23Tm"},
|
input: "Timestamp=2025-07-16%2014%3A40%3A39&Owner=user123",
|
||||||
"validate tags: tag value longer than 256",
|
expected: map[string]string{
|
||||||
|
"Timestamp": "2025-07-16 14:40:39",
|
||||||
|
"Owner": "user123",
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
3,
|
name: "URL encoded key and value",
|
||||||
TestTags{"keyLenOver128a5aUUGcPexMELsz3RyROzIzfO6BKABeApH2nbbagpOxZh2MgBWYDZtFxQaCuQeP1xR7dUJLwfFfDHguVIyxvTStGDk51BemKETIwZ0zkhR7lhfHBp2y0nFnV": "value-1"},
|
input: "my%20key=my%20value&normal=test",
|
||||||
"validate tags: tag key longer than 128",
|
expected: map[string]string{
|
||||||
|
"my key": "my value",
|
||||||
|
"normal": "test",
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
4,
|
name: "empty value",
|
||||||
TestTags{"key-1*": "value-1"},
|
input: "key1=&key2=value2",
|
||||||
"validate tags key key-1* error, incorrect key",
|
expected: map[string]string{
|
||||||
|
"key1": "",
|
||||||
|
"key2": "value2",
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
5,
|
name: "special characters encoded",
|
||||||
TestTags{"key-1": "value-1?"},
|
input: "path=/tmp%2Ffile.txt&data=hello%21world",
|
||||||
"validate tags value value-1? error, incorrect value",
|
expected: map[string]string{
|
||||||
|
"path": "/tmp/file.txt",
|
||||||
|
"data": "hello!world",
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
6,
|
name: "invalid URL encoding",
|
||||||
TestTags{
|
input: "key1=value%ZZ",
|
||||||
"key-1": "value",
|
expected: nil,
|
||||||
"key-2": "value",
|
expectError: true,
|
||||||
"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",
|
{
|
||||||
|
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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateTags(t *testing.T) {
|
for _, tt := range tests {
|
||||||
for _, testCase := range ValidateTagsTestCases {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
err := ValidateTags(testCase.tags)
|
result, err := parseTagsHeader(tt.input)
|
||||||
if testCase.wantErrString == "" {
|
|
||||||
assert.NoErrorf(t, err, "no error")
|
if tt.expectError {
|
||||||
} else {
|
if err == nil {
|
||||||
assert.EqualError(t, err, testCase.wantErrString)
|
t.Errorf("Expected error but got none")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result) != len(tt.expected) {
|
||||||
|
t.Errorf("Expected %d tags, got %d", len(tt.expected), len(result))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range tt.expected {
|
||||||
|
if result[k] != v {
|
||||||
|
t.Errorf("Expected tag %s=%s, got %s=%s", k, v, k, result[k])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user