mirror of
https://github.com/versity/versitygw.git
synced 2026-08-17 12:46:23 +00:00
Merge pull request #1931 from versity/sis/object-metadata-limits
fix: add request headers and metadata headers limit
This commit is contained in:
@@ -154,7 +154,6 @@ func (c S3ApiController) CreateMultipartUpload(ctx *fiber.Ctx) (*Response, error
|
||||
contentEncoding := ctx.Get("Content-Encoding")
|
||||
tagging := ctx.Get("X-Amz-Tagging")
|
||||
expires := ctx.Get("Expires")
|
||||
metadata := utils.GetUserMetaData(&ctx.Request().Header)
|
||||
// context locals
|
||||
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
|
||||
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
|
||||
@@ -180,6 +179,15 @@ func (c S3ApiController) CreateMultipartUpload(ctx *fiber.Ctx) (*Response, error
|
||||
}, err
|
||||
}
|
||||
|
||||
metadata, err := utils.GetUserMetaData(&ctx.Request().Header)
|
||||
if err != nil {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
objLockState, err := utils.ParsObjectLockHdrs(ctx)
|
||||
if err != nil {
|
||||
return &Response{
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
@@ -235,6 +236,23 @@ func TestS3ApiController_CreateMultipartUpload(t *testing.T) {
|
||||
err: s3err.GetAPIError(s3err.ErrAccessDenied),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid metadata header",
|
||||
input: testInput{
|
||||
locals: defaultLocals,
|
||||
headers: map[string]string{
|
||||
"x-amz-meta-key": strings.Repeat("a", 2048),
|
||||
},
|
||||
},
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
err: s3err.GetAPIError(s3err.ErrMetadataTooLarge),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid object lock headers",
|
||||
input: testInput{
|
||||
|
||||
@@ -551,7 +551,14 @@ func (c S3ApiController) CopyObject(ctx *fiber.Ctx) (*Response, error) {
|
||||
}, s3err.GetAPIError(s3err.ErrNonEmptyRequestBody)
|
||||
}
|
||||
|
||||
metadata := utils.GetUserMetaData(&ctx.Request().Header)
|
||||
metadata, err := utils.GetUserMetaData(&ctx.Request().Header)
|
||||
if err != nil {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
if metaDirective != "" && metaDirective != types.MetadataDirectiveCopy && metaDirective != types.MetadataDirectiveReplace {
|
||||
debuglogger.Logf("invalid metadata directive: %v", metaDirective)
|
||||
@@ -676,9 +683,6 @@ func (c S3ApiController) PutObject(ctx *fiber.Ctx) (*Response, error) {
|
||||
contentLengthStr = decodedLength
|
||||
}
|
||||
|
||||
// load the meta headers
|
||||
metadata := utils.GetUserMetaData(&ctx.Request().Header)
|
||||
|
||||
err := auth.VerifyAccess(ctx.Context(), c.be,
|
||||
auth.AccessOptions{
|
||||
Readonly: c.readonly,
|
||||
@@ -700,6 +704,16 @@ func (c S3ApiController) PutObject(ctx *fiber.Ctx) (*Response, error) {
|
||||
}, err
|
||||
}
|
||||
|
||||
// load the meta headers
|
||||
metadata, err := utils.GetUserMetaData(&ctx.Request().Header)
|
||||
if err != nil {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, []types.ObjectIdentifier{{Key: &key}}, true, IsBucketPublic, c.be, true)
|
||||
if err != nil {
|
||||
return &Response{
|
||||
|
||||
@@ -963,6 +963,24 @@ func TestS3ApiController_CopyObject(t *testing.T) {
|
||||
err: s3err.GetAPIError(s3err.ErrNonEmptyRequestBody),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid metadata header",
|
||||
input: testInput{
|
||||
locals: defaultLocals,
|
||||
headers: map[string]string{
|
||||
"X-Amz-Copy-Source": "bucket/object",
|
||||
"x-amz-meta-key": strings.Repeat("b", 2048),
|
||||
},
|
||||
},
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
err: s3err.GetAPIError(s3err.ErrMetadataTooLarge),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid metadata directive",
|
||||
input: testInput{
|
||||
@@ -1208,6 +1226,24 @@ func TestS3ApiController_PutObject(t *testing.T) {
|
||||
err: s3err.GetAPIError(s3err.ErrInvalidRequest),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid metadata header",
|
||||
input: testInput{
|
||||
locals: defaultLocals,
|
||||
extraMockErr: s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound),
|
||||
headers: map[string]string{
|
||||
"x-amz-meta-something": strings.Repeat("c", 2050),
|
||||
},
|
||||
},
|
||||
output: testOutput{
|
||||
response: &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
err: s3err.GetAPIError(s3err.ErrMetadataTooLarge),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid object lock headers",
|
||||
input: testInput{
|
||||
|
||||
@@ -91,6 +91,10 @@ func New(
|
||||
DisableStartupMessage: true,
|
||||
ErrorHandler: globalErrorHandler,
|
||||
Concurrency: server.maxConnections,
|
||||
// Sets buffer limit to read/parse incoming requests
|
||||
// if the limit is reached, fiber/fasthttp will throw an error
|
||||
// in the global error handler
|
||||
ReadBufferSize: 8 * 1024, // 8 KB
|
||||
})
|
||||
|
||||
server.app = app
|
||||
@@ -259,13 +263,20 @@ func globalErrorHandler(ctx *fiber.Ctx, er error) error {
|
||||
// handle the fiber specific errors
|
||||
var fiberErr *fiber.Error
|
||||
if errors.As(er, &fiberErr) {
|
||||
if errors.Is(fiberErr, fiber.ErrRequestHeaderFieldsTooLarge) {
|
||||
debuglogger.Logf("total request headers size exceeds the allowed 8KB")
|
||||
ctx.Status(http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
if strings.Contains(fiberErr.Message, "cannot parse Content-Length") {
|
||||
debuglogger.Logf("failed to parse Content-Length")
|
||||
ctx.Status(http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
if strings.Contains(fiberErr.Message, "error when reading request headers") {
|
||||
// This error means fiber failed to parse the incoming request
|
||||
// which is a malfoedmed one. Return a BadRequest in this case
|
||||
debuglogger.Logf("failed to parse the http request")
|
||||
err := s3err.GetAPIError(s3err.ErrCannotParseHTTPRequest)
|
||||
ctx.Status(err.HTTPStatusCode)
|
||||
return ctx.Send(s3err.GetAPIErrorResponse(err, "", "", ""))
|
||||
|
||||
+37
-5
@@ -55,20 +55,52 @@ func SetBucketNameValidationStrict(strict bool) {
|
||||
strictBucketNameValidation.Store(strict)
|
||||
}
|
||||
|
||||
func GetUserMetaData(headers *fasthttp.RequestHeader) (metadata map[string]string) {
|
||||
metadata = make(map[string]string)
|
||||
// maximum allowed size (2 KB) for all user-defined
|
||||
// object metadata combined, excluded the 'x-amz-meta-' prefix
|
||||
const maxMetadataSize = 2048
|
||||
|
||||
// GetUserMetaData extracts user metadata from headers with the "x-amz-meta-" prefix.
|
||||
// Keys are normalized to lowercase and duplicate headers are merged as
|
||||
// comma-separated values. The total metadata size is validated against the
|
||||
// 2 KB S3 limit.
|
||||
func GetUserMetaData(headers *fasthttp.RequestHeader) (map[string]string, error) {
|
||||
metadata := make(map[string]string)
|
||||
var metadataSize int
|
||||
|
||||
headers.DisableNormalizing()
|
||||
defer headers.EnableNormalizing()
|
||||
|
||||
for key, value := range headers.AllInOrder() {
|
||||
hKey := string(key)
|
||||
if strings.HasPrefix(strings.ToLower(hKey), "x-amz-meta-") {
|
||||
trimmedKey := strings.ToLower(hKey[11:])
|
||||
headerValue := string(value)
|
||||
metadata[trimmedKey] = headerValue
|
||||
|
||||
if existingVal, ok := metadata[trimmedKey]; ok {
|
||||
// S3 combines multiple metadata headers with the same key.
|
||||
// For example:
|
||||
// x-amz-meta-Key: value1
|
||||
// x-amz-meta-kEy: value2
|
||||
// x-amz-meta-keY: value3
|
||||
//
|
||||
// These are merged into a single lowercased key with the values
|
||||
// joined as a comma-separated list:
|
||||
// key: value1,value2,value3
|
||||
metadataSize += len(headerValue) + 1
|
||||
metadata[trimmedKey] = existingVal + "," + headerValue
|
||||
} else {
|
||||
metadataSize += len(trimmedKey) + len(headerValue)
|
||||
metadata[trimmedKey] = headerValue
|
||||
}
|
||||
|
||||
if metadataSize > maxMetadataSize {
|
||||
debuglogger.Logf("total meta headers size exceeded the maximum allowed: (size): %v, (max): %v", metadataSize, maxMetadataSize)
|
||||
return nil, s3err.GetAPIError(s3err.ErrMetadataTooLarge)
|
||||
}
|
||||
}
|
||||
}
|
||||
headers.EnableNormalizing()
|
||||
|
||||
return
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
func createHttpRequestFromCtx(ctx *fiber.Ctx, signedHdrs []string, contentLength int64, streamBody bool) (*http.Request, error) {
|
||||
|
||||
+119
-18
@@ -15,12 +15,14 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -94,35 +96,134 @@ func TestCreateHttpRequestFromCtx(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserMetaData(t *testing.T) {
|
||||
type args struct {
|
||||
headers *fasthttp.RequestHeader
|
||||
// a helper method to construct a raw http request with the given http request headers
|
||||
// to further parse with fasthttp.Request.Read and return fasthttp.RequestHeader
|
||||
func createHeadersFromRawRequest(t *testing.T, hdrs [][2]string) *fasthttp.RequestHeader {
|
||||
t.Helper()
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("PUT / HTTP/1.1\r\n")
|
||||
b.WriteString("Host: example.com\r\n")
|
||||
for _, kv := range hdrs {
|
||||
b.WriteString(kv[0])
|
||||
b.WriteString(": ")
|
||||
b.WriteString(kv[1])
|
||||
b.WriteString("\r\n")
|
||||
}
|
||||
b.WriteString("\r\n")
|
||||
|
||||
app := fiber.New()
|
||||
|
||||
// Case 1
|
||||
ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
|
||||
req := ctx.Request()
|
||||
var req fasthttp.Request
|
||||
if err := req.Read(bufio.NewReader(bytes.NewReader([]byte(b.String())))); err != nil {
|
||||
t.Fatalf("failed to parse raw request: %v", err)
|
||||
}
|
||||
return &req.Header
|
||||
}
|
||||
|
||||
func TestGetUserMetaData(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantMetadata map[string]string
|
||||
name string
|
||||
hdrs [][2]string
|
||||
want map[string]string
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "Success-empty-response",
|
||||
args: args{
|
||||
headers: &req.Header,
|
||||
name: "no metadata headers",
|
||||
hdrs: [][2]string{
|
||||
{"Content-Type", "application/json"},
|
||||
},
|
||||
want: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "single metadata header",
|
||||
hdrs: [][2]string{
|
||||
{"x-amz-meta-foo", "bar"},
|
||||
},
|
||||
want: map[string]string{"foo": "bar"},
|
||||
},
|
||||
{
|
||||
name: "multiple metadata headers",
|
||||
hdrs: [][2]string{
|
||||
{"x-amz-meta-foo", "bar"},
|
||||
{"x-amz-meta-baz", "qux"},
|
||||
},
|
||||
want: map[string]string{"foo": "bar", "baz": "qux"},
|
||||
},
|
||||
{
|
||||
name: "case-insensitive prefix and key lowercasing",
|
||||
hdrs: [][2]string{
|
||||
{"X-Amz-Meta-TestKey", "Value"},
|
||||
},
|
||||
want: map[string]string{"testkey": "Value"},
|
||||
},
|
||||
{
|
||||
name: "ignores non-metadata headers",
|
||||
hdrs: [][2]string{
|
||||
{"authorization", "token"},
|
||||
{"x-amz-meta-foo", "bar"},
|
||||
},
|
||||
want: map[string]string{"foo": "bar"},
|
||||
},
|
||||
{
|
||||
name: "metadata size exceeds limit (single header)",
|
||||
hdrs: [][2]string{
|
||||
{"x-amz-meta-big", strings.Repeat("a", maxMetadataSize+1)},
|
||||
},
|
||||
wantErr: s3err.GetAPIError(s3err.ErrMetadataTooLarge),
|
||||
},
|
||||
{
|
||||
name: "metadata cumulative size exceeds limit (multiple headers)",
|
||||
hdrs: [][2]string{
|
||||
{"x-amz-meta-a", strings.Repeat("a", maxMetadataSize/2)},
|
||||
{"x-amz-meta-b", strings.Repeat("b", maxMetadataSize/2+10)},
|
||||
},
|
||||
wantErr: s3err.GetAPIError(s3err.ErrMetadataTooLarge),
|
||||
},
|
||||
{
|
||||
name: "duplicate keys combined",
|
||||
hdrs: [][2]string{
|
||||
{"x-amz-meta-Foo", "first"},
|
||||
{"x-amz-meta-foo", "second"},
|
||||
},
|
||||
want: map[string]string{"foo": "first,second"},
|
||||
},
|
||||
{
|
||||
name: "duplicate same value keys combined",
|
||||
hdrs: [][2]string{
|
||||
{"x-amz-meta-Foo", "value"},
|
||||
{"x-amz-meta-foo", "value"},
|
||||
},
|
||||
want: map[string]string{"foo": "value,value"},
|
||||
},
|
||||
{
|
||||
name: "mixed keys",
|
||||
hdrs: [][2]string{
|
||||
{"x-amz-meta-Foo", "value2"},
|
||||
{"x-amz-meta-fOo", "value1"},
|
||||
{"x-amz-meta-foO", "value3"},
|
||||
{"x-amz-meta-bar", "baz"},
|
||||
{"x-amz-meta-quxx", "efg"},
|
||||
{"x-amz-meta-abc", "value"},
|
||||
{"x-amz-meta-Abc", "value"},
|
||||
{"x-amz-meta-aBc", "value"},
|
||||
{"x-amz-meta-abC", "value"},
|
||||
{"x-amz-meta-ABC", "value"},
|
||||
},
|
||||
want: map[string]string{
|
||||
"foo": "value2,value1,value3",
|
||||
"bar": "baz",
|
||||
"quxx": "efg",
|
||||
"abc": "value,value,value,value,value",
|
||||
},
|
||||
wantMetadata: map[string]string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotMetadata := GetUserMetaData(tt.args.headers); !reflect.DeepEqual(gotMetadata, tt.wantMetadata) {
|
||||
t.Errorf("GetUserMetaData() = %v, want %v", gotMetadata, tt.wantMetadata)
|
||||
}
|
||||
h := createHeadersFromRawRequest(t, tt.hdrs)
|
||||
|
||||
got, err := GetUserMetaData(h)
|
||||
assert.Equal(t, tt.wantErr, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,6 +185,7 @@ const (
|
||||
ErrMalformedTrailer
|
||||
ErrInvalidChunkSize
|
||||
ErrSlowDown
|
||||
ErrMetadataTooLarge
|
||||
|
||||
// Non-AWS errors
|
||||
ErrExistingObjectIsDirectory
|
||||
@@ -836,6 +837,12 @@ var errorCodeResponse = map[ErrorCode]APIError{
|
||||
Description: "Please reduce your request rate.",
|
||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||
},
|
||||
ErrMetadataTooLarge: {
|
||||
// TODO: should have 'Size' and 'MaxSizeAllowed' properties
|
||||
Code: "MetadataTooLarge",
|
||||
Description: "Your metadata headers exceed the maximum allowed metadata size",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
|
||||
// non aws errors
|
||||
ErrExistingObjectIsDirectory: {
|
||||
|
||||
@@ -424,6 +424,33 @@ func CopyObject_to_itself_with_new_metadata(s *S3Conf) error {
|
||||
})
|
||||
}
|
||||
|
||||
func CopyObject_long_metadata(s *S3Conf) error {
|
||||
testName := "CopyObject_long_metadata"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
srcObj, dstObj := "source-object", "destination-object"
|
||||
_, err := putObjectWithData(9, &s3.PutObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &srcObj,
|
||||
}, s3client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err = s3client.CopyObject(ctx, &s3.CopyObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &dstObj,
|
||||
CopySource: getPtr(fmt.Sprintf("%s/%s", bucket, srcObj)),
|
||||
Metadata: map[string]string{
|
||||
strings.Repeat("s", 2048): "value",
|
||||
},
|
||||
})
|
||||
cancel()
|
||||
|
||||
return checkApiErr(err, s3err.GetAPIError(s3err.ErrMetadataTooLarge))
|
||||
})
|
||||
}
|
||||
|
||||
func CopyObject_copy_source_starting_with_slash(s *S3Conf) error {
|
||||
testName := "CopyObject_CopySource_starting_with_slash"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
|
||||
@@ -38,6 +38,24 @@ func CreateMultipartUpload_non_existing_bucket(s *S3Conf) error {
|
||||
})
|
||||
}
|
||||
|
||||
func CreateMultipartUpload_long_metadata(s *S3Conf) error {
|
||||
testName := "CreateMultipartUpload_long_metadata"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err := s3client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
|
||||
Bucket: &bucket,
|
||||
Key: getPtr("obj"),
|
||||
Metadata: map[string]string{
|
||||
"key": "value",
|
||||
"key2": strings.Repeat("b", 2040),
|
||||
},
|
||||
})
|
||||
cancel()
|
||||
|
||||
return checkApiErr(err, s3err.GetAPIError(s3err.ErrMetadataTooLarge))
|
||||
})
|
||||
}
|
||||
|
||||
func CreateMultipartUpload_with_metadata(s *S3Conf) error {
|
||||
testName := "CreateMultipartUpload_with_metadata"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/versity/versitygw/s3err"
|
||||
@@ -466,6 +467,94 @@ func PutObject_conditional_writes(s *S3Conf) error {
|
||||
})
|
||||
}
|
||||
|
||||
func PutObject_should_combine_metadata(s *S3Conf) error {
|
||||
testName := "PutObject_should_combine_metadata"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
obj := "my-object"
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, fmt.Sprintf("%s/%s/%s", s.endpoint, bucket, obj), strings.NewReader("dummy data"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("x-amz-content-sha256", "UNSIGNED-PAYLOAD")
|
||||
req.Header.Add("x-amz-meta-key", "value-1")
|
||||
req.Header.Add("x-amz-meta-key", "value-2")
|
||||
req.Header.Add("x-amz-meta-key", "value-3")
|
||||
req.Header.Add("x-amz-meta-Key", "value-4")
|
||||
req.Header.Add("x-amz-meta-keY", "value-5")
|
||||
req.Header.Add("x-amz-meta-foo", "bar")
|
||||
req.Header.Add("x-amz-meta-baz", "abc")
|
||||
req.Header.Add("x-amz-meta-xyzz", "xxx-3")
|
||||
req.Header.Add("x-amz-meta-Xyzz", "xxx-1")
|
||||
req.Header.Add("x-amz-meta-xyzz", "xxx-2")
|
||||
req.Header.Add("x-amz-meta-hello", "world")
|
||||
req.Header.Add("x-amz-meta-boo", "bar")
|
||||
req.Header.Add("x-amz-meta-asdf", "ghk")
|
||||
|
||||
signer := v4.NewSigner()
|
||||
|
||||
err = signer.SignHTTP(req.Context(), aws.Credentials{AccessKeyID: s.awsID, SecretAccessKey: s.awsSecret}, req, "UNSIGNED-PAYLOAD", "s3", s.awsRegion, time.Now())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to sign the request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := s.httpClient.Do(req)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return fmt.Errorf("send request: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("expected the response status code to be %v, instead got %v", http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
||||
out, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &obj,
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expectedMeta := map[string]string{
|
||||
"key": "value-1,value-2,value-3,value-4,value-5",
|
||||
"foo": "bar",
|
||||
"baz": "abc",
|
||||
"xyzz": "xxx-3,xxx-1,xxx-2",
|
||||
"hello": "world",
|
||||
"boo": "bar",
|
||||
"asdf": "ghk",
|
||||
}
|
||||
|
||||
if !areMapsSame(expectedMeta, out.Metadata) {
|
||||
return fmt.Errorf("expected the object metadata to be %v, instead got %v", expectedMeta, out.Metadata)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func PutObject_long_metadata(s *S3Conf) error {
|
||||
testName := "PutObject_long_metadata"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
_, err := putObjectWithData(10, &s3.PutObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: getPtr("obj"),
|
||||
Metadata: map[string]string{
|
||||
"key": "value",
|
||||
"foo": "bar",
|
||||
"baz": "quxx",
|
||||
"something_long": strings.Repeat("a", 2048),
|
||||
},
|
||||
}, s3client)
|
||||
|
||||
return checkApiErr(err, s3err.GetAPIError(s3err.ErrMetadataTooLarge))
|
||||
})
|
||||
}
|
||||
|
||||
func PutObject_with_metadata(s *S3Conf) error {
|
||||
testName := "PutObject_with_metadata"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
|
||||
@@ -171,6 +171,7 @@ func TestPutObject(ts *TestState) {
|
||||
ts.Run(PutObject_past_retain_until_date)
|
||||
ts.Run(PutObject_invalid_retain_until_date)
|
||||
ts.Run(PutObject_conditional_writes)
|
||||
ts.Run(PutObject_should_combine_metadata)
|
||||
//TODO: remove the condition after implementing checksums in azure
|
||||
if !ts.conf.azureTests {
|
||||
ts.Run(PutObject_checksum_algorithm_and_header_mismatch)
|
||||
@@ -194,6 +195,7 @@ func TestPutObject(ts *TestState) {
|
||||
ts.Run(PutObject_invalid_credentials)
|
||||
ts.Run(PutObject_invalid_object_names)
|
||||
ts.Run(PutObject_object_acl_not_supported)
|
||||
ts.Run(PutObject_long_metadata)
|
||||
}
|
||||
|
||||
func TestHeadObject(ts *TestState) {
|
||||
@@ -331,6 +333,7 @@ func TestCopyObject(ts *TestState) {
|
||||
ts.Run(CopyObject_should_copy_tagging)
|
||||
ts.Run(CopyObject_invalid_tagging_directive)
|
||||
ts.Run(CopyObject_to_itself_with_new_metadata)
|
||||
ts.Run(CopyObject_long_metadata)
|
||||
ts.Run(CopyObject_copy_source_starting_with_slash)
|
||||
ts.Run(CopyObject_invalid_copy_source)
|
||||
ts.Run(CopyObject_non_existing_dir_object)
|
||||
@@ -383,6 +386,7 @@ func TestDeleteObjectTagging(ts *TestState) {
|
||||
|
||||
func TestCreateMultipartUpload(ts *TestState) {
|
||||
ts.Run(CreateMultipartUpload_non_existing_bucket)
|
||||
ts.Run(CreateMultipartUpload_long_metadata)
|
||||
ts.Run(CreateMultipartUpload_with_metadata)
|
||||
ts.Run(CreateMultipartUpload_with_tagging)
|
||||
ts.Run(CreateMultipartUpload_with_object_lock)
|
||||
@@ -847,6 +851,7 @@ func TestFullFlow(ts *TestState) {
|
||||
TestVersioning(ts)
|
||||
}
|
||||
TestIAM(ts)
|
||||
TestServer(ts)
|
||||
}
|
||||
|
||||
func TestPosix(ts *TestState) {
|
||||
@@ -1135,6 +1140,11 @@ func TestRouter(ts *TestState) {
|
||||
ts.Run(RouterListVersionsWithKey)
|
||||
}
|
||||
|
||||
func TestServer(ts *TestState) {
|
||||
// TestServer tests s3 api server specific behaviors
|
||||
ts.Run(Server_large_http_header)
|
||||
}
|
||||
|
||||
func TestUnsignedStreaminPayloadTrailer(ts *TestState) {
|
||||
// azure doesn't support checksums
|
||||
if !ts.conf.azureTests {
|
||||
@@ -1241,6 +1251,8 @@ func GetIntTests() IntTests {
|
||||
"PutObject_past_retain_until_date": PutObject_past_retain_until_date,
|
||||
"PutObject_invalid_retain_until_date": PutObject_invalid_retain_until_date,
|
||||
"PutObject_conditional_writes": PutObject_conditional_writes,
|
||||
"PutObject_should_combine_metadata": PutObject_should_combine_metadata,
|
||||
"PutObject_long_metadata": PutObject_long_metadata,
|
||||
"PutObject_with_metadata": PutObject_with_metadata,
|
||||
"PutObject_invalid_credentials": PutObject_invalid_credentials,
|
||||
"PutObject_checksum_algorithm_and_header_mismatch": PutObject_checksum_algorithm_and_header_mismatch,
|
||||
@@ -1410,6 +1422,7 @@ func GetIntTests() IntTests {
|
||||
"CopyObject_should_replace_tagging": CopyObject_should_replace_tagging,
|
||||
"CopyObject_should_copy_tagging": CopyObject_should_copy_tagging,
|
||||
"CopyObject_invalid_tagging_directive": CopyObject_invalid_tagging_directive,
|
||||
"CopyObject_long_metadata": CopyObject_long_metadata,
|
||||
"CopyObject_to_itself_with_new_metadata": CopyObject_to_itself_with_new_metadata,
|
||||
"CopyObject_copy_source_starting_with_slash": CopyObject_copy_source_starting_with_slash,
|
||||
"CopyObject_invalid_copy_source": CopyObject_invalid_copy_source,
|
||||
@@ -1447,6 +1460,7 @@ func GetIntTests() IntTests {
|
||||
"DeleteObjectTagging_success": DeleteObjectTagging_success,
|
||||
"DeleteObjectTagging_expected_bucket_owner": DeleteObjectTagging_expected_bucket_owner,
|
||||
"CreateMultipartUpload_non_existing_bucket": CreateMultipartUpload_non_existing_bucket,
|
||||
"CreateMultipartUpload_long_metadata": CreateMultipartUpload_long_metadata,
|
||||
"CreateMultipartUpload_with_metadata": CreateMultipartUpload_with_metadata,
|
||||
"CreateMultipartUpload_with_tagging": CreateMultipartUpload_with_tagging,
|
||||
"CreateMultipartUpload_with_object_lock": CreateMultipartUpload_with_object_lock,
|
||||
@@ -1887,5 +1901,6 @@ func GetIntTests() IntTests {
|
||||
"SignedStreamingPayloadTrailer_success": SignedStreamingPayloadTrailer_success,
|
||||
"NoAclMode_CreateBucket_with_acl": NoAclMode_CreateBucket_with_acl,
|
||||
"NoAclMode_PutBucketAcl": NoAclMode_PutBucketAcl,
|
||||
"Server_large_http_header": Server_large_http_header,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright 2026 Versity Software
|
||||
// This file is licensed under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
)
|
||||
|
||||
func Server_large_http_header(s *S3Conf) error {
|
||||
testName := "Server_large_http_header"
|
||||
return actionHandlerNoSetup(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
req, err := createSignedReq(http.MethodPut, s.endpoint, "/bucket/object", s.awsID, s.awsSecret, "s3", s.awsRegion, nil, time.Now(), map[string]string{
|
||||
"x-amz-custom-header": strings.Repeat("d", 1024*8),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
return fmt.Errorf("expected the response status to be %v, instead got %v", http.StatusBadRequest, resp.StatusCode)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(body) != 0 {
|
||||
return fmt.Errorf("expected empty response body, instead got %s", body)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user