From 9f786b3c2c60e0b3dc0bd2c4a1fb0d90eaec2ff1 Mon Sep 17 00:00:00 2001 From: niksis02 Date: Fri, 15 May 2026 03:31:55 +0400 Subject: [PATCH] feat: global error refactoring Fixes #2123 Fixes #2120 Fixes #2116 Fixes #2111 Fixes #2108 Fixes #2086 Fixes #2085 Fixes #2083 Fixes #2081 Fixes #2080 Fixes #2073 Fixes #2072 Fixes #2071 Fixes #2069 Fixes #2044 Fixes #2043 Fixes #2042 Fixes #2041 Fixes #2040 Fixes #2039 Fixes #2036 Fixes #2035 Fixes #2034 Fixes #2028 Fixes #2020 Fixes #1842 Fixes #1810 Fixes #1780 Fixes #1775 Fixes #1736 Fixes #1705 Fixes #1663 Fixes #1645 Fixes #1583 Fixes #1526 Fixes #1514 Fixes #1493 Fixes #1487 Fixes #959 Fixes #779 Closes #823 Closes #85 Refactor global S3 error handling around structured error types and centralized XML response generation. All S3 errors now share the common APIError base for the fields every error has: Code, HTTP status code, and Message. Non-traditional errors that need AWS-compatible XML fields now have dedicated typed errors in the s3err package. Each typed error implements the shared S3Error behavior so controllers and middleware can handle errors consistently while still emitting error-specific XML fields. Add a dedicated InvalidArgumentError type because InvalidArgument is used widely across request validation, auth, copy source handling, object lock validation, multipart validation, and header parsing. The new InvalidArgument path uses explicit InvalidArgErrorCode constants with predefined descriptions and ArgumentName values, keeping call sites readable while preserving the correct InvalidArgument XML shape and optional ArgumentValue. New structured errors added in s3err: - `AccessForbiddenError`: Method, ResourceType - `BadDigestError`: CalculatedDigest, ExpectedDigest - `BucketError`: BucketName - `ContentSHA256MismatchError`: ClientComputedContentSHA256, S3ComputedContentSHA256 - `EntityTooLargeError`: ProposedSize, MaxSizeAllowed - `EntityTooSmallError`: ProposedSize, MinSizeAllowed - `ExpiredPresignedURLError`: ServerTime, XAmzExpires, Expires - `InvalidAccessKeyIdError`: AWSAccessKeyId - `InvalidArgumentError`: Description, ArgumentName, ArgumentValue - `InvalidChunkSizeError`: Chunk, BadChunkSize - `InvalidDigestError`: ContentMD5 - `InvalidLocationConstraintError`: LocationConstraint - `InvalidPartError`: UploadId, PartNumber, ETag - `InvalidRangeError`: RangeRequested, ActualObjectSize - `InvalidTagError`: TagKey, TagValue - `KeyTooLongError`: Size, MaxSizeAllowed - `MetadataTooLargeError`: Size, MaxSizeAllowed - `MethodNotAllowedError`: Method, ResourceType, AllowedMethods - `NoSuchUploadError`: UploadId - `NoSuchVersionError`: Key, VersionId - `NotImplementedError`: Header, AdditionalMessage - `PreconditionFailedError`: Condition - `RequestTimeTooSkewedError`: RequestTime, ServerTime, MaxAllowedSkewMilliseconds - `SignatureDoesNotMatchError`: AWSAccessKeyId, StringToSign, SignatureProvided, StringToSignBytes, CanonicalRequest, CanonicalRequestBytes Fix CompleteMultipartUpload validation in the Azure backend so missing or empty `ETag` values return the appropriate S3 error instead of allowing a gateway panic. Fix presigned authentication expiration validation to compare server time in `UTC`, matching the `UTC` timestamp used by presigned URL signing. Add request ID and host ID support across S3 requests. Each request now receives AWS S3-like identifiers, returned in response headers as `x-amz-request-id` and `x-amz-id-2` and included in all XML error responses as RequestId and HostId. The generated ID structure is designed to resemble AWS S3 request IDs and host IDs. The request signature calculation/validation for streaming uploads was previously delayed until the request body was fully read, both for Authorization header authentication and presigned URLs. Now, the signature is validated immediately in the authorization middlewares without reading the request body, since the signature calculation itself does not depend on the request body. Instead, only the `x-amz-content-sha256` SHA-256 hash calculation is delayed. --- auth/access-control.go | 21 +- auth/acl.go | 2 +- auth/bucket_cors.go | 6 +- auth/bucket_cors_test.go | 8 +- auth/object_lock.go | 6 +- auth/post_policy.go | 4 +- auth/post_policy_test.go | 4 +- aws/signer/v4/v4.go | 25 +- aws/signer/v4/v4_test.go | 10 +- backend/azure/azure.go | 56 +-- backend/common.go | 97 +++-- backend/common_test.go | 12 +- backend/mp-lister.go | 4 +- backend/posix/posix.go | 373 +++++++++--------- backend/s3proxy/s3.go | 12 +- backend/scoutfs/scoutfs_compat.go | 8 +- metrics/metrics.go | 4 +- plugins/noop/noop.go | 2 +- s3api/admin-server.go | 2 + s3api/controllers/base.go | 43 +- s3api/controllers/base_test.go | 31 +- s3api/controllers/bucket-get_test.go | 8 +- s3api/controllers/bucket-list_test.go | 8 +- s3api/controllers/bucket-post_test.go | 2 +- s3api/controllers/bucket-put.go | 17 +- s3api/controllers/bucket-put_test.go | 14 +- s3api/controllers/object-get.go | 4 +- s3api/controllers/object-get_test.go | 8 +- s3api/controllers/object-head.go | 2 +- s3api/controllers/object-head_test.go | 2 +- s3api/controllers/object-post_test.go | 42 +- s3api/controllers/object-put.go | 8 +- s3api/controllers/object-put_test.go | 20 +- s3api/controllers/options.go | 7 +- s3api/controllers/options_test.go | 4 +- .../apply-bucket-cors-preflight.go | 2 +- s3api/middlewares/apply-bucket-cors.go | 8 +- s3api/middlewares/authentication.go | 49 ++- .../bucket-object-name-validator.go | 2 +- s3api/middlewares/checksum.go | 4 +- s3api/middlewares/object-post-auth.go | 15 +- s3api/middlewares/object-post-auth_test.go | 4 +- s3api/middlewares/presign-auth.go | 22 +- s3api/middlewares/public-bucket.go | 2 +- s3api/middlewares/rate-limiter.go | 5 +- s3api/middlewares/request-ids.go | 28 ++ s3api/middlewares/request-ids_test.go | 46 +++ s3api/router.go | 93 ++++- s3api/server.go | 12 +- s3api/utils/auth-reader.go | 121 ++---- s3api/utils/auth_test.go | 4 +- s3api/utils/chunk-reader.go | 18 +- s3api/utils/chunk-reader_test.go | 6 +- s3api/utils/context-keys.go | 18 +- s3api/utils/csum-reader.go | 13 +- s3api/utils/multipart_reader.go | 3 +- s3api/utils/multipart_reader_test.go | 2 +- s3api/utils/presign-auth-reader.go | 63 +-- s3api/utils/request_ids.go | 114 ++++++ s3api/utils/request_ids_test.go | 53 +++ s3api/utils/signed-chunk-reader.go | 70 ++-- s3api/utils/unsigned-chunk-reader_test.go | 6 +- s3api/utils/utils.go | 96 ++--- s3api/utils/utils_test.go | 74 ++-- s3err/access-forbidden-error.go | 57 +++ s3err/bad-digest-error.go | 57 +++ s3err/bucket-error.go | 56 +++ s3err/content-sha256-mismatch-error.go | 58 +++ s3err/entity-too-large-error.go | 57 +++ s3err/entity-too-small-error.go | 57 +++ s3err/expired-presigned-url-error.go | 61 +++ s3err/invalid-access-key-id-error.go | 53 +++ s3err/invalid-argument.go | 274 +++++++++++++ s3err/invalid-chunk-size-error.go | 57 +++ s3err/invalid-digest-error.go | 53 +++ s3err/invalid-location-constraint-error.go | 53 +++ s3err/invalid-part-error.go | 63 +++ s3err/invalid-part-number-range-error.go | 58 +++ s3err/invalid-range-error.go | 57 +++ s3err/invalid-tag-error.go | 59 +++ s3err/key-too-long-error.go | 57 +++ s3err/metadata-too-large-error.go | 57 +++ s3err/method-not-allowed-error.go | 67 ++++ s3err/no-such-upload-error.go | 53 +++ s3err/no-such-version-error.go | 57 +++ s3err/not-implemented-error.go | 66 ++++ s3err/post-object.go | 91 +++-- s3err/precondition-failed-error.go | 65 +++ s3err/presigned-urls.go | 30 +- s3err/request-time-too-skewed-error.go | 63 +++ s3err/s3err.go | 305 ++------------ s3err/signature-does-not-match-error.go | 73 ++++ s3err/sigv4.go | 68 +++- s3log/audit-logger.go | 15 +- s3log/file.go | 11 +- s3log/file_admin.go | 7 +- s3log/webhook.go | 11 +- s3response/s3response.go | 3 +- tests/integration/AbortMultipartUpload.go | 2 +- tests/integration/CompleteMultipartUpload.go | 75 +++- tests/integration/CopyObject.go | 50 +-- tests/integration/CreateBucket.go | 7 +- tests/integration/CreateMultipartUpload.go | 18 +- tests/integration/DeleteBucket.go | 2 +- tests/integration/DeleteBucketTagging.go | 2 +- tests/integration/DeleteObject.go | 2 +- tests/integration/DeleteObjectTagging.go | 2 +- tests/integration/GetObject.go | 8 +- tests/integration/GetObjectAttributes.go | 4 +- tests/integration/HeadObject.go | 4 + tests/integration/ListBuckets.go | 4 +- tests/integration/ListMultipartUploads.go | 11 +- tests/integration/ListObjectVersions.go | 2 +- tests/integration/ListObjects.go | 2 +- tests/integration/ListParts.go | 17 +- tests/integration/PostObject.go | 59 ++- tests/integration/PutBucketPolicy.go | 2 +- tests/integration/PutBucketTagging.go | 2 +- tests/integration/PutObject.go | 25 +- .../integration/PutObjectLockConfiguration.go | 5 +- tests/integration/PutObjectRetention.go | 2 +- tests/integration/UploadPart.go | 2 +- tests/integration/UploadPartCopy.go | 42 +- tests/integration/general.go | 22 +- tests/integration/group-tests.go | 3 + tests/integration/iam.go | 3 + tests/integration/presigned_urls.go | 20 +- tests/integration/public_bucket.go | 5 +- tests/integration/server.go | 2 +- tests/integration/sigv4_auth.go | 55 +-- tests/integration/utils.go | 305 ++++++++++++-- tests/integration/versioning.go | 26 +- 132 files changed, 3511 insertions(+), 1339 deletions(-) create mode 100644 s3api/middlewares/request-ids.go create mode 100644 s3api/middlewares/request-ids_test.go create mode 100644 s3api/utils/request_ids.go create mode 100644 s3api/utils/request_ids_test.go create mode 100644 s3err/access-forbidden-error.go create mode 100644 s3err/bad-digest-error.go create mode 100644 s3err/bucket-error.go create mode 100644 s3err/content-sha256-mismatch-error.go create mode 100644 s3err/entity-too-large-error.go create mode 100644 s3err/entity-too-small-error.go create mode 100644 s3err/expired-presigned-url-error.go create mode 100644 s3err/invalid-access-key-id-error.go create mode 100644 s3err/invalid-argument.go create mode 100644 s3err/invalid-chunk-size-error.go create mode 100644 s3err/invalid-digest-error.go create mode 100644 s3err/invalid-location-constraint-error.go create mode 100644 s3err/invalid-part-error.go create mode 100644 s3err/invalid-part-number-range-error.go create mode 100644 s3err/invalid-range-error.go create mode 100644 s3err/invalid-tag-error.go create mode 100644 s3err/key-too-long-error.go create mode 100644 s3err/metadata-too-large-error.go create mode 100644 s3err/method-not-allowed-error.go create mode 100644 s3err/no-such-upload-error.go create mode 100644 s3err/no-such-version-error.go create mode 100644 s3err/not-implemented-error.go create mode 100644 s3err/precondition-failed-error.go create mode 100644 s3err/request-time-too-skewed-error.go create mode 100644 s3err/signature-does-not-match-error.go diff --git a/auth/access-control.go b/auth/access-control.go index 7747f9d7..2cecc5d1 100644 --- a/auth/access-control.go +++ b/auth/access-control.go @@ -18,6 +18,7 @@ import ( "context" "encoding/json" "errors" + "net/http" "net/url" "strings" @@ -44,11 +45,11 @@ func VerifyObjectCopyAccess(ctx context.Context, be backend.Backend, copySource // Callers are expected to have already stripped any leading '/'. decodedSrc, err := url.QueryUnescape(copySource) if err != nil { - return s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding) + return s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, copySource) } srcBucket, srcObject, found := strings.Cut(decodedSrc, "/") if !found { - return s3err.GetAPIError(s3err.ErrInvalidCopySourceBucket) + return s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceBucket, copySource) } // Get source bucket ACL @@ -123,13 +124,6 @@ func VerifyAccess(ctx context.Context, be backend.Backend, opts AccessOptions) e return nil } -// Detects if the action is policy related -// e.g. -// 'GetBucketPolicy', 'PutBucketPolicy' -func isPolicyAction(action Action) bool { - return action == GetBucketPolicyAction || action == PutBucketPolicyAction -} - // VerifyPublicAccess checks if the bucket is publically accessible by ACL or Policy func VerifyPublicAccess(ctx context.Context, be backend.Backend, action Action, permission Permission, bucket, object string) error { // ACL disabled @@ -142,8 +136,13 @@ func VerifyPublicAccess(ctx context.Context, be backend.Backend, action Action, if err == nil { // if ACLs are disabled, and the bucket grants public access, // policy actions should return 'MethodNotAllowed' - if isPolicyAction(action) { - return s3err.GetAPIError(s3err.ErrMethodNotAllowed) + switch action { + case GetBucketPolicyAction: + return s3err.GetMethodNotAllowedErr(http.MethodGet, s3err.ResourceTypeBucketPolicy, nil) + case PutBucketPolicyAction: + return s3err.GetMethodNotAllowedErr(http.MethodPut, s3err.ResourceTypeBucketPolicy, nil) + case DeleteBucketPolicyAction: + return s3err.GetMethodNotAllowedErr(http.MethodDelete, s3err.ResourceTypeBucketPolicy, nil) } return nil diff --git a/auth/acl.go b/auth/acl.go index 9ce0c60e..45123223 100644 --- a/auth/acl.go +++ b/auth/acl.go @@ -512,6 +512,6 @@ func ValidateCannedACL(acl types.BucketCannedACL) error { return nil default: debuglogger.Logf("invalid bucket canned acl: %v", acl) - return s3err.GetAPIError(s3err.ErrInvalidArgument) + return s3err.GetInvalidArgumentErr(s3err.InvalidArgCannedAcl, string(acl)) } } diff --git a/auth/bucket_cors.go b/auth/bucket_cors.go index 3637bf00..8ad0de9e 100644 --- a/auth/bucket_cors.go +++ b/auth/bucket_cors.go @@ -122,12 +122,12 @@ type CORSAllowanceConfig struct { // IsAllowed walks through the CORS rules and finds the first one allowing access. // If no rule grants access, returns 'AccessForbidden' -func (cc *CORSConfiguration) IsAllowed(origin string, method CORSHTTPMethod, headers []CORSHeader) (*CORSAllowanceConfig, error) { +func (cc *CORSConfiguration) IsAllowed(origin string, method CORSHTTPMethod, headers []CORSHeader, rt s3err.ResourceType) (*CORSAllowanceConfig, error) { // if method is empty, anyways cors is forbidden // skip, without going through the rules if method.IsEmpty() { debuglogger.Logf("empty Access-Control-Request-Method") - return nil, s3err.GetAPIError(s3err.ErrCORSForbidden) + return nil, s3err.GetAccessForbiddenErr(s3err.ErrCORSForbidden, http.MethodOptions, rt) } for _, rule := range cc.Rules { // find the first rule granting access @@ -151,7 +151,7 @@ func (cc *CORSConfiguration) IsAllowed(origin string, method CORSHTTPMethod, hea } // if no matching rule is found, return AccessForbidden - return nil, s3err.GetAPIError(s3err.ErrCORSForbidden) + return nil, s3err.GetAccessForbiddenErr(s3err.ErrCORSForbidden, http.MethodOptions, rt) } type CORSRule struct { diff --git a/auth/bucket_cors_test.go b/auth/bucket_cors_test.go index 43c7c5e5..6eb35383 100644 --- a/auth/bucket_cors_test.go +++ b/auth/bucket_cors_test.go @@ -279,7 +279,7 @@ func TestCORSConfiguration_IsAllowed(t *testing.T) { }, output: output{ result: nil, - err: s3err.GetAPIError(s3err.ErrCORSForbidden), + err: s3err.GetAccessForbiddenErr(s3err.ErrCORSForbidden, http.MethodOptions, s3err.ResourceTypeBucket), }, }, { @@ -296,7 +296,7 @@ func TestCORSConfiguration_IsAllowed(t *testing.T) { }, output: output{ result: nil, - err: s3err.GetAPIError(s3err.ErrCORSForbidden), + err: s3err.GetAccessForbiddenErr(s3err.ErrCORSForbidden, http.MethodOptions, s3err.ResourceTypeBucket), }, }, { @@ -313,14 +313,14 @@ func TestCORSConfiguration_IsAllowed(t *testing.T) { }, output: output{ result: nil, - err: s3err.GetAPIError(s3err.ErrCORSForbidden), + err: s3err.GetAccessForbiddenErr(s3err.ErrCORSForbidden, http.MethodOptions, s3err.ResourceTypeBucket), }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := tt.input.cfg.IsAllowed(tt.input.origin, tt.input.method, tt.input.headers) + got, err := tt.input.cfg.IsAllowed(tt.input.origin, tt.input.method, tt.input.headers, s3err.ResourceTypeBucket) assert.EqualValues(t, tt.output.err, err) assert.EqualValues(t, tt.output.result, got) }) diff --git a/auth/object_lock.go b/auth/object_lock.go index 0fc60f92..bea03618 100644 --- a/auth/object_lock.go +++ b/auth/object_lock.go @@ -60,10 +60,10 @@ func ParseBucketLockConfigurationInput(input []byte) ([]byte, error) { } if retention.Days != nil && *retention.Days <= 0 { - return nil, s3err.GetAPIError(s3err.ErrObjectLockInvalidRetentionPeriod) + return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgObjectLockRetentionDays, fmt.Sprint(*retention.Days)) } if retention.Years != nil && *retention.Years <= 0 { - return nil, s3err.GetAPIError(s3err.ErrObjectLockInvalidRetentionPeriod) + return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgObjectLockRetentionYears, fmt.Sprint(*retention.Years)) } config.DefaultRetention = retention @@ -102,7 +102,7 @@ func ParseObjectLockRetentionInput(input []byte) (*s3response.PutObjectRetention if retention.RetainUntilDate.Before(time.Now()) { debuglogger.Logf("object lock retain until date must be in the future") - return nil, s3err.GetAPIError(s3err.ErrPastObjectLockRetainDate) + return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgPastObjectLockRetainDate, retention.RetainUntilDate.Format(time.RFC3339)) } switch retention.Mode { case types.ObjectLockRetentionModeCompliance: diff --git a/auth/post_policy.go b/auth/post_policy.go index a5a5fde0..3dc5ba86 100644 --- a/auth/post_policy.go +++ b/auth/post_policy.go @@ -299,11 +299,11 @@ func (c contentLengthRangeCondition) validate() error { func (c contentLengthRangeCondition) match(in PostPolicyEvalInput) error { if in.ContentLength > c.max { debuglogger.Logf("POST policy content length %d exceeds max %d", in.ContentLength, c.max) - return s3err.GetAPIError(s3err.ErrEntityTooLarge) + return s3err.GetEntityTooLargeErr(in.ContentLength, c.max) } if in.ContentLength < c.min { debuglogger.Logf("POST policy content length %d is smaller than min %d", in.ContentLength, c.min) - return s3err.GetAPIError(s3err.ErrEntityTooSmall) + return s3err.GetEntityTooSmallErr(in.ContentLength, c.min) } return nil diff --git a/auth/post_policy_test.go b/auth/post_policy_test.go index 72dfdbd8..0b0a0a87 100644 --- a/auth/post_policy_test.go +++ b/auth/post_policy_test.go @@ -288,7 +288,7 @@ func TestPOSTPolicyEvaluate_ConcretePolicyRejections(t *testing.T) { "x-amz-algorithm": "AWS4-HMAC-SHA256", }, }, - expected: s3err.GetAPIError(s3err.ErrEntityTooLarge), + expected: s3err.GetEntityTooLargeErr(10, 4), }, { name: "content too small", @@ -300,7 +300,7 @@ func TestPOSTPolicyEvaluate_ConcretePolicyRejections(t *testing.T) { "x-amz-algorithm": "AWS4-HMAC-SHA256", }, }, - expected: s3err.GetAPIError(s3err.ErrEntityTooSmall), + expected: s3err.GetEntityTooSmallErr(1, 2), }, } diff --git a/aws/signer/v4/v4.go b/aws/signer/v4/v4.go index b6a3a6d2..06298d4a 100644 --- a/aws/signer/v4/v4.go +++ b/aws/signer/v4/v4.go @@ -83,6 +83,11 @@ type keyDerivator interface { DeriveKey(credential aws.Credentials, service, region string, signingTime v4Internal.SigningTime) []byte } +type SignMetadata struct { + StringToSign string + CanonicalString string +} + // SignerOptions is the SigV4 Signer options. type SignerOptions struct { // Disables the Signer's moving HTTP header key/value pairs from the HTTP @@ -279,7 +284,7 @@ func buildAuthorizationHeader(credentialStr, signedHeadersStr, signingSignature // will not be lost. // // The passed in request will be modified in place. -func (s Signer) SignHTTP(ctx context.Context, credentials aws.Credentials, r *http.Request, payloadHash string, service string, region string, signingTime time.Time, signedHdrs []string, optFns ...func(options *SignerOptions)) error { +func (s Signer) SignHTTP(ctx context.Context, credentials aws.Credentials, r *http.Request, payloadHash string, service string, region string, signingTime time.Time, signedHdrs []string, optFns ...func(options *SignerOptions)) (*SignMetadata, error) { options := s.options for _, fn := range optFns { @@ -302,12 +307,15 @@ func (s Signer) SignHTTP(ctx context.Context, credentials aws.Credentials, r *ht signedRequest, err := signer.Build() if err != nil { - return err + return nil, err } logSigningInfo(ctx, options, &signedRequest, false) - return nil + return &SignMetadata{ + StringToSign: signedRequest.StringToSign, + CanonicalString: signedRequest.CanonicalString, + }, nil } // PresignHTTP signs AWS v4 requests with the payload hash, service name, region @@ -357,7 +365,7 @@ func (s *Signer) PresignHTTP( payloadHash string, service string, region string, signingTime time.Time, signedHdrs []string, optFns ...func(*SignerOptions), -) (signedURI string, signedHeaders http.Header, err error) { +) (string, http.Header, *SignMetadata, error) { options := s.options for _, fn := range optFns { @@ -381,12 +389,12 @@ func (s *Signer) PresignHTTP( signedRequest, err := signer.Build() if err != nil { - return "", nil, err + return "", nil, nil, err } logSigningInfo(ctx, options, &signedRequest, true) - signedHeaders = make(http.Header) + signedHeaders := make(http.Header) // For the signed headers we canonicalize the header keys in the returned map. // This avoids situations where can standard library double headers like host header. For example the standard @@ -396,7 +404,10 @@ func (s *Signer) PresignHTTP( signedHeaders[key] = append(signedHeaders[key], v...) } - return signedRequest.Request.URL.String(), signedHeaders, nil + return signedRequest.Request.URL.String(), signedHeaders, &SignMetadata{ + StringToSign: signedRequest.StringToSign, + CanonicalString: signedRequest.CanonicalString, + }, nil } func (s *httpSigner) buildCredentialScope() string { diff --git a/aws/signer/v4/v4_test.go b/aws/signer/v4/v4_test.go index 6c66acc1..8ebbd6a1 100644 --- a/aws/signer/v4/v4_test.go +++ b/aws/signer/v4/v4_test.go @@ -65,7 +65,7 @@ func TestPresignRequest(t *testing.T) { signedHdrs := []string{"content-length", "content-type", "host", "x-amz-date", "x-amz-meta-other-header", "x-amz-meta-other-header_with_underscore", "x-amz-security-token", "x-amz-target"} signer := NewSigner() - signed, headers, err := signer.PresignHTTP(context.Background(), testCredentials, req, body, "dynamodb", "us-east-1", time.Unix(0, 0), signedHdrs) + signed, headers, _, err := signer.PresignHTTP(context.Background(), testCredentials, req, body, "dynamodb", "us-east-1", time.Unix(0, 0), signedHdrs) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -118,7 +118,7 @@ func TestPresignBodyWithArrayRequest(t *testing.T) { signedHdrs := []string{"content-length", "content-type", "host", "x-amz-date", "x-amz-meta-other-header", "x-amz-meta-other-header_with_underscore", "x-amz-security-token", "x-amz-target"} signer := NewSigner() - signed, headers, err := signer.PresignHTTP(context.Background(), testCredentials, req, body, "dynamodb", "us-east-1", time.Unix(0, 0), signedHdrs) + signed, headers, _, err := signer.PresignHTTP(context.Background(), testCredentials, req, body, "dynamodb", "us-east-1", time.Unix(0, 0), signedHdrs) if err != nil { t.Fatalf("expect no error, got %v", err) } @@ -165,7 +165,7 @@ func TestSignRequest(t *testing.T) { req, body := buildRequest("dynamodb", "us-east-1", "{}") signer := NewSigner() signedHdrs := []string{"content-length", "content-type", "host", "x-amz-date", "x-amz-meta-other-header", "x-amz-meta-other-header_with_underscore", "x-amz-security-token", "x-amz-target"} - err := signer.SignHTTP(context.Background(), testCredentials, req, body, "dynamodb", "us-east-1", time.Unix(0, 0), signedHdrs) + _, err := signer.SignHTTP(context.Background(), testCredentials, req, body, "dynamodb", "us-east-1", time.Unix(0, 0), signedHdrs) if err != nil { t.Fatalf("expect no error, got %v", err) } @@ -213,7 +213,7 @@ func TestSigner_SignHTTP_NoReplaceRequestBody(t *testing.T) { origBody := req.Body - err := s.SignHTTP(context.Background(), testCredentials, req, bodyHash, "dynamodb", "us-east-1", time.Now(), []string{}) + _, err := s.SignHTTP(context.Background(), testCredentials, req, bodyHash, "dynamodb", "us-east-1", time.Now(), []string{}) if err != nil { t.Fatalf("expect no error, got %v", err) } @@ -353,6 +353,6 @@ func BenchmarkSignRequest(b *testing.B) { signer := NewSigner() req, bodyHash := buildRequest("dynamodb", "us-east-1", "{}") for i := 0; i < b.N; i++ { - signer.SignHTTP(context.Background(), testCredentials, req, bodyHash, "dynamodb", "us-east-1", time.Now(), []string{}) + _, _ = signer.SignHTTP(context.Background(), testCredentials, req, bodyHash, "dynamodb", "us-east-1", time.Now(), []string{}) } } diff --git a/backend/azure/azure.go b/backend/azure/azure.go index b3172ed2..ad411f11 100644 --- a/backend/azure/azure.go +++ b/backend/azure/azure.go @@ -230,9 +230,9 @@ func (az *Azure) CreateBucket(ctx context.Context, input *s3.CreateBucketInput, } if acl.Owner == acct.Access { - return s3err.GetAPIError(s3err.ErrBucketAlreadyOwnedByYou) + return s3err.GetBucketErr(s3err.ErrBucketAlreadyOwnedByYou, *input.Bucket) } - return s3err.GetAPIError(s3err.ErrBucketAlreadyExists) + return s3err.GetBucketErr(s3err.ErrBucketAlreadyExists, *input.Bucket) } return azureErrToS3Err(err) } @@ -322,7 +322,6 @@ func (az *Azure) DeleteBucket(ctx context.Context, bucket string) error { } } } - _, err := az.client.DeleteContainer(ctx, bucket, nil) return azureErrToS3Err(err) } @@ -338,7 +337,7 @@ func (az *Azure) GetBucketOwnershipControls(ctx context.Context, bucket string) return ownship, err } if len(ownership) == 0 { - return ownship, s3err.GetAPIError(s3err.ErrOwnershipControlsNotFound) + return ownship, s3err.GetBucketErr(s3err.ErrOwnershipControlsNotFound, bucket) } return types.ObjectOwnership(ownership), nil @@ -451,7 +450,7 @@ func (az *Azure) GetBucketTagging(ctx context.Context, bucket string) (map[strin } if len(tagsJson) == 0 { - return nil, s3err.GetAPIError(s3err.ErrBucketTaggingNotFound) + return nil, s3err.GetBucketErr(s3err.ErrBucketTaggingNotFound, bucket) } var tags map[string]string @@ -514,7 +513,7 @@ func (az *Azure) GetObject(ctx context.Context, input *s3.GetObjectInput) (*s3.G totalParts := int32(len(mpMeta.Parts)) partsCount = &totalParts if partNum > totalParts { - return nil, s3err.GetAPIError(s3err.ErrInvalidPartNumberRange) + return nil, s3err.GetInvalidPartNumberRangeErr(totalParts, partNum) } var startOffset int64 @@ -534,7 +533,7 @@ func (az *Azure) GetObject(ctx context.Context, input *s3.GetObjectInput) (*s3.G }, } } else if *input.PartNumber > 1 { - return nil, s3err.GetAPIError(s3err.ErrInvalidPartNumberRange) + return nil, s3err.GetInvalidPartNumberRangeErr(1, *input.PartNumber) } else { // partNumber=1 on a non-multipart object: fall through and serve the // full object without a range (opts remains nil) @@ -636,7 +635,7 @@ func (az *Azure) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3 totalParts := int32(len(mpMeta.Parts)) partsCount = &totalParts if partNum > totalParts { - return nil, s3err.GetAPIError(s3err.ErrInvalidPartNumberRange) + return nil, s3err.GetInvalidPartNumberRangeErr(totalParts, partNum) } var startOffset int64 @@ -646,7 +645,7 @@ func (az *Azure) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3 length = mpMeta.Parts[partNum-1] - startOffset contentRange = backend.GetPtrFromString(fmt.Sprintf("bytes %d-%d/%d", startOffset, startOffset+length-1, size)) } else if *input.PartNumber > 1 { - return nil, s3err.GetAPIError(s3err.ErrInvalidPartNumberRange) + return nil, s3err.GetInvalidPartNumberRangeErr(1, *input.PartNumber) } else { // partNumber=1 on a non-multipart object: return full object size, // no Content-Range, no PartsCount. @@ -1093,12 +1092,14 @@ func (az *Azure) DeleteObjects(ctx context.Context, input *s3.DeleteObjectsInput if err == nil { delResult = append(delResult, types.DeletedObject{Key: obj.Key}) } else { - serr, ok := err.(s3err.APIError) + serr, ok := err.(s3err.S3Error) if ok { + code := serr.BaseError().Code + message := serr.BaseError().Description errs = append(errs, types.Error{ Key: obj.Key, - Code: &serr.Code, - Message: &serr.Description, + Code: &code, + Message: &message, }) } else { errs = append(errs, types.Error{ @@ -1543,7 +1544,7 @@ func (az *Azure) ListParts(ctx context.Context, input *s3.ListPartsInput) (s3res partNumberMarker, err = strconv.Atoi(*input.PartNumberMarker) if err != nil { return s3response.ListPartsResult{}, - s3err.GetInvalidMaxLimiterErr("part-number-marker") + s3err.GetInvalidArgMaxLimiter("part-number-marker", *input.PartNumberMarker) } } if input.MaxParts != nil { @@ -1726,7 +1727,7 @@ func (az *Azure) AbortMultipartUpload(ctx context.Context, input *s3.AbortMultip } if resp.LastModified != nil && resp.LastModified.Unix() != input.IfMatchInitiatedTime.Unix() { - return s3err.GetAPIError(s3err.ErrPreconditionFailed) + return s3err.GetPreconditionFailedErr(s3err.ConditionIfMatchInitiatedTime) } } _, err := az.client.DeleteBlob(ctx, *input.Bucket, tmpPath, nil) @@ -1823,7 +1824,7 @@ func (az *Azure) CompleteMultipartUpload(ctx context.Context, input *s3.Complete } if len(blockList.UncommittedBlocks)+len(zbParts) != len(input.MultipartUpload.Parts) { - return res, "", s3err.GetAPIError(s3err.ErrInvalidPart) + return res, "", s3err.GetInvalidPartErr(*input.UploadId, 0, "") } uncommittedBlocks := map[int32]*blockblob.Block{} @@ -1844,10 +1845,13 @@ func (az *Azure) CompleteMultipartUpload(ctx context.Context, input *s3.Complete last := len(input.MultipartUpload.Parts) - 1 for i, part := range input.MultipartUpload.Parts { if part.PartNumber == nil { - return res, "", s3err.GetAPIError(s3err.ErrInvalidPart) + return res, "", s3err.GetAPIError(s3err.ErrMalformedXML) + } + if part.ETag == nil { + return res, "", s3err.GetAPIError(s3err.ErrMalformedXML) } if *part.PartNumber < 1 { - return res, "", s3err.GetAPIError(s3err.ErrInvalidCompleteMpPartNumber) + return res, "", s3err.GetInvalidArgumentErr(s3err.InvalidArgCompleteMpPartNumber, fmt.Sprint(*part.PartNumber)) } if *part.PartNumber <= partNumber { return res, "", s3err.GetAPIError(s3err.ErrInvalidPartOrder) @@ -1860,26 +1864,26 @@ func (az *Azure) CompleteMultipartUpload(ctx context.Context, input *s3.Complete if zbPartsMap[*part.PartNumber] { expectedETag := blockIDInt32ToBase64(*part.PartNumber) if getString(part.ETag) != expectedETag { - return res, "", s3err.GetAPIError(s3err.ErrInvalidPart) + return res, "", s3err.GetInvalidPartErr(*input.UploadId, *part.PartNumber, expectedETag) } // Non-last zero-byte parts violate the minimum part size. if i < last { - return res, "", s3err.GetAPIError(s3err.ErrEntityTooSmall) + return res, "", s3err.GetEntityTooSmallErr(0, backend.MinPartSize) } // Zero-byte parts contribute no data; skip adding to blockIds. partSizes = append(partSizes, totalSize) continue } - return res, "", s3err.GetAPIError(s3err.ErrInvalidPart) + return res, "", s3err.GetInvalidPartErr(*input.UploadId, *part.PartNumber, "") } if *part.ETag != *block.Name { - return res, "", s3err.GetAPIError(s3err.ErrInvalidPart) + return res, "", s3err.GetInvalidPartErr(*input.UploadId, *part.PartNumber, getString(part.ETag)) } // all parts except the last need to be greater, than // the minimum allowed size (5 Mib) if i < last && *block.Size < backend.MinPartSize { - return res, "", s3err.GetAPIError(s3err.ErrEntityTooSmall) + return res, "", s3err.GetEntityTooSmallErr(*block.Size, backend.MinPartSize) } totalSize += *block.Size partSizes = append(partSizes, totalSize) @@ -1958,7 +1962,7 @@ func (az *Azure) GetBucketPolicy(ctx context.Context, bucket string) ([]byte, er return nil, err } if len(p) == 0 { - return nil, s3err.GetAPIError(s3err.ErrNoSuchBucketPolicy) + return nil, s3err.GetBucketErr(s3err.ErrNoSuchBucketPolicy, bucket) } return p, nil } @@ -1981,7 +1985,7 @@ func (az *Azure) GetBucketCors(ctx context.Context, bucket string) ([]byte, erro return nil, err } if len(p) == 0 { - return nil, s3err.GetAPIError(s3err.ErrNoSuchCORSConfiguration) + return nil, s3err.GetBucketErr(s3err.ErrNoSuchCORSConfiguration, bucket) } return p, nil } @@ -2001,7 +2005,7 @@ func (az *Azure) GetObjectLockConfiguration(ctx context.Context, bucket string) } if len(cfg) == 0 { - return nil, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound) + return nil, s3err.GetBucketErr(s3err.ErrObjectLockConfigurationNotFound, bucket) } return cfg, nil @@ -2538,7 +2542,7 @@ func (az *Azure) checkIfMpExists(ctx context.Context, bucket, obj, uploadId stri _, err = blobClient.GetProperties(ctx, nil) if err != nil { - return s3err.GetAPIError(s3err.ErrNoSuchUpload) + return s3err.GetNoSuchUploadErr(uploadId) } return nil diff --git a/backend/common.go b/backend/common.go index 79c22fcc..8ffb71bc 100644 --- a/backend/common.go +++ b/backend/common.go @@ -89,10 +89,7 @@ func TrimEtag(etag *string) *string { } var ( - errInvalidRange = s3err.GetAPIError(s3err.ErrInvalidRange) - errInvalidCopySourceRange = s3err.GetAPIError(s3err.ErrInvalidCopySourceRange) - errPreconditionFailed = s3err.GetAPIError(s3err.ErrPreconditionFailed) - errNotModified = s3err.GetAPIError(s3err.ErrNotModified) + errNotModified = s3err.GetAPIError(s3err.ErrNotModified) ) // ParseObjectRange parses input range header and returns startoffset, length, isValid @@ -121,7 +118,7 @@ func ParseObjectRange(size int64, acceptRange string) (int64, int64, bool, error // Parse start; empty start indicates a suffix-byte-range-spec (e.g. bytes=-100) startOffset, err := strconv.ParseInt(bRange[0], 10, strconv.IntSize) if startOffset > int64(math.MaxInt) || startOffset < int64(math.MinInt) { - return 0, size, false, errInvalidRange + return 0, size, false, s3err.GetInvalidRangeErr(acceptRange, size) } if err != nil && bRange[0] != "" { // invalid numeric start (non-empty) -> ignore range return 0, size, false, nil @@ -134,7 +131,7 @@ func ParseObjectRange(size int64, acceptRange string) (int64, int64, bool, error } // start beyond or at size is unsatisfiable -> error (RequestedRangeNotSatisfiable) if startOffset >= size { - return 0, 0, false, errInvalidRange + return 0, 0, false, s3err.GetInvalidRangeErr(acceptRange, size) } // bytes=100- => from start to end return startOffset, size - startOffset, true, nil @@ -142,7 +139,7 @@ func ParseObjectRange(size int64, acceptRange string) (int64, int64, bool, error endOffset, err := strconv.ParseInt(bRange[1], 10, strconv.IntSize) if endOffset > int64(math.MaxInt) { - return 0, size, false, errInvalidRange + return 0, size, false, s3err.GetInvalidRangeErr(acceptRange, size) } if err != nil { // invalid numeric end -> ignore range return 0, size, false, nil @@ -152,7 +149,7 @@ func ParseObjectRange(size int64, acceptRange string) (int64, int64, bool, error if bRange[0] == "" { // Disallow -0 (always unsatisfiable) if endOffset == 0 { - return 0, 0, false, errInvalidRange + return 0, 0, false, s3err.GetInvalidRangeErr(acceptRange, size) } // For zero-sized objects any positive suffix is treated as invalid (ignored, no error) if size == 0 { @@ -169,7 +166,7 @@ func ParseObjectRange(size int64, acceptRange string) (int64, int64, bool, error } // Start beyond or at end of object -> error if startOffset >= size { - return 0, 0, false, errInvalidRange + return 0, 0, false, s3err.GetInvalidRangeErr(acceptRange, size) } // Adjust end beyond object size (trim) if endOffset >= size { @@ -185,6 +182,7 @@ func ParseCopySourceRange(size int64, acceptRange string) (int64, int64, error) return 0, size, nil } + errInvalidCopySourceRange := s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceRange, acceptRange) rangeKv := strings.Split(acceptRange, "=") if len(rangeKv) != 2 { @@ -206,7 +204,7 @@ func ParseCopySourceRange(size int64, acceptRange string) (int64, int64, error) } if startOffset >= size { - return 0, 0, s3err.CreateExceedingRangeErr(size) + return 0, 0, s3err.GetInvalidArgExceedingRange(size) } if bRange[1] == "" { @@ -223,7 +221,7 @@ func ParseCopySourceRange(size int64, acceptRange string) (int64, int64, error) } if endOffset >= size { - return 0, 0, s3err.CreateExceedingRangeErr(size) + return 0, 0, s3err.GetInvalidArgExceedingRange(size) } return startOffset, endOffset - startOffset + 1, nil @@ -252,12 +250,12 @@ func ParseCopySource(copySourceHeader string) (string, string, string, error) { // correctly before we split on a literal '/'. decoded, err := url.QueryUnescape(rawSource) if err != nil { - return "", "", "", s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding) + return "", "", "", s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, rawSource) } srcBucket, srcObject, ok := strings.Cut(decoded, "/") if !ok { - return "", "", "", s3err.GetAPIError(s3err.ErrInvalidCopySourceBucket) + return "", "", "", s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceBucket, rawSource) } return srcBucket, srcObject, versionId, nil @@ -281,47 +279,47 @@ func ParseObjectTags(tagging string) (map[string]string, error) { } key, value, found := strings.Cut(tag, "=") - // if key is empty, but "=" is present, return invalid url ecnoding err + // if key is empty, but "=" is present, return invalid url encoding err if found && key == "" { - return nil, s3err.GetAPIError(s3err.ErrInvalidURLEncodedTagging) + return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgURLEncodedTagging, tagging) } // return invalid tag key, if the key is longer than 128 if len(key) > 128 { - return nil, s3err.GetAPIError(s3err.ErrInvalidTagKey) + return nil, s3err.GetInvalidTagErr(s3err.ErrInvalidTagKey, key, "") } // return invalid tag value, if tag value is longer than 256 if len(value) > 256 { - return nil, s3err.GetAPIError(s3err.ErrInvalidTagValue) + return nil, s3err.GetInvalidTagErr(s3err.ErrInvalidTagValue, key, value) } // query unescape tag key key, err := url.QueryUnescape(key) if err != nil { - return nil, s3err.GetAPIError(s3err.ErrInvalidURLEncodedTagging) + return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgURLEncodedTagging, tagging) } // query unescape tag value value, err = url.QueryUnescape(value) if err != nil { - return nil, s3err.GetAPIError(s3err.ErrInvalidURLEncodedTagging) + return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgURLEncodedTagging, tagging) } // check tag key to be valid if !isValidTagComponent(key) { - return nil, s3err.GetAPIError(s3err.ErrInvalidTagKey) + return nil, s3err.GetInvalidTagErr(s3err.ErrInvalidTagKey, key, "") } // check tag value to be valid if !isValidTagComponent(value) { - return nil, s3err.GetAPIError(s3err.ErrInvalidTagValue) + return nil, s3err.GetInvalidTagErr(s3err.ErrInvalidTagValue, key, value) } // duplicate keys are not allowed: return invalid url encoding err _, ok := tagSet[key] if ok { - return nil, s3err.GetAPIError(s3err.ErrInvalidURLEncodedTagging) + return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgURLEncodedTagging, tagging) } tagSet[key] = value @@ -347,23 +345,23 @@ func ParseCreateBucketTags(tagging []types.Tag) (map[string]string, error) { // validate tag key length key := GetStringFromPtr(tag.Key) if len(key) == 0 || len(key) > 128 { - return nil, s3err.GetAPIError(s3err.ErrInvalidTagKey) + return nil, s3err.GetInvalidTagErr(s3err.ErrInvalidTagKey, key, "") } // validate tag key string chars if !isValidTagComponent(key) { - return nil, s3err.GetAPIError(s3err.ErrInvalidTagKey) + return nil, s3err.GetInvalidTagErr(s3err.ErrInvalidTagKey, key, "") } // validate tag value length value := GetStringFromPtr(tag.Value) if len(value) > 256 { - return nil, s3err.GetAPIError(s3err.ErrInvalidTagValue) + return nil, s3err.GetInvalidTagErr(s3err.ErrInvalidTagValue, key, value) } // validate tag value string chars if !isValidTagComponent(value) { - return nil, s3err.GetAPIError(s3err.ErrInvalidTagValue) + return nil, s3err.GetInvalidTagErr(s3err.ErrInvalidTagValue, key, value) } // make sure there are no duplicate keys @@ -390,9 +388,15 @@ func isValidTagComponent(str string) bool { func GetMultipartMD5(parts []types.CompletedPart) (string, error) { var partsEtagBytes []byte for _, part := range parts { + if part.ETag == nil { + return "", s3err.GetAPIError(s3err.ErrMalformedXML) + } + if part.PartNumber == nil { + return "", s3err.GetAPIError(s3err.ErrMalformedXML) + } bts, err := getEtagBytes(*part.ETag) if err != nil { - return "", fmt.Errorf("decode etag: %w", err) + return "", s3err.GetAPIError(s3err.ErrInvalidPart) } partsEtagBytes = append(partsEtagBytes, bts...) } @@ -614,9 +618,9 @@ func EvaluatePreconditions(etag string, modTime time.Time, preconditions PreCond } if ifMatch != nil { - // if `if-match` doesn't matches, return PreconditionFailed + // if `if-match` doesn't match, return PreconditionFailed if !*ifMatch { - return errPreconditionFailed + return s3err.GetPreconditionFailedErr(s3err.ConditionIfMatch) } // if-match matches @@ -646,7 +650,7 @@ func EvaluatePreconditions(etag string, modTime time.Time, preconditions PreCond // if `if-none-match` is true, but `if-unmodified-since` is false // return PreconditionFailed if ifUnmodeSince != nil && !*ifUnmodeSince { - return errPreconditionFailed + return s3err.GetPreconditionFailedErr(s3err.ConditionIfUnmodifiedSince) } // ignore `if-modified-since` as `if-none-match` is true @@ -655,7 +659,7 @@ func EvaluatePreconditions(etag string, modTime time.Time, preconditions PreCond // if `if-none-match` is false and `if-unmodified-since` is false // return PreconditionFailed if ifUnmodeSince != nil && !*ifUnmodeSince { - return errPreconditionFailed + return s3err.GetPreconditionFailedErr(s3err.ConditionIfUnmodifiedSince) } // in all other cases when `if-none-match` is false return NotModified @@ -667,7 +671,7 @@ func EvaluatePreconditions(etag string, modTime time.Time, preconditions PreCond // if both `if-modified-since` and `if-unmodified-since` are false // return PreconditionFailed if ifUnmodeSince != nil && !*ifUnmodeSince { - return errPreconditionFailed + return s3err.GetPreconditionFailedErr(s3err.ConditionIfUnmodifiedSince) } // if only `if-modified-since` is false, return NotModified @@ -676,20 +680,7 @@ func EvaluatePreconditions(etag string, modTime time.Time, preconditions PreCond // if `if-unmodified-since` is false return PreconditionFailed if ifUnmodeSince != nil && !*ifUnmodeSince { - return errPreconditionFailed - } - - return nil -} - -// EvaluateMatchPreconditions evaluates if-match and if-none-match preconditions -func EvaluateMatchPreconditions(etag string, ifMatch, ifNoneMatch *string) error { - etag = strings.Trim(etag, `"`) - if ifMatch != nil && *ifMatch != etag { - return errPreconditionFailed - } - if ifNoneMatch != nil && *ifNoneMatch == etag { - return errPreconditionFailed + return s3err.GetPreconditionFailedErr(s3err.ConditionIfUnmodifiedSince) } return nil @@ -703,15 +694,15 @@ func EvaluateObjectPutPreconditions(etag string, ifMatch, ifNoneMatch *string, o } if ifNoneMatch != nil && *ifNoneMatch != "*" { - return s3err.GetAPIError(s3err.ErrNotImplemented) + return s3err.GetNotImplementedErr("If-None-Match", s3err.NmpAdditionalMessageIfNoneMatch) } if ifNoneMatch != nil && ifMatch != nil { - return s3err.GetAPIError(s3err.ErrNotImplemented) + return s3err.GetNotImplementedErr("If-Match,If-None-Match", s3err.NmpAdditionalMessageMultipleCondHeaders) } if ifNoneMatch != nil && objExists { - return s3err.GetAPIError(s3err.ErrPreconditionFailed) + return s3err.GetPreconditionFailedErr(s3err.ConditionIfNoneMatch) } if ifMatch != nil && !objExists { @@ -721,7 +712,7 @@ func EvaluateObjectPutPreconditions(etag string, ifMatch, ifNoneMatch *string, o etag = strings.Trim(etag, `"`) if ifMatch != nil && *ifMatch != etag { - return s3err.GetAPIError(s3err.ErrPreconditionFailed) + return s3err.GetPreconditionFailedErr(s3err.ConditionIfMatch) } return nil @@ -738,17 +729,17 @@ func EvaluateObjectDeletePreconditions(etag string, modTime time.Time, size int6 etag = strings.Trim(etag, `"`) ifMatch := preconditions.IfMatch if ifMatch != nil && *ifMatch != etag { - return errPreconditionFailed + return s3err.GetPreconditionFailedErr(s3err.ConditionIfMatch) } ifMatchTime := preconditions.IfMatchLastModTime if ifMatchTime != nil && ifMatchTime.Unix() != modTime.Unix() { - return errPreconditionFailed + return s3err.GetPreconditionFailedErr(s3err.ConditionIfMatchLastModTime) } ifMatchSize := preconditions.IfMatchSize if ifMatchSize != nil && *ifMatchSize != size { - return errPreconditionFailed + return s3err.GetPreconditionFailedErr(s3err.ConditionIfMatchSize) } return nil diff --git a/backend/common_test.go b/backend/common_test.go index 3de8e337..993a7d8d 100644 --- a/backend/common_test.go +++ b/backend/common_test.go @@ -115,7 +115,7 @@ func TestParseCopySource(t *testing.T) { wantObject string wantVersionId string wantErr bool - wantErrCode s3err.ErrorCode + wantErrValue error }{ { name: "simple path", @@ -212,7 +212,7 @@ func TestParseCopySource(t *testing.T) { wantObject: "", wantVersionId: "", wantErr: true, - wantErrCode: s3err.ErrInvalidCopySourceEncoding, + wantErrValue: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "mybucket/object%"), }, { name: "invalid URL encoding - invalid hex", @@ -221,7 +221,7 @@ func TestParseCopySource(t *testing.T) { wantObject: "", wantVersionId: "", wantErr: true, - wantErrCode: s3err.ErrInvalidCopySourceEncoding, + wantErrValue: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "mybucket/object%ZZ"), }, { name: "missing object", @@ -230,7 +230,7 @@ func TestParseCopySource(t *testing.T) { wantObject: "", wantVersionId: "", wantErr: true, - wantErrCode: s3err.ErrInvalidCopySourceBucket, + wantErrValue: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceBucket, "mybucket"), }, } @@ -243,8 +243,8 @@ func TestParseCopySource(t *testing.T) { t.Errorf("ParseCopySource() error = nil, wantErr %v", tt.wantErr) return } - if !errors.Is(err, s3err.GetAPIError(tt.wantErrCode)) { - t.Errorf("ParseCopySource() error = %v, want error code %v", err, tt.wantErrCode) + if !errors.Is(err, tt.wantErrValue) { + t.Errorf("ParseCopySource() error = %v, want error %v", err, tt.wantErrValue) } return } diff --git a/backend/mp-lister.go b/backend/mp-lister.go index fb548690..75e8f7c7 100644 --- a/backend/mp-lister.go +++ b/backend/mp-lister.go @@ -80,11 +80,11 @@ func (l *MultipartUploadLister) Run() (*ListMultipartUploadsPage, error) { // any invalid uuid is considered as an invalid uploadIdMarker _, err := uuid.Parse(uploadIDMarker) if err != nil { - return nil, s3err.GetAPIError(s3err.ErrInvalidUploadIdMarker) + return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgUploadIdMarker, uploadIDMarker) } startIndex = l.findUploadIdMarkerIndex(uploadIDMarker) if startIndex == -1 { - return nil, s3err.GetAPIError(s3err.ErrInvalidUploadIdMarker) + return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgUploadIdMarker, uploadIDMarker) } if startIndex >= len(l.Uploads) { return out, nil diff --git a/backend/posix/posix.go b/backend/posix/posix.go index d8324e29..17ded144 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -355,7 +355,7 @@ func (p *Posix) validateVersionId(versionId string) error { } _, err := ulid.Parse(versionId) if err != nil { - return s3err.GetAPIError(s3err.ErrInvalidVersionId) + return s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, versionId) } return nil } @@ -363,7 +363,7 @@ func (p *Posix) validateVersionId(versionId string) error { func (p *Posix) doesBucketAndObjectExist(bucket, object string) error { _, err := os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return s3err.GetAPIError(s3err.ErrNoSuchBucket) + return s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return fmt.Errorf("stat bucket: %w", err) @@ -471,11 +471,11 @@ func (p *Posix) HeadBucket(ctx context.Context, input *s3.HeadBucketInput) (*s3. } defer release() if !p.isBucketValid(*input.Bucket) { - return nil, s3err.GetAPIError(s3err.ErrInvalidBucketName) + return nil, s3err.GetBucketErr(s3err.ErrInvalidBucketName, *input.Bucket) } _, err = os.Lstat(*input.Bucket) if errors.Is(err, fs.ErrNotExist) { - return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket) + return nil, s3err.GetBucketErr(s3err.ErrNoSuchBucket, *input.Bucket) } if err != nil { return nil, fmt.Errorf("stat bucket: %w", err) @@ -501,7 +501,7 @@ func (p *Posix) CreateBucket(ctx context.Context, input *s3.CreateBucketInput, a bucket := *input.Bucket if !p.isBucketValid(bucket) { - return s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } tagging, err := backend.ParseCreateBucketTags(input.CreateBucketConfiguration.Tags) @@ -522,9 +522,9 @@ func (p *Posix) CreateBucket(ctx context.Context, input *s3.CreateBucketInput, a } if acl.Owner == acct.Access { - return s3err.GetAPIError(s3err.ErrBucketAlreadyOwnedByYou) + return s3err.GetBucketErr(s3err.ErrBucketAlreadyOwnedByYou, bucket) } - return s3err.GetAPIError(s3err.ErrBucketAlreadyExists) + return s3err.GetBucketErr(s3err.ErrBucketAlreadyExists, bucket) } if err != nil { if errors.Is(err, syscall.EROFS) { @@ -597,9 +597,9 @@ func (p *Posix) isBucketEmpty(bucket string) error { } if err == nil { if len(ents) == 1 && ents[0].Name() != MetaTmpDir { - return s3err.GetAPIError(s3err.ErrVersionedBucketNotEmpty) + return s3err.GetBucketErr(s3err.ErrVersionedBucketNotEmpty, bucket) } else if len(ents) > 1 { - return s3err.GetAPIError(s3err.ErrVersionedBucketNotEmpty) + return s3err.GetBucketErr(s3err.ErrVersionedBucketNotEmpty, bucket) } } } @@ -609,12 +609,12 @@ func (p *Posix) isBucketEmpty(bucket string) error { return fmt.Errorf("readdir bucket: %w", err) } if errors.Is(err, fs.ErrNotExist) { - return s3err.GetAPIError(s3err.ErrNoSuchBucket) + return s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if len(ents) == 1 && ents[0].Name() != MetaTmpDir { - return s3err.GetAPIError(s3err.ErrBucketNotEmpty) + return s3err.GetBucketErr(s3err.ErrBucketNotEmpty, bucket) } else if len(ents) > 1 { - return s3err.GetAPIError(s3err.ErrBucketNotEmpty) + return s3err.GetBucketErr(s3err.ErrBucketNotEmpty, bucket) } return nil @@ -628,7 +628,7 @@ func (p *Posix) DeleteBucket(ctx context.Context, bucket string) error { defer release() if !p.isBucketValid(bucket) { - return s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } // Check if the bucket is empty err = p.isBucketEmpty(bucket) @@ -667,11 +667,11 @@ func (p *Posix) PutBucketOwnershipControls(ctx context.Context, bucket string, o defer release() if !p.isBucketValid(bucket) { - return s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } _, err = os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return s3err.GetAPIError(s3err.ErrNoSuchBucket) + return s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return fmt.Errorf("stat bucket: %w", err) @@ -693,11 +693,11 @@ func (p *Posix) GetBucketOwnershipControls(ctx context.Context, bucket string) ( var ownship types.ObjectOwnership if !p.isBucketValid(bucket) { - return ownship, s3err.GetAPIError(s3err.ErrInvalidBucketName) + return ownship, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } _, err = os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return ownship, s3err.GetAPIError(s3err.ErrNoSuchBucket) + return ownship, s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return ownship, fmt.Errorf("stat bucket: %w", err) @@ -705,7 +705,7 @@ func (p *Posix) GetBucketOwnershipControls(ctx context.Context, bucket string) ( ownership, err := p.meta.RetrieveAttribute(nil, bucket, "", ownershipkey) if errors.Is(err, meta.ErrNoSuchKey) { - return ownship, s3err.GetAPIError(s3err.ErrOwnershipControlsNotFound) + return ownship, s3err.GetBucketErr(s3err.ErrOwnershipControlsNotFound, bucket) } if err != nil { return ownship, fmt.Errorf("get bucket ownership status: %w", err) @@ -721,11 +721,11 @@ func (p *Posix) DeleteBucketOwnershipControls(ctx context.Context, bucket string defer release() if !p.isBucketValid(bucket) { - return s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } _, err = os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return s3err.GetAPIError(s3err.ErrNoSuchBucket) + return s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return fmt.Errorf("stat bucket: %w", err) @@ -751,14 +751,14 @@ func (p *Posix) PutBucketVersioning(ctx context.Context, bucket string, status t defer release() if !p.isBucketValid(bucket) { - return s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } if !p.versioningEnabled() { return s3err.GetAPIError(s3err.ErrVersioningNotConfigured) } _, err = os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return s3err.GetAPIError(s3err.ErrNoSuchBucket) + return s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return fmt.Errorf("stat bucket: %w", err) @@ -809,12 +809,12 @@ func (p *Posix) GetBucketVersioning(ctx context.Context, bucket string) (s3respo } if !p.isBucketValid(bucket) { - return s3response.GetBucketVersioningOutput{}, s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3response.GetBucketVersioningOutput{}, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } _, err = os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return s3response.GetBucketVersioningOutput{}, s3err.GetAPIError(s3err.ErrNoSuchBucket) + return s3response.GetBucketVersioningOutput{}, s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return s3response.GetBucketVersioningOutput{}, fmt.Errorf("stat bucket: %w", err) @@ -992,7 +992,7 @@ func (p *Posix) ListObjectVersions(ctx context.Context, input *s3.ListObjectVers var max int if !p.isBucketValid(bucket) { - return s3response.ListVersionsResult{}, s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3response.ListVersionsResult{}, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } if input.Prefix != nil { @@ -1013,7 +1013,7 @@ func (p *Posix) ListObjectVersions(ctx context.Context, input *s3.ListObjectVers _, err = os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return s3response.ListVersionsResult{}, s3err.GetAPIError(s3err.ErrNoSuchBucket) + return s3response.ListVersionsResult{}, s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return s3response.ListVersionsResult{}, fmt.Errorf("stat bucket: %w", err) @@ -1068,7 +1068,7 @@ func (p *Posix) ensureNotDeleteMarker(bucket, object, versionId string) error { _, err := p.meta.RetrieveAttribute(nil, bucket, object, deleteMarkerKey) if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { if versionId != "" { - return s3err.GetAPIError(s3err.ErrNoSuchVersion) + return s3err.GetNoSuchVersionErr(object, versionId) } return s3err.GetAPIError(s3err.ErrNoSuchKey) } @@ -1484,12 +1484,12 @@ func (p *Posix) CreateMultipartUpload(ctx context.Context, mpu s3response.Create object := *mpu.Key if !p.isBucketValid(bucket) { - return s3response.InitiateMultipartUploadResult{}, s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3response.InitiateMultipartUploadResult{}, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } _, err = os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return s3response.InitiateMultipartUploadResult{}, s3err.GetAPIError(s3err.ErrNoSuchBucket) + return s3response.InitiateMultipartUploadResult{}, s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return s3response.InitiateMultipartUploadResult{}, fmt.Errorf("stat bucket: %w", err) @@ -1765,7 +1765,7 @@ func (p *Posix) CompleteMultipartUploadWithCopy(ctx context.Context, input *s3.C return res, "", s3err.GetAPIError(s3err.ErrNoSuchKey) } if input.UploadId == nil { - return res, "", s3err.GetAPIError(s3err.ErrNoSuchUpload) + return res, "", s3err.GetNoSuchUploadErr("") } if input.MultipartUpload == nil { return res, "", s3err.GetAPIError(s3err.ErrInvalidRequest) @@ -1777,12 +1777,12 @@ func (p *Posix) CompleteMultipartUploadWithCopy(ctx context.Context, input *s3.C parts := input.MultipartUpload.Parts if !p.isBucketValid(bucket) { - return res, "", s3err.GetAPIError(s3err.ErrInvalidBucketName) + return res, "", s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } _, err := os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return res, "", s3err.GetAPIError(s3err.ErrNoSuchBucket) + return res, "", s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return res, "", fmt.Errorf("stat bucket: %w", err) @@ -1798,7 +1798,7 @@ func (p *Posix) CompleteMultipartUploadWithCopy(ctx context.Context, input *s3.C // Calculate s3 compatible md5sum for complete multipart. s3MD5, err := backend.GetMultipartMD5(parts) if err != nil { - return res, "", s3err.GetAPIError(s3err.ErrInvalidPart) + return res, "", err } activeUploadName := fmt.Sprintf("%s.%s%s", uploadID, strings.Trim(s3MD5, "\""), inProgressSuffix) uploadIDInProgress := filepath.Join(objdirFull, activeUploadName) @@ -1923,10 +1923,10 @@ func (p *Posix) CompleteMultipartUploadWithCopy(ctx context.Context, input *s3.C var partNumber int32 for i, part := range parts { if part.PartNumber == nil { - return res, "", s3err.GetAPIError(s3err.ErrInvalidPart) + return res, "", s3err.GetAPIError(s3err.ErrMalformedXML) } if *part.PartNumber < 1 { - return res, "", s3err.GetAPIError(s3err.ErrInvalidCompleteMpPartNumber) + return res, "", s3err.GetInvalidArgumentErr(s3err.InvalidArgCompleteMpPartNumber, fmt.Sprint(*part.PartNumber)) } if *part.PartNumber <= partNumber { return res, "", s3err.GetAPIError(s3err.ErrInvalidPartOrder) @@ -1938,7 +1938,7 @@ func (p *Posix) CompleteMultipartUploadWithCopy(ctx context.Context, input *s3.C fullPartPath := filepath.Join(bucket, partObjPath) fi, err := os.Lstat(fullPartPath) if err != nil { - return res, "", s3err.GetAPIError(s3err.ErrInvalidPart) + return res, "", s3err.GetInvalidPartErr(uploadID, *part.PartNumber, backend.GetStringFromPtr(part.ETag)) } totalsize += fi.Size() @@ -1946,7 +1946,7 @@ func (p *Posix) CompleteMultipartUploadWithCopy(ctx context.Context, input *s3.C // all parts except the last need to be greater, than or equal to // the minimum allowed size (5 Mib) if i < last && fi.Size() < backend.MinPartSize { - return res, "", s3err.GetAPIError(s3err.ErrEntityTooSmall) + return res, "", s3err.GetEntityTooSmallErr(fi.Size(), backend.MinPartSize) } b, err := p.meta.RetrieveAttribute(nil, bucket, partObjPath, etagkey) @@ -1954,8 +1954,11 @@ func (p *Posix) CompleteMultipartUploadWithCopy(ctx context.Context, input *s3.C if err != nil { etag = "" } - if parts[i].ETag == nil || !backend.AreEtagsSame(etag, *parts[i].ETag) { - return res, "", s3err.GetAPIError(s3err.ErrInvalidPart) + if parts[i].ETag == nil { + return res, "", s3err.GetAPIError(s3err.ErrMalformedXML) + } + if !backend.AreEtagsSame(etag, *parts[i].ETag) { + return res, "", s3err.GetInvalidPartErr(uploadID, *part.PartNumber, etag) } partChecksum, err := p.retrieveChecksums(nil, bucket, partObjPath) @@ -1964,7 +1967,7 @@ func (p *Posix) CompleteMultipartUploadWithCopy(ctx context.Context, input *s3.C } // If checksum has been provided on mp initialization - err = validatePartChecksum(partChecksum, part) + err = validatePartChecksum(partChecksum, part, uploadID) if err != nil { return res, "", err } @@ -2284,14 +2287,14 @@ func (p *Posix) CompleteMultipartUploadWithCopy(ctx context.Context, input *s3.C }, versionID, nil } -func validatePartChecksum(checksum s3response.Checksum, part types.CompletedPart) error { - n := numberOfChecksums(part) +func validatePartChecksum(checksum s3response.Checksum, part types.CompletedPart, uploadId string) error { + n, argValue := numberOfChecksums(part) if n > 1 { - return s3err.GetAPIError(s3err.ErrInvalidChecksumPart) + return s3err.GetInvalidArgumentErr(s3err.InvalidArgChecksumPart, argValue) } if checksum.Algorithm == "" { if n != 0 { - return s3err.GetAPIError(s3err.ErrInvalidPart) + return s3err.GetInvalidPartErr(uploadId, *part.PartNumber, *part.ETag) } return nil @@ -2327,12 +2330,12 @@ func validatePartChecksum(checksum s3response.Checksum, part types.CompletedPart } if !utils.IsValidChecksum(*cs.checksum, cs.algo) { - return s3err.GetAPIError(s3err.ErrInvalidChecksumPart) + return s3err.GetInvalidArgumentErr(s3err.InvalidArgChecksumPart, *cs.checksum) } if *cs.checksum != cs.expectedChecksum { if algo == cs.algo { - return s3err.GetAPIError(s3err.ErrInvalidPart) + return s3err.GetInvalidPartErr(uploadId, *part.PartNumber, *part.ETag) } return s3err.APIError{ @@ -2346,22 +2349,24 @@ func validatePartChecksum(checksum s3response.Checksum, part types.CompletedPart return nil } -func numberOfChecksums(part types.CompletedPart) int { +func numberOfChecksums(part types.CompletedPart) (int, string) { counter := 0 - if getString(part.ChecksumCRC32) != "" { - counter++ - } - if getString(part.ChecksumCRC32C) != "" { - counter++ - } - if getString(part.ChecksumSHA1) != "" { - counter++ - } - if getString(part.ChecksumSHA256) != "" { - counter++ - } - if getString(part.ChecksumCRC64NVME) != "" { - counter++ + builder := &strings.Builder{} + + for _, ch := range []struct { + algo types.ChecksumAlgorithm + value *string + }{ + {types.ChecksumAlgorithmCrc32, part.ChecksumCRC32}, + {types.ChecksumAlgorithmCrc32c, part.ChecksumCRC32C}, + {types.ChecksumAlgorithmCrc64nvme, part.ChecksumCRC64NVME}, + {types.ChecksumAlgorithmSha1, part.ChecksumSHA1}, + {types.ChecksumAlgorithmSha256, part.ChecksumSHA256}, + } { + if getString(ch.value) != "" { + counter++ + fmt.Fprintf(builder, "%s:%s;", string(ch.algo), getString(ch.value)) + } } if getString(part.ChecksumSHA512) != "" { counter++ @@ -2379,7 +2384,7 @@ func numberOfChecksums(part types.CompletedPart) int { counter++ } - return counter + return counter, builder.String() } func (p *Posix) checkUploadIDExists(bucket, object, uploadID string) ([32]byte, error) { @@ -2388,7 +2393,7 @@ func (p *Posix) checkUploadIDExists(bucket, object, uploadID string) ([32]byte, _, err := os.Stat(filepath.Join(objdir, uploadID)) if errors.Is(err, fs.ErrNotExist) { - return [32]byte{}, s3err.GetAPIError(s3err.ErrNoSuchUpload) + return [32]byte{}, s3err.GetNoSuchUploadErr(uploadID) } if err != nil { return [32]byte{}, fmt.Errorf("stat upload: %w", err) @@ -2606,7 +2611,7 @@ func (p *Posix) AbortMultipartUpload(ctx context.Context, mpu *s3.AbortMultipart return s3err.GetAPIError(s3err.ErrNoSuchKey) } if mpu.UploadId == nil { - return s3err.GetAPIError(s3err.ErrNoSuchUpload) + return s3err.GetNoSuchUploadErr("") } bucket := *mpu.Bucket @@ -2614,12 +2619,12 @@ func (p *Posix) AbortMultipartUpload(ctx context.Context, mpu *s3.AbortMultipart uploadID := *mpu.UploadId if !p.isBucketValid(bucket) { - return s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } _, err = os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return s3err.GetAPIError(s3err.ErrNoSuchBucket) + return s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return fmt.Errorf("stat bucket: %w", err) @@ -2630,12 +2635,12 @@ func (p *Posix) AbortMultipartUpload(ctx context.Context, mpu *s3.AbortMultipart f, err := os.Stat(filepath.Join(objdir, uploadID)) if err != nil { - return s3err.GetAPIError(s3err.ErrNoSuchUpload) + return s3err.GetNoSuchUploadErr(uploadID) } if mpu.IfMatchInitiatedTime != nil { if mpu.IfMatchInitiatedTime.Unix() != f.ModTime().Unix() { - return s3err.GetAPIError(s3err.ErrPreconditionFailed) + return s3err.GetPreconditionFailedErr(s3err.ConditionIfMatchInitiatedTime) } } @@ -2665,7 +2670,7 @@ func (p *Posix) ListMultipartUploads(ctx context.Context, mpu *s3.ListMultipartU bucket := *mpu.Bucket if !p.isBucketValid(bucket) { - return lmu, s3err.GetAPIError(s3err.ErrInvalidBucketName) + return lmu, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } var delimiter string @@ -2688,7 +2693,7 @@ func (p *Posix) ListMultipartUploads(ctx context.Context, mpu *s3.ListMultipartU _, err = os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return lmu, s3err.GetAPIError(s3err.ErrNoSuchBucket) + return lmu, s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return lmu, fmt.Errorf("stat bucket: %w", err) @@ -2796,7 +2801,7 @@ func (p *Posix) ListParts(ctx context.Context, input *s3.ListPartsInput) (s3resp return lpr, s3err.GetAPIError(s3err.ErrNoSuchKey) } if input.UploadId == nil { - return lpr, s3err.GetAPIError(s3err.ErrNoSuchUpload) + return lpr, s3err.GetNoSuchUploadErr("") } bucket := *input.Bucket @@ -2804,7 +2809,7 @@ func (p *Posix) ListParts(ctx context.Context, input *s3.ListPartsInput) (s3resp uploadID := *input.UploadId if !p.isBucketValid(bucket) { - return lpr, s3err.GetAPIError(s3err.ErrInvalidBucketName) + return lpr, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } stringMarker := "" if input.PartNumberMarker != nil { @@ -2818,13 +2823,13 @@ func (p *Posix) ListParts(ctx context.Context, input *s3.ListPartsInput) (s3resp var err error partNumberMarker, err = strconv.Atoi(stringMarker) if err != nil { - return lpr, s3err.GetInvalidMaxLimiterErr("part-number-marker") + return lpr, s3err.GetInvalidArgMaxLimiter("part-number-marker", *input.PartNumberMarker) } } _, err = os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return lpr, s3err.GetAPIError(s3err.ErrNoSuchBucket) + return lpr, s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return lpr, fmt.Errorf("stat bucket: %w", err) @@ -2840,7 +2845,7 @@ func (p *Posix) ListParts(ctx context.Context, input *s3.ListPartsInput) (s3resp ents, err := os.ReadDir(filepath.Join(tmpdir, uploadID)) if errors.Is(err, fs.ErrNotExist) { - return lpr, s3err.GetAPIError(s3err.ErrNoSuchUpload) + return lpr, s3err.GetNoSuchUploadErr(uploadID) } if err != nil { return lpr, fmt.Errorf("readdir upload: %w", err) @@ -2969,7 +2974,7 @@ func (p *Posix) UploadPartWithPostFunc(ctx context.Context, input *s3.UploadPart uploadID := *input.UploadId if !p.isBucketValid(bucket) { - return nil, s3err.GetAPIError(s3err.ErrInvalidBucketName) + return nil, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } part := input.PartNumber length := int64(0) @@ -2980,7 +2985,7 @@ func (p *Posix) UploadPartWithPostFunc(ctx context.Context, input *s3.UploadPart _, err := os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket) + return nil, s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return nil, fmt.Errorf("stat bucket: %w", err) @@ -2992,7 +2997,7 @@ func (p *Posix) UploadPartWithPostFunc(ctx context.Context, input *s3.UploadPart _, err = os.Stat(filepath.Join(bucket, mpPath)) if errors.Is(err, fs.ErrNotExist) { - return nil, s3err.GetAPIError(s3err.ErrNoSuchUpload) + return nil, s3err.GetNoSuchUploadErr(uploadID) } if err != nil { return nil, fmt.Errorf("stat uploadid: %w", err) @@ -3129,8 +3134,8 @@ func (p *Posix) UploadPartWithPostFunc(ctx context.Context, input *s3.UploadPart if errors.Is(err, syscall.ENOSPC) { return nil, s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice) } - // Return the error itself, if it's an 's3err.APIError' - if _, ok := err.(s3err.APIError); ok { + // Return the error itself, if it implements the s3err.S3Error interface + if _, ok := err.(s3err.S3Error); ok { return nil, err } return nil, fmt.Errorf("write part data: %w", err) @@ -3251,12 +3256,12 @@ func (p *Posix) UploadPartCopy(ctx context.Context, upi *s3.UploadPartCopyInput) } if !p.isBucketValid(*upi.Bucket) { - return s3response.CopyPartResult{}, s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3response.CopyPartResult{}, s3err.GetBucketErr(s3err.ErrInvalidBucketName, *upi.Bucket) } _, err = os.Stat(*upi.Bucket) if errors.Is(err, fs.ErrNotExist) { - return s3response.CopyPartResult{}, s3err.GetAPIError(s3err.ErrNoSuchBucket) + return s3response.CopyPartResult{}, s3err.GetBucketErr(s3err.ErrNoSuchBucket, *upi.Bucket) } if err != nil { return s3response.CopyPartResult{}, fmt.Errorf("stat bucket: %w", err) @@ -3267,10 +3272,10 @@ func (p *Posix) UploadPartCopy(ctx context.Context, upi *s3.UploadPartCopyInput) _, err = os.Stat(filepath.Join(*upi.Bucket, objdir, *upi.UploadId)) if errors.Is(err, fs.ErrNotExist) { - return s3response.CopyPartResult{}, s3err.GetAPIError(s3err.ErrNoSuchUpload) + return s3response.CopyPartResult{}, s3err.GetNoSuchUploadErr(*upi.UploadId) } if errors.Is(err, syscall.ENAMETOOLONG) { - return s3response.CopyPartResult{}, s3err.GetAPIError(s3err.ErrKeyTooLong) + return s3response.CopyPartResult{}, s3err.GetKeyTooLongErr(int64(len(*upi.Key)), 1024) } if err != nil { return s3response.CopyPartResult{}, fmt.Errorf("stat uploadid: %w", err) @@ -3288,7 +3293,7 @@ func (p *Posix) UploadPartCopy(ctx context.Context, upi *s3.UploadPartCopyInput) _, err = os.Stat(srcBucket) if errors.Is(err, fs.ErrNotExist) { - return s3response.CopyPartResult{}, s3err.GetAPIError(s3err.ErrNoSuchBucket) + return s3response.CopyPartResult{}, s3err.GetBucketErr(s3err.ErrNoSuchBucket, srcBucket) } if err != nil { return s3response.CopyPartResult{}, fmt.Errorf("stat bucket: %w", err) @@ -3316,7 +3321,7 @@ func (p *Posix) UploadPartCopy(ctx context.Context, upi *s3.UploadPartCopyInput) if srcVersionId != "" { if !p.versioningEnabled() || !vEnabled { - return s3response.CopyPartResult{}, s3err.GetAPIError(s3err.ErrInvalidVersionId) + return s3response.CopyPartResult{}, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, srcVersionId) } vId, err := p.meta.RetrieveAttribute(nil, srcBucket, srcObject, versionIdKey) if errors.Is(err, fs.ErrNotExist) { @@ -3336,12 +3341,12 @@ func (p *Posix) UploadPartCopy(ctx context.Context, upi *s3.UploadPartCopyInput) fi, err := os.Stat(objPath) if errors.Is(err, fs.ErrNotExist) { if p.versioningEnabled() && vEnabled { - return s3response.CopyPartResult{}, s3err.GetAPIError(s3err.ErrNoSuchVersion) + return s3response.CopyPartResult{}, s3err.GetNoSuchVersionErr(srcObject, srcVersionId) } return s3response.CopyPartResult{}, s3err.GetAPIError(s3err.ErrNoSuchKey) } if errors.Is(err, syscall.ENAMETOOLONG) { - return s3response.CopyPartResult{}, s3err.GetAPIError(s3err.ErrKeyTooLong) + return s3response.CopyPartResult{}, s3err.GetKeyTooLongErr(int64(len(srcObject)), 1024) } if err != nil { return s3response.CopyPartResult{}, fmt.Errorf("stat object: %w", err) @@ -3559,11 +3564,11 @@ func (p *Posix) PutObjectWithPostFunc(ctx context.Context, po s3response.PutObje return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrNoSuchKey) } if !p.isBucketValid(*po.Bucket) { - return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3response.PutObjectOutput{}, s3err.GetBucketErr(s3err.ErrInvalidBucketName, *po.Bucket) } _, err := os.Stat(*po.Bucket) if errors.Is(err, fs.ErrNotExist) { - return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrNoSuchBucket) + return s3response.PutObjectOutput{}, s3err.GetBucketErr(s3err.ErrNoSuchBucket, *po.Bucket) } if err != nil { return s3response.PutObjectOutput{}, fmt.Errorf("stat bucket: %w", err) @@ -3749,7 +3754,7 @@ func (p *Posix) PutObjectWithPostFunc(ctx context.Context, po s3response.PutObje } } if errors.Is(err, syscall.ENAMETOOLONG) { - return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrKeyTooLong) + return s3response.PutObjectOutput{}, s3err.GetKeyTooLongErr(int64(len(*po.Key)), 1024) } if errors.Is(err, syscall.ENOTDIR) { parentErr := handleParentDirError(name) @@ -3797,8 +3802,8 @@ func (p *Posix) PutObjectWithPostFunc(ctx context.Context, po s3response.PutObje if errors.Is(err, syscall.ENOSPC) { return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice) } - // Return the error itself, if it's an 's3err.APIError' - if _, ok := err.(s3err.APIError); ok { + // Return the error itself, if it implements the s3err.S3Error interface + if _, ok := err.(s3err.S3Error); ok { return s3response.PutObjectOutput{}, err } return s3response.PutObjectOutput{}, fmt.Errorf("write object data: %w", err) @@ -3987,7 +3992,7 @@ func (p *Posix) DeleteObject(ctx context.Context, input *s3.DeleteObjectInput) ( isDir := strings.HasSuffix(object, "/") if !p.isBucketValid(bucket) { - return nil, s3err.GetAPIError(s3err.ErrInvalidBucketName) + return nil, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } if err := p.validateVersionId(backend.GetStringFromPtr(input.VersionId)); err != nil { @@ -3996,7 +4001,7 @@ func (p *Posix) DeleteObject(ctx context.Context, input *s3.DeleteObjectInput) ( _, err = os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket) + return nil, s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return nil, fmt.Errorf("stat bucket: %w", err) @@ -4043,7 +4048,7 @@ func (p *Posix) DeleteObject(ctx context.Context, input *s3.DeleteObjectInput) ( return &s3.DeleteObjectOutput{}, nil } if errors.Is(err, syscall.ENAMETOOLONG) { - return nil, s3err.GetAPIError(s3err.ErrKeyTooLong) + return nil, s3err.GetKeyTooLongErr(int64(len(object)), 1024) } if err != nil { return nil, s3err.GetAPIError(s3err.ErrNoSuchKey) @@ -4247,10 +4252,10 @@ func (p *Posix) DeleteObject(ctx context.Context, input *s3.DeleteObjectInput) ( err = os.Remove(filepath.Join(versionPath, *input.VersionId)) if errors.Is(err, syscall.ENAMETOOLONG) { - return nil, s3err.GetAPIError(s3err.ErrKeyTooLong) + return nil, s3err.GetKeyTooLongErr(int64(len(object)), 1024) } if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { - return nil, s3err.GetAPIError(s3err.ErrInvalidVersionId) + return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, *input.VersionId) } if err != nil { return nil, fmt.Errorf("delete object: %w", err) @@ -4268,7 +4273,7 @@ func (p *Posix) DeleteObject(ctx context.Context, input *s3.DeleteObjectInput) ( fi, err := os.Stat(objpath) if errors.Is(err, syscall.ENAMETOOLONG) { - return nil, s3err.GetAPIError(s3err.ErrKeyTooLong) + return nil, s3err.GetKeyTooLongErr(int64(len(object)), 1024) } if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { // AWS returns success if the object does not exist @@ -4396,12 +4401,14 @@ func (p *Posix) DeleteObjects(ctx context.Context, input *s3.DeleteObjectsInput) delResult = append(delResult, delEntity) } else { - serr, ok := err.(s3err.APIError) + serr, ok := err.(s3err.S3Error) if ok { + errCode := serr.BaseError().Code + errMessage := serr.BaseError().Code errs = append(errs, types.Error{ Key: obj.Key, - Code: &serr.Code, - Message: &serr.Description, + Code: &errCode, + Message: &errMessage, }) } else { errs = append(errs, types.Error{ @@ -4440,16 +4447,16 @@ func (p *Posix) GetObject(ctx context.Context, input *s3.GetObjectInput) (*s3.Ge if !p.versioningEnabled() && versionId != "" { //TODO: Maybe we need to return our custom error here? - return nil, s3err.GetAPIError(s3err.ErrInvalidVersionId) + return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, versionId) } bucket := *input.Bucket if !p.isBucketValid(bucket) { - return nil, s3err.GetAPIError(s3err.ErrInvalidBucketName) + return nil, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } _, err = os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket) + return nil, s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return nil, fmt.Errorf("stat bucket: %w", err) @@ -4479,12 +4486,12 @@ func (p *Posix) GetObject(ctx context.Context, input *s3.GetObjectInput) (*s3.Ge fid, err := os.Stat(objPath) if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { if versionId != "" { - return nil, s3err.GetAPIError(s3err.ErrNoSuchVersion) + return nil, s3err.GetNoSuchVersionErr(*input.Key, versionId) } return nil, s3err.GetAPIError(s3err.ErrNoSuchKey) } if errors.Is(err, syscall.ENAMETOOLONG) { - return nil, s3err.GetAPIError(s3err.ErrKeyTooLong) + return nil, s3err.GetKeyTooLongErr(int64(len(*input.Key)), 1024) } if err != nil { return nil, fmt.Errorf("stat object: %w", err) @@ -4627,12 +4634,12 @@ func (p *Posix) GetObject(ctx context.Context, input *s3.GetObjectInput) (*s3.Ge fi, err := f.Stat() if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { if versionId != "" { - return nil, s3err.GetAPIError(s3err.ErrInvalidVersionId) + return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, versionId) } return nil, s3err.GetAPIError(s3err.ErrNoSuchKey) } if errors.Is(err, syscall.ENAMETOOLONG) { - return nil, s3err.GetAPIError(s3err.ErrKeyTooLong) + return nil, s3err.GetKeyTooLongErr(int64(len(*input.Key)), 1024) } if err != nil { return nil, fmt.Errorf("stat object: %w", err) @@ -4663,7 +4670,7 @@ func (p *Posix) GetObject(ctx context.Context, input *s3.GetObjectInput) (*s3.Ge totalParts := int32(len(mpMeta.Parts)) partsCount = &totalParts if partNum > totalParts { - return nil, s3err.GetAPIError(s3err.ErrInvalidPartNumberRange) + return nil, s3err.GetInvalidPartNumberRangeErr(totalParts, partNum) } // Parts holds cumulative sizes: Parts[i] = sum of sizes 1..i+1 @@ -4676,7 +4683,7 @@ func (p *Posix) GetObject(ctx context.Context, input *s3.GetObjectInput) (*s3.Ge // Non-multipart object: partNumber=1 means the whole object; anything // higher is out of range if *input.PartNumber > 1 { - return nil, s3err.GetAPIError(s3err.ErrInvalidPartNumberRange) + return nil, s3err.GetInvalidPartNumberRangeErr(1, *input.PartNumber) } length = objSize if objSize != 0 { @@ -4776,19 +4783,19 @@ func (p *Posix) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3. if !p.versioningEnabled() && versionId != "" { //TODO: Maybe we need to return our custom error here? - return nil, s3err.GetAPIError(s3err.ErrInvalidVersionId) + return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, versionId) } bucket := *input.Bucket object := *input.Key if !p.isBucketValid(bucket) { - return nil, s3err.GetAPIError(s3err.ErrInvalidBucketName) + return nil, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } _, err = os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket) + return nil, s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return nil, fmt.Errorf("stat bucket: %w", err) @@ -4818,12 +4825,12 @@ func (p *Posix) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3. fi, err := os.Stat(objPath) if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { if versionId != "" { - return nil, s3err.GetAPIError(s3err.ErrNoSuchVersion) + return nil, s3err.GetNoSuchVersionErr(*input.Key, versionId) } return nil, s3err.GetAPIError(s3err.ErrNoSuchKey) } if errors.Is(err, syscall.ENAMETOOLONG) { - return nil, s3err.GetAPIError(s3err.ErrKeyTooLong) + return nil, s3err.GetKeyTooLongErr(int64(len(*input.Key)), 1024) } if err != nil { return nil, fmt.Errorf("stat object: %w", err) @@ -4920,7 +4927,7 @@ func (p *Posix) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3. totalParts := int32(len(mpMeta.Parts)) partsCount = &totalParts if partNum > totalParts { - return nil, s3err.GetAPIError(s3err.ErrInvalidPartNumberRange) + return nil, s3err.GetInvalidPartNumberRangeErr(totalParts, partNum) } // Parts holds cumulative sizes: Parts[i] = sum of sizes 1..i+1 @@ -4933,7 +4940,7 @@ func (p *Posix) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3. // Non-multipart object: partNumber=1 means the whole object; anything // higher is out of range if *input.PartNumber > 1 { - return nil, s3err.GetAPIError(s3err.ErrInvalidPartNumberRange) + return nil, s3err.GetInvalidPartNumberRangeErr(1, *input.PartNumber) } length = size if length != 0 { @@ -5085,9 +5092,6 @@ func (p *Posix) CopyObject(ctx context.Context, input s3response.CopyObjectInput if input.Key == nil { return s3response.CopyObjectOutput{}, s3err.GetAPIError(s3err.ErrInvalidCopyDest) } - if input.CopySource == nil { - return s3response.CopyObjectOutput{}, s3err.GetAPIError(s3err.ErrInvalidCopySourceBucket) - } if input.ExpectedBucketOwner == nil { return s3response.CopyObjectOutput{}, s3err.GetAPIError(s3err.ErrInvalidRequest) } @@ -5100,18 +5104,18 @@ func (p *Posix) CopyObject(ctx context.Context, input s3response.CopyObjectInput return s3response.CopyObjectOutput{}, err } if !p.isBucketValid(srcBucket) { - return s3response.CopyObjectOutput{}, s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3response.CopyObjectOutput{}, s3err.GetBucketErr(s3err.ErrInvalidBucketName, srcBucket) } dstBucket := *input.Bucket dstObject := *input.Key if !p.isBucketValid(dstBucket) { - return s3response.CopyObjectOutput{}, s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3response.CopyObjectOutput{}, s3err.GetBucketErr(s3err.ErrInvalidBucketName, dstBucket) } _, err = os.Stat(srcBucket) if errors.Is(err, fs.ErrNotExist) { - return s3response.CopyObjectOutput{}, s3err.GetAPIError(s3err.ErrNoSuchBucket) + return s3response.CopyObjectOutput{}, s3err.GetBucketErr(s3err.ErrNoSuchBucket, srcBucket) } if err != nil { return s3response.CopyObjectOutput{}, fmt.Errorf("stat bucket: %w", err) @@ -5137,9 +5141,10 @@ func (p *Posix) CopyObject(ctx context.Context, input s3response.CopyObjectInput } vEnabled := p.isBucketVersioningEnabled(vStatus) + origSrcObject := srcObject if srcVersionId != "" { if !p.versioningEnabled() || !vEnabled { - return s3response.CopyObjectOutput{}, s3err.GetAPIError(s3err.ErrInvalidVersionId) + return s3response.CopyObjectOutput{}, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, srcVersionId) } vId, err := p.meta.RetrieveAttribute(nil, srcBucket, srcObject, versionIdKey) if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { @@ -5157,7 +5162,7 @@ func (p *Posix) CopyObject(ctx context.Context, input s3response.CopyObjectInput _, err = os.Stat(dstBucket) if errors.Is(err, fs.ErrNotExist) { - return s3response.CopyObjectOutput{}, s3err.GetAPIError(s3err.ErrNoSuchBucket) + return s3response.CopyObjectOutput{}, s3err.GetBucketErr(s3err.ErrNoSuchBucket, dstBucket) } if err != nil { return s3response.CopyObjectOutput{}, fmt.Errorf("stat bucket: %w", err) @@ -5167,12 +5172,12 @@ func (p *Posix) CopyObject(ctx context.Context, input s3response.CopyObjectInput f, err := os.Open(objPath) if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { if p.versioningEnabled() && vEnabled { - return s3response.CopyObjectOutput{}, s3err.GetAPIError(s3err.ErrNoSuchVersion) + return s3response.CopyObjectOutput{}, s3err.GetNoSuchVersionErr(origSrcObject, srcVersionId) } return s3response.CopyObjectOutput{}, s3err.GetAPIError(s3err.ErrNoSuchKey) } if errors.Is(err, syscall.ENAMETOOLONG) { - return s3response.CopyObjectOutput{}, s3err.GetAPIError(s3err.ErrKeyTooLong) + return s3response.CopyObjectOutput{}, s3err.GetKeyTooLongErr(int64(len(origSrcObject)), 1024) } if err != nil { return s3response.CopyObjectOutput{}, fmt.Errorf("open object: %w", err) @@ -5483,12 +5488,12 @@ func (p *Posix) ListObjectsParametrized(ctx context.Context, input *s3.ListObjec } if !p.isBucketValid(bucket) { - return s3response.ListObjectsResult{}, s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3response.ListObjectsResult{}, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } _, err := os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return s3response.ListObjectsResult{}, s3err.GetAPIError(s3err.ErrNoSuchBucket) + return s3response.ListObjectsResult{}, s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return s3response.ListObjectsResult{}, fmt.Errorf("stat bucket: %w", err) @@ -5517,7 +5522,7 @@ func (p *Posix) ListObjectsParametrized(ctx context.Context, input *s3.ListObjec func (p *Posix) FileToObj(bucket string, fetchOwner bool) backend.GetObjFunc { if !p.isBucketValid(bucket) { return func(string, fs.DirEntry) (s3response.Object, error) { - return s3response.Object{}, s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3response.Object{}, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } } @@ -5660,12 +5665,12 @@ func (p *Posix) ListObjectsV2Parametrized(ctx context.Context, input *s3.ListObj } if !p.isBucketValid(bucket) { - return s3response.ListObjectsV2Result{}, s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3response.ListObjectsV2Result{}, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } _, err := os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return s3response.ListObjectsV2Result{}, s3err.GetAPIError(s3err.ErrNoSuchBucket) + return s3response.ListObjectsV2Result{}, s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return s3response.ListObjectsV2Result{}, fmt.Errorf("stat bucket: %w", err) @@ -5703,11 +5708,11 @@ func (p *Posix) PutBucketAcl(ctx context.Context, bucket string, data []byte) er defer release() if !p.isBucketValid(bucket) { - return s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } _, err = os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return s3err.GetAPIError(s3err.ErrNoSuchBucket) + return s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return fmt.Errorf("stat bucket: %w", err) @@ -5729,11 +5734,11 @@ func (p *Posix) GetBucketAcl(ctx context.Context, input *s3.GetBucketAclInput) ( defer release() if !p.isBucketValid(*input.Bucket) { - return nil, s3err.GetAPIError(s3err.ErrInvalidBucketName) + return nil, s3err.GetBucketErr(s3err.ErrInvalidBucketName, *input.Bucket) } _, err = os.Stat(*input.Bucket) if errors.Is(err, fs.ErrNotExist) { - return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket) + return nil, s3err.GetBucketErr(s3err.ErrNoSuchBucket, *input.Bucket) } if err != nil { return nil, fmt.Errorf("stat bucket: %w", err) @@ -5757,11 +5762,11 @@ func (p *Posix) PutBucketTagging(ctx context.Context, bucket string, tags map[st defer release() if !p.isBucketValid(bucket) { - return s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } _, err = os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return s3err.GetAPIError(s3err.ErrNoSuchBucket) + return s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return fmt.Errorf("stat bucket: %w", err) @@ -5797,11 +5802,11 @@ func (p *Posix) GetBucketTagging(ctx context.Context, bucket string) (map[string defer release() if !p.isBucketValid(bucket) { - return nil, s3err.GetAPIError(s3err.ErrInvalidBucketName) + return nil, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } _, err = os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket) + return nil, s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return nil, fmt.Errorf("stat bucket: %w", err) @@ -5817,7 +5822,7 @@ func (p *Posix) GetBucketTagging(ctx context.Context, bucket string) (map[string func (p *Posix) DeleteBucketTagging(ctx context.Context, bucket string) error { if !p.isBucketValid(bucket) { - return s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } return p.PutBucketTagging(ctx, bucket, nil) } @@ -5830,11 +5835,11 @@ func (p *Posix) GetObjectTagging(ctx context.Context, bucket, object, versionId defer release() if !p.isBucketValid(bucket) { - return nil, s3err.GetAPIError(s3err.ErrInvalidBucketName) + return nil, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } _, err = os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket) + return nil, s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return nil, fmt.Errorf("stat bucket: %w", err) @@ -5860,7 +5865,7 @@ func (p *Posix) GetObjectTagging(ctx context.Context, bucket, object, versionId if versionId != "" { if !p.versioningEnabled() { //TODO: Maybe we need to return our custom error here? - return nil, s3err.GetAPIError(s3err.ErrInvalidVersionId) + return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, versionId) } vId, err := p.meta.RetrieveAttribute(nil, bucket, object, versionIdKey) if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { @@ -5889,7 +5894,7 @@ func (p *Posix) getAttrTags(bucket, object, versionId string) (map[string]string b, err := p.meta.RetrieveAttribute(nil, bucket, object, tagHdr) if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { if versionId != "" { - return nil, s3err.GetAPIError(s3err.ErrNoSuchVersion) + return nil, s3err.GetNoSuchVersionErr(object, versionId) } return nil, s3err.GetAPIError(s3err.ErrNoSuchKey) } @@ -5898,7 +5903,7 @@ func (p *Posix) getAttrTags(bucket, object, versionId string) (map[string]string // return empty tag set for object tagging return tags, nil } - return nil, s3err.GetAPIError(s3err.ErrBucketTaggingNotFound) + return nil, s3err.GetBucketErr(s3err.ErrBucketTaggingNotFound, bucket) } if err != nil { return nil, fmt.Errorf("get tags: %w", err) @@ -5920,11 +5925,11 @@ func (p *Posix) PutObjectTagging(ctx context.Context, bucket, object, versionId defer release() if !p.isBucketValid(bucket) { - return s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } _, err = os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return s3err.GetAPIError(s3err.ErrNoSuchBucket) + return s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return fmt.Errorf("stat bucket: %w", err) @@ -5950,7 +5955,7 @@ func (p *Posix) PutObjectTagging(ctx context.Context, bucket, object, versionId if versionId != "" { if !p.versioningEnabled() { //TODO: Maybe we need to return our custom error here? - return s3err.GetAPIError(s3err.ErrInvalidVersionId) + return s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, versionId) } vId, err := p.meta.RetrieveAttribute(nil, bucket, object, versionIdKey) if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { @@ -5975,7 +5980,7 @@ func (p *Posix) PutObjectTagging(ctx context.Context, bucket, object, versionId err = p.meta.DeleteAttribute(bucket, object, tagHdr) if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { if versionId != "" { - return s3err.GetAPIError(s3err.ErrNoSuchVersion) + return s3err.GetNoSuchVersionErr(object, versionId) } return s3err.GetAPIError(s3err.ErrNoSuchKey) } @@ -5996,7 +6001,7 @@ func (p *Posix) PutObjectTagging(ctx context.Context, bucket, object, versionId err = p.meta.StoreAttribute(nil, bucket, object, tagHdr, b) if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { if versionId != "" { - return s3err.GetAPIError(s3err.ErrNoSuchVersion) + return s3err.GetNoSuchVersionErr(object, versionId) } return s3err.GetAPIError(s3err.ErrNoSuchKey) } @@ -6009,7 +6014,7 @@ func (p *Posix) PutObjectTagging(ctx context.Context, bucket, object, versionId func (p *Posix) DeleteObjectTagging(ctx context.Context, bucket, object, versionId string) error { if !p.isBucketValid(bucket) { - return s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } return p.PutObjectTagging(ctx, bucket, object, versionId, nil) } @@ -6022,11 +6027,11 @@ func (p *Posix) PutBucketPolicy(ctx context.Context, bucket string, policy []byt defer release() if !p.isBucketValid(bucket) { - return s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } _, err = os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return s3err.GetAPIError(s3err.ErrNoSuchBucket) + return s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return fmt.Errorf("stat bucket: %w", err) @@ -6061,11 +6066,11 @@ func (p *Posix) GetBucketPolicy(ctx context.Context, bucket string) ([]byte, err defer release() if !p.isBucketValid(bucket) { - return nil, s3err.GetAPIError(s3err.ErrInvalidBucketName) + return nil, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } _, err = os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket) + return nil, s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return nil, fmt.Errorf("stat bucket: %w", err) @@ -6073,10 +6078,10 @@ func (p *Posix) GetBucketPolicy(ctx context.Context, bucket string) ([]byte, err policy, err := p.meta.RetrieveAttribute(nil, bucket, "", policykey) if errors.Is(err, meta.ErrNoSuchKey) { - return nil, s3err.GetAPIError(s3err.ErrNoSuchBucketPolicy) + return nil, s3err.GetBucketErr(s3err.ErrNoSuchBucketPolicy, bucket) } if errors.Is(err, fs.ErrNotExist) { - return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket) + return nil, s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return nil, fmt.Errorf("get bucket policy: %w", err) @@ -6087,7 +6092,7 @@ func (p *Posix) GetBucketPolicy(ctx context.Context, bucket string) ([]byte, err func (p *Posix) DeleteBucketPolicy(ctx context.Context, bucket string) error { if !p.isBucketValid(bucket) { - return s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } return p.PutBucketPolicy(ctx, bucket, nil) } @@ -6100,11 +6105,11 @@ func (p *Posix) PutBucketCors(ctx context.Context, bucket string, cors []byte) e defer release() if !p.isBucketValid(bucket) { - return s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } _, err = os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return s3err.GetAPIError(s3err.ErrNoSuchBucket) + return s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return fmt.Errorf("stat bucket: %w", err) @@ -6135,11 +6140,11 @@ func (p *Posix) GetBucketCors(ctx context.Context, bucket string) ([]byte, error defer release() if !p.isBucketValid(bucket) { - return nil, s3err.GetAPIError(s3err.ErrInvalidBucketName) + return nil, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } _, err = os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket) + return nil, s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return nil, fmt.Errorf("stat bucket: %w", err) @@ -6147,7 +6152,7 @@ func (p *Posix) GetBucketCors(ctx context.Context, bucket string) ([]byte, error cors, err := p.meta.RetrieveAttribute(nil, bucket, "", corskey) if errors.Is(err, meta.ErrNoSuchKey) { - return nil, s3err.GetAPIError(s3err.ErrNoSuchCORSConfiguration) + return nil, s3err.GetBucketErr(s3err.ErrNoSuchCORSConfiguration, bucket) } if err != nil { return nil, err @@ -6158,7 +6163,7 @@ func (p *Posix) GetBucketCors(ctx context.Context, bucket string) ([]byte, error func (p *Posix) DeleteBucketCors(ctx context.Context, bucket string) error { if !p.isBucketValid(bucket) { - return s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } return p.PutBucketCors(ctx, bucket, nil) } @@ -6166,7 +6171,7 @@ func (p *Posix) DeleteBucketCors(ctx context.Context, bucket string) error { func (p *Posix) isBucketObjectLockEnabled(bucket string) error { cfg, err := p.meta.RetrieveAttribute(nil, bucket, "", bucketLockKey) if errors.Is(err, fs.ErrNotExist) { - return s3err.GetAPIError(s3err.ErrNoSuchBucket) + return s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if errors.Is(err, meta.ErrNoSuchKey) { return s3err.GetAPIError(s3err.ErrMissingObjectLockConfiguration) @@ -6195,11 +6200,11 @@ func (p *Posix) PutObjectLockConfiguration(ctx context.Context, bucket string, c defer release() if !p.isBucketValid(bucket) { - return s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } _, err = os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return s3err.GetAPIError(s3err.ErrNoSuchBucket) + return s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return fmt.Errorf("stat bucket: %w", err) @@ -6238,11 +6243,11 @@ func (p *Posix) GetObjectLockConfiguration(ctx context.Context, bucket string) ( defer release() if !p.isBucketValid(bucket) { - return nil, s3err.GetAPIError(s3err.ErrInvalidBucketName) + return nil, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } _, err = os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket) + return nil, s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket) } if err != nil { return nil, fmt.Errorf("stat bucket: %w", err) @@ -6250,7 +6255,7 @@ func (p *Posix) GetObjectLockConfiguration(ctx context.Context, bucket string) ( cfg, err := p.meta.RetrieveAttribute(nil, bucket, "", bucketLockKey) if errors.Is(err, meta.ErrNoSuchKey) { - return nil, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound) + return nil, s3err.GetBucketErr(s3err.ErrObjectLockConfigurationNotFound, bucket) } if err != nil { return nil, fmt.Errorf("get object lock config: %w", err) @@ -6267,7 +6272,7 @@ func (p *Posix) PutObjectLegalHold(ctx context.Context, bucket, object, versionI defer release() if !p.isBucketValid(bucket) { - return s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } err = p.doesBucketAndObjectExist(bucket, object) if err != nil { @@ -6292,7 +6297,7 @@ func (p *Posix) PutObjectLegalHold(ctx context.Context, bucket, object, versionI if versionId != "" { if !p.versioningEnabled() { //TODO: Maybe we need to return our custom error here? - return s3err.GetAPIError(s3err.ErrInvalidVersionId) + return s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, versionId) } vId, err := p.meta.RetrieveAttribute(nil, bucket, object, versionIdKey) if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { @@ -6316,7 +6321,7 @@ func (p *Posix) PutObjectLegalHold(ctx context.Context, bucket, object, versionI err = p.meta.StoreAttribute(nil, bucket, object, objectLegalHoldKey, statusData) if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { if versionId != "" { - return s3err.GetAPIError(s3err.ErrNoSuchVersion) + return s3err.GetNoSuchVersionErr(object, versionId) } return s3err.GetAPIError(s3err.ErrNoSuchKey) } @@ -6335,7 +6340,7 @@ func (p *Posix) GetObjectLegalHold(ctx context.Context, bucket, object, versionI defer release() if !p.isBucketValid(bucket) { - return nil, s3err.GetAPIError(s3err.ErrInvalidBucketName) + return nil, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } err = p.doesBucketAndObjectExist(bucket, object) if err != nil { @@ -6353,7 +6358,7 @@ func (p *Posix) GetObjectLegalHold(ctx context.Context, bucket, object, versionI if versionId != "" { if !p.versioningEnabled() { //TODO: Maybe we need to return our custom error here? - return nil, s3err.GetAPIError(s3err.ErrInvalidVersionId) + return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, versionId) } vId, err := p.meta.RetrieveAttribute(nil, bucket, object, versionIdKey) if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { @@ -6377,7 +6382,7 @@ func (p *Posix) GetObjectLegalHold(ctx context.Context, bucket, object, versionI data, err := p.meta.RetrieveAttribute(nil, bucket, object, objectLegalHoldKey) if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { if versionId != "" { - return nil, s3err.GetAPIError(s3err.ErrNoSuchVersion) + return nil, s3err.GetNoSuchVersionErr(object, versionId) } return nil, s3err.GetAPIError(s3err.ErrNoSuchKey) } @@ -6401,7 +6406,7 @@ func (p *Posix) PutObjectRetention(ctx context.Context, bucket, object, versionI defer release() if !p.isBucketValid(bucket) { - return s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } err = p.doesBucketAndObjectExist(bucket, object) if err != nil { @@ -6419,7 +6424,7 @@ func (p *Posix) PutObjectRetention(ctx context.Context, bucket, object, versionI if versionId != "" { if !p.versioningEnabled() { //TODO: Maybe we need to return our custom error here? - return s3err.GetAPIError(s3err.ErrInvalidVersionId) + return s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, versionId) } vId, err := p.meta.RetrieveAttribute(nil, bucket, object, versionIdKey) if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { @@ -6456,7 +6461,7 @@ func (p *Posix) GetObjectRetention(ctx context.Context, bucket, object, versionI defer release() if !p.isBucketValid(bucket) { - return nil, s3err.GetAPIError(s3err.ErrInvalidBucketName) + return nil, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } err = p.doesBucketAndObjectExist(bucket, object) if err != nil { @@ -6474,7 +6479,7 @@ func (p *Posix) GetObjectRetention(ctx context.Context, bucket, object, versionI if versionId != "" { if !p.versioningEnabled() { //TODO: Maybe we need to return our custom error here? - return nil, s3err.GetAPIError(s3err.ErrInvalidVersionId) + return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, versionId) } vId, err := p.meta.RetrieveAttribute(nil, bucket, object, versionIdKey) if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { @@ -6498,7 +6503,7 @@ func (p *Posix) GetObjectRetention(ctx context.Context, bucket, object, versionI data, err := p.meta.RetrieveAttribute(nil, bucket, object, objectRetentionKey) if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) { if versionId != "" { - return nil, s3err.GetAPIError(s3err.ErrNoSuchVersion) + return nil, s3err.GetNoSuchVersionErr(object, versionId) } return nil, s3err.GetAPIError(s3err.ErrNoSuchKey) } @@ -6520,7 +6525,7 @@ func (p *Posix) ChangeBucketOwner(ctx context.Context, bucket, owner string) err defer release() if !p.isBucketValid(bucket) { - return s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } return auth.UpdateBucketACLOwner(ctx, p, bucket, owner) } diff --git a/backend/s3proxy/s3.go b/backend/s3proxy/s3.go index fb703d7f..715ad959 100644 --- a/backend/s3proxy/s3.go +++ b/backend/s3proxy/s3.go @@ -179,7 +179,7 @@ func (s *S3Proxy) CreateBucket(ctx context.Context, input *s3.CreateBucketInput, input.GrantWriteACP = nil } if *input.Bucket == s.metaBucket { - return s3err.GetAPIError(s3err.ErrBucketAlreadyExists) + return s3err.GetBucketErr(s3err.ErrBucketAlreadyExists, *input.Bucket) } acct, ok := ctx.Value("account").(auth.Account) @@ -196,9 +196,9 @@ func (s *S3Proxy) CreateBucket(ctx context.Context, input *s3.CreateBucketInput, } if acl.Owner == acct.Access { - return s3err.GetAPIError(s3err.ErrBucketAlreadyOwnedByYou) + return s3err.GetBucketErr(s3err.ErrBucketAlreadyOwnedByYou, *input.Bucket) } - return s3err.GetAPIError(s3err.ErrBucketAlreadyExists) + return s3err.GetBucketErr(s3err.ErrBucketAlreadyExists, *input.Bucket) } } @@ -1645,7 +1645,7 @@ func (s *S3Proxy) PutObjectLockConfiguration(ctx context.Context, bucket string, } func (s *S3Proxy) GetObjectLockConfiguration(ctx context.Context, bucket string) ([]byte, error) { - return nil, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound) + return nil, s3err.GetBucketErr(s3err.ErrObjectLockConfigurationNotFound, bucket) } func (s *S3Proxy) PutObjectRetention(ctx context.Context, bucket, object, versionId string, retention []byte) error { @@ -1769,9 +1769,9 @@ func handleMetaBucketObjectNotFoundErr(prefix metaPrefix) ([]byte, error) { // If bucket acl is not found, return default acl return []byte{}, nil case metaPrefixPolicy: - return nil, s3err.GetAPIError(s3err.ErrNoSuchBucketPolicy) + return nil, s3err.GetBucketErr(s3err.ErrNoSuchBucketPolicy, "") case metaPrefixCors: - return nil, s3err.GetAPIError(s3err.ErrNoSuchCORSConfiguration) + return nil, s3err.GetBucketErr(s3err.ErrNoSuchCORSConfiguration, "") } return []byte{}, nil diff --git a/backend/scoutfs/scoutfs_compat.go b/backend/scoutfs/scoutfs_compat.go index 4afb171f..7b242f7b 100644 --- a/backend/scoutfs/scoutfs_compat.go +++ b/backend/scoutfs/scoutfs_compat.go @@ -334,12 +334,12 @@ func (s *ScoutFS) GetObject(ctx context.Context, input *s3.GetObjectInput) (*s3. object := *input.Key if !s.isBucketValid(bucket) { - return nil, s3err.GetAPIError(s3err.ErrInvalidBucketName) + return nil, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } _, err := os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return nil, s3err.GetAPIError(s3err.ErrNoSuchBucket) + return nil, s3err.GetBucketErr(s3err.ErrNoSuchBucket, *input.Bucket) } if err != nil { return nil, fmt.Errorf("stat bucket: %w", err) @@ -429,12 +429,12 @@ func (s *ScoutFS) RestoreObject(_ context.Context, input *s3.RestoreObjectInput) object := *input.Key if !s.isBucketValid(bucket) { - return s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } _, err := os.Stat(bucket) if errors.Is(err, fs.ErrNotExist) { - return s3err.GetAPIError(s3err.ErrNoSuchBucket) + return s3err.GetBucketErr(s3err.ErrNoSuchBucket, *input.Bucket) } if err != nil { return fmt.Errorf("stat bucket: %w", err) diff --git a/metrics/metrics.go b/metrics/metrics.go index f9b79519..f191c70d 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -140,9 +140,9 @@ func (m *manager) Send(ctx *fiber.Ctx, err error, action string, count int64, st reqStatus := status if err != nil { - var apierr s3err.APIError + var apierr s3err.S3Error if errors.As(err, &apierr) { - reqStatus = apierr.HTTPStatusCode + reqStatus = apierr.StatusCode() } else { reqStatus = http.StatusInternalServerError } diff --git a/plugins/noop/noop.go b/plugins/noop/noop.go index 6b7497e2..004f8033 100644 --- a/plugins/noop/noop.go +++ b/plugins/noop/noop.go @@ -219,7 +219,7 @@ func (n *noOp) GetBucketPolicy(_ context.Context, bucket string) ([]byte, error) } func (n *noOp) GetObjectLockConfiguration(_ context.Context, bucket string) ([]byte, error) { - return nil, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound) + return nil, s3err.GetBucketErr(s3err.ErrObjectLockConfigurationNotFound, bucket) } func (n *noOp) PutObjectTagging(context.Context, string, string, string, map[string]string) error { diff --git a/s3api/admin-server.go b/s3api/admin-server.go index e49cac91..547cef55 100644 --- a/s3api/admin-server.go +++ b/s3api/admin-server.go @@ -79,6 +79,8 @@ func NewAdminServer(be backend.Backend, root middlewares.RootUserConfig, region Format: "${time} | adm | ${status} | ${latency} | ${ip} | ${method} | ${path} | ${error} | ${queryParams}\n", })) } + // initialize requestId middleware + app.Use(middlewares.RequestIDs()) // initialize total requests cap limiter middleware app.Use(middlewares.RateLimiter(server.maxRequests, nil, l)) diff --git a/s3api/controllers/base.go b/s3api/controllers/base.go index 0b9e3114..028f4e48 100644 --- a/s3api/controllers/base.go +++ b/s3api/controllers/base.go @@ -150,6 +150,8 @@ func ProcessHandlers(controller Controller, s3action string, svc *Services, hand // and metrics. It also handles the error parsing func WrapMiddleware(handler fiber.Handler, logger s3log.AuditLogger, mm metrics.Manager) fiber.Handler { return func(ctx *fiber.Ctx) error { + requestID, hostID := utils.EnsureRequestIDs(ctx) + err := handler(ctx) if err != nil { if mm != nil { @@ -163,18 +165,19 @@ func WrapMiddleware(handler fiber.Handler, logger s3log.AuditLogger, mm metrics. ctx.Response().Header.SetContentType(fiber.MIMEApplicationXML) - serr, ok := err.(s3err.APIError) - if ok { - ctx.Status(serr.HTTPStatusCode) - return ctx.Send(s3err.GetAPIErrorResponse(serr, "", "", "")) + if serr, ok := err.(s3err.S3Error); ok { + if mnaErr, ok := serr.(s3err.MethodNotAllowedError); ok && len(mnaErr.AllowedMethods) != 0 { + // for MethodNotAllowed errors, set the 'Allow' header + ctx.Response().Header.Set("Allow", mnaErr.AllowedMethodsString()) + } + return ctx.Status(serr.StatusCode()).Send(serr.XMLBody(requestID, hostID)) } debuglogger.InternalError(err) ctx.Status(http.StatusInternalServerError) - // If the error is not 's3err.APIError' return 'InternalError' - return ctx.Send(s3err.GetAPIErrorResponse( - s3err.GetAPIError(s3err.ErrInternalError), "", "", "")) + // If the error is not 's3err.S3Error' return 'InternalError' + return ctx.Send(s3err.GetAPIError(s3err.ErrInternalError).XMLBody(requestID, hostID)) } return ctx.Next() @@ -188,6 +191,7 @@ func ProcessController(ctx *fiber.Ctx, controller Controller, s3action string, s // Set the response headers SetResponseHeaders(ctx, response.Headers) + requestID, hostID := utils.EnsureRequestIDs(ctx) ensureExposeMetaHeaders(ctx) opts := response.MetaOpts @@ -216,18 +220,17 @@ func ProcessController(ctx *fiber.Ctx, controller Controller, s3action string, s // set content type to application/xml ctx.Response().Header.SetContentType(fiber.MIMEApplicationXML) - serr, ok := err.(s3err.APIError) - if ok { - ctx.Status(serr.HTTPStatusCode) - return ctx.Send(s3err.GetAPIErrorResponse(serr, "", "", "")) + if serr, ok := err.(s3err.S3Error); ok { + if mnaErr, ok := serr.(s3err.MethodNotAllowedError); ok && len(mnaErr.AllowedMethods) != 0 { + ctx.Response().Header.Set("Allow", mnaErr.AllowedMethodsString()) + } + return ctx.Status(serr.StatusCode()).Send(serr.XMLBody(requestID, hostID)) } debuglogger.InternalError(err) - ctx.Status(http.StatusInternalServerError) - // If the error is not 's3err.APIError' return 'InternalError' - return ctx.Send(s3err.GetAPIErrorResponse( - s3err.GetAPIError(s3err.ErrInternalError), "", "", "")) + // If the error is not 's3err.S3Error' return 'InternalError' + return ctx.Status(http.StatusInternalServerError).Send(s3err.GetAPIError(s3err.ErrInternalError).XMLBody(requestID, hostID)) } // At this point, the S3 action has succeeded in the backend and @@ -276,8 +279,9 @@ func ProcessController(ctx *fiber.Ctx, controller Controller, s3action string, s ObjectSize: opts.ObjectSize, }) } - return ctx.Status(http.StatusInternalServerError).Send(s3err.GetAPIErrorResponse( - s3err.GetAPIError(s3err.ErrInternalError), "", "", "")) + + err := s3err.GetAPIError(s3err.ErrInternalError) + return ctx.Status(err.HTTPStatusCode).Send(err.XMLBody(requestID, hostID)) } if len(responseBytes) > 0 { @@ -312,13 +316,12 @@ func ProcessController(ctx *fiber.Ctx, controller Controller, s3action string, s ObjectSize: opts.ObjectSize, }) } - ctx.Status(http.StatusInternalServerError) // set content type to application/xml ctx.Response().Header.SetContentType(fiber.MIMEApplicationXML) - return ctx.Send(s3err.GetAPIErrorResponse( - s3err.GetAPIError(s3err.ErrInternalError), "", "", "")) + err := s3err.GetAPIError(s3err.ErrInternalError) + return ctx.Status(err.HTTPStatusCode).Send(err.XMLBody(requestID, hostID)) } res := make([]byte, 0, msglen) res = append(res, xmlhdr...) diff --git a/s3api/controllers/base_test.go b/s3api/controllers/base_test.go index 73dd82ec..d5335d10 100644 --- a/s3api/controllers/base_test.go +++ b/s3api/controllers/base_test.go @@ -39,6 +39,9 @@ import ( ) var ( + testRequestID = "5MRQJ97RHWJ4FMX9" + testHostID = "eS8nILxNKeV1pNi2Z7Pv6mwC+nuquA2UTBwrBSxGq62e9NZ6f2G9aJPRetuD0/lF3OgqRF7N3GU=" + defaultLocals map[utils.ContextKey]any = map[utils.ContextKey]any{ utils.ContextKeyIsRoot: true, utils.ContextKeyParsedAcl: auth.ACL{ @@ -109,7 +112,7 @@ func testController(t *testing.T, ctrl Controller, resp *Response, expectedErr e assert.Error(t, err) switch expectedErr.(type) { - case s3err.APIError: + case s3err.S3Error: assert.EqualValues(t, expectedErr, err) default: assert.ErrorContains(t, err, expectedErr.Error()) @@ -323,7 +326,7 @@ func TestProcessController(t *testing.T) { }, expected: expected{ status: http.StatusBadRequest, - body: s3err.GetAPIErrorResponse(s3err.GetAPIError(s3err.ErrInvalidRequest), "", "", ""), + body: s3err.GetAPIError(s3err.ErrInvalidRequest).XMLBody(testRequestID, testHostID), }, }, { @@ -336,7 +339,7 @@ func TestProcessController(t *testing.T) { }, expected: expected{ status: http.StatusInternalServerError, - body: s3err.GetAPIErrorResponse(s3err.GetAPIError(s3err.ErrInternalError), "", "", ""), + body: s3err.GetAPIError(s3err.ErrInternalError).XMLBody(testRequestID, testHostID), }, }, { @@ -351,7 +354,7 @@ func TestProcessController(t *testing.T) { }, expected: expected{ status: http.StatusInternalServerError, - body: s3err.GetAPIErrorResponse(s3err.GetAPIError(s3err.ErrInternalError), "", "", ""), + body: s3err.GetAPIError(s3err.ErrInternalError).XMLBody(testRequestID, testHostID), }, }, { @@ -426,7 +429,7 @@ func TestProcessController(t *testing.T) { }, }, { - name: "large paylod: should return internal error", + name: "large payload: should return internal error", args: args{ svc: services, controller: func(ctx *fiber.Ctx) (*Response, error) { @@ -464,7 +467,7 @@ func TestProcessController(t *testing.T) { }, }, expected: expected{ - body: s3err.GetAPIErrorResponse(s3err.GetAPIError(s3err.ErrInternalError), "", "", ""), + body: s3err.GetAPIError(s3err.ErrInternalError).XMLBody(testRequestID, testHostID), status: http.StatusInternalServerError, }, }, @@ -492,11 +495,15 @@ func TestProcessController(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := fiber.New().AcquireCtx(&fasthttp.RequestCtx{}) + utils.ContextKeyRequestID.Set(ctx, testRequestID) + utils.ContextKeyHostID.Set(ctx, testHostID) err := ProcessController(ctx, tt.args.controller, metrics.ActionAbortMultipartUpload, tt.args.svc) assert.NoError(t, err) // check the status assert.Equal(t, tt.expected.status, ctx.Response().StatusCode()) + assert.Equal(t, testRequestID, string(ctx.Response().Header.Peek(utils.HeaderAmzRequestID))) + assert.Equal(t, testHostID, string(ctx.Response().Header.Peek(utils.HeaderAmzID2))) // check the response headers to be set if tt.expected.headers != nil { @@ -556,7 +563,7 @@ func TestProcessHandlers(t *testing.T) { svc: &Services{}, }, expected: expected{ - body: s3err.GetAPIErrorResponse(s3err.GetAPIError(s3err.ErrAccessDenied), "", "", ""), + body: s3err.GetAPIError(s3err.ErrAccessDenied).XMLBody(testRequestID, testHostID), }, }, { @@ -591,6 +598,9 @@ func TestProcessHandlers(t *testing.T) { app := fiber.New() app.Post("/:bucket/*", func(ctx *fiber.Ctx) error { + utils.ContextKeyRequestID.Set(ctx, testRequestID) + utils.ContextKeyHostID.Set(ctx, testHostID) + // set the request locals if tt.args.locals != nil { for key, val := range tt.args.locals { @@ -654,7 +664,7 @@ func TestWrapMiddleware(t *testing.T) { logger: &mockAuditLogger{}, }, expected: expected{ - body: s3err.GetAPIErrorResponse(s3err.GetAPIError(s3err.ErrAclNotSupported), "", "", ""), + body: s3err.GetAPIError(s3err.ErrAclNotSupported).XMLBody(testRequestID, testHostID), }, }, { @@ -665,7 +675,7 @@ func TestWrapMiddleware(t *testing.T) { }, }, expected: expected{ - body: s3err.GetAPIErrorResponse(s3err.GetAPIError(s3err.ErrInternalError), "", "", ""), + body: s3err.GetAPIError(s3err.ErrInternalError).XMLBody(testRequestID, testHostID), }, }, } @@ -675,6 +685,9 @@ func TestWrapMiddleware(t *testing.T) { app := fiber.New() app.Post("/:bucket/*", func(ctx *fiber.Ctx) error { + utils.ContextKeyRequestID.Set(ctx, testRequestID) + utils.ContextKeyHostID.Set(ctx, testHostID) + // call the controller by passing the ctx err := mdlwr(ctx) assert.NoError(t, err) diff --git a/s3api/controllers/bucket-get_test.go b/s3api/controllers/bucket-get_test.go index fb91171b..5736adcd 100644 --- a/s3api/controllers/bucket-get_test.go +++ b/s3api/controllers/bucket-get_test.go @@ -663,7 +663,7 @@ func TestS3ApiController_ListObjectVersions(t *testing.T) { BucketOwner: "root", }, }, - err: s3err.GetInvalidMaxLimiterErr(utils.LimiterTypeMaxKeys), + err: s3err.GetInvalidArgMaxLimiter(string(utils.LimiterTypeMaxKeys), "invalid"), }, }, { @@ -988,7 +988,7 @@ func TestS3ApiController_ListMultipartUploads(t *testing.T) { BucketOwner: "root", }, }, - err: s3err.GetInvalidMaxLimiterErr(utils.LimiterTypeMaxUploads), + err: s3err.GetInvalidArgMaxLimiter(string(utils.LimiterTypeMaxUploads), "invalid"), }, }, { @@ -1095,7 +1095,7 @@ func TestS3ApiController_ListObjectsV2(t *testing.T) { BucketOwner: "root", }, }, - err: s3err.GetNegativeMaxLimiterErr(utils.LimiterTypeMaxKeys), + err: s3err.GetInvalidArgNegativeMaxLimiter(string(utils.LimiterTypeMaxKeys), "-1"), }, }, { @@ -1204,7 +1204,7 @@ func TestS3ApiController_ListObjects(t *testing.T) { BucketOwner: "root", }, }, - err: s3err.GetInvalidMaxLimiterErr(utils.LimiterTypeMaxKeys), + err: s3err.GetInvalidArgMaxLimiter(string(utils.LimiterTypeMaxKeys), "bla"), }, }, { diff --git a/s3api/controllers/bucket-list_test.go b/s3api/controllers/bucket-list_test.go index 6de64afd..bfa16f6a 100644 --- a/s3api/controllers/bucket-list_test.go +++ b/s3api/controllers/bucket-list_test.go @@ -51,7 +51,7 @@ func TestS3ApiController_ListBuckets(t *testing.T) { response: &Response{ MetaOpts: &MetaOptions{}, }, - err: s3err.GetAPIError(s3err.ErrInvalidMaxBuckets), + err: s3err.GetInvalidArgumentErr(s3err.InvalidArgMaxBuckets, "-1"), }, }, { @@ -66,7 +66,7 @@ func TestS3ApiController_ListBuckets(t *testing.T) { response: &Response{ MetaOpts: &MetaOptions{}, }, - err: s3err.GetAPIError(s3err.ErrInvalidMaxBuckets), + err: s3err.GetInvalidArgumentErr(s3err.InvalidArgMaxBuckets, "10001"), }, }, { @@ -81,7 +81,7 @@ func TestS3ApiController_ListBuckets(t *testing.T) { response: &Response{ MetaOpts: &MetaOptions{}, }, - err: s3err.GetAPIError(s3err.ErrInvalidMaxBuckets), + err: s3err.GetInvalidArgumentErr(s3err.InvalidArgMaxBuckets, "0"), }, }, { @@ -96,7 +96,7 @@ func TestS3ApiController_ListBuckets(t *testing.T) { response: &Response{ MetaOpts: &MetaOptions{}, }, - err: s3err.GetInvalidMaxLimiterErr("max-buckets"), + err: s3err.GetInvalidArgMaxLimiter("max-buckets", "bla"), }, }, { diff --git a/s3api/controllers/bucket-post_test.go b/s3api/controllers/bucket-post_test.go index dc76c1dc..0788c04f 100644 --- a/s3api/controllers/bucket-post_test.go +++ b/s3api/controllers/bucket-post_test.go @@ -433,7 +433,7 @@ func TestS3ApiController_POSTObject(t *testing.T) { BucketOwner: "root", }, }, - err: s3err.GetAPIError(s3err.ErrMetadataTooLarge), + err: s3err.GetMetadataTooLargeErr(2053, 2048), }, }, { diff --git a/s3api/controllers/bucket-put.go b/s3api/controllers/bucket-put.go index b6d8f762..04c0311f 100644 --- a/s3api/controllers/bucket-put.go +++ b/s3api/controllers/bucket-put.go @@ -17,7 +17,6 @@ package controllers import ( "encoding/xml" "errors" - "fmt" "net/http" "strings" @@ -530,7 +529,7 @@ func (c S3ApiController) CreateBucket(ctx *fiber.Ctx) (*Response, error) { MetaOpts: &MetaOptions{ BucketOwner: bucketOwner.Access, }, - }, s3err.GetAPIError(s3err.ErrInvalidBucketName) + }, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } // both bucket canned ACL and acl grants is not allowed @@ -566,14 +565,10 @@ func (c S3ApiController) CreateBucket(ctx *fiber.Ctx) (*Response, error) { // validate the object ownership value if ok := utils.IsValidOwnership(objectOwnership); !ok { return &Response{ - MetaOpts: &MetaOptions{ - BucketOwner: bucketOwner.Access, - }, - }, s3err.APIError{ - Code: "InvalidArgument", - Description: fmt.Sprintf("Invalid x-amz-object-ownership header: %v", objectOwnership), - HTTPStatusCode: http.StatusBadRequest, - } + MetaOpts: &MetaOptions{ + BucketOwner: bucketOwner.Access, + }, + }, s3err.GetInvalidArgObjectOwnership(string(objectOwnership)) } // any bucket ACL(canned, grants) is not allowed with object ownership 'BucketOwnerEnforced' @@ -610,7 +605,7 @@ func (c S3ApiController) CreateBucket(ctx *fiber.Ctx) (*Response, error) { MetaOpts: &MetaOptions{ BucketOwner: bucketOwner.Access, }, - }, s3err.GetAPIError(s3err.ErrInvalidLocationConstraint) + }, s3err.GetInvalidLocationConstraintErr(*body.LocationConstraint) } } } diff --git a/s3api/controllers/bucket-put_test.go b/s3api/controllers/bucket-put_test.go index 58e258c1..bffe4662 100644 --- a/s3api/controllers/bucket-put_test.go +++ b/s3api/controllers/bucket-put_test.go @@ -737,7 +737,7 @@ func TestS3ApiController_CreateBucket(t *testing.T) { BucketOwner: adminAcc.Access, }, }, - err: s3err.GetAPIError(s3err.ErrInvalidBucketName), + err: s3err.GetBucketErr(s3err.ErrInvalidBucketName, "invalid_bucket_name"), }, }, { @@ -770,7 +770,7 @@ func TestS3ApiController_CreateBucket(t *testing.T) { response: &Response{ MetaOpts: &MetaOptions{BucketOwner: adminAcc.Access}, }, - err: s3err.GetAPIError(s3err.ErrInvalidArgument), + err: s3err.GetInvalidArgumentErr(s3err.InvalidArgCannedAcl, "invalid_acl"), }, }, { @@ -786,7 +786,7 @@ func TestS3ApiController_CreateBucket(t *testing.T) { response: &Response{ MetaOpts: &MetaOptions{BucketOwner: adminAcc.Access}, }, - err: s3err.GetAPIError(s3err.ErrInvalidLocationConstraint), + err: s3err.GetInvalidLocationConstraintErr("us-west-1"), }, }, { @@ -805,11 +805,7 @@ func TestS3ApiController_CreateBucket(t *testing.T) { BucketOwner: adminAcc.Access, }, }, - err: s3err.APIError{ - Code: "InvalidArgument", - Description: "Invalid x-amz-object-ownership header: invalid_ownership", - HTTPStatusCode: http.StatusBadRequest, - }, + err: s3err.GetInvalidArgObjectOwnership("invalid_ownership"), }, }, { @@ -1110,7 +1106,7 @@ func TestS3ApiController_PutBucketAcl(t *testing.T) { BucketOwner: "root", }, }, - err: s3err.GetAPIError(s3err.ErrInvalidArgument), + err: s3err.GetInvalidArgumentErr(s3err.InvalidArgCannedAcl, "invalid_acl"), }, }, { diff --git a/s3api/controllers/object-get.go b/s3api/controllers/object-get.go index 62c616d6..a7575807 100644 --- a/s3api/controllers/object-get.go +++ b/s3api/controllers/object-get.go @@ -457,7 +457,7 @@ func (c S3ApiController) GetObject(ctx *fiber.Ctx) (*Response, error) { MetaOpts: &MetaOptions{ BucketOwner: parsedAcl.Owner, }, - }, s3err.GetAPIError(s3err.ErrInvalidPartNumber) + }, s3err.GetInvalidArgumentErr(s3err.InvalidArgPartNumber, ctx.Query("partNumber")) } if acceptRange != "" { @@ -533,7 +533,7 @@ func (c S3ApiController) GetObject(ctx *fiber.Ctx) (*Response, error) { BucketOwner: parsedAcl.Owner, Status: status, }, - }, s3err.GetAPIError(s3err.ErrInvalidRange) + }, s3err.GetInvalidRangeErr("", *res.ContentLength) } contentLen = int(*res.ContentLength) } diff --git a/s3api/controllers/object-get_test.go b/s3api/controllers/object-get_test.go index 5349cdee..2fdc417f 100644 --- a/s3api/controllers/object-get_test.go +++ b/s3api/controllers/object-get_test.go @@ -455,7 +455,7 @@ func TestS3ApiController_ListParts(t *testing.T) { BucketOwner: "root", }, }, - err: s3err.GetInvalidMaxLimiterErr(utils.LimiterTypePartNumberMarker), + err: s3err.GetInvalidArgMaxLimiter(string(utils.LimiterTypePartNumberMarker), "foo"), }, }, { @@ -472,7 +472,7 @@ func TestS3ApiController_ListParts(t *testing.T) { BucketOwner: "root", }, }, - err: s3err.GetNegativeMaxLimiterErr(utils.LimiterTypeMaxParts), + err: s3err.GetInvalidArgNegativeMaxLimiter(string(utils.LimiterTypeMaxParts), "-1"), }, }, { @@ -580,7 +580,7 @@ func TestS3ApiController_GetObjectAttributes(t *testing.T) { BucketOwner: "root", }, }, - err: s3err.GetAPIError(s3err.ErrInvalidObjectAttributes), + err: s3err.GetInvalidArgumentErr(s3err.InvalidArgObjectAttributes, "invalid_attribute"), }, }, { @@ -719,7 +719,7 @@ func TestS3ApiController_GetObject(t *testing.T) { BucketOwner: "root", }, }, - err: s3err.GetAPIError(s3err.ErrInvalidPartNumber), + err: s3err.GetInvalidArgumentErr(s3err.InvalidArgPartNumber, "-2"), }, }, { diff --git a/s3api/controllers/object-head.go b/s3api/controllers/object-head.go index 12c8078c..789c8e8f 100644 --- a/s3api/controllers/object-head.go +++ b/s3api/controllers/object-head.go @@ -105,7 +105,7 @@ func (c S3ApiController) HeadObject(ctx *fiber.Ctx) (*Response, error) { MetaOpts: &MetaOptions{ BucketOwner: parsedAcl.Owner, }, - }, s3err.GetAPIError(s3err.ErrInvalidPartNumber) + }, s3err.GetInvalidArgumentErr(s3err.InvalidArgPartNumber, ctx.Query("partNumber")) } if objRange != "" { diff --git a/s3api/controllers/object-head_test.go b/s3api/controllers/object-head_test.go index b3293ad6..ade3d4c4 100644 --- a/s3api/controllers/object-head_test.go +++ b/s3api/controllers/object-head_test.go @@ -96,7 +96,7 @@ func TestS3ApiController_HeadObject(t *testing.T) { BucketOwner: "root", }, }, - err: s3err.GetAPIError(s3err.ErrInvalidPartNumber), + err: s3err.GetInvalidArgumentErr(s3err.InvalidArgPartNumber, "-4"), }, }, { diff --git a/s3api/controllers/object-post_test.go b/s3api/controllers/object-post_test.go index b24aff6e..ecf99f72 100644 --- a/s3api/controllers/object-post_test.go +++ b/s3api/controllers/object-post_test.go @@ -250,7 +250,7 @@ func TestS3ApiController_CreateMultipartUpload(t *testing.T) { BucketOwner: "root", }, }, - err: s3err.GetAPIError(s3err.ErrMetadataTooLarge), + err: s3err.GetMetadataTooLargeErr(2051, 2048), }, }, { @@ -267,7 +267,7 @@ func TestS3ApiController_CreateMultipartUpload(t *testing.T) { BucketOwner: "root", }, }, - err: s3err.GetAPIError(s3err.ErrObjectLockInvalidHeaders), + err: s3err.GetInvalidArgumentErr(s3err.InvalidArgMissingObjectLockRetainDate, ""), }, }, { @@ -362,17 +362,9 @@ func TestS3ApiController_CompleteMultipartUpload(t *testing.T) { Parts: []types.CompletedPart{}, }) assert.NoError(t, err) - pn := int32(1) - validMpBody, err := xml.Marshal(s3response.CompleteMultipartUploadRequestBody{ - Parts: []types.CompletedPart{ - { - PartNumber: &pn, - ETag: utils.GetStringPtr("ETag"), - }, - }, - }) - assert.NoError(t, err) + validMpBody := []byte(`1ETag`) + s3cmdMpBody := []byte("1ETag") versionId, ETag := "versionId", "mock-ETag" @@ -569,6 +561,32 @@ func TestS3ApiController_CompleteMultipartUpload(t *testing.T) { }, }, }, + { + name: "successful response with s3cmd request body", + input: testInput{ + locals: defaultLocals, + body: s3cmdMpBody, + beRes: s3response.CompleteMultipartUploadResult{ETag: &ETag}, + extraMockErr: s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound), + }, + output: testOutput{ + response: &Response{ + Data: s3response.CompleteMultipartUploadResult{ + ETag: &ETag, + Location: utils.GetStringPtr("http://example.com/bucket/object"), + }, + Headers: map[string]*string{ + "x-amz-version-id": &versionId, + }, + MetaOpts: &MetaOptions{ + BucketOwner: "root", + EventName: s3event.EventCompleteMultipartUpload, + VersionId: &versionId, + ObjectETag: &ETag, + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/s3api/controllers/object-put.go b/s3api/controllers/object-put.go index cefef2c2..d2335267 100644 --- a/s3api/controllers/object-put.go +++ b/s3api/controllers/object-put.go @@ -261,7 +261,7 @@ func (c S3ApiController) UploadPart(ctx *fiber.Ctx) (*Response, error) { MetaOpts: &MetaOptions{ BucketOwner: parsedAcl.Owner, }, - }, s3err.GetAPIError(s3err.ErrInvalidPartNumber) + }, s3err.GetInvalidArgumentErr(s3err.InvalidArgPartNumber, ctx.Query("partNumber")) } contentLength, err := strconv.ParseInt(contentLengthStr, 10, 64) @@ -396,7 +396,7 @@ func (c S3ApiController) UploadPartCopy(ctx *fiber.Ctx) (*Response, error) { MetaOpts: &MetaOptions{ BucketOwner: parsedAcl.Owner, }, - }, s3err.GetAPIError(s3err.ErrInvalidPartNumber) + }, s3err.GetInvalidArgumentErr(s3err.InvalidArgPartNumber, ctx.Query("partNumber")) } preconditionHdrs := utils.ParsePreconditionHeaders(ctx, utils.WithCopySource()) @@ -566,7 +566,7 @@ func (c S3ApiController) CopyObject(ctx *fiber.Ctx) (*Response, error) { MetaOpts: &MetaOptions{ BucketOwner: parsedAcl.Owner, }, - }, s3err.GetAPIError(s3err.ErrInvalidMetadataDirective) + }, s3err.GetInvalidArgumentErr(s3err.InvalidArgMetadataDirective, string(metaDirective)) } if taggingDirective != "" && taggingDirective != types.TaggingDirectiveCopy && taggingDirective != types.TaggingDirectiveReplace { @@ -575,7 +575,7 @@ func (c S3ApiController) CopyObject(ctx *fiber.Ctx) (*Response, error) { MetaOpts: &MetaOptions{ BucketOwner: parsedAcl.Owner, }, - }, s3err.GetAPIError(s3err.ErrInvalidTaggingDirective) + }, s3err.GetInvalidArgumentErr(s3err.InvalidArgTaggingDirective, string(taggingDirective)) } checksumAlgorithm := types.ChecksumAlgorithm(ctx.Get("x-amz-checksum-algorithm")) diff --git a/s3api/controllers/object-put_test.go b/s3api/controllers/object-put_test.go index c88274d5..deb3641f 100644 --- a/s3api/controllers/object-put_test.go +++ b/s3api/controllers/object-put_test.go @@ -439,7 +439,7 @@ func TestS3ApiController_UploadPart(t *testing.T) { BucketOwner: "root", }, }, - err: s3err.GetAPIError(s3err.ErrInvalidPartNumber), + err: s3err.GetInvalidArgumentErr(s3err.InvalidArgPartNumber, "-2"), }, }, { @@ -620,7 +620,7 @@ func TestS3ApiController_UploadPartCopy(t *testing.T) { BucketOwner: "root", }, }, - err: s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding), + err: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "bad%G1"), }, }, { @@ -661,7 +661,7 @@ func TestS3ApiController_UploadPartCopy(t *testing.T) { BucketOwner: "root", }, }, - err: s3err.GetAPIError(s3err.ErrInvalidPartNumber), + err: s3err.GetInvalidArgumentErr(s3err.InvalidArgPartNumber, "-2"), }, }, { @@ -860,7 +860,7 @@ func TestS3ApiController_CopyObject(t *testing.T) { BucketOwner: "root", }, }, - err: s3err.GetAPIError(s3err.ErrInvalidCopySourceBucket), + err: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceBucket, ""), }, }, { @@ -896,7 +896,7 @@ func TestS3ApiController_CopyObject(t *testing.T) { BucketOwner: "root", }, }, - err: s3err.GetAPIError(s3err.ErrMetadataTooLarge), + err: s3err.GetMetadataTooLargeErr(2051, 2048), }, }, { @@ -916,7 +916,7 @@ func TestS3ApiController_CopyObject(t *testing.T) { BucketOwner: "root", }, }, - err: s3err.GetAPIError(s3err.ErrInvalidMetadataDirective), + err: s3err.GetInvalidArgumentErr(s3err.InvalidArgMetadataDirective, "invalid_metadat_directive"), }, }, { @@ -934,7 +934,7 @@ func TestS3ApiController_CopyObject(t *testing.T) { BucketOwner: "root", }, }, - err: s3err.GetAPIError(s3err.ErrInvalidTaggingDirective), + err: s3err.GetInvalidArgumentErr(s3err.InvalidArgTaggingDirective, "invalid_tagging_directive"), }, }, { @@ -970,7 +970,7 @@ func TestS3ApiController_CopyObject(t *testing.T) { BucketOwner: "root", }, }, - err: s3err.GetAPIError(s3err.ErrObjectLockInvalidHeaders), + err: s3err.GetInvalidArgumentErr(s3err.InvalidArgMissingObjectLockRetainDate, ""), }, }, { @@ -1159,7 +1159,7 @@ func TestS3ApiController_PutObject(t *testing.T) { BucketOwner: "root", }, }, - err: s3err.GetAPIError(s3err.ErrMetadataTooLarge), + err: s3err.GetMetadataTooLargeErr(2059, 2048), }, }, { @@ -1177,7 +1177,7 @@ func TestS3ApiController_PutObject(t *testing.T) { BucketOwner: "root", }, }, - err: s3err.GetAPIError(s3err.ErrObjectLockInvalidHeaders), + err: s3err.GetInvalidArgumentErr(s3err.InvalidArgMissingObjectLockRetainDate, ""), }, }, { diff --git a/s3api/controllers/options.go b/s3api/controllers/options.go index a50ae701..a78dd596 100644 --- a/s3api/controllers/options.go +++ b/s3api/controllers/options.go @@ -16,6 +16,7 @@ package controllers import ( "errors" + "net/http" "github.com/gofiber/fiber/v2" "github.com/versity/versitygw/auth" @@ -32,6 +33,7 @@ func (s S3ApiController) CORSOptions(ctx *fiber.Ctx) (*Response, error) { origin := ctx.Get("Origin") method := auth.CORSHTTPMethod(ctx.Get("Access-Control-Request-Method")) headers := ctx.Get("Access-Control-Request-Headers") + resourceType := utils.DetectResourceType(ctx) // Origin is required if origin == "" { @@ -67,7 +69,8 @@ func (s S3ApiController) CORSOptions(ctx *fiber.Ctx) (*Response, error) { if err != nil { debuglogger.Logf("failed to get bucket cors: %v", err) if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchCORSConfiguration)) { - err = s3err.GetAPIError(s3err.ErrCORSIsNotEnabled) + // weirdly s3 always returns BUCKET resource type + err = s3err.GetAccessForbiddenErr(s3err.ErrCORSIsNotEnabled, http.MethodOptions, s3err.ResourceTypeBucket) debuglogger.Logf("bucket cors is not set: %v", err) } return &Response{ @@ -86,7 +89,7 @@ func (s S3ApiController) CORSOptions(ctx *fiber.Ctx) (*Response, error) { }, err } - allowConfig, err := corsConfig.IsAllowed(origin, method, parsedHeaders) + allowConfig, err := corsConfig.IsAllowed(origin, method, parsedHeaders, resourceType) if err != nil { debuglogger.Logf("cors access forbidden: %v", err) return &Response{ diff --git a/s3api/controllers/options_test.go b/s3api/controllers/options_test.go index 492870fc..89be795a 100644 --- a/s3api/controllers/options_test.go +++ b/s3api/controllers/options_test.go @@ -143,7 +143,7 @@ func TestS3ApiController_CORSOptions(t *testing.T) { BucketOwner: "root", }, }, - err: s3err.GetAPIError(s3err.ErrCORSIsNotEnabled), + err: s3err.GetAccessForbiddenErr(s3err.ErrCORSIsNotEnabled, http.MethodOptions, s3err.ResourceTypeBucket), }, }, { @@ -183,7 +183,7 @@ func TestS3ApiController_CORSOptions(t *testing.T) { BucketOwner: "root", }, }, - err: s3err.GetAPIError(s3err.ErrCORSForbidden), + err: s3err.GetAccessForbiddenErr(s3err.ErrCORSForbidden, http.MethodOptions, s3err.ResourceTypeObject), }, }, { diff --git a/s3api/middlewares/apply-bucket-cors-preflight.go b/s3api/middlewares/apply-bucket-cors-preflight.go index 8ffa99c4..051a1d2f 100644 --- a/s3api/middlewares/apply-bucket-cors-preflight.go +++ b/s3api/middlewares/apply-bucket-cors-preflight.go @@ -43,7 +43,7 @@ func ApplyBucketCORSPreflightFallback(be backend.Backend, fallbackOrigin string) bucket := ctx.Params("bucket") _, err := be.GetBucketCors(ctx.Context(), bucket) if err != nil { - if s3Err, ok := err.(s3err.APIError); ok && (s3Err.Code == "NoSuchCORSConfiguration" || s3Err.Code == "NoSuchBucket") { + if s3Err, ok := err.(s3err.S3Error); ok && (s3Err.BaseError().Code == "NoSuchCORSConfiguration" || s3Err.BaseError().Code == "NoSuchBucket") { if len(ctx.Response().Header.Peek("Access-Control-Allow-Origin")) == 0 { ctx.Response().Header.Add("Access-Control-Allow-Origin", fallbackOrigin) } diff --git a/s3api/middlewares/apply-bucket-cors.go b/s3api/middlewares/apply-bucket-cors.go index 930dd161..9a19543e 100644 --- a/s3api/middlewares/apply-bucket-cors.go +++ b/s3api/middlewares/apply-bucket-cors.go @@ -48,8 +48,8 @@ func ApplyBucketCORS(be backend.Backend, fallbackOrigin string) fiber.Handler { if err != nil { // If CORS is not configured, S3Error will have code NoSuchCORSConfiguration. // In this case, we can safely continue. For any other error, we should log it. - s3Err, ok := err.(s3err.APIError) - if ok && (s3Err.Code == "NoSuchCORSConfiguration" || s3Err.Code == "NoSuchBucket") { + s3Err, ok := err.(s3err.S3Error) + if ok && (s3Err.BaseError().Code == "NoSuchCORSConfiguration" || s3Err.BaseError().Code == "NoSuchBucket") { // Optional global fallback: add Access-Control-Allow-Origin for buckets // without a specific CORS configuration. if fallbackOrigin != "" { @@ -63,7 +63,7 @@ func ApplyBucketCORS(be backend.Backend, fallbackOrigin string) fiber.Handler { } return nil } - if !ok || s3Err.Code != "NoSuchCORSConfiguration" { + if !ok || s3Err.BaseError().Code != "NoSuchCORSConfiguration" { debuglogger.Logf("failed to get bucket cors for bucket %q: %v", bucket, err) } return nil @@ -99,7 +99,7 @@ func ApplyBucketCORS(be backend.Backend, fallbackOrigin string) fiber.Handler { return err } - allowConfig, err := cors.IsAllowed(origin, method, parsedHeaders) + allowConfig, err := cors.IsAllowed(origin, method, parsedHeaders, "") if err != nil { // if bucket cors rules doesn't grant access, skip // and don't add any response headers diff --git a/s3api/middlewares/authentication.go b/s3api/middlewares/authentication.go index f55345c3..ace622a4 100644 --- a/s3api/middlewares/authentication.go +++ b/s3api/middlewares/authentication.go @@ -75,7 +75,7 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, region string, authorization := ctx.Get("Authorization") if authorization == "" { - return s3err.GetAPIError(s3err.ErrInvalidAuthHeader) + return s3err.GetInvalidArgumentErr(s3err.InvalidArgAuthHeader, authorization) } authData, err := utils.ParseAuthorization(authorization) @@ -91,7 +91,7 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, region string, account, err := acct.getAccount(authData.Access) if err == auth.ErrNoSuchUser { - return s3err.GetAPIError(s3err.ErrInvalidAccessKeyID) + return s3err.GetInvalidAccessKeyIdErr(authData.Access) } if err != nil { return err @@ -117,29 +117,43 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, region string, if requireContentSha256 && hashPayload == "" { return s3err.GetAPIError(s3err.ErrMissingContentSha256) } - if !utils.IsValidSh256PayloadHeader(hashPayload) { - return s3err.GetAPIError(s3err.ErrInvalidSHA256Paylod) + if !utils.IsValidSha256PayloadHeader(hashPayload) { + return s3err.GetInvalidArgumentErr(s3err.InvalidArgSHA256Payload, hashPayload) } // the streaming payload type is allowed only in PutObject and UploadPart // e.g. STREAMING-UNSIGNED-PAYLOAD-TRAILER if !streamBody && utils.IsStreamingPayload(hashPayload) { return s3err.GetAPIError(s3err.ErrInvalidSHA256PayloadUsage) } - if streamBody { - // for streaming PUT actions, authorization is deferred - // until end of stream due to need to get length and - // checksum of the stream to validate authorization - wrapBodyReader(ctx, func(r io.Reader) io.Reader { - return utils.NewAuthReader(ctx, r, authData, account.Secret) - }) + canonicalString, err := utils.CheckValidSignature(ctx, authData, account.Secret, hashPayload, tdate, contentLength) + if err != nil { + return err + } + + if streamBody { + // store the request body stream reader in context locals + wrapBodyReader(ctx, func(r io.Reader) io.Reader { + return r + }) + // wrap the io.Reader with sha256 hex hash reader, if x-amz-content-sha256 + // is the content sha256 - not a special payload type + if !utils.IsSpecialPayload(hashPayload) { + wrapBodyReader(ctx, func(r io.Reader) io.Reader { + var cr io.Reader + cr, err = utils.NewHashReader(r, hashPayload, utils.HashTypeSha256Hex) + return cr + }) + if err != nil { + return err + } + } // wrap the io.Reader with ChunkReader if x-amz-content-sha256 // provide chunk encoding value if utils.IsStreamingPayload(hashPayload) { - var err error wrapBodyReader(ctx, func(r io.Reader) io.Reader { var cr io.Reader - cr, err = utils.NewChunkReader(ctx, r, authData, account.Secret, tdate) + cr, err = utils.NewChunkReader(ctx, r, authData, canonicalString, account.Secret, tdate) return cr }) if err != nil { @@ -156,7 +170,7 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, region string, // the upload limit for big data actions: PutObject, UploadPart // is 5gb. If the size exceeds the limit, return 'EntityTooLarge' err if contentLength > maxObjSizeLimit { - return s3err.GetAPIError(s3err.ErrEntityTooLarge) + return s3err.GetEntityTooLargeErr(contentLength, maxObjSizeLimit) } return nil @@ -169,15 +183,10 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, region string, // Compare the calculated hash with the hash provided if hashPayload != hexPayload { - return s3err.GetAPIError(s3err.ErrContentSHA256Mismatch) + return s3err.GetContentSHA256MismatchErr(hashPayload, hexPayload) } } - err = utils.CheckValidSignature(ctx, authData, account.Secret, hashPayload, tdate, contentLength, false) - if err != nil { - return err - } - return nil } } diff --git a/s3api/middlewares/bucket-object-name-validator.go b/s3api/middlewares/bucket-object-name-validator.go index f099afd8..860b0e88 100644 --- a/s3api/middlewares/bucket-object-name-validator.go +++ b/s3api/middlewares/bucket-object-name-validator.go @@ -28,7 +28,7 @@ func BucketObjectNameValidator() fiber.Handler { // check if the provided bucket name is valid if !utils.IsValidBucketName(bucket) { - return s3err.GetAPIError(s3err.ErrInvalidBucketName) + return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket) } // check if the provided object name is valid diff --git a/s3api/middlewares/checksum.go b/s3api/middlewares/checksum.go index 2b182487..ca1fb5fb 100644 --- a/s3api/middlewares/checksum.go +++ b/s3api/middlewares/checksum.go @@ -44,7 +44,7 @@ func VerifyChecksums(streamBody bool, requireBody bool, requireChecksum bool) fi } if !isValidMD5(md5sum) { - return s3err.GetAPIError(s3err.ErrInvalidDigest) + return s3err.GetInvalidDigestErr(md5sum) } var err error @@ -67,7 +67,7 @@ func VerifyChecksums(streamBody bool, requireBody bool, requireChecksum bool) fi var err error if md5sum != "" { if !isValidMD5(md5sum) { - return s3err.GetAPIError(s3err.ErrInvalidDigest) + return s3err.GetInvalidDigestErr(md5sum) } rdr, err = utils.NewHashReader(bytes.NewReader(body), md5sum, utils.HashTypeContentMD5) diff --git a/s3api/middlewares/object-post-auth.go b/s3api/middlewares/object-post-auth.go index 65779423..10cabdef 100644 --- a/s3api/middlewares/object-post-auth.go +++ b/s3api/middlewares/object-post-auth.go @@ -60,7 +60,7 @@ func AuthorizePostObject(root RootUserConfig, iam auth.IAMService, region string mediaType, params, err := mime.ParseMediaType(ctx.Get("Content-Type")) if err != nil || mediaType != fiber.MIMEMultipartForm { debuglogger.Logf("invalid POST object Content-Type %q: mediaType=%q err=%v", ctx.Get("Content-Type"), mediaType, err) - return s3err.GetAPIError(s3err.ErrPreconditionFailed) + return s3err.GetPreconditionFailedErr(s3err.ConditionPostBucket) } boundary := params["boundary"] @@ -127,14 +127,14 @@ func AuthorizePostObject(root RootUserConfig, iam auth.IAMService, region string if algorithm != aws4HMACSHA256 { debuglogger.Logf("unsupported POST object signing algorithm: %s", algorithm) - return s3err.GetAPIError(s3err.ErrOnlyAws4HmacSha256) + return s3err.GetInvalidArgumentErr(s3err.InvalidArgOnlyAws4HmacSha256, algorithm) } // Parse the date and check the date validity tdate, err := time.Parse(iso8601Format, amzDate) if err != nil { debuglogger.Logf("invalid POST object x-amz-date %q: %v", amzDate, err) - return s3err.GetAPIError(s3err.ErrInvalidDateHeader) + return s3err.GetInvalidArgumentErr(s3err.InvalidArgDateHeader, amzDate) } // the signing date can't be older than an hour @@ -151,13 +151,13 @@ func AuthorizePostObject(root RootUserConfig, iam auth.IAMService, region string if region != creds.Region { debuglogger.Logf("incorrect POST object credential region: got %q want %q", creds.Region, region) - return s3err.PostAuth.IncorrectRegion(region, creds.Region) + return s3err.PostAuth.IncorrectRegion(credentialStr, region, creds.Region) } account, err := acct.getAccount(creds.Access) if err == auth.ErrNoSuchUser { debuglogger.Logf("POST object access key not found: %s", creds.Access) - return s3err.GetAPIError(s3err.ErrInvalidAccessKeyID) + return s3err.GetInvalidAccessKeyIdErr(creds.Access) } if err != nil { debuglogger.Logf("failed to resolve POST object account %q: %v", creds.Access, err) @@ -174,7 +174,10 @@ func AuthorizePostObject(root RootUserConfig, iam auth.IAMService, region string if expectedSig != signatureHex { debuglogger.Logf("POST object signature mismatch: expected %s got %s", expectedSig, signatureHex) - return s3err.GetAPIError(s3err.ErrSignatureDoesNotMatch) + // The String to sign for POST request is the base64 encoded policy + // For POST incorrect signature no canonical request and canonical request bytes are returned + // as the calculation is not based on canonical request string + return s3err.GetSignatureDoesNotMatchErr(account.Access, policyB64, signatureHex, utils.HexBytes(policyB64), "", "") } // Mark this request as authenticated so that diff --git a/s3api/middlewares/object-post-auth_test.go b/s3api/middlewares/object-post-auth_test.go index a26cb6d9..429bd0f5 100644 --- a/s3api/middlewares/object-post-auth_test.go +++ b/s3api/middlewares/object-post-auth_test.go @@ -52,8 +52,8 @@ func chainHandlers(handlers ...fiber.Handler) fiber.Handler { func postObjectTestApp(root RootUserConfig, region string, next fiber.Handler) *fiber.App { app := fiber.New(fiber.Config{ ErrorHandler: func(c *fiber.Ctx, err error) error { - if apiErr, ok := err.(s3err.APIError); ok { - return c.Status(apiErr.HTTPStatusCode).SendString(apiErr.Code) + if s3Err, ok := err.(s3err.S3Error); ok { + return c.Status(s3Err.StatusCode()).Send(s3Err.XMLBody("", "")) } return c.Status(500).SendString(err.Error()) }, diff --git a/s3api/middlewares/presign-auth.go b/s3api/middlewares/presign-auth.go index 23de0aef..7e086aa1 100644 --- a/s3api/middlewares/presign-auth.go +++ b/s3api/middlewares/presign-auth.go @@ -58,7 +58,7 @@ func VerifyPresignedV4Signature(root RootUserConfig, iam auth.IAMService, region account, err := acct.getAccount(authData.Access) if err == auth.ErrNoSuchUser { - return s3err.GetAPIError(s3err.ErrInvalidAccessKeyID) + return s3err.GetInvalidAccessKeyIdErr(authData.Access) } if err != nil { return err @@ -75,7 +75,15 @@ func VerifyPresignedV4Signature(root RootUserConfig, iam auth.IAMService, region } } + err = utils.CheckPresignedSignature(ctx, authData, account.Secret) + if err != nil { + return err + } + if streamBody { + wrapBodyReader(ctx, func(r io.Reader) io.Reader { + return r + }) // Content-Length has to be set for data uploads: PutObject, UploadPart if contentLengthStr == "" { return s3err.GetAPIError(s3err.ErrMissingContentLength) @@ -83,18 +91,8 @@ func VerifyPresignedV4Signature(root RootUserConfig, iam auth.IAMService, region // the upload limit for big data actions: PutObject, UploadPart // is 5gb. If the size exceeds the limit, return 'EntityTooLarge' err if contentLength > maxObjSizeLimit { - return s3err.GetAPIError(s3err.ErrEntityTooLarge) + return s3err.GetEntityTooLargeErr(contentLength, maxObjSizeLimit) } - wrapBodyReader(ctx, func(r io.Reader) io.Reader { - return utils.NewPresignedAuthReader(ctx, r, authData, account.Secret) - }) - - return nil - } - - err = utils.CheckPresignedSignature(ctx, authData, account.Secret, streamBody) - if err != nil { - return err } return nil diff --git a/s3api/middlewares/public-bucket.go b/s3api/middlewares/public-bucket.go index f0fd37fa..3a74bef1 100644 --- a/s3api/middlewares/public-bucket.go +++ b/s3api/middlewares/public-bucket.go @@ -119,7 +119,7 @@ func AuthorizePublicBucketAccess(be backend.Backend, s3action string, policyPerm // Compare the calculated hash with the hash provided if payloadHash != hexPayload { - return s3err.GetAPIError(s3err.ErrContentSHA256Mismatch) + return s3err.GetContentSHA256MismatchErr(payloadHash, hexPayload) } } diff --git a/s3api/middlewares/rate-limiter.go b/s3api/middlewares/rate-limiter.go index 9402eefd..c34be084 100644 --- a/s3api/middlewares/rate-limiter.go +++ b/s3api/middlewares/rate-limiter.go @@ -17,6 +17,7 @@ package middlewares import ( "github.com/gofiber/fiber/v2" "github.com/versity/versitygw/metrics" + "github.com/versity/versitygw/s3api/utils" "github.com/versity/versitygw/s3err" "github.com/versity/versitygw/s3log" "golang.org/x/sync/semaphore" @@ -28,6 +29,8 @@ func RateLimiter(limit int, mm metrics.Manager, logger s3log.AuditLogger) fiber. sem := semaphore.NewWeighted(int64(limit)) return func(ctx *fiber.Ctx) error { + requestID, hostID := utils.EnsureRequestIDs(ctx) + if !sem.TryAcquire(1) { // limit reached err := s3err.GetAPIError(s3err.ErrSlowDown) @@ -42,7 +45,7 @@ func RateLimiter(limit int, mm metrics.Manager, logger s3log.AuditLogger) fiber. } ctx.Status(err.HTTPStatusCode) - return ctx.Send(s3err.GetAPIErrorResponse(err, "", "", "")) + return ctx.Send(err.XMLBody(requestID, hostID)) } defer sem.Release(1) return ctx.Next() diff --git a/s3api/middlewares/request-ids.go b/s3api/middlewares/request-ids.go new file mode 100644 index 00000000..f4597692 --- /dev/null +++ b/s3api/middlewares/request-ids.go @@ -0,0 +1,28 @@ +// 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 middlewares + +import ( + "github.com/gofiber/fiber/v2" + "github.com/versity/versitygw/s3api/utils" +) + +// RequestIDs sets requestID and hostID in context locals +func RequestIDs() fiber.Handler { + return func(ctx *fiber.Ctx) error { + utils.EnsureRequestIDs(ctx) + return ctx.Next() + } +} diff --git a/s3api/middlewares/request-ids_test.go b/s3api/middlewares/request-ids_test.go new file mode 100644 index 00000000..26683e1b --- /dev/null +++ b/s3api/middlewares/request-ids_test.go @@ -0,0 +1,46 @@ +// 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 middlewares + +import ( + "net/http" + "net/http/httptest" + "regexp" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/stretchr/testify/assert" + "github.com/versity/versitygw/s3api/utils" +) + +func TestRequestIDs(t *testing.T) { + app := fiber.New() + app.Use(RequestIDs()) + app.Get("/", func(ctx *fiber.Ctx) error { + assert.NotEmpty(t, utils.RequestID(ctx)) + assert.NotEmpty(t, utils.HostID(ctx)) + return ctx.SendStatus(http.StatusNoContent) + }) + + resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/", nil)) + assert.NoError(t, err) + + requestID := resp.Header.Get(utils.HeaderAmzRequestID) + hostID := resp.Header.Get(utils.HeaderAmzID2) + + assert.Regexp(t, regexp.MustCompile(`^[0-9A-Z]{16}$`), requestID) + assert.NotEmpty(t, hostID) + assert.NotEqual(t, requestID, hostID) +} diff --git a/s3api/router.go b/s3api/router.go index 78610d2a..b1cfdd98 100644 --- a/s3api/router.go +++ b/s3api/router.go @@ -15,6 +15,8 @@ package s3api import ( + "net/http" + "github.com/gofiber/fiber/v2" "github.com/versity/versitygw/auth" "github.com/versity/versitygw/backend" @@ -156,7 +158,9 @@ func (sa *S3ApiRouter) Init() { // copy source is not allowed on '/' sa.app.Get("/", middlewares.MatchHeader("X-Amz-Copy-Source"), controllers.ProcessHandlers( - ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), + func(ctx *fiber.Ctx) (*controllers.Response, error) { + return &controllers.Response{}, s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySource, ctx.Get("X-Amz-Copy-Source")) + }, metrics.ActionUndetected, services, middlewares.ApplyDefaultCORS(sa.corsAllowOrigin), @@ -467,7 +471,13 @@ func (sa *S3ApiRouter) Init() { // copy source is not allowed on bucket HEAD operation bucketRouter.Head("/", middlewares.MatchHeader("X-Amz-Copy-Source"), - controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), metrics.ActionUndetected, services), + controllers.ProcessHandlers( + func(ctx *fiber.Ctx) (*controllers.Response, error) { + return &controllers.Response{}, s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySource, ctx.Get("X-Amz-Copy-Source")) + }, + metrics.ActionUndetected, + services, + ), ) bucketRouter.Head("", @@ -487,7 +497,13 @@ func (sa *S3ApiRouter) Init() { // copy source is not allowed on bucket DELETE operation bucketRouter.Delete("/", middlewares.MatchHeader("X-Amz-Copy-Source"), - controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), metrics.ActionUndetected, services), + controllers.ProcessHandlers( + func(ctx *fiber.Ctx) (*controllers.Response, error) { + return &controllers.Response{}, s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySource, ctx.Get("X-Amz-Copy-Source")) + }, + metrics.ActionUndetected, + services, + ), ) bucketRouter.Delete("", @@ -676,7 +692,13 @@ func (sa *S3ApiRouter) Init() { // copy source is not allowed on bucket GET operation bucketRouter.Get("/", middlewares.MatchHeader("X-Amz-Copy-Source"), - controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), metrics.ActionUndetected, services), + controllers.ProcessHandlers( + func(ctx *fiber.Ctx) (*controllers.Response, error) { + return &controllers.Response{}, s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySource, ctx.Get("X-Amz-Copy-Source")) + }, + metrics.ActionUndetected, + services, + ), ) bucketRouter.Get("", @@ -1074,7 +1096,13 @@ func (sa *S3ApiRouter) Init() { bucketRouter.Post("/", middlewares.MatchHeader("X-Amz-Copy-Source"), middlewares.MatchQueryArgs("uploadId"), - controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), metrics.ActionUndetected, services), + controllers.ProcessHandlers( + func(ctx *fiber.Ctx) (*controllers.Response, error) { + return &controllers.Response{}, s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySource, ctx.Get("X-Amz-Copy-Source")) + }, + metrics.ActionUndetected, + services, + ), ) // DeleteObjects action @@ -1108,7 +1136,13 @@ func (sa *S3ApiRouter) Init() { // object HEAD operation is not allowed with copy source objectRouter.Head("/", middlewares.MatchHeader("X-Amz-Copy-Source"), - controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), metrics.ActionUndetected, services), + controllers.ProcessHandlers( + func(ctx *fiber.Ctx) (*controllers.Response, error) { + return &controllers.Response{}, s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySource, ctx.Get("X-Amz-Copy-Source")) + }, + metrics.ActionUndetected, + services, + ), ) // HeadObject @@ -1142,7 +1176,13 @@ func (sa *S3ApiRouter) Init() { // object GET operation is not allowed with copy source objectRouter.Get("/", middlewares.MatchHeader("X-Amz-Copy-Source"), - controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), metrics.ActionUndetected, services), + controllers.ProcessHandlers( + func(ctx *fiber.Ctx) (*controllers.Response, error) { + return &controllers.Response{}, s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySource, ctx.Get("X-Amz-Copy-Source")) + }, + metrics.ActionUndetected, + services, + ), ) objectRouter.Get("", @@ -1241,7 +1281,13 @@ func (sa *S3ApiRouter) Init() { // object DELETE operation is not allowed with copy source objectRouter.Delete("/", middlewares.MatchHeader("X-Amz-Copy-Source"), - controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), metrics.ActionUndetected, services), + controllers.ProcessHandlers( + func(ctx *fiber.Ctx) (*controllers.Response, error) { + return &controllers.Response{}, s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySource, ctx.Get("X-Amz-Copy-Source")) + }, + metrics.ActionUndetected, + services, + ), ) objectRouter.Delete("", @@ -1289,7 +1335,13 @@ func (sa *S3ApiRouter) Init() { objectRouter.Post("/", middlewares.MatchHeader("X-Amz-Copy-Source"), middlewares.MatchQueryArgs("uploadId"), - controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), metrics.ActionUndetected, services), + controllers.ProcessHandlers( + func(ctx *fiber.Ctx) (*controllers.Response, error) { + return &controllers.Response{}, s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySource, ctx.Get("X-Amz-Copy-Source")) + }, + metrics.ActionUndetected, + services, + ), ) objectRouter.Post("", @@ -1437,13 +1489,28 @@ func (sa *S3ApiRouter) Init() { // return error if partNumber is used without uploadId objectRouter.Put("", middlewares.MatchQueryArgs("partNumber"), - controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrMissingUploadId)), metrics.ActionUndetected, services)) + controllers.ProcessHandlers( + ctrl.HandleErrorRoute(s3err.GetInvalidArgumentErr(s3err.InvalidArgMissingUploadId, "partNumber")), + metrics.ActionUndetected, + services, + ), + ) // return 'MethodNotAllowed' if uploadId is provided without partNumber // before the router reaches to 'PutObject' objectRouter.Put("", middlewares.MatchQueryArgs("uploadId"), - controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrMethodNotAllowed)), metrics.ActionUndetected, services)) + controllers.ProcessHandlers( + ctrl.HandleErrorRoute( + s3err.GetMethodNotAllowedErr( + http.MethodPut, + s3err.ResourceTypeUpload, + []string{http.MethodDelete, http.MethodPost, http.MethodGet}, + ), + ), + metrics.ActionUndetected, + services, + )) objectRouter.Put("", middlewares.MatchHeader("X-Amz-Copy-Source"), @@ -1489,5 +1556,7 @@ func (sa *S3ApiRouter) Init() { ) // Return MethodNotAllowed for all the unmatched routes - sa.app.All("*", controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrMethodNotAllowed)), metrics.ActionUndetected, services)) + sa.app.All("*", controllers.ProcessHandlers(func(ctx *fiber.Ctx) (*controllers.Response, error) { + return &controllers.Response{}, s3err.GetMethodNotAllowedErr(ctx.Method(), s3err.ResourceTypeService, nil) + }, metrics.ActionUndetected, services)) } diff --git a/s3api/server.go b/s3api/server.go index 84943091..482786e7 100644 --- a/s3api/server.go +++ b/s3api/server.go @@ -131,6 +131,10 @@ func New( Format: "${time} | vgw | ${status} | ${latency} | ${ip} | ${method} | ${path} | ${error} | ${queryParams}\n", })) } + + // initialize requestId middleware + app.Use(middlewares.RequestIDs()) + // Set up health endpoint if specified if server.health != "" { app.Get(server.health, func(ctx *fiber.Ctx) error { @@ -379,6 +383,8 @@ func stackTraceHandler(ctx *fiber.Ctx, e any) { // globalErrorHandler catches the errors before reaching to // the handlers and any system panics func globalErrorHandler(ctx *fiber.Ctx, er error) error { + requestID, hostID := utils.EnsureRequestIDs(ctx) + // set content type to application/xml ctx.Response().Header.SetContentType(fiber.MIMEApplicationXML) @@ -406,8 +412,7 @@ func globalErrorHandler(ctx *fiber.Ctx, er error) error { // 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, "", "", "")) + return ctx.Status(err.StatusCode()).Send(err.XMLBody(requestID, hostID)) } } @@ -417,6 +422,5 @@ func globalErrorHandler(ctx *fiber.Ctx, er error) error { ctx.Status(http.StatusInternalServerError) - return ctx.Send(s3err.GetAPIErrorResponse( - s3err.GetAPIError(s3err.ErrInternalError), "", "", "")) + return ctx.Send(s3err.GetAPIError(s3err.ErrInternalError).XMLBody(requestID, hostID)) } diff --git a/s3api/utils/auth-reader.go b/s3api/utils/auth-reader.go index e7a845bf..3a967b97 100644 --- a/s3api/utils/auth-reader.go +++ b/s3api/utils/auth-reader.go @@ -18,9 +18,7 @@ import ( "crypto/hmac" "crypto/sha256" "encoding/hex" - "errors" "fmt" - "io" "os" "strings" "time" @@ -39,77 +37,15 @@ const ( yyyymmdd = "20060102" ) -// AuthReader is an io.Reader that validates the request authorization -// once the underlying reader returns io.EOF. This is needed for streaming -// data requests where the data size and checksum are not known until -// the data is completely read. -type AuthReader struct { - ctx *fiber.Ctx - auth AuthData - secret string - size int - r *HashReader -} +func HexBytes(s string) string { + b := []byte(s) // raw UTF-8 bytes -// NewAuthReader initializes an io.Reader that will verify the request -// v4 auth when the underlying reader returns io.EOF. This postpones the -// authorization check until the reader is consumed. So it is important that -// the consumer of this reader checks for the auth errors while reading. -func NewAuthReader(ctx *fiber.Ctx, r io.Reader, auth AuthData, secret string) *AuthReader { - var hr *HashReader - hashPayload := ctx.Get("X-Amz-Content-Sha256") - if !IsSpecialPayload(hashPayload) { - hr, _ = NewHashReader(r, "", HashTypeSha256Hex) - } else { - hr, _ = NewHashReader(r, "", HashTypeNone) + parts := make([]string, len(b)) + for i, v := range b { + parts[i] = fmt.Sprintf("%02x", v) } - return &AuthReader{ - ctx: ctx, - r: hr, - auth: auth, - secret: secret, - } -} - -// Read allows *AuthReader to be used as an io.Reader -func (ar *AuthReader) Read(p []byte) (int, error) { - n, err := ar.r.Read(p) - ar.size += n - - if errors.Is(err, io.EOF) { - verr := ar.validateSignature() - if verr != nil { - return n, verr - } - } - - return n, err -} - -func (ar *AuthReader) validateSignature() error { - date := ar.ctx.Get("X-Amz-Date") - if date == "" { - return s3err.GetAPIError(s3err.ErrMissingDateHeader) - } - - hashPayload := ar.ctx.Get("X-Amz-Content-Sha256") - if !IsSpecialPayload(hashPayload) { - hexPayload := ar.r.Sum() - - // Compare the calculated hash with the hash provided - if hashPayload != hexPayload { - return s3err.GetAPIError(s3err.ErrContentSHA256Mismatch) - } - } - - // Parse the date and check the date validity - tdate, err := time.Parse(iso8601Format, date) - if err != nil { - return s3err.GetAPIError(s3err.ErrMissingDateHeader) - } - - return CheckValidSignature(ar.ctx, ar.auth, ar.secret, hashPayload, tdate, int64(ar.size), true) + return strings.Join(parts, " ") } const ( @@ -117,18 +53,18 @@ const ( ) // CheckValidSignature validates the ctx v4 auth signature -func CheckValidSignature(ctx *fiber.Ctx, auth AuthData, secret, checksum string, tdate time.Time, contentLen int64, streamBody bool) error { +func CheckValidSignature(ctx *fiber.Ctx, auth AuthData, secret, checksum string, tdate time.Time, contentLen int64) (string, error) { signedHdrs := strings.Split(auth.SignedHeaders, ";") // Create a new http request instance from fasthttp request - req, err := createHttpRequestFromCtx(ctx, signedHdrs, contentLen, streamBody) + req, err := createHttpRequestFromCtx(ctx, signedHdrs, contentLen) if err != nil { - return fmt.Errorf("create http request from context: %w", err) + return "", fmt.Errorf("create http request from context: %w", err) } signer := v4.NewSigner() - signErr := signer.SignHTTP(req.Context(), + signMeta, err := signer.SignHTTP(req.Context(), aws.Credentials{ AccessKeyID: auth.Access, SecretAccessKey: secret, @@ -141,20 +77,27 @@ func CheckValidSignature(ctx *fiber.Ctx, auth AuthData, secret, checksum string, options.Logger = logging.NewStandardLogger(os.Stderr) } }) - if signErr != nil { - return fmt.Errorf("sign generated http request: %w", err) + if err != nil { + return "", fmt.Errorf("sign generated http request: %w", err) } genAuth, err := ParseAuthorization(req.Header.Get("Authorization")) if err != nil { - return err + return "", err } if auth.Signature != genAuth.Signature { - return s3err.GetAPIError(s3err.ErrSignatureDoesNotMatch) + return "", s3err.GetSignatureDoesNotMatchErr( + auth.Access, + signMeta.StringToSign, + auth.Signature, + HexBytes(signMeta.StringToSign), + signMeta.CanonicalString, + HexBytes(signMeta.CanonicalString), + ) } - return nil + return signMeta.CanonicalString, nil } // AuthData is the parsed authorization data from the header @@ -187,7 +130,7 @@ func ParseAuthorization(authorization string) (AuthData, error) { } if len(authParts) < 2 { - return a, s3err.GetAPIError(s3err.ErrInvalidAuthHeader) + return a, s3err.GetInvalidArgumentErr(s3err.InvalidArgAuthHeader, authorization) } algo := authParts[0] @@ -196,7 +139,7 @@ func ParseAuthorization(authorization string) (AuthData, error) { return a, s3err.GetAPIError(s3err.ErrUnsupportedAuthorizationMechanism) } if algo != "AWS4-HMAC-SHA256" { - return a, s3err.GetAPIError(s3err.ErrUnsupportedAuthorizationType) + return a, s3err.GetInvalidArgumentErr(s3err.InvalidArgAuthorizationType, algo) } kvData := authParts[1] @@ -263,26 +206,26 @@ type CredentialsScope struct { } type CredsError interface { - MalformedCredential() s3err.APIError - IncorrectService(string) s3err.APIError - IncorrectTerminal(string) s3err.APIError - InvalidDateFormat(string) s3err.APIError + MalformedCredential(string) s3err.S3Error + IncorrectService(string, string) s3err.S3Error + IncorrectTerminal(string, string) s3err.S3Error + InvalidDateFormat(string, string) s3err.S3Error } func ParseCredentials(input string, errHandler CredsError) (*CredentialsScope, error) { creds := strings.Split(input, "/") if len(creds) != 5 { - return nil, errHandler.MalformedCredential() + return nil, errHandler.MalformedCredential(input) } if creds[3] != "s3" { - return nil, errHandler.IncorrectService(creds[3]) + return nil, errHandler.IncorrectService(input, creds[3]) } if creds[4] != "aws4_request" { - return nil, errHandler.IncorrectTerminal(creds[4]) + return nil, errHandler.IncorrectTerminal(input, creds[4]) } _, err := time.Parse(yyyymmdd, creds[1]) if err != nil { - return nil, errHandler.InvalidDateFormat(creds[1]) + return nil, errHandler.InvalidDateFormat(input, creds[1]) } return &CredentialsScope{ Access: creds[0], diff --git a/s3api/utils/auth_test.go b/s3api/utils/auth_test.go index 995243de..20085b00 100644 --- a/s3api/utils/auth_test.go +++ b/s3api/utils/auth_test.go @@ -92,7 +92,7 @@ func Test_Client_UserAgent(t *testing.T) { } app.Get("/", func(c *fiber.Ctx) error { - req, err := createHttpRequestFromCtx(c, signedHdrs, int64(c.Request().Header.ContentLength()), true) + req, err := createHttpRequestFromCtx(c, signedHdrs, int64(c.Request().Header.ContentLength())) if err != nil { t.Fatal(err) } @@ -102,7 +102,7 @@ func Test_Client_UserAgent(t *testing.T) { signer := v4.NewSigner() - signErr := signer.SignHTTP(req.Context(), + _, signErr := signer.SignHTTP(req.Context(), aws.Credentials{ AccessKeyID: access, SecretAccessKey: secret, diff --git a/s3api/utils/chunk-reader.go b/s3api/utils/chunk-reader.go index 14f61da2..038a6273 100644 --- a/s3api/utils/chunk-reader.go +++ b/s3api/utils/chunk-reader.go @@ -116,15 +116,15 @@ func IsSpecialPayload(str string) bool { return specialValues[payloadType(str)] } -// IsValidSh256PayloadHeader checks if the provided x-amz-content-sha256 -// paylod header is valid special paylod type or a valid sh256 hash -func IsValidSh256PayloadHeader(value string) bool { +// IsValidSha256PayloadHeader checks if the provided x-amz-content-sha256 +// payload header is valid special payload type or a valid sh256 hash +func IsValidSha256PayloadHeader(value string) bool { // empty header is valid if value == "" { return true } // special values are valid - if specialValues[payloadType(value)] { + if IsSpecialPayload(value) { return true } @@ -162,7 +162,7 @@ func IsUnsignedPaylod(hash string) bool { return hash == string(payloadTypeUnsigned) } -// IsChunkEncoding checks for streaming/unsigned authorization types +// IsStreamingPayload checks for streaming/unsigned authorization types func IsStreamingPayload(str string) bool { pt := payloadType(str) return pt == payloadTypeStreamingUnsignedTrailer || @@ -186,13 +186,13 @@ func ParseDecodedContentLength(ctx *fiber.Ctx) (int64, error) { if decContLength > maxObjSizeLimit { debuglogger.Logf("the object size exceeds the allowed limit: (size): %v, (limit): %v", decContLength, int64(maxObjSizeLimit)) - return 0, s3err.GetAPIError(s3err.ErrEntityTooLarge) + return 0, s3err.GetEntityTooLargeErr(decContLength, maxObjSizeLimit) } return decContLength, nil } -func NewChunkReader(ctx *fiber.Ctx, r io.Reader, authdata AuthData, secret string, date time.Time) (io.Reader, error) { +func NewChunkReader(ctx *fiber.Ctx, r io.Reader, authdata AuthData, canonicalString, secret string, date time.Time) (io.Reader, error) { cLength, err := ParseDecodedContentLength(ctx) if err != nil { return nil, err @@ -214,9 +214,9 @@ func NewChunkReader(ctx *fiber.Ctx, r io.Reader, authdata AuthData, secret strin case payloadTypeStreamingUnsignedTrailer: return NewUnsignedChunkReader(r, checksumType, cLength) case payloadTypeStreamingSignedTrailer: - return NewSignedChunkReader(r, authdata, secret, date, checksumType, true, cLength) + return NewSignedChunkReader(r, authdata, canonicalString, secret, date, checksumType, true, cLength) case payloadTypeStreamingSigned: - return NewSignedChunkReader(r, authdata, secret, date, "", false, cLength) + return NewSignedChunkReader(r, authdata, canonicalString, secret, date, "", false, cLength) // return not supported for: // - STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD // - STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD-TRAILER diff --git a/s3api/utils/chunk-reader_test.go b/s3api/utils/chunk-reader_test.go index b0f8d26f..5f7c78a0 100644 --- a/s3api/utils/chunk-reader_test.go +++ b/s3api/utils/chunk-reader_test.go @@ -16,7 +16,7 @@ package utils import "testing" -func TestIsValidSh256PayloadHeader(t *testing.T) { +func TestIsValidSha256PayloadHeader(t *testing.T) { tests := []struct { name string hash string @@ -35,8 +35,8 @@ func TestIsValidSh256PayloadHeader(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := IsValidSh256PayloadHeader(tt.hash); got != tt.want { - t.Errorf("IsValidSh256PayloadHeader() = %v, want %v", got, tt.want) + if got := IsValidSha256PayloadHeader(tt.hash); got != tt.want { + t.Errorf("IsValidSha256PayloadHeader() = %v, want %v", got, tt.want) } }) } diff --git a/s3api/utils/context-keys.go b/s3api/utils/context-keys.go index d3503adc..dd642efa 100644 --- a/s3api/utils/context-keys.go +++ b/s3api/utils/context-keys.go @@ -38,24 +38,10 @@ const ( ContextKeyStack ContextKey = "stack" ContextKeyBucketOwner ContextKey = "bucket-owner" ContextKeyObjectPostResult ContextKey = "object-post-result" + ContextKeyRequestID ContextKey = "request-id" + ContextKeyHostID ContextKey = "host-id" ) -func (ck ContextKey) Values() []ContextKey { - return []ContextKey{ - ContextKeyRegion, - ContextKeyStartTime, - ContextKeyIsRoot, - ContextKeyRootAccessKey, - ContextKeyAccount, - ContextKeyAuthenticated, - ContextKeyPublicBucket, - ContextKeyParsedAcl, - ContextKeySkipResBodyLog, - ContextKeyBodyReader, - ContextKeyBucketOwner, - } -} - func (ck ContextKey) Set(ctx *fiber.Ctx, val any) { ctx.Locals(string(ck), val) } diff --git a/s3api/utils/csum-reader.go b/s3api/utils/csum-reader.go index d34efd5e..ec1964d5 100644 --- a/s3api/utils/csum-reader.go +++ b/s3api/utils/csum-reader.go @@ -136,7 +136,7 @@ func (hr *HashReader) Read(p []byte) (int, error) { case HashTypeContentMD5: sum := hr.Sum() if sum != hr.sum { - return n, s3err.GetAPIError(s3err.ErrBadDigest) + return n, s3err.GetBadDigestErr(sum, hr.base64ToHex(hr.sum)) } case HashTypeMd5: sum := hr.Sum() @@ -146,7 +146,7 @@ func (hr *HashReader) Read(p []byte) (int, error) { case HashTypeSha256Hex: sum := hr.Sum() if sum != hr.sum { - return n, s3err.GetAPIError(s3err.ErrContentSHA256Mismatch) + return n, s3err.GetContentSHA256MismatchErr(hr.sum, sum) } case HashTypeCRC32: sum := hr.Sum() @@ -200,6 +200,15 @@ func (hr *HashReader) Read(p []byte) (int, error) { return n, readerr } +func (hr *HashReader) base64ToHex(s string) string { + b, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return "" + } + + return hex.EncodeToString(b) +} + func (hr *HashReader) SetReader(r io.Reader) { hr.r = r } diff --git a/s3api/utils/multipart_reader.go b/s3api/utils/multipart_reader.go index 04f59c26..df615ba4 100644 --- a/s3api/utils/multipart_reader.go +++ b/s3api/utils/multipart_reader.go @@ -224,7 +224,8 @@ func (mp *MultipartParser) readFieldValue() (string, error) { case finalBoundaryLine: debuglogger.Logf("multipart POST ended before file part was found") - return "", s3err.GetAPIError(s3err.ErrPOSTFileRequired) + // S3 returns '0' as ArgumentValue + return "", s3err.GetInvalidArgumentErr(s3err.InvalidArgPOSTFileRequired, "0") default: buf.Write(raw) diff --git a/s3api/utils/multipart_reader_test.go b/s3api/utils/multipart_reader_test.go index 074454fd..a9420b04 100644 --- a/s3api/utils/multipart_reader_test.go +++ b/s3api/utils/multipart_reader_test.go @@ -292,7 +292,7 @@ func TestMultipartParserParseErrors(t *testing.T) { "value\r\n", "--abc--\r\n", }, ""), - want: s3err.GetAPIError(s3err.ErrPOSTFileRequired), + want: s3err.GetInvalidArgumentErr(s3err.InvalidArgPOSTFileRequired, "0"), }, { name: "line without crlf terminator", diff --git a/s3api/utils/presign-auth-reader.go b/s3api/utils/presign-auth-reader.go index a7c040f3..613ba78c 100644 --- a/s3api/utils/presign-auth-reader.go +++ b/s3api/utils/presign-auth-reader.go @@ -15,9 +15,7 @@ package utils import ( - "errors" "fmt" - "io" "net/url" "os" "strconv" @@ -25,9 +23,9 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/aws" - v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" "github.com/aws/smithy-go/logging" "github.com/gofiber/fiber/v2" + v4 "github.com/versity/versitygw/aws/signer/v4" "github.com/versity/versitygw/debuglogger" "github.com/versity/versitygw/s3err" ) @@ -39,42 +37,8 @@ const ( algoECDSA string = "AWS4-ECDSA-P256-SHA256" ) -// PresignedAuthReader is an io.Reader that validates presigned request authorization -// once the underlying reader returns io.EOF. This is needed for streaming -// data requests where the data size is not known until -// the data is completely read. -type PresignedAuthReader struct { - ctx *fiber.Ctx - auth AuthData - secret string - r io.Reader -} - -func NewPresignedAuthReader(ctx *fiber.Ctx, r io.Reader, auth AuthData, secret string) *PresignedAuthReader { - return &PresignedAuthReader{ - ctx: ctx, - r: r, - auth: auth, - secret: secret, - } -} - -// Read allows *PresignedAuthReader to be used as an io.Reader -func (pr *PresignedAuthReader) Read(p []byte) (int, error) { - n, err := pr.r.Read(p) - - if errors.Is(err, io.EOF) { - cerr := CheckPresignedSignature(pr.ctx, pr.auth, pr.secret, true) - if cerr != nil { - return n, cerr - } - } - - return n, err -} - // CheckPresignedSignature validates presigned request signature -func CheckPresignedSignature(ctx *fiber.Ctx, auth AuthData, secret string, streamBody bool) error { +func CheckPresignedSignature(ctx *fiber.Ctx, auth AuthData, secret string) error { signedHdrs := strings.Split(auth.SignedHeaders, ";") var contentLength int64 @@ -88,7 +52,7 @@ func CheckPresignedSignature(ctx *fiber.Ctx, auth AuthData, secret string, strea } // Create a new http request instance from fasthttp request - req, err := createPresignedHttpRequestFromCtx(ctx, signedHdrs, contentLength, streamBody) + req, err := createPresignedHttpRequestFromCtx(ctx, signedHdrs, contentLength) if err != nil { return fmt.Errorf("create http request from context: %w", err) } @@ -96,10 +60,10 @@ func CheckPresignedSignature(ctx *fiber.Ctx, auth AuthData, secret string, strea date, _ := time.Parse(iso8601Format, auth.Date) signer := v4.NewSigner() - uri, _, signErr := signer.PresignHTTP(ctx.Context(), aws.Credentials{ + uri, _, signMeta, signErr := signer.PresignHTTP(ctx.Context(), aws.Credentials{ AccessKeyID: auth.Access, SecretAccessKey: secret, - }, req, unsignedPayload, service, auth.Region, date, func(options *v4.SignerOptions) { + }, req, unsignedPayload, service, auth.Region, date, signedHdrs, func(options *v4.SignerOptions) { options.DisableURIPathEscaping = true if debuglogger.IsDebugEnabled() { options.LogSigning = true @@ -117,7 +81,14 @@ func CheckPresignedSignature(ctx *fiber.Ctx, auth AuthData, secret string, strea signature := urlParts.Query().Get("X-Amz-Signature") if signature != auth.Signature { - return s3err.GetAPIError(s3err.ErrSignatureDoesNotMatch) + return s3err.GetSignatureDoesNotMatchErr( + auth.Access, + signMeta.StringToSign, + auth.Signature, + HexBytes(signMeta.StringToSign), + signMeta.CanonicalString, + HexBytes(signMeta.CanonicalString), + ) } return nil @@ -218,11 +189,11 @@ func validateExpiration(str string, date time.Time) error { return s3err.QueryAuthErrors.ExpiresTooLarge() } - now := time.Now() - passed := int(now.Sub(date).Seconds()) + now := time.Now().UTC() + expiresAt := date.Add(time.Duration(exp) * time.Second) - if passed > exp { - return s3err.GetAPIError(s3err.ErrExpiredPresignRequest) + if expiresAt.Before(now) { + return s3err.GetExpiredPresignedURLError(exp, expiresAt.Format(time.RFC3339), now.Format(time.RFC3339)) } return nil diff --git a/s3api/utils/request_ids.go b/s3api/utils/request_ids.go new file mode 100644 index 00000000..77630e31 --- /dev/null +++ b/s3api/utils/request_ids.go @@ -0,0 +1,114 @@ +// 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 utils + +import ( + "crypto/rand" + "encoding/base64" + + "github.com/gofiber/fiber/v2" + "github.com/versity/versitygw/debuglogger" +) + +const ( + HeaderAmzRequestID = "x-amz-request-id" + HeaderAmzID2 = "x-amz-id-2" + + s3RequestIDAlphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + s3RequestIDLength = 16 + s3HostIDBytes = 65 +) + +// NewS3RequestID returns a request ID, for example +// "5MRQJ97RHWJ4FMX9". +func NewS3RequestID() string { + return randomBase36(s3RequestIDLength) +} + +func randomBase36(length int) string { + const maxUnbiasedByte = byte(252) // 36 * 7 + + out := make([]byte, length) + buf := make([]byte, length*2) + for i := 0; i < length; { + mustReadRandom(buf) + for _, b := range buf { + if b >= maxUnbiasedByte { + continue + } + out[i] = s3RequestIDAlphabet[int(b)%len(s3RequestIDAlphabet)] + i++ + if i == length { + break + } + } + } + + return string(out) +} + +// NewS3HostID generates a new s3-style host ID +func NewS3HostID() string { + b := make([]byte, s3HostIDBytes) + mustReadRandom(b) + return base64.StdEncoding.EncodeToString(b) +} + +// EnsureRequestIDs makes sure the request-local IDs exist and are present +// in the response headers. Existing local values are reused so headers and XML +// bodies stay consistent throughout the request. +func EnsureRequestIDs(ctx *fiber.Ctx) (requestID, hostID string) { + requestID = RequestID(ctx) + if requestID == "" { + requestID = NewS3RequestID() + ContextKeyRequestID.Set(ctx, requestID) + } + + hostID = HostID(ctx) + if hostID == "" { + hostID = NewS3HostID() + ContextKeyHostID.Set(ctx, hostID) + } + + ctx.Response().Header.Set(HeaderAmzRequestID, requestID) + ctx.Response().Header.Set(HeaderAmzID2, hostID) + + return requestID, hostID +} + +func RequestID(ctx *fiber.Ctx) string { + requestID, _ := ContextKeyRequestID.Get(ctx).(string) + if requestID != "" { + return requestID + } + + return string(ctx.Response().Header.Peek(HeaderAmzRequestID)) +} + +func HostID(ctx *fiber.Ctx) string { + hostID, _ := ContextKeyHostID.Get(ctx).(string) + if hostID != "" { + return hostID + } + + return string(ctx.Response().Header.Peek(HeaderAmzID2)) +} + +func mustReadRandom(b []byte) { + if _, err := rand.Read(b); err != nil { + debuglogger.Logf("randomize ID bytes: %v", err) + } +} diff --git a/s3api/utils/request_ids_test.go b/s3api/utils/request_ids_test.go new file mode 100644 index 00000000..d2822ebd --- /dev/null +++ b/s3api/utils/request_ids_test.go @@ -0,0 +1,53 @@ +// 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 utils + +import ( + "encoding/base64" + "regexp" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/stretchr/testify/assert" + "github.com/valyala/fasthttp" +) + +func TestNewS3RequestID(t *testing.T) { + id := NewS3RequestID() + + assert.Regexp(t, regexp.MustCompile(`^[0-9A-Z]{16}$`), id) +} + +func TestNewS3HostID(t *testing.T) { + id := NewS3HostID() + + decoded, err := base64.StdEncoding.DecodeString(id) + assert.NoError(t, err) + assert.Len(t, decoded, s3HostIDBytes) +} + +func TestEnsureRequestIDs(t *testing.T) { + ctx := fiber.New().AcquireCtx(&fasthttp.RequestCtx{}) + + requestID, hostID := EnsureRequestIDs(ctx) + requestIDAgain, hostIDAgain := EnsureRequestIDs(ctx) + + assert.Equal(t, requestID, requestIDAgain) + assert.Equal(t, hostID, hostIDAgain) + assert.Equal(t, requestID, RequestID(ctx)) + assert.Equal(t, hostID, HostID(ctx)) + assert.Equal(t, requestID, string(ctx.Response().Header.Peek(HeaderAmzRequestID))) + assert.Equal(t, hostID, string(ctx.Response().Header.Peek(HeaderAmzID2))) +} diff --git a/s3api/utils/signed-chunk-reader.go b/s3api/utils/signed-chunk-reader.go index f194d652..0782d719 100644 --- a/s3api/utils/signed-chunk-reader.go +++ b/s3api/utils/signed-chunk-reader.go @@ -58,46 +58,50 @@ var ( // ChunkReader reads from chunked upload request body, and returns // object data stream type ChunkReader struct { - r io.Reader - signingKey []byte - prevSig string - parsedSig string - chunkDataLeft int64 - trailer checksumType - trailerSig string - parsedChecksum string - stash []byte - chunkHash hash.Hash - checksumHash hash.Hash - isEOF bool - isFirstHeader bool - region string - date time.Time - requireTrailer bool - chunkSizes []int64 - cLength int64 - dataRead int64 + r io.Reader + signingKey []byte + prevSig string + parsedSig string + canonicalString string + accessKey string + chunkDataLeft int64 + trailer checksumType + trailerSig string + parsedChecksum string + stash []byte + chunkHash hash.Hash + checksumHash hash.Hash + isEOF bool + isFirstHeader bool + region string + date time.Time + requireTrailer bool + chunkSizes []int64 + cLength int64 + dataRead int64 } // NewChunkReader reads from request body io.Reader and parses out the // chunk metadata in stream. The headers are validated for proper signatures. // Reading from the chunk reader will read only the object data stream // without the chunk headers/trailers. -func NewSignedChunkReader(r io.Reader, authdata AuthData, secret string, date time.Time, chType checksumType, requireTrailer bool, cLength int64) (io.Reader, error) { +func NewSignedChunkReader(r io.Reader, authdata AuthData, canonicalString, secret string, date time.Time, chType checksumType, requireTrailer bool, cLength int64) (io.Reader, error) { chRdr := &ChunkReader{ r: r, signingKey: getSigningKey(secret, authdata.Region, date), // the authdata.Signature is validated in the auth-reader, // so we can use that here without any other checks - prevSig: authdata.Signature, - chunkHash: sha256.New(), - isFirstHeader: true, - date: date, - region: authdata.Region, - trailer: chType, - requireTrailer: requireTrailer, - chunkSizes: []int64{}, - cLength: cLength, + prevSig: authdata.Signature, + canonicalString: canonicalString, + accessKey: authdata.Access, + chunkHash: sha256.New(), + isFirstHeader: true, + date: date, + region: authdata.Region, + trailer: chType, + requireTrailer: requireTrailer, + chunkSizes: []int64{}, + cLength: cLength, } if chType != "" { @@ -224,7 +228,7 @@ func (cr *ChunkReader) verifyTrailerSignature() error { if sig != cr.trailerSig { debuglogger.Logf("incorrect trailing signature: (calculated): %v, (got): %v", sig, cr.trailerSig) - return s3err.GetAPIError(s3err.ErrSignatureDoesNotMatch) + return s3err.GetSignatureDoesNotMatchErr(cr.accessKey, strToSign, cr.trailerSig, HexBytes(strToSign), cr.canonicalString, HexBytes(cr.canonicalString)) } return nil @@ -251,7 +255,7 @@ func (cr *ChunkReader) checkSignature() error { if cr.prevSig != cr.parsedSig { debuglogger.Logf("incorrect signature: (calculated): %v, (got) %v", cr.prevSig, cr.parsedSig) - return s3err.GetAPIError(s3err.ErrSignatureDoesNotMatch) + return s3err.GetSignatureDoesNotMatchErr(cr.accessKey, sigstr, cr.parsedSig, HexBytes(sigstr), cr.canonicalString, HexBytes(cr.canonicalString)) } cr.parsedSig = "" return nil @@ -327,7 +331,7 @@ func (cr *ChunkReader) parseAndRemoveChunkInfo(p []byte) (int, error) { n, err := cr.parseAndRemoveChunkInfo(p[chunkSize:n]) if (chunkSize + int64(n)) > math.MaxInt { debuglogger.Logf("exceeding the limit of maximum integer allowed: (value): %v, (limit): %v", chunkSize+int64(n), math.MaxInt) - return 0, s3err.GetAPIError(s3err.ErrSignatureDoesNotMatch) + return 0, s3err.GetAPIError(s3err.ErrIncompleteBody) } return n + int(chunkSize), err } @@ -541,7 +545,7 @@ func (cr *ChunkReader) parseChunkSize(rdr *bufio.Reader, header []byte) (int64, } if !cr.isValidChunkSize(chunkSize) { - return 0, s3err.GetAPIError(s3err.ErrInvalidChunkSize) + return 0, s3err.GetInvalidChunkSizeErr(len(cr.chunkSizes)+1, chunkSize) } return chunkSize, nil diff --git a/s3api/utils/unsigned-chunk-reader_test.go b/s3api/utils/unsigned-chunk-reader_test.go index d3eec17b..96bc85f4 100644 --- a/s3api/utils/unsigned-chunk-reader_test.go +++ b/s3api/utils/unsigned-chunk-reader_test.go @@ -167,11 +167,11 @@ func requireAPIErrorCode(t *testing.T, err error, code string) { if err == nil { t.Fatalf("expected %s error, got nil", code) } - var apiErr s3err.APIError + var apiErr s3err.S3Error if !errors.As(err, &apiErr) { t.Fatalf("expected APIError, got %T: %v", err, err) } - if apiErr.Code != code { - t.Fatalf("APIError code = %q, want %q", apiErr.Code, code) + if apiErr.BaseError().Code != code { + t.Fatalf("APIError code = %q, want %q", apiErr.BaseError().Code, code) } } diff --git a/s3api/utils/utils.go b/s3api/utils/utils.go index 833f7297..91482196 100644 --- a/s3api/utils/utils.go +++ b/s3api/utils/utils.go @@ -15,7 +15,6 @@ package utils import ( - "bytes" "crypto/tls" "encoding/base64" "encoding/xml" @@ -94,7 +93,7 @@ func GetUserMetaData(headers *fasthttp.RequestHeader) (map[string]string, error) 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) + return nil, s3err.GetMetadataTooLargeErr(metadataSize, maxMetadataSize) } } } @@ -115,7 +114,7 @@ func ExtractMetadataFromFields(fields map[string]string) (map[string]string, err metadataSize += len(trimmedKey) + len(value) 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) + return nil, s3err.GetMetadataTooLargeErr(metadataSize, maxMetadataSize) } metadata[trimmedKey] = value @@ -124,18 +123,12 @@ func ExtractMetadataFromFields(fields map[string]string) (map[string]string, err return metadata, nil } -func createHttpRequestFromCtx(ctx *fiber.Ctx, signedHdrs []string, contentLength int64, streamBody bool) (*http.Request, error) { +func createHttpRequestFromCtx(ctx *fiber.Ctx, signedHdrs []string, contentLength int64) (*http.Request, error) { req := ctx.Request() - var body io.Reader - if streamBody { - body = req.BodyStream() - } else { - body = bytes.NewReader(req.Body()) - } uri := ctx.OriginalURL() - httpReq, err := http.NewRequest(string(req.Header.Method()), uri, body) + httpReq, err := http.NewRequest(string(req.Header.Method()), uri, nil) if err != nil { return nil, errors.New("error in creating an http request") } @@ -179,14 +172,8 @@ var ( } ) -func createPresignedHttpRequestFromCtx(ctx *fiber.Ctx, signedHdrs []string, contentLength int64, streamBody bool) (*http.Request, error) { +func createPresignedHttpRequestFromCtx(ctx *fiber.Ctx, signedHdrs []string, contentLength int64) (*http.Request, error) { req := ctx.Request() - var body io.Reader - if streamBody { - body = req.BodyStream() - } else { - body = bytes.NewReader(req.Body()) - } uri, _, _ := strings.Cut(ctx.OriginalURL(), "?") isFirst := true @@ -204,7 +191,7 @@ func createPresignedHttpRequestFromCtx(ctx *fiber.Ctx, signedHdrs []string, cont } } - httpReq, err := http.NewRequest(string(req.Header.Method()), uri, body) + httpReq, err := http.NewRequest(string(req.Header.Method()), uri, nil) if err != nil { return nil, errors.New("error in creating an http request") } @@ -279,14 +266,14 @@ func ParseMaxLimiter(limiter string, lt LimiterType) (int32, error) { lt = LimiterTypeMaxKeys } debuglogger.Logf("invalid %s provided: %s\n", lt, limiter) - return 0, s3err.GetInvalidMaxLimiterErr(string(lt)) + return 0, s3err.GetInvalidArgMaxLimiter(string(lt), limiter) } // max-buckets has distinct range rules and errors. if lt == LimiterTypeMaxBuckets { if num < 1 || num > int64(defaultMaxBuckets) { debuglogger.Logf("invalid max-buckets: %v", num) - return 0, s3err.GetAPIError(s3err.ErrInvalidMaxBuckets) + return 0, s3err.GetInvalidArgumentErr(s3err.InvalidArgMaxBuckets, limiter) } return int32(num), nil } @@ -302,10 +289,10 @@ func ParseMaxLimiter(limiter string, lt LimiterType) (int32, error) { // versions_max_keys uses the MaxKeys negative error. if lt == LimiterTypeVersionsMaxKeys { - return 0, s3err.GetAPIError(s3err.ErrNegativeMaxKeys) + return 0, s3err.GetInvalidArgumentErr(s3err.InvalidArgNegativeMaxKeys, limiter) } - return 0, s3err.GetNegativeMaxLimiterErr(string(lt)) + return 0, s3err.GetInvalidArgNegativeMaxLimiter(string(lt), limiter) } // Clamp excessive limiters to defaultMaxLimiter. @@ -367,7 +354,7 @@ func includeHeader(hdr string, signedHdrs []string) bool { // expiration time window // https://docs.aws.amazon.com/AmazonS3/latest/userguide/RESTAuthentication.html#RESTAuthenticationTimeStamp -const timeExpirationSec = 15 * 60 +const timeExpirationSec = 15 * 60 // seconds func ValidateDate(date time.Time) error { now := time.Now().UTC() @@ -375,7 +362,7 @@ func ValidateDate(date time.Time) error { // Checks the dates difference to be within allotted window if diff > timeExpirationSec || diff < -timeExpirationSec { - return s3err.GetAPIError(s3err.ErrRequestTimeTooSkewed) + return s3err.GetRequestTimeTooSkewedErr(date.Format(iso8601Format), now.Format(time.RFC3339), timeExpirationSec*1000) } return nil @@ -427,7 +414,7 @@ func ParseObjectAttributes(ctx *fiber.Ctx) (map[s3response.ObjectAttributes]stru attr := s3response.ObjectAttributes(a) if !attr.IsValid() { debuglogger.Logf("invalid object attribute: %v\n", attr) - err = s3err.GetAPIError(s3err.ErrInvalidObjectAttributes) + err = s3err.GetInvalidArgumentErr(s3err.InvalidArgObjectAttributes, string(value)) break } attrs[attr] = struct{}{} @@ -458,9 +445,13 @@ func ParsObjectLockHdrs(ctx *fiber.Ctx) (*objLockCfg, error) { objLockModeHdr := ctx.Get("X-Amz-Object-Lock-Mode") objLockDate := ctx.Get("X-Amz-Object-Lock-Retain-Until-Date") - if (objLockDate != "" && objLockModeHdr == "") || (objLockDate == "" && objLockModeHdr != "") { - debuglogger.Logf("one of 2 required params is missing: (lock date): %v, (lock mode): %v\n", objLockDate, objLockModeHdr) - return nil, s3err.GetAPIError(s3err.ErrObjectLockInvalidHeaders) + if objLockDate != "" && objLockModeHdr == "" { + debuglogger.Logf("the missing x-amz-object-lock-mode is required with x-amz-object-lock-retain-until-date") + return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgMissingObjectLockMode, "") + } + if objLockDate == "" && objLockModeHdr != "" { + debuglogger.Logf("the missing x-amz-object-lock-retain-until-date is required with x-amz-object-lock-mode") + return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgMissingObjectLockRetainDate, "") } var retainUntilDate time.Time @@ -468,11 +459,11 @@ func ParsObjectLockHdrs(ctx *fiber.Ctx) (*objLockCfg, error) { rDate, err := time.Parse(time.RFC3339, objLockDate) if err != nil { debuglogger.Logf("failed to parse retain until date: %v\n", err) - return nil, s3err.GetAPIError(s3err.ErrInvalidRetainUntilDate) + return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgRetainUntilDate, objLockDate) } if rDate.Before(time.Now()) { debuglogger.Logf("expired retain until date: %v\n", rDate.Format(time.RFC3339)) - return nil, s3err.GetAPIError(s3err.ErrPastObjectLockRetainDate) + return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgPastObjectLockRetainDate, objLockDate) } retainUntilDate = rDate } @@ -483,14 +474,14 @@ func ParsObjectLockHdrs(ctx *fiber.Ctx) (*objLockCfg, error) { objLockMode != types.ObjectLockModeCompliance && objLockMode != types.ObjectLockModeGovernance { debuglogger.Logf("invalid object lock mode: %v\n", objLockMode) - return nil, s3err.GetAPIError(s3err.ErrInvalidObjectLockMode) + return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgObjectLockMode, objLockModeHdr) } legalHold := types.ObjectLockLegalHoldStatus(legalHoldHdr) if legalHold != "" && legalHold != types.ObjectLockLegalHoldStatusOff && legalHold != types.ObjectLockLegalHoldStatusOn { debuglogger.Logf("invalid object lock legal hold status: %v\n", legalHold) - return nil, s3err.GetAPIError(s3err.ErrInvalidLegalHoldStatus) + return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgLegalHoldStatus, legalHoldHdr) } return &objLockCfg{ @@ -928,25 +919,25 @@ func ParseTagging(data []byte, limit TagLimit) (map[string]string, error) { // validate tag key length if len(tag.Key) == 0 || len(tag.Key) > 128 { debuglogger.Logf("tag key should 0 < tag.Key <= 128, key: %v", tag.Key) - return nil, s3err.GetAPIError(s3err.ErrInvalidTagKey) + return nil, s3err.GetInvalidTagErr(s3err.ErrInvalidTagKey, tag.Key, "") } // validate tag key string chars if !tagRule.MatchString(tag.Key) { debuglogger.Logf("invalid tag key: %s", tag.Key) - return nil, s3err.GetAPIError(s3err.ErrInvalidTagKey) + return nil, s3err.GetInvalidTagErr(s3err.ErrInvalidTagKey, tag.Key, "") } // validate tag value length if len(tag.Value) > 256 { debuglogger.Logf("invalid long tag value: (length): %v, (value): %v", len(tag.Value), tag.Value) - return nil, s3err.GetAPIError(s3err.ErrInvalidTagValue) + return nil, s3err.GetInvalidTagErr(s3err.ErrInvalidTagValue, tag.Key, tag.Value) } // validate tag value string chars if !tagRule.MatchString(tag.Value) { debuglogger.Logf("invalid tag value: %s", tag.Value) - return nil, s3err.GetAPIError(s3err.ErrInvalidTagValue) + return nil, s3err.GetInvalidTagErr(s3err.ErrInvalidTagValue, tag.Key, tag.Value) } // make sure there are no duplicate keys @@ -1028,18 +1019,17 @@ func GetInt64(n *int64) int64 { } // ValidateCopySource parses and validates the copy-source -func ValidateCopySource(copysource string) error { - var err error - copysource, err = url.QueryUnescape(copysource) +func ValidateCopySource(input string) error { + copysource, err := url.QueryUnescape(input) if err != nil { - debuglogger.Logf("invalid copy source encoding: %s", copysource) - return s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding) + debuglogger.Logf("invalid copy source encoding: %s", input) + return s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, input) } bucket, rest, _ := strings.Cut(copysource, "/") if !IsValidBucketName(bucket) { debuglogger.Logf("invalid copy source bucket: %s", bucket) - return s3err.GetAPIError(s3err.ErrInvalidCopySourceBucket) + return s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceBucket, input) } // cut till the versionId as it's the only query param @@ -1051,7 +1041,7 @@ func ValidateCopySource(copysource string) error { // in the gateway if !IsObjectNameValid(object) { debuglogger.Logf("invalid copy source object: %s", object) - return s3err.GetAPIError(s3err.ErrInvalidCopySourceObject) + return s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceObject, object) } return nil @@ -1132,3 +1122,21 @@ func NewTLSListener(network string, address string, getCertificateFunc func(*tls } return tls.NewListener(ln, config), nil } + +func DetectResourceType(ctx *fiber.Ctx) s3err.ResourceType { + path := ctx.Path() + if path == "" || path == "/" { + return s3err.ResourceTypeService + } + + path = strings.TrimPrefix(path, "/") + _, rest, found := strings.Cut(path, "/") + if !found { + return s3err.ResourceTypeBucket + } + if rest == "" { + return s3err.ResourceTypeBucket + } + + return s3err.ResourceTypeObject +} diff --git a/s3api/utils/utils_test.go b/s3api/utils/utils_test.go index fd6aa45c..bd6f0402 100644 --- a/s3api/utils/utils_test.go +++ b/s3api/utils/utils_test.go @@ -84,7 +84,7 @@ func TestCreateHttpRequestFromCtx(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := createHttpRequestFromCtx(tt.args.ctx, tt.hdrs, 0, true) + got, err := createHttpRequestFromCtx(tt.args.ctx, tt.hdrs, 0) if (err != nil) != tt.wantErr { t.Errorf("CreateHttpRequestFromCtx() error = %v, wantErr %v", err, tt.wantErr) return @@ -169,7 +169,7 @@ func TestGetUserMetaData(t *testing.T) { hdrs: [][2]string{ {"x-amz-meta-big", strings.Repeat("a", maxMetadataSize+1)}, }, - wantErr: s3err.GetAPIError(s3err.ErrMetadataTooLarge), + wantErr: s3err.GetMetadataTooLargeErr(2052, maxMetadataSize), }, { name: "metadata cumulative size exceeds limit (multiple headers)", @@ -177,7 +177,7 @@ func TestGetUserMetaData(t *testing.T) { {"x-amz-meta-a", strings.Repeat("a", maxMetadataSize/2)}, {"x-amz-meta-b", strings.Repeat("b", maxMetadataSize/2+10)}, }, - wantErr: s3err.GetAPIError(s3err.ErrMetadataTooLarge), + wantErr: s3err.GetMetadataTooLargeErr(2060, maxMetadataSize), }, { name: "duplicate keys combined", @@ -398,7 +398,7 @@ func TestParseMaxLimiter(t *testing.T) { lt: LimiterTypeMaxParts, }, expected: expected{ - err: s3err.GetInvalidMaxLimiterErr(string(LimiterTypeMaxParts)), + err: s3err.GetInvalidArgMaxLimiter(string(LimiterTypeMaxParts), "bla"), res: 0, }, }, @@ -409,7 +409,7 @@ func TestParseMaxLimiter(t *testing.T) { lt: LimiterTypeMaxUploads, }, expected: expected{ - err: s3err.GetInvalidMaxLimiterErr(string(LimiterTypeMaxUploads)), + err: s3err.GetInvalidArgMaxLimiter(string(LimiterTypeMaxUploads), "invalid"), res: 0, }, }, @@ -420,7 +420,7 @@ func TestParseMaxLimiter(t *testing.T) { lt: LimiterTypeMaxBuckets, }, expected: expected{ - err: s3err.GetInvalidMaxLimiterErr(string(LimiterTypeMaxBuckets)), + err: s3err.GetInvalidArgMaxLimiter(string(LimiterTypeMaxBuckets), "invalid"), res: 0, }, }, @@ -431,7 +431,7 @@ func TestParseMaxLimiter(t *testing.T) { lt: LimiterTypeMaxKeys, }, expected: expected{ - err: s3err.GetInvalidMaxLimiterErr(string(LimiterTypeMaxKeys)), + err: s3err.GetInvalidArgMaxLimiter(string(LimiterTypeMaxKeys), "invalid"), res: 0, }, }, @@ -442,7 +442,7 @@ func TestParseMaxLimiter(t *testing.T) { lt: LimiterTypeMaxKeys, }, expected: expected{ - err: s3err.GetNegativeMaxLimiterErr(string(LimiterTypeMaxKeys)), + err: s3err.GetInvalidArgNegativeMaxLimiter(string(LimiterTypeMaxKeys), "-5"), res: 0, }, }, @@ -453,7 +453,7 @@ func TestParseMaxLimiter(t *testing.T) { lt: LimiterTypePartNumberMarker, }, expected: expected{ - err: s3err.GetNegativeMaxLimiterErr(string(LimiterTypePartNumberMarker)), + err: s3err.GetInvalidArgNegativeMaxLimiter(string(LimiterTypePartNumberMarker), "-5"), res: 0, }, }, @@ -464,7 +464,7 @@ func TestParseMaxLimiter(t *testing.T) { lt: LimiterTypeMaxBuckets, }, expected: expected{ - err: s3err.GetAPIError(s3err.ErrInvalidMaxBuckets), + err: s3err.GetInvalidArgumentErr(s3err.InvalidArgMaxBuckets, "-12"), res: 0, }, }, @@ -475,7 +475,7 @@ func TestParseMaxLimiter(t *testing.T) { lt: LimiterTypeVersionsMaxKeys, }, expected: expected{ - err: s3err.GetAPIError(s3err.ErrNegativeMaxKeys), + err: s3err.GetInvalidArgumentErr(s3err.InvalidArgNegativeMaxKeys, "-12"), res: 0, }, }, @@ -486,7 +486,7 @@ func TestParseMaxLimiter(t *testing.T) { lt: LimiterTypeMaxBuckets, }, expected: expected{ - err: s3err.GetAPIError(s3err.ErrInvalidMaxBuckets), + err: s3err.GetInvalidArgumentErr(s3err.InvalidArgMaxBuckets, "25000"), res: 0, }, }, @@ -989,7 +989,7 @@ func TestExtractMetadataFromFields(t *testing.T) { fields: map[string]string{ "x-amz-meta-big": strings.Repeat("a", maxMetadataSize-len("big")+1), }, - wantErr: s3err.GetAPIError(s3err.ErrMetadataTooLarge), + wantErr: s3err.GetMetadataTooLargeErr(2049, maxMetadataSize), }, } @@ -1574,32 +1574,32 @@ func TestValidateCopySource(t *testing.T) { err error }{ // invalid encoding - {"invalid encoding 1", "%", s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding)}, - {"invalid encoding 2", "%2", s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding)}, - {"invalid encoding 3", "%G1", s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding)}, - {"invalid encoding 4", "%1Z", s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding)}, - {"invalid encoding 5", "%0H", s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding)}, - {"invalid encoding 6", "%XY", s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding)}, - {"invalid encoding 7", "%E", s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding)}, - {"invalid encoding 8", "hello%", s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding)}, - {"invalid encoding 9", "%%", s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding)}, - {"invalid encoding 10", "%2Gmore", s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding)}, - {"invalid encoding 11", "100%%sure", s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding)}, - {"invalid encoding 12", "%#00", s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding)}, - {"invalid encoding 13", "%0%0", s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding)}, - {"invalid encoding 14", "%?versionId=id", s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding)}, + {"invalid encoding 1", "%", s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "%")}, + {"invalid encoding 2", "%2", s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "%2")}, + {"invalid encoding 3", "%G1", s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "%G1")}, + {"invalid encoding 4", "%1Z", s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "%1Z")}, + {"invalid encoding 5", "%0H", s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "%0H")}, + {"invalid encoding 6", "%XY", s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "%XY")}, + {"invalid encoding 7", "%E", s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "%E")}, + {"invalid encoding 8", "hello%", s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "hello%")}, + {"invalid encoding 9", "%%", s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "%%")}, + {"invalid encoding 10", "%2Gmore", s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "%2Gmore")}, + {"invalid encoding 11", "100%%sure", s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "100%%sure")}, + {"invalid encoding 12", "%#00", s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "%#00")}, + {"invalid encoding 13", "%0%0", s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "%0%0")}, + {"invalid encoding 14", "%?versionId=id", s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "%?versionId=id")}, // invalid bucket name - {"invalid bucket name 1", "168.200.1.255/obj/foo", s3err.GetAPIError(s3err.ErrInvalidCopySourceBucket)}, - {"invalid bucket name 2", "/0000:0db8:85a3:0000:0000:8a2e:0370:7224/smth", s3err.GetAPIError(s3err.ErrInvalidCopySourceBucket)}, - {"invalid bucket name 3", "", s3err.GetAPIError(s3err.ErrInvalidCopySourceBucket)}, - {"invalid bucket name 4", "//obj/foo", s3err.GetAPIError(s3err.ErrInvalidCopySourceBucket)}, - {"invalid bucket name 5", "//obj/foo?versionId=id", s3err.GetAPIError(s3err.ErrInvalidCopySourceBucket)}, + {"invalid bucket name 1", "168.200.1.255/obj/foo", s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceBucket, "168.200.1.255/obj/foo")}, + {"invalid bucket name 2", "/0000:0db8:85a3:0000:0000:8a2e:0370:7224/smth", s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceBucket, "/0000:0db8:85a3:0000:0000:8a2e:0370:7224/smth")}, + {"invalid bucket name 3", "", s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceBucket, "")}, + {"invalid bucket name 4", "//obj/foo", s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceBucket, "//obj/foo")}, + {"invalid bucket name 5", "//obj/foo?versionId=id", s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceBucket, "//obj/foo?versionId=id")}, // invalid object name - {"invalid object name 1", "bucket/../foo", s3err.GetAPIError(s3err.ErrInvalidCopySourceObject)}, - {"invalid object name 2", "bucket/", s3err.GetAPIError(s3err.ErrInvalidCopySourceObject)}, - {"invalid object name 3", "bucket", s3err.GetAPIError(s3err.ErrInvalidCopySourceObject)}, - {"invalid object name 4", "bucket/../foo/dir/../../../", s3err.GetAPIError(s3err.ErrInvalidCopySourceObject)}, - {"invalid object name 5", "bucket/.?versionId=smth", s3err.GetAPIError(s3err.ErrInvalidCopySourceObject)}, + {"invalid object name 1", "bucket/../foo", s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceObject, "../foo")}, + {"invalid object name 2", "bucket/", s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceObject, "")}, + {"invalid object name 3", "bucket", s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceObject, "")}, + {"invalid object name 4", "bucket/../foo/dir/../../../", s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceObject, "../foo/dir/../../../")}, + {"invalid object name 5", "bucket/.?versionId=smth", s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceObject, ".")}, // success {"no error 1", "bucket/object", nil}, {"no error 2", "bucket/object/key", nil}, diff --git a/s3err/access-forbidden-error.go b/s3err/access-forbidden-error.go new file mode 100644 index 00000000..085c3d36 --- /dev/null +++ b/s3err/access-forbidden-error.go @@ -0,0 +1,57 @@ +// 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 s3err + +import "encoding/xml" + +// AccessForbiddenError is returned when a CORS request is not allowed. +// Produces and fields in the XML response. +type AccessForbiddenError struct { + APIError + Method string + ResourceType ResourceType +} + +func (e AccessForbiddenError) XMLBody(requestID, hostID string) []byte { + return encodeResponse(struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + Method string `xml:",omitempty"` + ResourceType ResourceType `xml:",omitempty"` + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` + }{ + Code: e.Code, + Message: e.Description, + Method: e.Method, + ResourceType: e.ResourceType, + RequestID: requestID, + HostID: hostID, + }) +} + +func (e AccessForbiddenError) Is(target error) bool { + t, ok := target.(APIError) + return ok && e.APIError == t +} + +func GetAccessForbiddenErr(code ErrorCode, method string, resourceType ResourceType) AccessForbiddenError { + return AccessForbiddenError{ + APIError: GetAPIError(code), + Method: method, + ResourceType: resourceType, + } +} diff --git a/s3err/bad-digest-error.go b/s3err/bad-digest-error.go new file mode 100644 index 00000000..1b209a4d --- /dev/null +++ b/s3err/bad-digest-error.go @@ -0,0 +1,57 @@ +// 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 s3err + +import "encoding/xml" + +// BadDigestError is returned when the Content-MD5 does not match the received data. +// Produces and fields in the XML response. +type BadDigestError struct { + APIError + CalculatedDigest string + ExpectedDigest string +} + +func (e BadDigestError) XMLBody(requestID, hostID string) []byte { + return encodeResponse(struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + CalculatedDigest string `xml:",omitempty"` + ExpectedDigest string `xml:",omitempty"` + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` + }{ + Code: e.Code, + Message: e.Description, + CalculatedDigest: e.CalculatedDigest, + ExpectedDigest: e.ExpectedDigest, + RequestID: requestID, + HostID: hostID, + }) +} + +func (e BadDigestError) Is(target error) bool { + t, ok := target.(APIError) + return ok && e.APIError == t +} + +func GetBadDigestErr(calculated, expected string) BadDigestError { + return BadDigestError{ + APIError: GetAPIError(ErrBadDigest), + CalculatedDigest: calculated, + ExpectedDigest: expected, + } +} diff --git a/s3err/bucket-error.go b/s3err/bucket-error.go new file mode 100644 index 00000000..0bbff389 --- /dev/null +++ b/s3err/bucket-error.go @@ -0,0 +1,56 @@ +// 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 s3err + +import "encoding/xml" + +// BucketError is returned for errors that include the bucket name. +// Produces a field in the XML response. +type BucketError struct { + APIError + BucketName string +} + +func (e BucketError) XMLBody(requestID, hostID string) []byte { + return encodeResponse(struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + BucketName string + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` + }{ + Code: e.Code, + Message: e.Description, + BucketName: e.BucketName, + RequestID: requestID, + HostID: hostID, + }) +} + +func (e BucketError) Is(target error) bool { + t, ok := target.(APIError) + return ok && e.APIError == t +} + +// GetBucketErr creates a BucketError for the given error code and bucket name. +// Use for: ErrNoSuchBucketPolicy, ErrOwnershipControlsNotFound, ErrBucketNotEmpty, +// ErrNoSuchCORSConfiguration, and similar errors that should include the bucket name. +func GetBucketErr(code ErrorCode, bucket string) BucketError { + return BucketError{ + APIError: GetAPIError(code), + BucketName: bucket, + } +} diff --git a/s3err/content-sha256-mismatch-error.go b/s3err/content-sha256-mismatch-error.go new file mode 100644 index 00000000..22e9738f --- /dev/null +++ b/s3err/content-sha256-mismatch-error.go @@ -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 s3err + +import "encoding/xml" + +// ContentSHA256MismatchError is returned when the x-amz-content-sha256 header does not match +// the computed hash of the request payload. +// Produces and fields. +type ContentSHA256MismatchError struct { + APIError + ClientComputedContentSHA256 string + S3ComputedContentSHA256 string +} + +func (e ContentSHA256MismatchError) XMLBody(requestID, hostID string) []byte { + return encodeResponse(struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + ClientComputedContentSHA256 string `xml:",omitempty"` + S3ComputedContentSHA256 string `xml:",omitempty"` + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` + }{ + Code: e.Code, + Message: e.Description, + ClientComputedContentSHA256: e.ClientComputedContentSHA256, + S3ComputedContentSHA256: e.S3ComputedContentSHA256, + RequestID: requestID, + HostID: hostID, + }) +} + +func (e ContentSHA256MismatchError) Is(target error) bool { + t, ok := target.(APIError) + return ok && e.APIError == t +} + +func GetContentSHA256MismatchErr(clientHash, s3Hash string) ContentSHA256MismatchError { + return ContentSHA256MismatchError{ + APIError: GetAPIError(ErrContentSHA256Mismatch), + ClientComputedContentSHA256: clientHash, + S3ComputedContentSHA256: s3Hash, + } +} diff --git a/s3err/entity-too-large-error.go b/s3err/entity-too-large-error.go new file mode 100644 index 00000000..24f40868 --- /dev/null +++ b/s3err/entity-too-large-error.go @@ -0,0 +1,57 @@ +// 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 s3err + +import "encoding/xml" + +// EntityTooLargeError is returned when the proposed upload exceeds the maximum allowed size. +// Produces and fields in the XML response. +type EntityTooLargeError struct { + APIError + ProposedSize int64 + MaxSizeAllowed int64 +} + +func (e EntityTooLargeError) XMLBody(requestID, hostID string) []byte { + return encodeResponse(struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + ProposedSize int64 `xml:",omitempty"` + MaxSizeAllowed int64 `xml:",omitempty"` + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` + }{ + Code: e.Code, + Message: e.Description, + ProposedSize: e.ProposedSize, + MaxSizeAllowed: e.MaxSizeAllowed, + RequestID: requestID, + HostID: hostID, + }) +} + +func (e EntityTooLargeError) Is(target error) bool { + t, ok := target.(APIError) + return ok && e.APIError == t +} + +func GetEntityTooLargeErr(proposedSize, maxSizeAllowed int64) EntityTooLargeError { + return EntityTooLargeError{ + APIError: GetAPIError(ErrEntityTooLarge), + ProposedSize: proposedSize, + MaxSizeAllowed: maxSizeAllowed, + } +} diff --git a/s3err/entity-too-small-error.go b/s3err/entity-too-small-error.go new file mode 100644 index 00000000..d1c522b0 --- /dev/null +++ b/s3err/entity-too-small-error.go @@ -0,0 +1,57 @@ +// 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 s3err + +import "encoding/xml" + +// EntityTooSmallError is returned when the proposed upload is smaller than the minimum allowed size. +// Produces and fields in the XML response. +type EntityTooSmallError struct { + APIError + ProposedSize int64 + MinSizeAllowed int64 +} + +func (e EntityTooSmallError) XMLBody(requestID, hostID string) []byte { + return encodeResponse(struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + ProposedSize int64 `xml:",omitempty"` + MinSizeAllowed int64 `xml:",omitempty"` + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` + }{ + Code: e.Code, + Message: e.Description, + ProposedSize: e.ProposedSize, + MinSizeAllowed: e.MinSizeAllowed, + RequestID: requestID, + HostID: hostID, + }) +} + +func (e EntityTooSmallError) Is(target error) bool { + t, ok := target.(APIError) + return ok && e.APIError == t +} + +func GetEntityTooSmallErr(proposedSize, minSizeAllowed int64) EntityTooSmallError { + return EntityTooSmallError{ + APIError: GetAPIError(ErrEntityTooSmall), + ProposedSize: proposedSize, + MinSizeAllowed: minSizeAllowed, + } +} diff --git a/s3err/expired-presigned-url-error.go b/s3err/expired-presigned-url-error.go new file mode 100644 index 00000000..f4406c86 --- /dev/null +++ b/s3err/expired-presigned-url-error.go @@ -0,0 +1,61 @@ +// 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 s3err + +import "encoding/xml" + +// ExpiredPresignedURLError is returned when the presigned url is expired +// Produces , and fields in the XML response. +type ExpiredPresignedURLError struct { + APIError + ServerTime string + XAmzExpires int + Expires string +} + +func (e ExpiredPresignedURLError) XMLBody(requestID, hostID string) []byte { + return encodeResponse(struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + ServerTime string `xml:",omitempty"` + XAmzExpires int `xml:"X-Amz-Expires,omitempty"` + Expires string `xml:",omitempty"` + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` + }{ + Code: e.Code, + Message: e.Description, + ServerTime: e.ServerTime, + XAmzExpires: e.XAmzExpires, + Expires: e.Expires, + RequestID: requestID, + HostID: hostID, + }) +} + +func (e ExpiredPresignedURLError) Is(target error) bool { + t, ok := target.(APIError) + return ok && e.APIError == t +} + +func GetExpiredPresignedURLError(XAmzExpires int, expires, serverTime string) ExpiredPresignedURLError { + return ExpiredPresignedURLError{ + APIError: GetAPIError(ErrExpiredPresignRequest), + XAmzExpires: XAmzExpires, + Expires: expires, + ServerTime: serverTime, + } +} diff --git a/s3err/invalid-access-key-id-error.go b/s3err/invalid-access-key-id-error.go new file mode 100644 index 00000000..b70efa3c --- /dev/null +++ b/s3err/invalid-access-key-id-error.go @@ -0,0 +1,53 @@ +// 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 s3err + +import "encoding/xml" + +// InvalidAccessKeyIdError is returned when the provided AWS access key ID does not exist. +// Produces an field in the XML response. +type InvalidAccessKeyIdError struct { + APIError + AWSAccessKeyId string +} + +func (e InvalidAccessKeyIdError) XMLBody(requestID, hostID string) []byte { + return encodeResponse(struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + AWSAccessKeyId string `xml:",omitempty"` + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` + }{ + Code: e.Code, + Message: e.Description, + AWSAccessKeyId: e.AWSAccessKeyId, + RequestID: requestID, + HostID: hostID, + }) +} + +func (e InvalidAccessKeyIdError) Is(target error) bool { + t, ok := target.(APIError) + return ok && e.APIError == t +} + +func GetInvalidAccessKeyIdErr(accessKeyId string) InvalidAccessKeyIdError { + return InvalidAccessKeyIdError{ + APIError: GetAPIError(ErrInvalidAccessKeyID), + AWSAccessKeyId: accessKeyId, + } +} diff --git a/s3err/invalid-argument.go b/s3err/invalid-argument.go new file mode 100644 index 00000000..ae14ca81 --- /dev/null +++ b/s3err/invalid-argument.go @@ -0,0 +1,274 @@ +// 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 s3err + +import ( + "bytes" + "encoding/xml" + "fmt" + "net/http" +) + +type InvalidArgErrorCode int + +const ( + InvalidArgMaxBuckets InvalidArgErrorCode = iota + InvalidArgNegativeMaxKeys + InvalidArgObjectAttributes + InvalidArgPartNumber + InvalidArgCompleteMpPartNumber + InvalidArgCopySourceRange + InvalidArgCopySourceBucket + InvalidArgCopySourceObject + InvalidArgCopySourceEncoding + InvalidArgURLEncodedTagging + InvalidArgAuthHeader + InvalidArgAuthorizationType + InvalidArgPOSTFileRequired + InvalidArgSHA256Payload + InvalidArgCopySource + InvalidArgRetainUntilDate + InvalidArgPastObjectLockRetainDate + InvalidArgObjectLockRetentionDays + InvalidArgObjectLockRetentionYears + InvalidArgMissingObjectLockRetainDate + InvalidArgMissingObjectLockMode + InvalidArgLegalHoldStatus + InvalidArgObjectLockMode + InvalidArgMetadataDirective + InvalidArgTaggingDirective + InvalidArgVersionId + InvalidArgChecksumPart + InvalidArgMissingUploadId + InvalidArgUploadIdMarker + InvalidArgCannedAcl + InvalidArgOnlyAws4HmacSha256 + InvalidArgDateHeader +) + +var invalidArgErrResponses = map[InvalidArgErrorCode]InvalidArgumentError{ + InvalidArgMaxBuckets: { + Description: "Argument max-buckets must be an integer between 1 and 10000.", + ArgumentName: "max-buckets", + }, + InvalidArgNegativeMaxKeys: { + Description: "max-keys cannot be negative", + ArgumentName: "maxKeys", + }, + InvalidArgObjectAttributes: { + Description: "Invalid attribute name specified.", + ArgumentName: "x-amz-object-attributes", + }, + InvalidArgPartNumber: { + Description: "Part number must be an integer between 1 and 10000, inclusive.", + ArgumentName: "partNumber", + }, + InvalidArgCompleteMpPartNumber: { + Description: "PartNumber must be >= 1", + ArgumentName: "PartNumber", + }, + InvalidArgCopySourceRange: { + Description: "The x-amz-copy-source-range value must be of the form bytes=first-last where first and last are the zero-based offsets of the first and last bytes to copy", + ArgumentName: "x-amz-copy-source-range", + }, + InvalidArgCopySourceBucket: { + Description: "Invalid copy source bucket name", + ArgumentName: "x-amz-copy-source", + }, + InvalidArgCopySourceObject: { + Description: "Invalid copy source object key", + ArgumentName: "x-amz-copy-source", + }, + InvalidArgCopySourceEncoding: { + Description: "Invalid copy source encoding", + ArgumentName: "x-amz-copy-source", + }, + InvalidArgURLEncodedTagging: { + Description: "The header 'x-amz-tagging' shall be encoded as UTF-8 then URLEncoded URL query parameters without tag name duplicates.", + ArgumentName: "x-amz-tagging", + }, + InvalidArgAuthHeader: { + Description: "Authorization header is invalid -- one and only one ' ' (space) required.", + ArgumentName: "Authorization", + }, + InvalidArgAuthorizationType: { + Description: "Unsupported Authorization Type", + ArgumentName: "Authorization", + }, + InvalidArgPOSTFileRequired: { + Description: "POST requires exactly one file upload per request.", + ArgumentName: "file", + }, + InvalidArgSHA256Payload: { + Description: "x-amz-content-sha256 must be UNSIGNED-PAYLOAD, STREAMING-UNSIGNED-PAYLOAD-TRAILER, STREAMING-AWS4-HMAC-SHA256-PAYLOAD, STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER, STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD, STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD-TRAILER or a valid sha256 value.", + ArgumentName: "x-amz-content-sha256", + }, + InvalidArgCopySource: { + Description: "You can only specify a copy source header for copy requests.", + ArgumentName: "x-amz-copy-source", + }, + InvalidArgRetainUntilDate: { + Description: "The retain until date must be provided in ISO 8601 format", + ArgumentName: "x-amz-object-lock-retain-until-date", + }, + InvalidArgPastObjectLockRetainDate: { + Description: "The retain until date must be in the future!", + ArgumentName: "x-amz-object-lock-retain-until-date", + }, + InvalidArgMissingObjectLockRetainDate: { + Description: "x-amz-object-lock-retain-until-date and x-amz-object-lock-mode must both be supplied.", + ArgumentName: "x-amz-object-lock-retain-until-date", + }, + InvalidArgMissingObjectLockMode: { + Description: "x-amz-object-lock-retain-until-date and x-amz-object-lock-mode must both be supplied.", + ArgumentName: "x-amz-object-lock-mode", + }, + InvalidArgObjectLockRetentionDays: { + Description: "Default retention period must be a positive integer value.", + ArgumentName: "Days", + }, + InvalidArgObjectLockRetentionYears: { + Description: "Default retention period must be a positive integer value.", + ArgumentName: "Years", + }, + InvalidArgLegalHoldStatus: { + Description: "Legal Hold must be either of 'ON' or 'OFF'", + ArgumentName: "x-amz-object-lock-legal-hold", + }, + InvalidArgObjectLockMode: { + Description: "Unknown wormMode directive.", + ArgumentName: "x-amz-object-lock-mode", + }, + InvalidArgMetadataDirective: { + Description: "Unknown metadata directive.", + ArgumentName: "x-amz-metadata-directive", + }, + InvalidArgTaggingDirective: { + Description: "Unknown tagging directive.", + ArgumentName: "x-amz-tagging-directive", + }, + InvalidArgVersionId: { + Description: "Invalid version id specified", + ArgumentName: "versionId", + }, + InvalidArgChecksumPart: { + Description: "Invalid Base64 or multiple checksums present in request", + ArgumentName: "Checksum", + }, + InvalidArgMissingUploadId: { + Description: "This operation does not accept partNumber without uploadId", + ArgumentName: "partNumber", + }, + InvalidArgUploadIdMarker: { + Description: "Invalid uploadId marker", + ArgumentName: "upload-id-marker", + }, + InvalidArgCannedAcl: { + Description: "", + ArgumentName: "x-amz-acl", + }, + InvalidArgOnlyAws4HmacSha256: { + Description: "Only AWS4-HMAC-SHA256 is supported", + ArgumentName: "X-Amz-Algorithm", + }, + InvalidArgDateHeader: { + Description: "X-Amz-Date must be formated via ISO8601 Long format", + ArgumentName: "X-Amz-Date", + }, +} + +// InvalidArgumentError is returned when a request argument is invalid. +// Produces and fields in the XML response. +type InvalidArgumentError struct { + Description string + ArgumentName string + ArgumentValue string +} + +func (e InvalidArgumentError) BaseError() APIError { + return APIError{ + Code: "InvalidArgument", + Description: e.Description, + HTTPStatusCode: http.StatusBadRequest, + } +} + +// InvalidArgumentError http status code is always 400 +func (e InvalidArgumentError) StatusCode() int { return http.StatusBadRequest } + +func (e InvalidArgumentError) Error() string { + var bytesBuffer bytes.Buffer + bytesBuffer.WriteString(xml.Header) + enc := xml.NewEncoder(&bytesBuffer) + _ = enc.Encode(e) + return bytesBuffer.String() +} + +func (e InvalidArgumentError) XMLBody(requestID, hostID string) []byte { + return encodeResponse(struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + ArgumentName string `xml:"ArgumentName,omitempty"` + ArgumentValue string `xml:"ArgumentValue,omitempty"` + RequestId string `xml:"RequestId,omitempty"` + HostId string `xml:"HostId,omitempty"` + }{ + Code: "InvalidArgument", + Message: e.Description, + ArgumentName: e.ArgumentName, + ArgumentValue: e.ArgumentValue, + RequestId: requestID, + HostId: hostID, + }) +} + +func GetInvalidArgumentErr(code InvalidArgErrorCode, value string) InvalidArgumentError { + err := invalidArgErrResponses[code] + err.ArgumentValue = value + return err +} + +func GetInvalidArgMaxLimiter(name, value string) InvalidArgumentError { + return InvalidArgumentError{ + ArgumentName: name, + ArgumentValue: value, + Description: fmt.Sprintf("Provided %s not an integer or within integer range", value), + } +} + +func GetInvalidArgNegativeMaxLimiter(name, value string) InvalidArgumentError { + return InvalidArgumentError{ + ArgumentName: name, + ArgumentValue: value, + Description: fmt.Sprintf("Argument %s must be an integer between 0 and 2147483647", value), + } +} + +func GetInvalidArgExceedingRange(size int64) InvalidArgumentError { + return InvalidArgumentError{ + ArgumentName: "x-amz-copy-source-range", + ArgumentValue: fmt.Sprint(size), + Description: fmt.Sprintf("Range specified is not valid for source object of size: %d", size), + } +} + +func GetInvalidArgObjectOwnership(value string) InvalidArgumentError { + return InvalidArgumentError{ + ArgumentName: "x-amz-object-ownership", + // no ArgumentValue is returned for this error + Description: fmt.Sprintf("Invalid x-amz-object-ownership header: %s", value), + } +} diff --git a/s3err/invalid-chunk-size-error.go b/s3err/invalid-chunk-size-error.go new file mode 100644 index 00000000..86eff473 --- /dev/null +++ b/s3err/invalid-chunk-size-error.go @@ -0,0 +1,57 @@ +// 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 s3err + +import "encoding/xml" + +// InvalidChunkSizeError is returned when a chunk size in a streaming upload is invalid. +// Produces and fields in the XML response. +type InvalidChunkSizeError struct { + APIError + Chunk int + BadChunkSize int64 +} + +func (e InvalidChunkSizeError) XMLBody(requestID, hostID string) []byte { + return encodeResponse(struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + Chunk int `xml:",omitempty"` + BadChunkSize int64 `xml:",omitempty"` + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` + }{ + Code: e.Code, + Message: e.Description, + Chunk: e.Chunk, + BadChunkSize: e.BadChunkSize, + RequestID: requestID, + HostID: hostID, + }) +} + +func (e InvalidChunkSizeError) Is(target error) bool { + t, ok := target.(APIError) + return ok && e.APIError == t +} + +func GetInvalidChunkSizeErr(chunk int, badChunkSize int64) InvalidChunkSizeError { + return InvalidChunkSizeError{ + APIError: GetAPIError(ErrInvalidChunkSize), + Chunk: chunk, + BadChunkSize: badChunkSize, + } +} diff --git a/s3err/invalid-digest-error.go b/s3err/invalid-digest-error.go new file mode 100644 index 00000000..537d0906 --- /dev/null +++ b/s3err/invalid-digest-error.go @@ -0,0 +1,53 @@ +// 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 s3err + +import "encoding/xml" + +// InvalidDigestError is returned when the Content-MD5 header value is invalid. +// Produces a field in the XML response. +type InvalidDigestError struct { + APIError + ContentMD5 string +} + +func (e InvalidDigestError) XMLBody(requestID, hostID string) []byte { + return encodeResponse(struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + ContentMD5 string `xml:"Content-MD5"` + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` + }{ + Code: e.Code, + Message: e.Description, + ContentMD5: e.ContentMD5, + RequestID: requestID, + HostID: hostID, + }) +} + +func (e InvalidDigestError) Is(target error) bool { + t, ok := target.(APIError) + return ok && e.APIError == t +} + +func GetInvalidDigestErr(contentMD5 string) InvalidDigestError { + return InvalidDigestError{ + APIError: GetAPIError(ErrInvalidDigest), + ContentMD5: contentMD5, + } +} diff --git a/s3err/invalid-location-constraint-error.go b/s3err/invalid-location-constraint-error.go new file mode 100644 index 00000000..290d6428 --- /dev/null +++ b/s3err/invalid-location-constraint-error.go @@ -0,0 +1,53 @@ +// 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 s3err + +import "encoding/xml" + +// InvalidLocationConstraintError is returned when an invalid location constraint is provided. +// Produces a field in the XML response. +type InvalidLocationConstraintError struct { + APIError + LocationConstraint string +} + +func (e InvalidLocationConstraintError) XMLBody(requestID, hostID string) []byte { + return encodeResponse(struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + LocationConstraint string + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` + }{ + Code: e.Code, + Message: e.Description, + LocationConstraint: e.LocationConstraint, + RequestID: requestID, + HostID: hostID, + }) +} + +func (e InvalidLocationConstraintError) Is(target error) bool { + t, ok := target.(APIError) + return ok && e.APIError == t +} + +func GetInvalidLocationConstraintErr(constraint string) InvalidLocationConstraintError { + return InvalidLocationConstraintError{ + APIError: GetAPIError(ErrInvalidLocationConstraint), + LocationConstraint: constraint, + } +} diff --git a/s3err/invalid-part-error.go b/s3err/invalid-part-error.go new file mode 100644 index 00000000..ed47cdcb --- /dev/null +++ b/s3err/invalid-part-error.go @@ -0,0 +1,63 @@ +// 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 s3err + +import ( + "encoding/xml" +) + +// InvalidPartError is returned when one or more specified parts cannot be found. +// Produces , , and fields in the XML response. +type InvalidPartError struct { + APIError + UploadId string + PartNumber int32 + ETag string +} + +func (e InvalidPartError) XMLBody(requestID, hostID string) []byte { + return encodeResponse(struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + UploadId string `xml:",omitempty"` + PartNumber int32 `xml:",omitempty"` + ETag string `xml:",omitempty"` + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` + }{ + Code: e.Code, + Message: e.Description, + UploadId: e.UploadId, + PartNumber: e.PartNumber, + ETag: e.ETag, + RequestID: requestID, + HostID: hostID, + }) +} + +func (e InvalidPartError) Is(target error) bool { + t, ok := target.(APIError) + return ok && e.APIError == t +} + +func GetInvalidPartErr(uploadId string, partNumber int32, etag string) InvalidPartError { + return InvalidPartError{ + APIError: GetAPIError(ErrInvalidPart), + UploadId: uploadId, + PartNumber: partNumber, + ETag: etag, + } +} diff --git a/s3err/invalid-part-number-range-error.go b/s3err/invalid-part-number-range-error.go new file mode 100644 index 00000000..5edd9fea --- /dev/null +++ b/s3err/invalid-part-number-range-error.go @@ -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 s3err + +import "encoding/xml" + +// InvalidPartNumberRangeError is returned when the requested part number exceeds +// the number of parts available for the object. +// Produces and fields in the XML response. +type InvalidPartNumberRangeError struct { + APIError + ActualPartCount int32 + PartNumberRequested int32 +} + +func (e InvalidPartNumberRangeError) XMLBody(requestID, hostID string) []byte { + return encodeResponse(struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + ActualPartCount int32 + PartNumberRequested int32 + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` + }{ + Code: e.Code, + Message: e.Description, + ActualPartCount: e.ActualPartCount, + PartNumberRequested: e.PartNumberRequested, + RequestID: requestID, + HostID: hostID, + }) +} + +func (e InvalidPartNumberRangeError) Is(target error) bool { + t, ok := target.(APIError) + return ok && e.APIError == t +} + +func GetInvalidPartNumberRangeErr(actualPartCount, partNumberRequested int32) InvalidPartNumberRangeError { + return InvalidPartNumberRangeError{ + APIError: GetAPIError(ErrInvalidPartNumberRange), + ActualPartCount: actualPartCount, + PartNumberRequested: partNumberRequested, + } +} diff --git a/s3err/invalid-range-error.go b/s3err/invalid-range-error.go new file mode 100644 index 00000000..37773768 --- /dev/null +++ b/s3err/invalid-range-error.go @@ -0,0 +1,57 @@ +// 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 s3err + +import "encoding/xml" + +// InvalidRangeError is returned when the requested byte range cannot be satisfied. +// Produces and fields in the XML response. +type InvalidRangeError struct { + APIError + RangeRequested string + ActualObjectSize int64 +} + +func (e InvalidRangeError) XMLBody(requestID, hostID string) []byte { + return encodeResponse(struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + RangeRequested string `xml:",omitempty"` + ActualObjectSize int64 `xml:",omitempty"` + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` + }{ + Code: e.Code, + Message: e.Description, + RangeRequested: e.RangeRequested, + ActualObjectSize: e.ActualObjectSize, + RequestID: requestID, + HostID: hostID, + }) +} + +func (e InvalidRangeError) Is(target error) bool { + t, ok := target.(APIError) + return ok && e.APIError == t +} + +func GetInvalidRangeErr(rangeRequested string, actualObjectSize int64) InvalidRangeError { + return InvalidRangeError{ + APIError: GetAPIError(ErrInvalidRange), + RangeRequested: rangeRequested, + ActualObjectSize: actualObjectSize, + } +} diff --git a/s3err/invalid-tag-error.go b/s3err/invalid-tag-error.go new file mode 100644 index 00000000..981c5183 --- /dev/null +++ b/s3err/invalid-tag-error.go @@ -0,0 +1,59 @@ +// 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 s3err + +import "encoding/xml" + +// InvalidTagError is returned when a tag key or value is invalid. +// Produces and optionally fields in the XML response. +type InvalidTagError struct { + APIError + TagKey string + TagValue string +} + +func (e InvalidTagError) XMLBody(requestID, hostID string) []byte { + return encodeResponse(struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + TagKey string `xml:",omitempty"` + TagValue string `xml:",omitempty"` + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` + }{ + Code: e.Code, + Message: e.Description, + TagKey: e.TagKey, + TagValue: e.TagValue, + RequestID: requestID, + HostID: hostID, + }) +} + +func (e InvalidTagError) Is(target error) bool { + t, ok := target.(APIError) + return ok && e.APIError == t +} + +// GetInvalidTagErr creates an InvalidTagError for the given error code, tag key, and optional tag value. +// code should be ErrInvalidTagKey or ErrInvalidTagValue. +func GetInvalidTagErr(code ErrorCode, tagKey, tagValue string) InvalidTagError { + return InvalidTagError{ + APIError: GetAPIError(code), + TagKey: tagKey, + TagValue: tagValue, + } +} diff --git a/s3err/key-too-long-error.go b/s3err/key-too-long-error.go new file mode 100644 index 00000000..d95fdd8a --- /dev/null +++ b/s3err/key-too-long-error.go @@ -0,0 +1,57 @@ +// 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 s3err + +import "encoding/xml" + +// KeyTooLongError is returned when the object key exceeds the maximum allowed length. +// Produces and fields in the XML response. +type KeyTooLongError struct { + APIError + Size int64 + MaxSizeAllowed int64 +} + +func (e KeyTooLongError) XMLBody(requestID, hostID string) []byte { + return encodeResponse(struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + Size int64 `xml:",omitempty"` + MaxSizeAllowed int64 `xml:",omitempty"` + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` + }{ + Code: e.Code, + Message: e.Description, + Size: e.Size, + MaxSizeAllowed: e.MaxSizeAllowed, + RequestID: requestID, + HostID: hostID, + }) +} + +func (e KeyTooLongError) Is(target error) bool { + t, ok := target.(APIError) + return ok && e.APIError == t +} + +func GetKeyTooLongErr(size, maxSizeAllowed int64) KeyTooLongError { + return KeyTooLongError{ + APIError: GetAPIError(ErrKeyTooLong), + Size: size, + MaxSizeAllowed: maxSizeAllowed, + } +} diff --git a/s3err/metadata-too-large-error.go b/s3err/metadata-too-large-error.go new file mode 100644 index 00000000..02bc2692 --- /dev/null +++ b/s3err/metadata-too-large-error.go @@ -0,0 +1,57 @@ +// 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 s3err + +import "encoding/xml" + +// MetadataTooLargeError is returned when the request metadata headers exceed the allowed size. +// Produces and fields in the XML response. +type MetadataTooLargeError struct { + APIError + Size int + MaxSizeAllowed int +} + +func (e MetadataTooLargeError) XMLBody(requestID, hostID string) []byte { + return encodeResponse(struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + Size int `xml:",omitempty"` + MaxSizeAllowed int `xml:",omitempty"` + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` + }{ + Code: e.Code, + Message: e.Description, + Size: e.Size, + MaxSizeAllowed: e.MaxSizeAllowed, + RequestID: requestID, + HostID: hostID, + }) +} + +func (e MetadataTooLargeError) Is(target error) bool { + t, ok := target.(APIError) + return ok && e.APIError == t +} + +func GetMetadataTooLargeErr(size, maxSizeAllowed int) MetadataTooLargeError { + return MetadataTooLargeError{ + APIError: GetAPIError(ErrMetadataTooLarge), + Size: size, + MaxSizeAllowed: maxSizeAllowed, + } +} diff --git a/s3err/method-not-allowed-error.go b/s3err/method-not-allowed-error.go new file mode 100644 index 00000000..881c9acf --- /dev/null +++ b/s3err/method-not-allowed-error.go @@ -0,0 +1,67 @@ +// 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 s3err + +import ( + "encoding/xml" + "strings" +) + +// MethodNotAllowedError is returned when an HTTP method is not permitted on a resource. +// Produces and fields in the XML response. +// AllowedMethods is used to populate the HTTP Allow: response header. +type MethodNotAllowedError struct { + APIError + Method string + ResourceType ResourceType + AllowedMethods []string +} + +func (mna *MethodNotAllowedError) AllowedMethodsString() string { + return strings.Join(mna.AllowedMethods, ", ") +} + +func (e MethodNotAllowedError) XMLBody(requestID, hostID string) []byte { + return encodeResponse(struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + Method string + ResourceType ResourceType + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` + }{ + Code: e.Code, + Message: e.Description, + Method: e.Method, + ResourceType: e.ResourceType, + RequestID: requestID, + HostID: hostID, + }) +} + +func (e MethodNotAllowedError) Is(target error) bool { + t, ok := target.(APIError) + return ok && e.APIError == t +} + +func GetMethodNotAllowedErr(method string, resourceType ResourceType, allowed []string) MethodNotAllowedError { + return MethodNotAllowedError{ + APIError: GetAPIError(ErrMethodNotAllowed), + Method: method, + ResourceType: resourceType, + AllowedMethods: allowed, + } +} diff --git a/s3err/no-such-upload-error.go b/s3err/no-such-upload-error.go new file mode 100644 index 00000000..a1576a43 --- /dev/null +++ b/s3err/no-such-upload-error.go @@ -0,0 +1,53 @@ +// 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 s3err + +import "encoding/xml" + +// NoSuchUploadError is returned when the specified multipart upload does not exist. +// Produces an field in the XML response. +type NoSuchUploadError struct { + APIError + UploadId string +} + +func (e NoSuchUploadError) XMLBody(requestID, hostID string) []byte { + return encodeResponse(struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + UploadId string `xml:",omitempty"` + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` + }{ + Code: e.Code, + Message: e.Description, + UploadId: e.UploadId, + RequestID: requestID, + HostID: hostID, + }) +} + +func (e NoSuchUploadError) Is(target error) bool { + t, ok := target.(APIError) + return ok && e.APIError == t +} + +func GetNoSuchUploadErr(uploadId string) NoSuchUploadError { + return NoSuchUploadError{ + APIError: GetAPIError(ErrNoSuchUpload), + UploadId: uploadId, + } +} diff --git a/s3err/no-such-version-error.go b/s3err/no-such-version-error.go new file mode 100644 index 00000000..8cd36e60 --- /dev/null +++ b/s3err/no-such-version-error.go @@ -0,0 +1,57 @@ +// 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 s3err + +import "encoding/xml" + +// NoSuchVersionError is returned when the specified version does not exist. +// Produces and fields in the XML response. +type NoSuchVersionError struct { + APIError + Key string + VersionId string +} + +func (e NoSuchVersionError) XMLBody(requestID, hostID string) []byte { + return encodeResponse(struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + Key string `xml:",omitempty"` + VersionId string `xml:",omitempty"` + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` + }{ + Code: e.Code, + Message: e.Description, + Key: e.Key, + VersionId: e.VersionId, + RequestID: requestID, + HostID: hostID, + }) +} + +func (e NoSuchVersionError) Is(target error) bool { + t, ok := target.(APIError) + return ok && e.APIError == t +} + +func GetNoSuchVersionErr(key, versionId string) NoSuchVersionError { + return NoSuchVersionError{ + APIError: GetAPIError(ErrNoSuchVersion), + Key: key, + VersionId: versionId, + } +} diff --git a/s3err/not-implemented-error.go b/s3err/not-implemented-error.go new file mode 100644 index 00000000..64569a69 --- /dev/null +++ b/s3err/not-implemented-error.go @@ -0,0 +1,66 @@ +// 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 s3err + +import ( + "encoding/xml" +) + +type NmpAdditionalMessage string + +const ( + NmpAdditionalMessageIfNoneMatch NmpAdditionalMessage = "We don\\'t accept the provided value of If-None-Match header for this API" + NmpAdditionalMessageMultipleCondHeaders NmpAdditionalMessage = "Multiple conditional request headers present in the request" +) + +// NotImplementedError is returned when a header implies unsupported functionality. +// Produces
and fields in the XML response. +type NotImplementedError struct { + APIError + Header string + AdditionalMessage NmpAdditionalMessage +} + +func (e NotImplementedError) XMLBody(requestID, hostID string) []byte { + return encodeResponse(struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + Header string `xml:",omitempty"` + AdditionalMessage NmpAdditionalMessage `xml:"additionalMessage,omitempty"` + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` + }{ + Code: e.Code, + Message: e.Description, + Header: e.Header, + AdditionalMessage: e.AdditionalMessage, + RequestID: requestID, + HostID: hostID, + }) +} + +func (e NotImplementedError) Is(target error) bool { + t, ok := target.(APIError) + return ok && e.APIError == t +} + +func GetNotImplementedErr(header string, additionalMessage NmpAdditionalMessage) NotImplementedError { + return NotImplementedError{ + APIError: GetAPIError(ErrNotImplemented), + Header: header, + AdditionalMessage: additionalMessage, + } +} diff --git a/s3err/post-object.go b/s3err/post-object.go index 02ae5989..e99bce2a 100644 --- a/s3err/post-object.go +++ b/s3err/post-object.go @@ -20,43 +20,68 @@ import ( ) // Factory for building s3 object POST authentication errors. -func invalidPOSTObjectAuthErr(format string, args ...any) APIError { - return APIError{ - Code: "InvalidArgument", - Description: fmt.Sprintf(format, args...), - HTTPStatusCode: http.StatusBadRequest, +func invalidPOSTObjectAuthErr(argName, argValue, format string, args ...any) S3Error { + return InvalidArgumentError{ + ArgumentName: argName, + ArgumentValue: argValue, + Description: fmt.Sprintf(format, args...), } } type invalidPostAuthErr struct{} -func (invalidPostAuthErr) InvalidDateFormat(s string) APIError { +func (invalidPostAuthErr) InvalidDateFormat(creds, date string) S3Error { return invalidPOSTObjectAuthErr( + "x-amz-credential", + creds, "incorrect date format %q. This date in the credential must be in the format \"yyyyMMdd\".", - s, + date, ) } -func (invalidPostAuthErr) MalformedCredential() APIError { +func (invalidPostAuthErr) MalformedCredential(creds string) S3Error { return invalidPOSTObjectAuthErr( + "x-amz-credential", + creds, "the Credential is mal-formed; expecting \"/YYYYMMDD/REGION/SERVICE/aws4_request\".", ) } -func (invalidPostAuthErr) IncorrectTerminal(s string) APIError { - return invalidPOSTObjectAuthErr("incorrect terminal %q. This endpoint uses \"aws4_request\".", s) +func (invalidPostAuthErr) IncorrectTerminal(creds, terminal string) S3Error { + return invalidPOSTObjectAuthErr( + "x-amz-credential", + creds, + "incorrect terminal %q. This endpoint uses \"aws4_request\".", + terminal, + ) } -func (invalidPostAuthErr) IncorrectRegion(expected, actual string) APIError { - return invalidPOSTObjectAuthErr("the region %q is wrong; expecting %q", actual, expected) +func (invalidPostAuthErr) IncorrectRegion(creds, expected, actual string) S3Error { + return invalidPOSTObjectAuthErr( + "x-amz-credential", + creds, + "the region %q is wrong; expecting %q", + actual, + expected, + ) } -func (invalidPostAuthErr) IncorrectService(s string) APIError { - return invalidPOSTObjectAuthErr("incorrect service %q. This endpoint belongs to \"s3\".", s) +func (invalidPostAuthErr) IncorrectService(creds, service string) S3Error { + return invalidPOSTObjectAuthErr( + "x-amz-credential", + creds, + "incorrect service %q. This endpoint belongs to \"s3\".", + service, + ) } -func (invalidPostAuthErr) MissingField(field string) APIError { - return invalidPOSTObjectAuthErr("Bucket POST must contain a field named '%s'. If it is specified, please check the order of the fields.", field) +func (invalidPostAuthErr) MissingField(field string) S3Error { + return invalidPOSTObjectAuthErr( + field, + "", + "Bucket POST must contain a field named '%s'. If it is specified, please check the order of the fields.", + field, + ) } var PostAuth invalidPostAuthErr @@ -80,71 +105,71 @@ func invalidAccordingToPolicyErr(format string, args ...any) APIError { type invalidPolicyDocument struct{} -func (invalidPolicyDocument) EmptyPolicy() APIError { +func (invalidPolicyDocument) EmptyPolicy() S3Error { return invalidPolicyDocumentErr("Invalid Policy: Expecting '{' but found End-of-Input") } -func (invalidPolicyDocument) InvalidBase64Encoding() APIError { +func (invalidPolicyDocument) InvalidBase64Encoding() S3Error { return invalidPolicyDocumentErr("Invalid Policy: invalid Base64 encoding.") } -func (invalidPolicyDocument) InvalidJSON() APIError { +func (invalidPolicyDocument) InvalidJSON() S3Error { return invalidPolicyDocumentErr("Invalid Policy: Invalid JSON.") } -func (invalidPolicyDocument) UnexpectedField(field string) APIError { +func (invalidPolicyDocument) UnexpectedField(field string) S3Error { return invalidPolicyDocumentErr("Invalid Policy: Unexpected: %q", field) } -func (invalidPolicyDocument) MissingExpiration() APIError { +func (invalidPolicyDocument) MissingExpiration() S3Error { return invalidPolicyDocumentErr("Invalid Policy: Policy missing expiration.") } -func (invalidPolicyDocument) InvalidExpiration(exp string) APIError { +func (invalidPolicyDocument) InvalidExpiration(exp string) S3Error { return invalidPolicyDocumentErr("Invalid Policy: Invalid 'expiration' value: '%s'", exp) } -func (invalidPolicyDocument) InvalidConditions() APIError { +func (invalidPolicyDocument) InvalidConditions() S3Error { return invalidPolicyDocumentErr("Invalid Policy: Invalid 'conditions' value: must be a List.") } -func (invalidPolicyDocument) InvalidCondition() APIError { +func (invalidPolicyDocument) InvalidCondition() S3Error { return invalidPolicyDocumentErr("Invalid Policy: Invalid condition test: must be a List or Object.") } -func (invalidPolicyDocument) MissingConditionOperationIdentifier() APIError { +func (invalidPolicyDocument) MissingConditionOperationIdentifier() S3Error { return invalidPolicyDocumentErr("Invalid Policy: Invalid Condition: missing operation identifier.") } -func (invalidPolicyDocument) UnknownConditionOperation(op string) APIError { +func (invalidPolicyDocument) UnknownConditionOperation(op string) S3Error { return invalidPolicyDocumentErr("Invalid Policy: Invalid Condition: unknown operation '%s'.", op) } -func (invalidPolicyDocument) IncorrectConditionArgumentsNumber(op string) APIError { +func (invalidPolicyDocument) IncorrectConditionArgumentsNumber(op string) S3Error { return invalidPolicyDocumentErr("Invalid Policy: Invalid %s: wrong number of arguments.", op) } -func (invalidPolicyDocument) MissingConditions() APIError { +func (invalidPolicyDocument) MissingConditions() S3Error { return invalidPolicyDocumentErr("Invalid Policy: Policy missing conditions.") } -func (invalidPolicyDocument) OnePropSimpleCondition() APIError { +func (invalidPolicyDocument) OnePropSimpleCondition() S3Error { return invalidPolicyDocumentErr("Invalid Policy: Invalid Simple-Condition: Simple-Conditions must have exactly one property specified.") } -func (invalidPolicyDocument) InvalidSimpleCondition() APIError { +func (invalidPolicyDocument) InvalidSimpleCondition() S3Error { return invalidPolicyDocumentErr("Invalid Policy: Invalid Simple-Condition: value must be a string.") } -func (invalidPolicyDocument) ConditionFailed(condition string) APIError { +func (invalidPolicyDocument) ConditionFailed(condition string) S3Error { return invalidAccordingToPolicyErr("Invalid according to Policy: Policy Condition failed: %s", condition) } -func (invalidPolicyDocument) ExtraInputField(field string) APIError { +func (invalidPolicyDocument) ExtraInputField(field string) S3Error { return invalidAccordingToPolicyErr("Invalid according to Policy: Extra input fields: %s", field) } -func (invalidPolicyDocument) PolicyExpired() APIError { +func (invalidPolicyDocument) PolicyExpired() S3Error { return invalidAccordingToPolicyErr("Invalid according to Policy: Policy expired.") } diff --git a/s3err/precondition-failed-error.go b/s3err/precondition-failed-error.go new file mode 100644 index 00000000..0e8fe3fb --- /dev/null +++ b/s3err/precondition-failed-error.go @@ -0,0 +1,65 @@ +// 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 s3err + +import "encoding/xml" + +type Condition string + +const ( + ConditionIfMatch Condition = "If-Match" + ConditionIfNoneMatch Condition = "If-None-Match" + ConditionIfUnmodifiedSince Condition = "If-Unmodified-Since" + ConditionIfMatchSize Condition = "If-Match-Size" + ConditionIfMatchInitiatedTime Condition = "If-Match-Initiated-Time" + ConditionIfMatchLastModTime Condition = "If-Match-Last-Mod-Time" + ConditionPostBucket Condition = "Bucket POST must be of the enclosure-type multipart/form-data" +) + +// PreconditionFailedError is returned when a conditional request precondition is not met. +// Produces a field in the XML response. +type PreconditionFailedError struct { + APIError + Condition Condition +} + +func (e PreconditionFailedError) XMLBody(requestID, hostID string) []byte { + return encodeResponse(struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + Condition Condition `xml:",omitempty"` + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` + }{ + Code: e.Code, + Message: e.Description, + Condition: e.Condition, + RequestID: requestID, + HostID: hostID, + }) +} + +func (e PreconditionFailedError) Is(target error) bool { + t, ok := target.(APIError) + return ok && e.APIError == t +} + +func GetPreconditionFailedErr(condition Condition) PreconditionFailedError { + return PreconditionFailedError{ + APIError: GetAPIError(ErrPreconditionFailed), + Condition: condition, + } +} diff --git a/s3err/presigned-urls.go b/s3err/presigned-urls.go index 7a65a5d0..20bcfa3d 100644 --- a/s3err/presigned-urls.go +++ b/s3err/presigned-urls.go @@ -20,7 +20,7 @@ import ( ) // Factory for building AuthorizationQueryParametersError errors. -func authQueryParamError(format string, args ...any) APIError { +func authQueryParamError(format string, args ...any) S3Error { return APIError{ Code: "AuthorizationQueryParametersError", Description: fmt.Sprintf(format, args...), @@ -30,60 +30,60 @@ func authQueryParamError(format string, args ...any) APIError { type queryAuthErrors struct{} -func (queryAuthErrors) UnsupportedAlgorithm() APIError { +func (queryAuthErrors) UnsupportedAlgorithm() S3Error { return authQueryParamError(`X-Amz-Algorithm only supports "AWS4-HMAC-SHA256 and AWS4-ECDSA-P256-SHA256"`) } -func (queryAuthErrors) MalformedCredential() APIError { +func (queryAuthErrors) MalformedCredential(_ string) S3Error { return authQueryParamError(`Error parsing the X-Amz-Credential parameter; the Credential is mal-formed; expecting "/YYYYMMDD/REGION/SERVICE/aws4_request".`) } -func (queryAuthErrors) IncorrectService(s string) APIError { +func (queryAuthErrors) IncorrectService(_, s string) S3Error { return authQueryParamError(`Error parsing the X-Amz-Credential parameter; incorrect service %q. This endpoint belongs to "s3".`, s) } -func (queryAuthErrors) IncorrectRegion(expected, actual string) APIError { +func (queryAuthErrors) IncorrectRegion(expected, actual string) S3Error { return authQueryParamError(`Error parsing the X-Amz-Credential parameter; the region %q is wrong; expecting %q`, actual, expected) } -func (queryAuthErrors) IncorrectTerminal(s string) APIError { +func (queryAuthErrors) IncorrectTerminal(_, s string) S3Error { return authQueryParamError(`Error parsing the X-Amz-Credential parameter; incorrect terminal %q. This endpoint uses "aws4_request".`, s) } -func (queryAuthErrors) InvalidDateFormat(s string) APIError { +func (queryAuthErrors) InvalidDateFormat(_, s string) S3Error { return authQueryParamError(`Error parsing the X-Amz-Credential parameter; incorrect date format %q. This date in the credential must be in the format "yyyyMMdd".`, s) } -func (queryAuthErrors) DateMismatch(expected, actual string) APIError { +func (queryAuthErrors) DateMismatch(expected, actual string) S3Error { return authQueryParamError(`Invalid credential date %q. This date is not the same as X-Amz-Date: %q.`, expected, actual) } -func (queryAuthErrors) ExpiresTooLarge() APIError { +func (queryAuthErrors) ExpiresTooLarge() S3Error { return authQueryParamError("X-Amz-Expires must be less than a week (in seconds); that is, the given X-Amz-Expires must be less than 604800 seconds") } -func (queryAuthErrors) ExpiresNegative() APIError { +func (queryAuthErrors) ExpiresNegative() S3Error { return authQueryParamError("X-Amz-Expires must be non-negative") } -func (queryAuthErrors) ExpiresNumber() APIError { +func (queryAuthErrors) ExpiresNumber() S3Error { return authQueryParamError("X-Amz-Expires should be a number") } -func (queryAuthErrors) MissingRequiredParams() APIError { +func (queryAuthErrors) MissingRequiredParams() S3Error { return authQueryParamError("Query-string authentication version 4 requires the X-Amz-Algorithm, X-Amz-Credential, X-Amz-Signature, X-Amz-Date, X-Amz-SignedHeaders, and X-Amz-Expires parameters.") } -func (queryAuthErrors) InvalidXAmzDateFormat() APIError { +func (queryAuthErrors) InvalidXAmzDateFormat() S3Error { return authQueryParamError(`X-Amz-Date must be in the ISO8601 Long Format "yyyyMMdd'T'HHmmss'Z'"`) } // a custom non-AWS error -func (queryAuthErrors) OnlyHMACSupported() APIError { +func (queryAuthErrors) OnlyHMACSupported() S3Error { return authQueryParamError("X-Amz-Algorithm only supports \"AWS4-HMAC-SHA256\"") } -func (queryAuthErrors) SecurityTokenNotSupported() APIError { +func (queryAuthErrors) SecurityTokenNotSupported() S3Error { return authQueryParamError("Authorization with X-Amz-Security-Token is not supported") } diff --git a/s3err/request-time-too-skewed-error.go b/s3err/request-time-too-skewed-error.go new file mode 100644 index 00000000..b40eb109 --- /dev/null +++ b/s3err/request-time-too-skewed-error.go @@ -0,0 +1,63 @@ +// 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 s3err + +import ( + "encoding/xml" +) + +// RequestTimeTooSkewedError is returned when the request timestamp is too far from server time. +// Produces , , and fields. +type RequestTimeTooSkewedError struct { + APIError + RequestTime string + ServerTime string + MaxAllowedSkewMilliseconds int +} + +func (e RequestTimeTooSkewedError) XMLBody(requestID, hostID string) []byte { + return encodeResponse(struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + RequestTime string + ServerTime string + MaxAllowedSkewMilliseconds int + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` + }{ + Code: e.Code, + Message: e.Description, + RequestTime: e.RequestTime, + ServerTime: e.ServerTime, + MaxAllowedSkewMilliseconds: e.MaxAllowedSkewMilliseconds, + RequestID: requestID, + HostID: hostID, + }) +} + +func (e RequestTimeTooSkewedError) Is(target error) bool { + t, ok := target.(APIError) + return ok && e.APIError == t +} + +func GetRequestTimeTooSkewedErr(requestTime, serverTime string, maxAllowedMilliseconds int) RequestTimeTooSkewedError { + return RequestTimeTooSkewedError{ + APIError: GetAPIError(ErrRequestTimeTooSkewed), + RequestTime: requestTime, + ServerTime: serverTime, + MaxAllowedSkewMilliseconds: maxAllowedMilliseconds, + } +} diff --git a/s3err/s3err.go b/s3err/s3err.go index 3d0ea1e4..ddbec479 100644 --- a/s3err/s3err.go +++ b/s3err/s3err.go @@ -24,6 +24,15 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3/types" ) +// S3Error is the interface implemented by all S3 error types. +// It allows centralized error handling while supporting per-error-type XML fields. +type S3Error interface { + error + StatusCode() int + BaseError() APIError + XMLBody(requestID, hostID string) []byte +} + // APIError structure type APIError struct { Code string @@ -31,17 +40,8 @@ type APIError struct { HTTPStatusCode int } -// APIErrorResponse - error response format -type APIErrorResponse struct { - XMLName xml.Name `xml:"Error" json:"-"` - Code string - Message string - Key string `xml:"Key,omitempty" json:"Key,omitempty"` - BucketName string `xml:"BucketName,omitempty" json:"BucketName,omitempty"` - Resource string - Region string `xml:"Region,omitempty" json:"Region,omitempty"` - RequestID string `xml:"RequestId" json:"RequestId"` - HostID string `xml:"HostId" json:"HostId"` +func (e APIError) BaseError() APIError { + return e } func (A APIError) Error() string { @@ -52,6 +52,23 @@ func (A APIError) Error() string { return bytesBuffer.String() } +func (e APIError) StatusCode() int { return e.HTTPStatusCode } + +func (e APIError) XMLBody(requestID, hostID string) []byte { + return encodeResponse(struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` + }{ + Code: e.Code, + Message: e.Description, + RequestID: requestID, + HostID: hostID, + }) +} + // ErrorCode type of error status. type ErrorCode int @@ -76,48 +93,28 @@ const ( ErrInvalidBucketName ErrInvalidDigest ErrBadDigest - ErrInvalidMaxBuckets - ErrNegativeMaxKeys - ErrInvalidObjectAttributes ErrInvalidPart - ErrInvalidPartNumber ErrInvalidPartNumberRange ErrRangeAndPartNumber ErrInvalidPartOrder - ErrInvalidCompleteMpPartNumber ErrInternalError ErrNonEmptyRequestBody ErrIncompleteBody ErrInvalidCopyDest - ErrInvalidCopySourceRange - ErrInvalidCopySourceBucket - ErrInvalidCopySourceObject - ErrInvalidCopySourceEncoding ErrInvalidTagKey ErrInvalidTagValue ErrDuplicateTagKey ErrBucketTaggingLimited ErrObjectTaggingLimited ErrCannotParseHTTPRequest - ErrInvalidURLEncodedTagging - ErrInvalidAuthHeader - ErrUnsupportedAuthorizationType ErrMalformedPOSTRequest - ErrPOSTFileRequired - ErrPostPolicyConditionInvalidFormat ErrEntityTooSmall ErrEntityTooLarge - ErrMissingFields - ErrMissingCredTag ErrMalformedXML ErrMalformedCredentialDate - ErrMissingSignHeadersTag - ErrMissingSignTag - ErrUnsignedHeaders ErrExpiredPresignRequest ErrSignatureDoesNotMatch ErrContentSHA256Mismatch - ErrInvalidSHA256Paylod ErrInvalidSHA256PayloadUsage ErrUnsupportedAnonymousSignedStreaming ErrMissingContentLength @@ -127,7 +124,6 @@ const ( ErrMissingDateHeader ErrGetUploadsWithKey ErrVersionsWithKey - ErrCopySourceNotAllowed ErrInvalidRequest ErrAuthNotSetup ErrNotImplemented @@ -141,14 +137,8 @@ const ( ErrMissingObjectLockConfigurationNoSpaces ErrObjectLockConfigurationNotAllowed ErrObjectLocked - ErrInvalidRetainUntilDate - ErrPastObjectLockRetainDate - ErrObjectLockInvalidRetentionPeriod - ErrInvalidLegalHoldStatus - ErrInvalidObjectLockMode ErrNoSuchBucketPolicy ErrBucketTaggingNotFound - ErrObjectLockInvalidHeaders ErrObjectAttributesInvalidHeader ErrRequestTimeTooSkewed ErrInvalidBucketAclWithObjectOwnership @@ -158,10 +148,7 @@ const ( ErrMalformedACL ErrUnexpectedContent ErrMissingSecurityHeader - ErrInvalidMetadataDirective - ErrInvalidTaggingDirective ErrKeyTooLong - ErrInvalidVersionId ErrNoSuchVersion ErrSuspendedVersioningNotAllowed ErrMissingRequestBody @@ -170,26 +157,20 @@ const ( ErrChecksumRequired ErrMissingContentSha256 ErrInvalidChecksumAlgorithm - ErrInvalidChecksumPart ErrChecksumTypeWithAlgo ErrInvalidChecksumHeader ErrTrailerHeaderNotSupported ErrBadRequest - ErrMissingUploadId - ErrInvalidUploadIdMarker ErrNoSuchCORSConfiguration ErrCORSForbidden ErrMissingCORSOrigin ErrCORSIsNotEnabled ErrNotModified ErrInvalidLocationConstraint - ErrInvalidArgument ErrMalformedTrailer ErrInvalidChunkSize ErrSlowDown ErrMetadataTooLarge - ErrOnlyAws4HmacSha256 - ErrInvalidDateHeader ErrUnsupportedAuthorizationMechanism // Non-AWS errors @@ -288,21 +269,6 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "The Content-MD5 you specified did not match what we received.", HTTPStatusCode: http.StatusBadRequest, }, - ErrInvalidMaxBuckets: { - Code: "InvalidArgument", - Description: "Argument max-buckets must be an integer between 1 and 10000.", - HTTPStatusCode: http.StatusBadRequest, - }, - ErrNegativeMaxKeys: { - Code: "InvalidArgument", - Description: "max-keys cannot be negative", - HTTPStatusCode: http.StatusBadRequest, - }, - ErrInvalidObjectAttributes: { - Code: "InvalidArgument", - Description: "Invalid attribute name specified.", - HTTPStatusCode: http.StatusBadRequest, - }, ErrNoSuchBucket: { Code: "NoSuchBucket", Description: "The specified bucket does not exist.", @@ -338,11 +304,6 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "One or more of the specified parts could not be found. The part may not have been uploaded, or the specified entity tag may not match the part's entity tag.", HTTPStatusCode: http.StatusBadRequest, }, - ErrInvalidPartNumber: { - Code: "InvalidArgument", - Description: "Part number must be an integer between 1 and 10000, inclusive.", - HTTPStatusCode: http.StatusBadRequest, - }, ErrInvalidPartNumberRange: { Code: "InvalidPartNumber", Description: "The requested partnumber is not satisfiable.", @@ -358,36 +319,11 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "The list of parts was not in ascending order. Parts must be ordered by part number.", HTTPStatusCode: http.StatusBadRequest, }, - ErrInvalidCompleteMpPartNumber: { - Code: "InvalidArgument", - Description: "PartNumber must be >= 1", - HTTPStatusCode: http.StatusBadRequest, - }, ErrInvalidCopyDest: { Code: "InvalidRequest", Description: "This copy request is illegal because it is trying to copy an object to itself without changing the object's metadata, storage class, website redirect location or encryption attributes.", HTTPStatusCode: http.StatusBadRequest, }, - ErrInvalidCopySourceRange: { - Code: "InvalidArgument", - Description: "The x-amz-copy-source-range value must be of the form bytes=first-last where first and last are the zero-based offsets of the first and last bytes to copy", - HTTPStatusCode: http.StatusBadRequest, - }, - ErrInvalidCopySourceBucket: { - Code: "InvalidArgument", - Description: "Invalid copy source bucket name", - HTTPStatusCode: http.StatusBadRequest, - }, - ErrInvalidCopySourceObject: { - Code: "InvalidArgument", - Description: "Invalid copy source object key", - HTTPStatusCode: http.StatusBadRequest, - }, - ErrInvalidCopySourceEncoding: { - Code: "InvalidArgument", - Description: "Invalid copy source encoding", - HTTPStatusCode: http.StatusBadRequest, - }, ErrInvalidTagKey: { Code: "InvalidTag", Description: "The TagKey you have provided is invalid", @@ -418,41 +354,16 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "An error occurred when parsing the HTTP request.", HTTPStatusCode: http.StatusBadRequest, }, - ErrInvalidURLEncodedTagging: { - Code: "InvalidArgument", - Description: "The header 'x-amz-tagging' shall be encoded as UTF-8 then URLEncoded URL query parameters without tag name duplicates.", - HTTPStatusCode: http.StatusBadRequest, - }, ErrMalformedXML: { Code: "MalformedXML", Description: "The XML you provided was not well-formed or did not validate against our published schema.", HTTPStatusCode: http.StatusBadRequest, }, - ErrInvalidAuthHeader: { - Code: "InvalidArgument", - Description: "Authorization header is invalid -- one and only one ' ' (space) required.", - HTTPStatusCode: http.StatusBadRequest, - }, - ErrUnsupportedAuthorizationType: { - Code: "InvalidArgument", - Description: "Unsupported Authorization Type", - HTTPStatusCode: http.StatusBadRequest, - }, ErrMalformedPOSTRequest: { Code: "MalformedPOSTRequest", Description: "The body of your POST request is not well-formed multipart/form-data.", HTTPStatusCode: http.StatusBadRequest, }, - ErrPOSTFileRequired: { - Code: "InvalidArgument", - Description: "POST requires exactly one file upload per request.", - HTTPStatusCode: http.StatusBadRequest, - }, - ErrPostPolicyConditionInvalidFormat: { - Code: "PostPolicyInvalidKeyName", - Description: "Invalid according to Policy: Policy Condition failed.", - HTTPStatusCode: http.StatusForbidden, - }, ErrEntityTooSmall: { Code: "EntityTooSmall", Description: "Your proposed upload is smaller than the minimum allowed size", @@ -463,31 +374,6 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "Your proposed upload exceeds the maximum allowed object size.", HTTPStatusCode: http.StatusBadRequest, }, - ErrMissingFields: { - Code: "MissingFields", - Description: "Missing fields in request.", - HTTPStatusCode: http.StatusBadRequest, - }, - ErrMissingCredTag: { - Code: "InvalidRequest", - Description: "Missing Credential field for this request.", - HTTPStatusCode: http.StatusBadRequest, - }, - ErrMissingSignHeadersTag: { - Code: "InvalidArgument", - Description: "Signature header missing SignedHeaders field.", - HTTPStatusCode: http.StatusBadRequest, - }, - ErrMissingSignTag: { - Code: "AccessDenied", - Description: "Signature header missing Signature field.", - HTTPStatusCode: http.StatusBadRequest, - }, - ErrUnsignedHeaders: { - Code: "AccessDenied", - Description: "There were headers present in the request which were not signed.", - HTTPStatusCode: http.StatusBadRequest, - }, ErrExpiredPresignRequest: { Code: "AccessDenied", Description: "Request has expired.", @@ -513,11 +399,6 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "The provided 'x-amz-content-sha256' header does not match what was computed.", HTTPStatusCode: http.StatusBadRequest, }, - ErrInvalidSHA256Paylod: { - Code: "InvalidArgument", - Description: "x-amz-content-sha256 must be UNSIGNED-PAYLOAD, STREAMING-UNSIGNED-PAYLOAD-TRAILER, STREAMING-AWS4-HMAC-SHA256-PAYLOAD, STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER, STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD, STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD-TRAILER or a valid sha256 value.", - HTTPStatusCode: http.StatusBadRequest, - }, ErrInvalidSHA256PayloadUsage: { Code: "InvalidRequest", Description: "The value of x-amz-content-sha256 header is invalid.", @@ -553,11 +434,6 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "There is no such thing as the ?versions sub-resource for a key", HTTPStatusCode: http.StatusBadRequest, }, - ErrCopySourceNotAllowed: { - Code: "InvalidArgument", - Description: "You can only specify a copy source header for copy requests.", - HTTPStatusCode: http.StatusBadRequest, - }, ErrInvalidRequest: { Code: "InvalidRequest", Description: "Invalid Request.", @@ -623,31 +499,6 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "Access Denied because object protected by object lock.", HTTPStatusCode: http.StatusForbidden, }, - ErrInvalidRetainUntilDate: { - Code: "InvalidArgument", - Description: "The retain until date must be provided in ISO 8601 format", - HTTPStatusCode: http.StatusBadRequest, - }, - ErrPastObjectLockRetainDate: { - Code: "InvalidArgument", - Description: "The retain until date must be in the future!", - HTTPStatusCode: http.StatusBadRequest, - }, - ErrObjectLockInvalidRetentionPeriod: { - Code: "InvalidArgument", - Description: "Default retention period must be a positive integer value.", - HTTPStatusCode: http.StatusBadRequest, - }, - ErrInvalidLegalHoldStatus: { - Code: "InvalidArgument", - Description: "Legal Hold must be either of 'ON' or 'OFF'", - HTTPStatusCode: http.StatusBadRequest, - }, - ErrInvalidObjectLockMode: { - Code: "InvalidArgument", - Description: "Unknown wormMode directive.", - HTTPStatusCode: http.StatusBadRequest, - }, ErrNoSuchBucketPolicy: { Code: "NoSuchBucketPolicy", Description: "The bucket policy does not exist.", @@ -658,11 +509,6 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "The TagSet does not exist.", HTTPStatusCode: http.StatusNotFound, }, - ErrObjectLockInvalidHeaders: { - Code: "InvalidRequest", - Description: "x-amz-object-lock-retain-until-date and x-amz-object-lock-mode must both be supplied.", - HTTPStatusCode: http.StatusBadRequest, - }, ErrObjectAttributesInvalidHeader: { Code: "InvalidRequest", Description: "The x-amz-object-attributes header specifying the attributes to be retrieved is either missing or empty", @@ -708,21 +554,6 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "Your request was missing a required header.", HTTPStatusCode: http.StatusNotFound, }, - ErrInvalidMetadataDirective: { - Code: "InvalidArgument", - Description: "Unknown metadata directive.", - HTTPStatusCode: http.StatusBadRequest, - }, - ErrInvalidTaggingDirective: { - Code: "InvalidArgument", - Description: "Unknown tagging directive.", - HTTPStatusCode: http.StatusBadRequest, - }, - ErrInvalidVersionId: { - Code: "InvalidArgument", - Description: "Invalid version id specified", - HTTPStatusCode: http.StatusBadRequest, - }, ErrKeyTooLong: { Code: "KeyTooLongError", Description: "Your key is too long.", @@ -768,11 +599,6 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "Checksum algorithm provided is unsupported. Please try again with any of the valid types: [CRC32, CRC32C, CRC64NVME, MD5, SHA1, SHA256, SHA512, XXHASH128, XXHASH3, XXHASH64]", HTTPStatusCode: http.StatusBadRequest, }, - ErrInvalidChecksumPart: { - Code: "InvalidArgument", - Description: "Invalid Base64 or multiple checksums present in request", - HTTPStatusCode: http.StatusBadRequest, - }, ErrChecksumTypeWithAlgo: { Code: "InvalidRequest", Description: "The x-amz-checksum-type header can only be used with the x-amz-checksum-algorithm header.", @@ -793,16 +619,6 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "Bad Request", HTTPStatusCode: http.StatusBadRequest, }, - ErrMissingUploadId: { - Code: "InvalidArgument", - Description: "This operation does not accept partNumber without uploadId", - HTTPStatusCode: http.StatusBadRequest, - }, - ErrInvalidUploadIdMarker: { - Code: "InvalidArgument", - Description: "Invalid uploadId marker", - HTTPStatusCode: http.StatusBadRequest, - }, ErrNoSuchCORSConfiguration: { Code: "NoSuchCORSConfiguration", Description: "The CORS configuration does not exist", @@ -833,11 +649,6 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "The specified location-constraint is not valid", HTTPStatusCode: http.StatusBadRequest, }, - ErrInvalidArgument: { - Code: "InvalidArgument", - Description: "", - HTTPStatusCode: http.StatusBadRequest, - }, ErrMalformedTrailer: { Code: "MalformedTrailerError", Description: "The request contained trailing data that was not well-formed or did not conform to our published schema.", @@ -854,21 +665,10 @@ var errorCodeResponse = map[ErrorCode]APIError{ 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, }, - ErrOnlyAws4HmacSha256: { - Code: "InvalidArgument", - Description: "Only AWS4-HMAC-SHA256 is supported", - HTTPStatusCode: http.StatusBadRequest, - }, - ErrInvalidDateHeader: { - Code: "InvalidArgument", - Description: "X-Amz-Date must be formated via ISO8601 Long format", - HTTPStatusCode: http.StatusBadRequest, - }, ErrUnsupportedAuthorizationMechanism: { Code: "InvalidRequest", Description: "The authorization mechanism you have provided is not supported. Please use AWS4-HMAC-SHA256.", @@ -960,21 +760,6 @@ func GetAPIError(code ErrorCode) APIError { return errorCodeResponse[code] } -// getErrorResponse gets in standard error and resource value and -// provides a encodable populated response values -func GetAPIErrorResponse(err APIError, resource, requestID, hostID string) []byte { - return encodeResponse(APIErrorResponse{ - Code: err.Code, - Message: err.Description, - BucketName: "", - Key: "", - Resource: resource, - Region: "", - RequestID: requestID, - HostID: hostID, - }) -} - // Encodes the response headers into XML format. func encodeResponse(response any) []byte { var bytesBuffer bytes.Buffer @@ -1061,14 +846,6 @@ func GetInvalidMpObjectSizeErr(val string) APIError { } } -func CreateExceedingRangeErr(objSize int64) APIError { - return APIError{ - Code: "InvalidArgument", - Description: fmt.Sprintf("Range specified is not valid for source object of size: %d", objSize), - HTTPStatusCode: http.StatusBadRequest, - } -} - func GetInvalidCORSHeaderErr(header string) APIError { return APIError{ Code: "InvalidRequest", @@ -1109,22 +886,6 @@ func GetInvalidCORSMethodErr(method string) APIError { } } -func GetInvalidMaxLimiterErr(limiter string) APIError { - return APIError{ - Code: "InvalidArgument", - Description: fmt.Sprintf("Provided %s not an integer or within integer range", limiter), - HTTPStatusCode: http.StatusBadRequest, - } -} - -func GetNegativeMaxLimiterErr(limiter string) APIError { - return APIError{ - Code: "InvalidArgument", - Description: fmt.Sprintf("Argument %s must be an integer between 0 and 2147483647", limiter), - HTTPStatusCode: http.StatusBadRequest, - } -} - func GetCopySourceObjectTooLargeErr(limit int64) APIError { return APIError{ Code: "InvalidRequest", @@ -1132,3 +893,13 @@ func GetCopySourceObjectTooLargeErr(limit int64) APIError { HTTPStatusCode: http.StatusBadRequest, } } + +type ResourceType string + +const ( + ResourceTypeBucket ResourceType = "BUCKET" + ResourceTypeObject ResourceType = "OBJECT" + ResourceTypeService ResourceType = "SERVICE" + ResourceTypeBucketPolicy ResourceType = "BUCKETPOLICY" + ResourceTypeUpload ResourceType = "UPLOAD" +) diff --git a/s3err/signature-does-not-match-error.go b/s3err/signature-does-not-match-error.go new file mode 100644 index 00000000..97cfeb96 --- /dev/null +++ b/s3err/signature-does-not-match-error.go @@ -0,0 +1,73 @@ +// 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 s3err + +import "encoding/xml" + +// SignatureDoesNotMatchError is returned when request signature verification fails. +// Produces diagnostic fields to help callers debug the mismatch. +type SignatureDoesNotMatchError struct { + APIError + AWSAccessKeyId string + StringToSign string + SignatureProvided string + StringToSignBytes string + CanonicalRequest string + CanonicalRequestBytes string +} + +func (e SignatureDoesNotMatchError) XMLBody(requestID, hostID string) []byte { + return encodeResponse(struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + AWSAccessKeyId string `xml:",omitempty"` + StringToSign string `xml:",omitempty"` + SignatureProvided string `xml:",omitempty"` + StringToSignBytes string `xml:",omitempty"` + CanonicalRequest string `xml:",omitempty"` + CanonicalRequestBytes string `xml:",omitempty"` + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` + }{ + Code: e.Code, + Message: e.Description, + AWSAccessKeyId: e.AWSAccessKeyId, + StringToSign: e.StringToSign, + SignatureProvided: e.SignatureProvided, + StringToSignBytes: e.StringToSignBytes, + CanonicalRequest: e.CanonicalRequest, + CanonicalRequestBytes: e.CanonicalRequestBytes, + RequestID: requestID, + HostID: hostID, + }) +} + +func (e SignatureDoesNotMatchError) Is(target error) bool { + t, ok := target.(APIError) + return ok && e.APIError == t +} + +func GetSignatureDoesNotMatchErr(accessKeyId, stringToSign, signatureProvided, stringToSignBytes, canonicalRequest, canonicalRequestBytes string) SignatureDoesNotMatchError { + return SignatureDoesNotMatchError{ + APIError: GetAPIError(ErrSignatureDoesNotMatch), + AWSAccessKeyId: accessKeyId, + StringToSign: stringToSign, + SignatureProvided: signatureProvided, + StringToSignBytes: stringToSignBytes, + CanonicalRequest: canonicalRequest, + CanonicalRequestBytes: canonicalRequestBytes, + } +} diff --git a/s3err/sigv4.go b/s3err/sigv4.go index e831a67a..a868089d 100644 --- a/s3err/sigv4.go +++ b/s3err/sigv4.go @@ -15,69 +15,103 @@ package s3err import ( + "encoding/xml" "fmt" "net/http" ) +// MalformedAuthError is returned when a Signature V4 authorization header is malformed. +// Produces a field in the XML response when the expected gateway region is known. +type MalformedAuthError struct { + APIError + Region string +} + +func (e MalformedAuthError) XMLBody(requestID, hostID string) []byte { + return encodeResponse(struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + Region string `xml:",omitempty"` + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` + }{ + Code: e.Code, + Message: e.Description, + Region: e.Region, + RequestID: requestID, + HostID: hostID, + }) +} + +func (e MalformedAuthError) Is(target error) bool { + t, ok := target.(APIError) + return ok && e.APIError == t +} + // Factory for building AuthorizationHeaderMalformed errors. -func malformedAuthError(format string, args ...any) APIError { - return APIError{ - Code: "AuthorizationHeaderMalformed", - Description: fmt.Sprintf("The authorization header is malformed; %s", fmt.Sprintf(format, args...)), - HTTPStatusCode: http.StatusBadRequest, +func malformedAuthError(format string, args ...any) MalformedAuthError { + return MalformedAuthError{ + APIError: APIError{ + Code: "AuthorizationHeaderMalformed", + Description: fmt.Sprintf("The authorization header is malformed; %s", fmt.Sprintf(format, args...)), + HTTPStatusCode: http.StatusBadRequest, + }, } } type malformedAuthErrors struct{} -func (malformedAuthErrors) InvalidDateFormat(s string) APIError { +func (malformedAuthErrors) InvalidDateFormat(_, s string) S3Error { return malformedAuthError( "incorrect date format %q. This date in the credential must be in the format \"yyyyMMdd\".", s, ) } -func (malformedAuthErrors) MalformedCredential() APIError { +func (malformedAuthErrors) MalformedCredential(_ string) S3Error { return malformedAuthError( "the Credential is mal-formed; expecting \"/YYYYMMDD/REGION/SERVICE/aws4_request\".", ) } -func (malformedAuthErrors) MissingCredential() APIError { +func (malformedAuthErrors) MissingCredential() S3Error { return malformedAuthError("missing Credential.") } -func (malformedAuthErrors) MissingSignature() APIError { +func (malformedAuthErrors) MissingSignature() S3Error { return malformedAuthError("missing Signature.") } -func (malformedAuthErrors) MissingSignedHeaders() APIError { +func (malformedAuthErrors) MissingSignedHeaders() S3Error { return malformedAuthError("missing SignedHeaders.") } -func (malformedAuthErrors) IncorrectTerminal(s string) APIError { +func (malformedAuthErrors) IncorrectTerminal(_, s string) S3Error { return malformedAuthError("incorrect terminal %q. This endpoint uses \"aws4_request\".", s) } -func (malformedAuthErrors) IncorrectRegion(expected, actual string) APIError { - return malformedAuthError("the region %q is wrong; expecting %q", actual, expected) +func (malformedAuthErrors) IncorrectRegion(expected, actual string) S3Error { + err := malformedAuthError("the region %q is wrong; expecting %q", actual, expected) + err.Region = expected + return err } -func (malformedAuthErrors) IncorrectService(s string) APIError { +func (malformedAuthErrors) IncorrectService(_, s string) S3Error { return malformedAuthError("incorrect service %q. This endpoint belongs to \"s3\".", s) } -func (malformedAuthErrors) MalformedComponent(s string) APIError { +func (malformedAuthErrors) MalformedComponent(s string) S3Error { return malformedAuthError("the authorization component %q is malformed.", s) } -func (malformedAuthErrors) MissingComponents() APIError { +func (malformedAuthErrors) MissingComponents() S3Error { return malformedAuthError( "the authorization header requires three components: Credential, SignedHeaders, and Signature.", ) } -func (malformedAuthErrors) DateMismatch() APIError { +func (malformedAuthErrors) DateMismatch() S3Error { return malformedAuthError( "The authorization header is malformed; Invalid credential date. Date is not the same as X-Amz-Date.", ) diff --git a/s3log/audit-logger.go b/s3log/audit-logger.go index 24796a45..2d655fbd 100644 --- a/s3log/audit-logger.go +++ b/s3log/audit-logger.go @@ -16,10 +16,7 @@ package s3log import ( "crypto/tls" - "encoding/hex" "fmt" - "math/rand" - "strings" "time" "github.com/gofiber/fiber/v2" @@ -78,6 +75,7 @@ type AdminLogFields struct { RemoteIP string Requester string RequestID string + HostID string Operation string RequestURI string HttpStatus int @@ -135,17 +133,6 @@ func InitLogger(cfg *LogConfig) (*Loggers, error) { return loggers, nil } -func genID() string { - src := rand.New(rand.NewSource(time.Now().UnixNano())) - b := make([]byte, 8) - - if _, err := src.Read(b); err != nil { - panic(err) - } - - return strings.ToUpper(hex.EncodeToString(b)) -} - func getTLSVersionName(version uint16) string { switch version { case tls.VersionTLS10: diff --git a/s3log/file.go b/s3log/file.go index e2e8b417..8b6f78ca 100644 --- a/s3log/file.go +++ b/s3log/file.go @@ -86,10 +86,10 @@ func (f *FileLogger) Log(ctx *fiber.Ctx, err error, body []byte, meta LogMeta) { } if err != nil { - serr, ok := err.(s3err.APIError) + serr, ok := err.(s3err.S3Error) if ok { - errorCode = serr.Code - httpStatus = serr.HTTPStatusCode + errorCode = serr.BaseError().Code + httpStatus = serr.BaseError().HTTPStatusCode } else { errorCode = err.Error() httpStatus = 500 @@ -111,7 +111,6 @@ func (f *FileLogger) Log(ctx *fiber.Ctx, err error, body []byte, meta LogMeta) { lf.Time = time.Now() lf.RemoteIP = ctx.IP() lf.Requester = access - lf.RequestID = genID() lf.Operation = meta.Action lf.Key = object lf.RequestURI = reqURI @@ -124,11 +123,13 @@ func (f *FileLogger) Log(ctx *fiber.Ctx, err error, body []byte, meta LogMeta) { lf.Referer = ctx.Get("Referer") lf.UserAgent = ctx.Get("User-Agent") lf.VersionID = ctx.Query("versionId") - lf.HostID = ctx.Get("X-Amz-Id-2") lf.SignatureVersion = "SigV4" lf.AuthenticationType = "AuthHeader" lf.AccessPointARN = fmt.Sprintf("arn:aws:s3:::%v", strings.Join(path, "/")) lf.AclRequired = "Yes" + requestID, hostID := utils.EnsureRequestIDs(ctx) + lf.RequestID = requestID + lf.HostID = hostID f.writeLog(lf) } diff --git a/s3log/file_admin.go b/s3log/file_admin.go index 9b6f8129..a3fb5414 100644 --- a/s3log/file_admin.go +++ b/s3log/file_admin.go @@ -80,7 +80,6 @@ func (f *AdminFileLogger) Log(ctx *fiber.Ctx, err error, body []byte, meta LogMe lf.Time = time.Now() lf.RemoteIP = ctx.IP() lf.Requester = access - lf.RequestID = genID() lf.Operation = meta.Action lf.RequestURI = reqURI lf.HttpStatus = meta.HttpStatus @@ -92,6 +91,9 @@ func (f *AdminFileLogger) Log(ctx *fiber.Ctx, err error, body []byte, meta LogMe lf.UserAgent = ctx.Get("User-Agent") lf.SignatureVersion = "SigV4" lf.AuthenticationType = "AuthHeader" + requestID, hostID := utils.EnsureRequestIDs(ctx) + lf.RequestID = requestID + lf.HostID = hostID f.writeLog(lf) } @@ -125,7 +127,7 @@ func (f *AdminFileLogger) writeLog(lf AdminLogFields) { lf.TLSVersion = "-" } - log := fmt.Sprintf("%v %v %v %v %v %v %v %v %v %v %v %v %v %v %v %v %v\n", + log := fmt.Sprintf("%v %v %v %v %v %v %v %v %v %v %v %v %v %v %v %v %v %v\n", fmt.Sprintf("[%v]", lf.Time.Format(timeFormat)), lf.RemoteIP, lf.Requester, @@ -139,6 +141,7 @@ func (f *AdminFileLogger) writeLog(lf AdminLogFields) { lf.TurnAroundTime, lf.Referer, lf.UserAgent, + lf.HostID, lf.SignatureVersion, lf.CipherSuite, lf.AuthenticationType, diff --git a/s3log/webhook.go b/s3log/webhook.go index 989977ef..c5f6d45a 100644 --- a/s3log/webhook.go +++ b/s3log/webhook.go @@ -83,10 +83,10 @@ func (wl *WebhookLogger) Log(ctx *fiber.Ctx, err error, body []byte, meta LogMet } if err != nil { - serr, ok := err.(s3err.APIError) + serr, ok := err.(s3err.S3Error) if ok { - errorCode = serr.Code - httpStatus = serr.HTTPStatusCode + errorCode = serr.BaseError().Code + httpStatus = serr.StatusCode() } else { errorCode = err.Error() httpStatus = 500 @@ -103,7 +103,6 @@ func (wl *WebhookLogger) Log(ctx *fiber.Ctx, err error, body []byte, meta LogMet lf.Time = time.Now() lf.RemoteIP = ctx.IP() lf.Requester = access - lf.RequestID = genID() lf.Operation = meta.Action lf.Key = object lf.RequestURI = reqURI @@ -116,12 +115,14 @@ func (wl *WebhookLogger) Log(ctx *fiber.Ctx, err error, body []byte, meta LogMet lf.Referer = ctx.Get("Referer") lf.UserAgent = ctx.Get("User-Agent") lf.VersionID = ctx.Query("versionId") - lf.HostID = ctx.Get("X-Amz-Id-2") lf.SignatureVersion = "SigV4" lf.AuthenticationType = "AuthHeader" lf.HostHeader = fmt.Sprintf("s3.%v.amazonaws.com", utils.ContextKeyRegion.Get(ctx).(string)) lf.AccessPointARN = fmt.Sprintf("arn:aws:s3:::%v", strings.Join(path, "/")) lf.AclRequired = "Yes" + requestID, hostID := utils.EnsureRequestIDs(ctx) + lf.RequestID = requestID + lf.HostID = hostID wl.sendLog(lf) } diff --git a/s3response/s3response.go b/s3response/s3response.go index 5df79d93..3c67ebdd 100644 --- a/s3response/s3response.go +++ b/s3response/s3response.go @@ -445,7 +445,8 @@ func (r CopyPartResult) MarshalXML(e *xml.Encoder, start xml.StartElement) error } type CompleteMultipartUploadRequestBody struct { - Parts []types.CompletedPart `xml:"Part"` + XMLName xml.Name `xml:"CompleteMultipartUpload" json:"-"` + Parts []types.CompletedPart `xml:"Part"` } type CompleteMultipartUploadResult struct { diff --git a/tests/integration/AbortMultipartUpload.go b/tests/integration/AbortMultipartUpload.go index 9616723f..12e2e177 100644 --- a/tests/integration/AbortMultipartUpload.go +++ b/tests/integration/AbortMultipartUpload.go @@ -132,7 +132,7 @@ func AbortMultipartUpload_success_status_code(s *S3Conf) error { req, err := createSignedReq(http.MethodDelete, s.endpoint, fmt.Sprintf("%v/%v?uploadId=%v", bucket, obj, *out.UploadId), - s.awsID, s.awsSecret, "s3", s.awsRegion, nil, time.Now(), nil) + s.awsID, s.awsSecret, "s3", s.awsRegion, "", nil, time.Now(), nil) if err != nil { return err } diff --git a/tests/integration/CompleteMultipartUpload.go b/tests/integration/CompleteMultipartUpload.go index 625b20be..6a4f28b9 100644 --- a/tests/integration/CompleteMultipartUpload.go +++ b/tests/integration/CompleteMultipartUpload.go @@ -23,9 +23,11 @@ import ( "fmt" "io" "net/http" + "net/url" "slices" "strings" "sync" + "time" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3/types" @@ -226,7 +228,7 @@ func CompleteMultipartUpload_invalid_checksum_part(s *S3Conf) error { ChecksumType: types.ChecksumTypeFullObject, }) cancel() - if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidChecksumPart)); err != nil { + if err := checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgChecksumPart, "invalid_checksum")); err != nil { return err } @@ -264,6 +266,7 @@ func CompleteMultipartUpload_multiple_checksum_part(s *S3Conf) error { cParts[0].ChecksumSHA1 = getPtr("Kq5sNclPz7QV2+lfQIuc6R7oRu0=") } } + argValue := fmt.Sprintf("CRC32:%s;SHA1:%s;", getString(cParts[0].ChecksumCRC32), getString(cParts[0].ChecksumSHA1)) ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) _, err = s3client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{ @@ -276,7 +279,7 @@ func CompleteMultipartUpload_multiple_checksum_part(s *S3Conf) error { ChecksumType: types.ChecksumTypeComposite, }) cancel() - if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidChecksumPart)); err != nil { + if err := checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgChecksumPart, argValue)); err != nil { return err } @@ -1259,6 +1262,72 @@ func CompleteMultipartUpload_empty_parts(s *S3Conf) error { }) } +func CompleteMultipartUpload_missing_part_fields(s *S3Conf) error { + testName := "CompleteMultipartUpload_missing_part_fields" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + tests := []struct { + name string + body func(types.Part) []byte + }{ + { + name: "missing ETag", + body: func(part types.Part) []byte { + return fmt.Appendf(nil, `%d`, + getInt32(part.PartNumber)) + }, + }, + { + name: "missing PartNumber", + body: func(part types.Part) []byte { + return fmt.Appendf(nil, `%s`, + getString(part.ETag)) + }, + }, + } + + for i, test := range tests { + obj := fmt.Sprintf("my-obj-%d", i) + mp, err := createMp(s3client, bucket, obj) + if err != nil { + return fmt.Errorf("%s: %w", test.name, err) + } + + parts, _, err := uploadParts(s3client, 5*1024*1024, 1, bucket, obj, *mp.UploadId) + if err != nil { + return fmt.Errorf("%s: %w", test.name, err) + } + + req, err := createSignedReq( + http.MethodPost, + s.endpoint, + fmt.Sprintf("%s/%s?uploadId=%s", bucket, obj, url.QueryEscape(*mp.UploadId)), + s.awsID, + s.awsSecret, + "s3", + s.awsRegion, + "", + test.body(parts[0]), + time.Now(), + map[string]string{"Content-Type": "application/xml"}, + ) + if err != nil { + return fmt.Errorf("%s: %w", test.name, err) + } + + resp, err := s.httpClient.Do(req) + if err != nil { + return fmt.Errorf("%s: %w", test.name, err) + } + + if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrMalformedXML)); err != nil { + return fmt.Errorf("%s: %w", test.name, err) + } + } + + return nil + }) +} + func CompleteMultipartUpload_incorrect_parts_order(s *S3Conf) error { testName := "CompleteMultipartUpload_incorrect_parts_order" return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { @@ -1616,7 +1685,7 @@ func CompleteMultipartUpload_invalid_part_number(s *S3Conf) error { }, }) cancel() - if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidCompleteMpPartNumber)); err != nil { + if err := checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgCompleteMpPartNumber, fmt.Sprint(invPartNumber))); err != nil { return err } diff --git a/tests/integration/CopyObject.go b/tests/integration/CopyObject.go index f2eae7f7..0907b29f 100644 --- a/tests/integration/CopyObject.go +++ b/tests/integration/CopyObject.go @@ -135,7 +135,7 @@ func CopyObject_copy_to_itself_invalid_directive(s *S3Conf) error { MetadataDirective: types.MetadataDirective("invalid"), }) cancel() - if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidMetadataDirective)); err != nil { + if err := checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgMetadataDirective, "invalid")); err != nil { return err } return nil @@ -158,7 +158,7 @@ func CopyObject_invalid_tagging_directive(s *S3Conf) error { TaggingDirective: types.TaggingDirective("invalid"), }) cancel() - if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidTaggingDirective)); err != nil { + if err := checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgTaggingDirective, "invalid")); err != nil { return err } return nil @@ -246,7 +246,7 @@ func CopyObject_should_replace_tagging(s *S3Conf) error { return err } switch eErr := expectedErr.(type) { - case s3err.APIError: + case s3err.S3Error: return checkApiErr(err, eErr) default: return fmt.Errorf("invalid err provided: %w", expectedErr) @@ -299,10 +299,10 @@ func CopyObject_should_replace_tagging(s *S3Conf) error { {"key1=val1&key2=val2", map[string]string{"key1": "val1", "key2": "val2"}, nil}, {"key@=val@", map[string]string{"key@": "val@"}, nil}, // invalid url-encoded - {"=", nil, s3err.GetAPIError(s3err.ErrInvalidURLEncodedTagging)}, - {"key%", nil, s3err.GetAPIError(s3err.ErrInvalidURLEncodedTagging)}, + {"=", nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgURLEncodedTagging, "")}, + {"key%", nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgURLEncodedTagging, "")}, // duplicate keys - {"key=val&key=val", nil, s3err.GetAPIError(s3err.ErrInvalidURLEncodedTagging)}, + {"key=val&key=val", nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgURLEncodedTagging, "")}, // invalid tag keys {"key?=val", nil, s3err.GetAPIError(s3err.ErrInvalidTagKey)}, {"key(=val", nil, s3err.GetAPIError(s3err.ErrInvalidTagKey)}, @@ -520,87 +520,87 @@ func CopyObject_invalid_copy_source(s *S3Conf) error { return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { for _, test := range []struct { copySource string - expectedErr s3err.APIError + expectedErr s3err.S3Error }{ // invalid encoding { // Invalid hex digits copySource: "bucket/%ZZ", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "bucket/%ZZ"), }, { // Ends with incomplete escape copySource: "100%/foo/bar/baz", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "100%/foo/bar/baz"), }, { // Only one digit after % copySource: "bucket/%A/bar", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "bucket/%A/bar"), }, { // 'G' is not a hex digit copySource: "bucket/%G1/", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "bucket/%G1/"), }, { // Just a single percent sign copySource: "%", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "%"), }, { // Only one hex digit copySource: "bucket/%1", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "bucket/%1"), }, { // Incomplete multibyte UTF-8 copySource: "bucket/%C3%", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "bucket/%C3%"), }, // invalid bucket name { // ip v4 address copySource: "192.168.1.1/foo", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceBucket), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceBucket, "192.168.1.1/foo"), }, { // ip v6 address copySource: "2001:0db8:85a3:0000:0000:8a2e:0370:7334/something", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceBucket), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceBucket, "2001:0db8:85a3:0000:0000:8a2e:0370:7334/something"), }, { // some special chars copySource: "my-buc@k&()t/obj", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceBucket), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceBucket, "my-buc@k&()t/obj"), }, // invalid object key { // object is missing copySource: "bucket", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceObject), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceObject, ""), }, { // object is missing copySource: "bucket/", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceObject), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceObject, ""), }, // directory navigation object keys { copySource: "bucket/.", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceObject), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceObject, "."), }, { copySource: "bucket/..", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceObject), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceObject, ".."), }, { copySource: "bucket/../", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceObject), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceObject, "../"), }, { copySource: "bucket/foo/ba/../../../r/baz", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceObject), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceObject, "foo/ba/../../../r/baz"), }, } { ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) @@ -876,7 +876,7 @@ func CopyObject_invalid_legal_hold(s *S3Conf) error { ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatus("invalid_status"), }) cancel() - return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidLegalHoldStatus)) + return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgLegalHoldStatus, "invalid_status")) }, withLock()) } @@ -902,7 +902,7 @@ func CopyObject_invalid_object_lock_mode(s *S3Conf) error { ObjectLockMode: types.ObjectLockMode("invalid_mode"), }) cancel() - return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidObjectLockMode)) + return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgObjectLockMode, "invalid_mode")) }, withLock()) } diff --git a/tests/integration/CreateBucket.go b/tests/integration/CreateBucket.go index 6eb552c3..2ebe3da8 100644 --- a/tests/integration/CreateBucket.go +++ b/tests/integration/CreateBucket.go @@ -18,7 +18,6 @@ import ( "context" "errors" "fmt" - "net/http" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3/types" @@ -137,11 +136,7 @@ func CreateBucket_invalid_ownership(s *S3Conf) error { invalidOwnership := types.ObjectOwnership("invalid_ownership") err := setup(s, getBucketName(), withOwnership(invalidOwnership)) - if err := checkApiErr(err, s3err.APIError{ - Code: "InvalidArgument", - Description: fmt.Sprintf("Invalid x-amz-object-ownership header: %v", invalidOwnership), - HTTPStatusCode: http.StatusBadRequest, - }); err != nil { + if err := checkApiErr(err, s3err.GetInvalidArgObjectOwnership(string(invalidOwnership))); err != nil { failF("%v: %v", testName, err) return fmt.Errorf("%v: %w", testName, err) } diff --git a/tests/integration/CreateMultipartUpload.go b/tests/integration/CreateMultipartUpload.go index 59b64340..e95e2b37 100644 --- a/tests/integration/CreateMultipartUpload.go +++ b/tests/integration/CreateMultipartUpload.go @@ -266,7 +266,7 @@ func CreateMultipartUpload_with_object_lock_invalid_retention(s *S3Conf) error { ObjectLockMode: types.ObjectLockModeGovernance, }) cancel() - if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLockInvalidHeaders)); err != nil { + if err := checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgMissingObjectLockRetainDate, "")); err != nil { return err } @@ -277,7 +277,7 @@ func CreateMultipartUpload_with_object_lock_invalid_retention(s *S3Conf) error { ObjectLockRetainUntilDate: &retentionDate, }) cancel() - if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLockInvalidHeaders)); err != nil { + if err := checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgMissingObjectLockMode, "")); err != nil { return err } @@ -298,7 +298,7 @@ func CreateMultipartUpload_past_retain_until_date(s *S3Conf) error { ObjectLockRetainUntilDate: &rDate, }) cancel() - if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrPastObjectLockRetainDate)); err != nil { + if err := checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgPastObjectLockRetainDate, rDate.Format(time.RFC3339))); err != nil { return err } @@ -316,7 +316,7 @@ func CreateMultipartUpload_invalid_legal_hold(s *S3Conf) error { ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatus("invalid_status"), }) cancel() - return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidLegalHoldStatus)) + return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgLegalHoldStatus, "invalid_status")) }, withLock()) } @@ -332,7 +332,7 @@ func CreateMultipartUpload_invalid_object_lock_mode(s *S3Conf) error { ObjectLockRetainUntilDate: &rDate, }) cancel() - return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidObjectLockMode)) + return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgObjectLockMode, "invalid_mode")) }, withLock()) } @@ -468,7 +468,7 @@ func CreateMultipartUpload_with_tagging(s *S3Conf) error { return err } switch eErr := expectedErr.(type) { - case s3err.APIError: + case s3err.S3Error: return checkApiErr(err, eErr) default: return fmt.Errorf("invalid err provided: %w", expectedErr) @@ -548,10 +548,10 @@ func CreateMultipartUpload_with_tagging(s *S3Conf) error { {"key1=val1&key2=val2", map[string]string{"key1": "val1", "key2": "val2"}, nil}, {"key@=val@", map[string]string{"key@": "val@"}, nil}, // invalid url-encoded - {"=", nil, s3err.GetAPIError(s3err.ErrInvalidURLEncodedTagging)}, - {"key%", nil, s3err.GetAPIError(s3err.ErrInvalidURLEncodedTagging)}, + {"=", nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgURLEncodedTagging, "")}, + {"key%", nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgURLEncodedTagging, "")}, // duplicate keys - {"key=val&key=val", nil, s3err.GetAPIError(s3err.ErrInvalidURLEncodedTagging)}, + {"key=val&key=val", nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgURLEncodedTagging, "")}, // invalid tag keys {"key?=val", nil, s3err.GetAPIError(s3err.ErrInvalidTagKey)}, {"key(=val", nil, s3err.GetAPIError(s3err.ErrInvalidTagKey)}, diff --git a/tests/integration/DeleteBucket.go b/tests/integration/DeleteBucket.go index 7120a7f8..8e3aca26 100644 --- a/tests/integration/DeleteBucket.go +++ b/tests/integration/DeleteBucket.go @@ -74,7 +74,7 @@ func DeleteBucket_success_status_code(s *S3Conf) error { return fmt.Errorf("%v: %w", testName, err) } - req, err := createSignedReq(http.MethodDelete, s.endpoint, bucket, s.awsID, s.awsSecret, "s3", s.awsRegion, nil, time.Now(), nil) + req, err := createSignedReq(http.MethodDelete, s.endpoint, bucket, s.awsID, s.awsSecret, "s3", s.awsRegion, "", nil, time.Now(), nil) if err != nil { failF("%v: %v", testName, err) return fmt.Errorf("%v: %w", testName, err) diff --git a/tests/integration/DeleteBucketTagging.go b/tests/integration/DeleteBucketTagging.go index 6e75a9e5..7bc1116b 100644 --- a/tests/integration/DeleteBucketTagging.go +++ b/tests/integration/DeleteBucketTagging.go @@ -62,7 +62,7 @@ func DeleteBucketTagging_success_status(s *S3Conf) error { return err } - req, err := createSignedReq(http.MethodDelete, s.endpoint, fmt.Sprintf("%v?tagging", bucket), s.awsID, s.awsSecret, "s3", s.awsRegion, nil, time.Now(), nil) + req, err := createSignedReq(http.MethodDelete, s.endpoint, fmt.Sprintf("%v?tagging", bucket), s.awsID, s.awsSecret, "s3", s.awsRegion, "", nil, time.Now(), nil) if err != nil { return err } diff --git a/tests/integration/DeleteObject.go b/tests/integration/DeleteObject.go index 8c9a0fd0..c1160d9c 100644 --- a/tests/integration/DeleteObject.go +++ b/tests/integration/DeleteObject.go @@ -333,7 +333,7 @@ func DeleteObject_success_status_code(s *S3Conf) error { req, err := createSignedReq(http.MethodDelete, s.endpoint, fmt.Sprintf("%v/%v", bucket, obj), s.awsID, s.awsSecret, "s3", - s.awsRegion, nil, time.Now(), nil) + s.awsRegion, "", nil, time.Now(), nil) if err != nil { return err } diff --git a/tests/integration/DeleteObjectTagging.go b/tests/integration/DeleteObjectTagging.go index 14791cfc..4b521bb2 100644 --- a/tests/integration/DeleteObjectTagging.go +++ b/tests/integration/DeleteObjectTagging.go @@ -72,7 +72,7 @@ func DeleteObjectTagging_success_status(s *S3Conf) error { req, err := createSignedReq(http.MethodDelete, s.endpoint, fmt.Sprintf("%v/%v?tagging", bucket, obj), s.awsID, s.awsSecret, - "s3", s.awsRegion, nil, time.Now(), nil) + "s3", s.awsRegion, "", nil, time.Now(), nil) if err != nil { return err } diff --git a/tests/integration/GetObject.go b/tests/integration/GetObject.go index 25cac77d..eb6def24 100644 --- a/tests/integration/GetObject.go +++ b/tests/integration/GetObject.go @@ -729,6 +729,7 @@ func GetObject_by_range_resp_status(s *S3Conf) error { s.awsSecret, "s3", s.awsRegion, + "", nil, time.Now(), map[string]string{"Range": rng}, @@ -1075,6 +1076,7 @@ func GetObject_overrides_presign_success(s *S3Conf) error { s.awsSecret, "s3", s.awsRegion, + "", nil, time.Now(), nil, @@ -1121,6 +1123,7 @@ func GetObject_overrides_presign_success(s *S3Conf) error { s.awsSecret, "s3", s.awsRegion, + "", nil, time.Now(), nil, @@ -1294,7 +1297,7 @@ func GetObject_invalid_part_number(s *S3Conf) error { PartNumber: getPtr(int32(-3)), }) - return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidPartNumber)) + return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgPartNumber, "-3")) }) } @@ -1369,7 +1372,7 @@ func GetObject_mp_part_number_exceeds_parts_count(s *S3Conf) error { PartNumber: &pn, }) cancel() - return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidPartNumberRange)) + return checkApiErr(err, s3err.GetInvalidPartNumberRangeErr(int32(partCount), pn)) }) } @@ -1630,6 +1633,7 @@ func GetObject_mp_part_number_resp_status(s *S3Conf) error { s.awsSecret, "s3", s.awsRegion, + "", nil, time.Now(), nil, diff --git a/tests/integration/GetObjectAttributes.go b/tests/integration/GetObjectAttributes.go index bf087fcb..23bc1d4b 100644 --- a/tests/integration/GetObjectAttributes.go +++ b/tests/integration/GetObjectAttributes.go @@ -84,7 +84,7 @@ func GetObjectAttributes_invalid_attrs(s *S3Conf) error { }, }) cancel() - if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidObjectAttributes)); err != nil { + if err := checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgObjectAttributes, "Invalid_argument")); err != nil { return err } @@ -133,7 +133,7 @@ func GetObjectAttributes_invalid_single_attribute(s *S3Conf) error { }, }) cancel() - if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidObjectAttributes)); err != nil { + if err := checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgObjectAttributes, "invalid_attr")); err != nil { return err } diff --git a/tests/integration/HeadObject.go b/tests/integration/HeadObject.go index b3f504da..df30d5a4 100644 --- a/tests/integration/HeadObject.go +++ b/tests/integration/HeadObject.go @@ -828,6 +828,7 @@ func HeadObject_overrides_presign_success(s *S3Conf) error { s.awsSecret, "s3", s.awsRegion, + "", nil, time.Now(), nil, @@ -863,6 +864,7 @@ func HeadObject_overrides_presign_success(s *S3Conf) error { s.awsSecret, "s3", s.awsRegion, + "", nil, time.Now(), nil, @@ -1118,6 +1120,7 @@ func HeadObject_by_range_resp_status(s *S3Conf) error { s.awsSecret, "s3", s.awsRegion, + "", nil, time.Now(), map[string]string{"Range": rng}, @@ -1268,6 +1271,7 @@ func HeadObject_mp_part_number_resp_status(s *S3Conf) error { s.awsSecret, "s3", s.awsRegion, + "", nil, time.Now(), nil, diff --git a/tests/integration/ListBuckets.go b/tests/integration/ListBuckets.go index 857c9100..9c967676 100644 --- a/tests/integration/ListBuckets.go +++ b/tests/integration/ListBuckets.go @@ -226,13 +226,13 @@ func ListBuckets_invalid_max_buckets(s *S3Conf) error { invMaxBuckets := int32(-3) err := listBuckets(invMaxBuckets) - if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidMaxBuckets)); err != nil { + if err := checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgMaxBuckets, fmt.Sprint(invMaxBuckets))); err != nil { return err } invMaxBuckets = 2000000 err = listBuckets(invMaxBuckets) - if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidMaxBuckets)); err != nil { + if err := checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgMaxBuckets, fmt.Sprint(invMaxBuckets))); err != nil { return err } diff --git a/tests/integration/ListMultipartUploads.go b/tests/integration/ListMultipartUploads.go index 9bf8b267..b92f3b69 100644 --- a/tests/integration/ListMultipartUploads.go +++ b/tests/integration/ListMultipartUploads.go @@ -74,7 +74,7 @@ func ListMultipartUploads_invalid_max_uploads(s *S3Conf) error { MaxUploads: &maxUploads, }) cancel() - if err := checkApiErr(err, s3err.GetNegativeMaxLimiterErr("max-uploads")); err != nil { + if err := checkApiErr(err, s3err.GetInvalidArgNegativeMaxLimiter("max-uploads", fmt.Sprint(maxUploads))); err != nil { return err } @@ -251,19 +251,20 @@ func ListMultipartUploads_invalid_uploadId_marker(s *S3Conf) error { UploadIdMarker: getPtr("invalid_uploadId_marker"), }) cancel() - if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidUploadIdMarker)); err != nil { + if err := checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgUploadIdMarker, "invalid_uploadId_marker")); err != nil { return err } // valid UUID, but not from the list ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + uploadIDMarker := uuid.New().String() _, err = s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{ Bucket: &bucket, KeyMarker: getPtr("obj-2"), - UploadIdMarker: getPtr(uuid.New().String()), + UploadIdMarker: &uploadIDMarker, }) cancel() - if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidUploadIdMarker)); err != nil { + if err := checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgUploadIdMarker, uploadIDMarker)); err != nil { return err } @@ -275,7 +276,7 @@ func ListMultipartUploads_invalid_uploadId_marker(s *S3Conf) error { UploadIdMarker: uploads[4].UploadId, }) cancel() - return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidUploadIdMarker)) + return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgUploadIdMarker, *uploads[4].UploadId)) }) } diff --git a/tests/integration/ListObjectVersions.go b/tests/integration/ListObjectVersions.go index d13a2eba..bca81d66 100644 --- a/tests/integration/ListObjectVersions.go +++ b/tests/integration/ListObjectVersions.go @@ -90,7 +90,7 @@ func ListObjectVersions_negative_max_keys(s *S3Conf) error { MaxKeys: getPtr(int32(-123)), }) cancel() - return checkApiErr(err, s3err.GetAPIError(s3err.ErrNegativeMaxKeys)) + return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgNegativeMaxKeys, "-123")) }, withLock()) } diff --git a/tests/integration/ListObjects.go b/tests/integration/ListObjects.go index 8e875f12..e0abf1c9 100644 --- a/tests/integration/ListObjects.go +++ b/tests/integration/ListObjects.go @@ -185,7 +185,7 @@ func ListObjects_invalid_max_keys(s *S3Conf) error { MaxKeys: &maxKeys, }) cancel() - if err := checkApiErr(err, s3err.GetNegativeMaxLimiterErr("max-keys")); err != nil { + if err := checkApiErr(err, s3err.GetInvalidArgNegativeMaxLimiter("max-keys", fmt.Sprint(maxKeys))); err != nil { return err } diff --git a/tests/integration/ListParts.go b/tests/integration/ListParts.go index 8ae454ee..c1859f89 100644 --- a/tests/integration/ListParts.go +++ b/tests/integration/ListParts.go @@ -83,7 +83,7 @@ func ListParts_invalid_max_parts(s *S3Conf) error { MaxParts: &invMaxParts, }) cancel() - if err := checkApiErr(err, s3err.GetNegativeMaxLimiterErr("max-parts")); err != nil { + if err := checkApiErr(err, s3err.GetInvalidArgNegativeMaxLimiter("max-parts", fmt.Sprint(invMaxParts))); err != nil { return err } @@ -113,18 +113,21 @@ func ListParts_invalid_part_number_marker(s *S3Conf) error { } // invalid part number marker - err = listparts("invalid") - if err := checkApiErr(err, s3err.GetInvalidMaxLimiterErr("part-number-marker")); err != nil { + partNumberMarker := "invalid" + err = listparts(partNumberMarker) + if err := checkApiErr(err, s3err.GetInvalidArgMaxLimiter("part-number-marker", partNumberMarker)); err != nil { return err } // out of in range part number marker - err = listparts("2736457823532448723") - if err := checkApiErr(err, s3err.GetInvalidMaxLimiterErr("part-number-marker")); err != nil { + partNumberMarker = "2736457823532448723" + err = listparts(partNumberMarker) + if err := checkApiErr(err, s3err.GetInvalidArgMaxLimiter("part-number-marker", partNumberMarker)); err != nil { return err } // negative part number marker - err = listparts("-14") - if err := checkApiErr(err, s3err.GetNegativeMaxLimiterErr("part-number-marker")); err != nil { + partNumberMarker = "-14" + err = listparts(partNumberMarker) + if err := checkApiErr(err, s3err.GetInvalidArgNegativeMaxLimiter("part-number-marker", partNumberMarker)); err != nil { return err } diff --git a/tests/integration/PostObject.go b/tests/integration/PostObject.go index 04a7c7e5..962a2443 100644 --- a/tests/integration/PostObject.go +++ b/tests/integration/PostObject.go @@ -49,7 +49,7 @@ func PostObject_invalid_content_type(s *S3Conf) error { return err } - return checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrPreconditionFailed)) + return checkHTTPResponseApiErr(resp, s3err.GetPreconditionFailedErr(s3err.ConditionPostBucket)) }) } @@ -123,7 +123,7 @@ func PostObject_invalid_algorithm(s *S3Conf) error { return err } - return checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrOnlyAws4HmacSha256)) + return checkHTTPResponseApiErr(resp, s3err.GetInvalidArgumentErr(s3err.InvalidArgOnlyAws4HmacSha256, "invalid")) }) } @@ -143,7 +143,7 @@ func PostObject_invalid_date(s *S3Conf) error { return err } - return checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrInvalidDateHeader)) + return checkHTTPResponseApiErr(resp, s3err.GetInvalidArgumentErr(s3err.InvalidArgDateHeader, "invalid_date")) }) } @@ -163,7 +163,7 @@ func PostObject_invalid_credential_format(s *S3Conf) error { return err } - return checkHTTPResponseApiErr(resp, s3err.PostAuth.MalformedCredential()) + return checkHTTPResponseApiErr(resp, s3err.PostAuth.MalformedCredential("malformed-no-slashes")) }) } @@ -186,18 +186,20 @@ func PostObject_incorrect_region(s *S3Conf) error { return err } - return checkHTTPResponseApiErr(resp, s3err.PostAuth.IncorrectRegion(s.awsRegion, wrongRegion)) + expectedCreds := fmt.Sprintf("%s/%s/%s/s3/aws4_request", s.awsID, time.Now().UTC().Format("20060102"), wrongRegion) + return checkHTTPResponseApiErr(resp, s3err.PostAuth.IncorrectRegion(expectedCreds, s.awsRegion, wrongRegion)) }) } func PostObject_non_existing_access_key(s *S3Conf) error { testName := "PostObject_non_existing_access_key" return actionHandlerNoSetup(s, testName, func(s3client *s3.Client, bucket string) error { + accessKeyID := "this_access_key_id_can_not_really_exist" resp, err := sendPostObject(PostRequestConfig{ bucket: bucket, key: "test-object", s3Conf: s, - access: "this_access_key_id_can_not_really_exist", + access: accessKeyID, secret: "a_very_secure_secret_access_key", fileContent: []byte("data"), }) @@ -205,28 +207,57 @@ func PostObject_non_existing_access_key(s *S3Conf) error { return err } - return checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrInvalidAccessKeyID)) + return checkHTTPResponseApiErr(resp, s3err.GetInvalidAccessKeyIdErr(accessKeyID)) }) } func PostObject_signature_mismatch(s *S3Conf) error { testName := "PostObject_signature_mismatch" return actionHandlerNoSetup(s, testName, func(s3client *s3.Client, bucket string) error { - resp, err := sendPostObject(PostRequestConfig{ + const signature = "incorrect_signature" + req, fields, err := newPostObjectRequest(PostRequestConfig{ bucket: bucket, key: "test-object", s3Conf: s, fileContent: []byte("data"), extraFields: map[string]string{ - "x-amz-signature": "incorrect_signature", + "x-amz-signature": signature, }, }) if err != nil { return err } - defer resp.Body.Close() - return checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrSignatureDoesNotMatch)) + resp, err := s.httpClient.Do(req) + if err != nil { + return err + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + resp.Body.Close() + + var errResp APIErrorResponse + err = xml.Unmarshal(body, &errResp) + if err != nil { + return err + } + + expected := s3err.GetSignatureDoesNotMatchErr(s.awsID, fields["policy"], signature, hexBytes(fields["policy"]), "", "") + + if resp.StatusCode != expected.HTTPStatusCode { + return fmt.Errorf("expected response status code to be %v, instead got %v", expected.HTTPStatusCode, resp.StatusCode) + } + return compareS3ApiErrFields( + compareErrField("AWSAccessKeyId", expected.AWSAccessKeyId, errResp.AWSAccessKeyId), + compareErrField("StringToSign", expected.StringToSign, errResp.StringToSign), + compareErrField("SignatureProvided", expected.SignatureProvided, errResp.SignatureProvided), + compareErrField("StringToSignBytes", expected.StringToSignBytes, errResp.StringToSignBytes), + checkErrFieldEmptiness("CanonicalRequest", expected.CanonicalRequest, false), + checkErrFieldEmptiness("CanonicalRequestBytes", expected.CanonicalRequestBytes, false), + ) }) } @@ -466,7 +497,7 @@ func PostObject_invalid_policy_document(s *S3Conf) error { policy *string expiration time.Time conditions []any - err s3err.APIError + err s3err.S3Error }{ // empty policy document {getPtr(""), time.Time{}, []any{}, s3err.InvalidPolicyDocument.EmptyPolicy()}, @@ -616,7 +647,7 @@ func PostObject_policy_content_length_too_large(s *S3Conf) error { return err } - return checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrEntityTooLarge)) + return checkHTTPResponseApiErr(resp, s3err.GetEntityTooLargeErr(10, 5)) }) } @@ -637,7 +668,7 @@ func PostObject_policy_content_length_too_small(s *S3Conf) error { return err } - return checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrEntityTooSmall)) + return checkHTTPResponseApiErr(resp, s3err.GetEntityTooSmallErr(2, 100)) }) } diff --git a/tests/integration/PutBucketPolicy.go b/tests/integration/PutBucketPolicy.go index d17e9bed..7dd7e6cc 100644 --- a/tests/integration/PutBucketPolicy.go +++ b/tests/integration/PutBucketPolicy.go @@ -700,7 +700,7 @@ func PutBucketPolicy_status(s *S3Conf) error { testname := "PutBucketPolicy_status" return actionHandler(s, testname, func(s3client *s3.Client, bucket string) error { doc := genPolicyDoc("Allow", `"*"`, `"s3:GetObject"`, fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket)) - req, err := createSignedReq(http.MethodPut, s.endpoint, bucket+"?policy", s.awsID, s.awsSecret, "s3", s.awsRegion, []byte(doc), time.Now(), nil) + req, err := createSignedReq(http.MethodPut, s.endpoint, bucket+"?policy", s.awsID, s.awsSecret, "s3", s.awsRegion, "", []byte(doc), time.Now(), nil) if err != nil { return err } diff --git a/tests/integration/PutBucketTagging.go b/tests/integration/PutBucketTagging.go index a9f60993..751f0b36 100644 --- a/tests/integration/PutBucketTagging.go +++ b/tests/integration/PutBucketTagging.go @@ -222,7 +222,7 @@ func PutBucketTagging_success_status(s *S3Conf) error { sum := hasher.Sum(nil) md5Sum := base64.StdEncoding.EncodeToString(sum) - req, err := createSignedReq(http.MethodPut, s.endpoint, fmt.Sprintf("%v?tagging=", bucket), s.awsID, s.awsSecret, "s3", s.awsRegion, taggingParsed, time.Now(), map[string]string{ + req, err := createSignedReq(http.MethodPut, s.endpoint, fmt.Sprintf("%v?tagging=", bucket), s.awsID, s.awsSecret, "s3", s.awsRegion, "", taggingParsed, time.Now(), map[string]string{ "Content-Md5": md5Sum, }) if err != nil { diff --git a/tests/integration/PutObject.go b/tests/integration/PutObject.go index 1a01c5aa..c0eb48da 100644 --- a/tests/integration/PutObject.go +++ b/tests/integration/PutObject.go @@ -98,7 +98,7 @@ func PutObject_tagging(s *S3Conf) error { return err } switch eErr := expectedErr.(type) { - case s3err.APIError: + case s3err.S3Error: return checkApiErr(err, eErr) default: return fmt.Errorf("invalid err provided: %w", expectedErr) @@ -151,10 +151,10 @@ func PutObject_tagging(s *S3Conf) error { {"key1=val1&key2=val2", map[string]string{"key1": "val1", "key2": "val2"}, nil}, {"key@=val@", map[string]string{"key@": "val@"}, nil}, // invalid url-encoded - {"=", nil, s3err.GetAPIError(s3err.ErrInvalidURLEncodedTagging)}, - {"key%", nil, s3err.GetAPIError(s3err.ErrInvalidURLEncodedTagging)}, + {"=", nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgURLEncodedTagging, "")}, + {"key%", nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgURLEncodedTagging, "")}, // duplicate keys - {"key=val&key=val", nil, s3err.GetAPIError(s3err.ErrInvalidURLEncodedTagging)}, + {"key=val&key=val", nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgURLEncodedTagging, "")}, // invalid tag keys {"key?=val", nil, s3err.GetAPIError(s3err.ErrInvalidTagKey)}, {"key(=val", nil, s3err.GetAPIError(s3err.ErrInvalidTagKey)}, @@ -212,13 +212,13 @@ func PutObject_missing_object_lock_retention_config(s *S3Conf) error { ObjectLockMode: types.ObjectLockModeCompliance, }) cancel() - if err := checkSdkApiErr(err, "InvalidRequest"); err != nil { + if err := checkSdkApiErr(err, "InvalidArgument"); err != nil { return err } // client sdk regression issue prevents getting full error message, // change back to below once this is fixed: // https://github.com/aws/aws-sdk-go-v2/issues/2921 - // if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLockInvalidHeaders)); err != nil { + // if err := checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgMissingObjectLockRetainDate, "")); err != nil { // return err // } @@ -231,13 +231,13 @@ func PutObject_missing_object_lock_retention_config(s *S3Conf) error { ObjectLockRetainUntilDate: &retainDate, }) cancel() - if err := checkSdkApiErr(err, "InvalidRequest"); err != nil { + if err := checkSdkApiErr(err, "InvalidArgument"); err != nil { return err } // client sdk regression issue prevents getting full error message, // change back to below once this is fixed: // https://github.com/aws/aws-sdk-go-v2/issues/2921 - // if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLockInvalidHeaders)); err != nil { + // if err := checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgMissingObjectLockMode, "")); err != nil { // return err // } @@ -315,7 +315,7 @@ func PutObject_invalid_legal_hold(s *S3Conf) error { Key: getPtr("foo"), ObjectLockLegalHoldStatus: types.ObjectLockLegalHoldStatus("invalid_status"), }, s3client) - return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidLegalHoldStatus)) + return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgLegalHoldStatus, "invalid_status")) }, withLock()) } @@ -329,7 +329,7 @@ func PutObject_invalid_object_lock_mode(s *S3Conf) error { ObjectLockRetainUntilDate: &rDate, ObjectLockMode: types.ObjectLockMode("invalid_mode"), }, s3client) - return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidObjectLockMode)) + return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgObjectLockMode, "invalid_mode")) }, withLock()) } @@ -344,7 +344,7 @@ func PutObject_past_retain_until_date(s *S3Conf) error { ObjectLockRetainUntilDate: &rDate, }, s3client) - return checkApiErr(err, s3err.GetAPIError(s3err.ErrPastObjectLockRetainDate)) + return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgPastObjectLockRetainDate, rDate.Format(time.RFC3339))) }, withLock()) } @@ -359,6 +359,7 @@ func PutObject_invalid_retain_until_date(s *S3Conf) error { s.awsSecret, "s3", s.awsRegion, + "", nil, time.Now(), map[string]string{ @@ -375,7 +376,7 @@ func PutObject_invalid_retain_until_date(s *S3Conf) error { return err } - return checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrInvalidRetainUntilDate)) + return checkHTTPResponseApiErr(resp, s3err.GetInvalidArgumentErr(s3err.InvalidArgRetainUntilDate, "invalid_date")) }, withLock()) } diff --git a/tests/integration/PutObjectLockConfiguration.go b/tests/integration/PutObjectLockConfiguration.go index ac1e8582..6ce6778c 100644 --- a/tests/integration/PutObjectLockConfiguration.go +++ b/tests/integration/PutObjectLockConfiguration.go @@ -88,6 +88,7 @@ func PutObjectLockConfiguration_malformed_body(s *S3Conf) error { s.awsSecret, "s3", s.awsRegion, + "", body, time.Now(), map[string]string{"Content-Md5": md5Sum}, @@ -227,7 +228,7 @@ func PutObjectLockConfiguration_invalid_years_days(s *S3Conf) error { }, }) cancel() - if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLockInvalidRetentionPeriod)); err != nil { + if err := checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgObjectLockRetentionDays, fmt.Sprint(days))); err != nil { return err } ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) @@ -244,7 +245,7 @@ func PutObjectLockConfiguration_invalid_years_days(s *S3Conf) error { }, }) cancel() - if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLockInvalidRetentionPeriod)); err != nil { + if err := checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgObjectLockRetentionYears, fmt.Sprint(years))); err != nil { return err } diff --git a/tests/integration/PutObjectRetention.go b/tests/integration/PutObjectRetention.go index 8e13c62c..e384ca1f 100644 --- a/tests/integration/PutObjectRetention.go +++ b/tests/integration/PutObjectRetention.go @@ -107,7 +107,7 @@ func PutObjectRetention_expired_retain_until_date(s *S3Conf) error { }, }) cancel() - if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrPastObjectLockRetainDate)); err != nil { + if err := checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgPastObjectLockRetainDate, date.Format(time.RFC3339))); err != nil { return err } diff --git a/tests/integration/UploadPart.go b/tests/integration/UploadPart.go index 1b394b34..8576f6e8 100644 --- a/tests/integration/UploadPart.go +++ b/tests/integration/UploadPart.go @@ -70,7 +70,7 @@ func UploadPart_invalid_part_number(s *S3Conf) error { PartNumber: &el, }) cancel() - if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidPartNumber)); err != nil { + if err := checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgPartNumber, fmt.Sprint(el))); err != nil { return err } } diff --git a/tests/integration/UploadPartCopy.go b/tests/integration/UploadPartCopy.go index f5f86c4e..3e405fa8 100644 --- a/tests/integration/UploadPartCopy.go +++ b/tests/integration/UploadPartCopy.go @@ -142,7 +142,7 @@ func UploadPartCopy_invalid_part_number(s *S3Conf) error { PartNumber: &partNumber, }) cancel() - if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidPartNumber)); err != nil { + if err := checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgPartNumber, fmt.Sprint(partNumber))); err != nil { return err } @@ -156,87 +156,87 @@ func UploadPartCopy_invalid_copy_source(s *S3Conf) error { partNumber := int32(1) for _, test := range []struct { copySource string - expectedErr s3err.APIError + expectedErr s3err.S3Error }{ // invalid encoding { // Invalid hex digits copySource: "bucket/%ZZ", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "bucket/%ZZ"), }, { // Ends with incomplete escape copySource: "100%/foo/bar/baz", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "100%/foo/bar/baz"), }, { // Only one digit after % copySource: "bucket/%A/bar", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "bucket/%A/bar"), }, { // 'G' is not a hex digit copySource: "bucket/%G1/", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "bucket/%G1/"), }, { // Just a single percent sign copySource: "%", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "%"), }, { // Only one hex digit copySource: "bucket/%1", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "bucket/%1"), }, { // Incomplete multibyte UTF-8 copySource: "bucket/%C3%", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceEncoding), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, "bucket/%C3%"), }, // invalid bucket name { // ip v4 address copySource: "192.168.1.1/foo", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceBucket), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceBucket, "192.168.1.1/foo"), }, { // ip v6 address copySource: "2001:0db8:85a3:0000:0000:8a2e:0370:7334/something", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceBucket), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceBucket, "2001:0db8:85a3:0000:0000:8a2e:0370:7334/something"), }, { // some special chars copySource: "my-buc@k&()t/obj", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceBucket), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceBucket, "my-buc@k&()t/obj"), }, // invalid object key { // object is missing copySource: "bucket", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceObject), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceObject, ""), }, { // object is missing copySource: "bucket/", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceObject), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceObject, ""), }, // directory navigation object keys { copySource: "bucket/.", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceObject), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceObject, "."), }, { copySource: "bucket/..", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceObject), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceObject, ".."), }, { copySource: "bucket/../", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceObject), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceObject, "../"), }, { copySource: "bucket/foo/ba/../../../r/baz", - expectedErr: s3err.GetAPIError(s3err.ErrInvalidCopySourceObject), + expectedErr: s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceObject, "foo/ba/../../../r/baz"), }, } { ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) @@ -429,7 +429,7 @@ func UploadPartCopy_by_range_invalid_ranges(s *S3Conf) error { CopySourceRange: &csRange, }) cancel() - if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidCopySourceRange)); err != nil { + if err := checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceRange, csRange)); err != nil { return err } @@ -493,7 +493,7 @@ func UploadPartCopy_exceeding_copy_source_range(s *S3Conf) error { CopySourceRange: &csRange, }) cancel() - return checkApiErr(err, s3err.CreateExceedingRangeErr(objSize)) + return checkApiErr(err, s3err.GetInvalidArgExceedingRange(objSize)) } for i, rg := range []string{ @@ -549,7 +549,7 @@ func UploadPartCopy_greater_range_than_obj_size(s *S3Conf) error { PartNumber: &partNumber, }) cancel() - if err := checkApiErr(err, s3err.CreateExceedingRangeErr(int64(srcObjSize))); err != nil { + if err := checkApiErr(err, s3err.GetInvalidArgExceedingRange(int64(srcObjSize))); err != nil { return err } diff --git a/tests/integration/general.go b/tests/integration/general.go index f5647b7a..82e88650 100644 --- a/tests/integration/general.go +++ b/tests/integration/general.go @@ -50,7 +50,7 @@ func RouterPutPartNumberWithoutUploadId(s *S3Conf) error { return err } - if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrMissingUploadId)); err != nil { + if err := checkHTTPResponseApiErr(resp, s3err.GetInvalidArgumentErr(s3err.InvalidArgMissingUploadId, "partNumber")); err != nil { return err } @@ -71,7 +71,7 @@ func RouterPostRoot(s *S3Conf) error { return err } - if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrMethodNotAllowed)); err != nil { + if err := checkHTTPResponseApiErr(resp, s3err.GetMethodNotAllowedErr(http.MethodPost, s3err.ResourceTypeService, nil)); err != nil { return err } @@ -92,7 +92,7 @@ func RouterPostObjectWithoutQuery(s *S3Conf) error { return err } - if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrMethodNotAllowed)); err != nil { + if err := checkHTTPResponseApiErr(resp, s3err.GetMethodNotAllowedErr(http.MethodPost, s3err.ResourceTypeService, nil)); err != nil { return err } @@ -117,7 +117,11 @@ func RouterPUTObjectOnlyUploadId(s *S3Conf) error { return err } - if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrMethodNotAllowed)); err != nil { + if err := checkHTTPResponseApiErr(resp, s3err.GetMethodNotAllowedErr( + http.MethodPut, + s3err.ResourceTypeUpload, + []string{http.MethodDelete, http.MethodPost, http.MethodGet}, + )); err != nil { return err } @@ -178,7 +182,7 @@ func RouterCopySourceNotAllowed(s *S3Conf) error { return fmt.Errorf("expected 400 status code for HEAD %s request, instead got %v", path, resp.StatusCode) } } else { - if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)); err != nil { + if err := checkHTTPResponseApiErr(resp, s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySource, "bucket/object")); err != nil { return fmt.Errorf("%s %s: %w", method, path, err) } } @@ -226,7 +230,7 @@ func CORSMiddleware_invalid_method(s *S3Conf) error { } // create a PutObject signed request - req, err := createSignedReq(http.MethodPut, s.endpoint, bucket+"/my-obj", s.awsID, s.awsSecret, "s3", s.awsRegion, nil, time.Now(), map[string]string{ + req, err := createSignedReq(http.MethodPut, s.endpoint, bucket+"/my-obj", s.awsID, s.awsSecret, "s3", s.awsRegion, "", nil, time.Now(), map[string]string{ "Origin": "http://www.example.com", "Access-Control-Request-Method": "invalid_method", }) @@ -267,7 +271,7 @@ func CORSMiddleware_invalid_headers(s *S3Conf) error { } // create a PutObject signed request - req, err := createSignedReq(http.MethodPut, s.endpoint, bucket+"/my-obj", s.awsID, s.awsSecret, "s3", s.awsRegion, nil, time.Now(), map[string]string{ + req, err := createSignedReq(http.MethodPut, s.endpoint, bucket+"/my-obj", s.awsID, s.awsSecret, "s3", s.awsRegion, "", nil, time.Now(), map[string]string{ "Origin": "http://www.example.com", "Access-Control-Request-Headers": "invalid header", }) @@ -330,7 +334,7 @@ func CORSMiddleware_access_forbidden(s *S3Conf) error { // origin match, method not (2nd rule) {"https://any-origin.com", http.MethodPost, ""}, } { - req, err := createSignedReq(http.MethodPut, s.endpoint, bucket+"/my-obj", s.awsID, s.awsSecret, "s3", s.awsRegion, nil, time.Now(), map[string]string{ + req, err := createSignedReq(http.MethodPut, s.endpoint, bucket+"/my-obj", s.awsID, s.awsSecret, "s3", s.awsRegion, "", nil, time.Now(), map[string]string{ "Origin": test.origin, "Access-Control-Request-Headers": test.headers, "Access-Control-Request-Method": test.method, @@ -413,7 +417,7 @@ func CORSMiddleware_access_granted(s *S3Conf) error { {"something.net", http.MethodPut, "Authorization", PreflightResult{"something.net", "POST, PUT", "authorization", "Content-Disposition, Content-Encoding", "3000", "true", varyHdr, nil}}, {"something.net", http.MethodPost, "", PreflightResult{"something.net", "POST, PUT", "", "Content-Disposition, Content-Encoding", "3000", "true", varyHdr, nil}}, } { - req, err := createSignedReq(http.MethodPut, s.endpoint, bucket+"/my-obj", s.awsID, s.awsSecret, "s3", s.awsRegion, nil, time.Now(), map[string]string{ + req, err := createSignedReq(http.MethodPut, s.endpoint, bucket+"/my-obj", s.awsID, s.awsSecret, "s3", s.awsRegion, "", nil, time.Now(), map[string]string{ "Origin": test.origin, "Access-Control-Request-Headers": test.headers, "Access-Control-Request-Method": test.method, diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go index 61100b53..fe7f5df1 100644 --- a/tests/integration/group-tests.go +++ b/tests/integration/group-tests.go @@ -529,6 +529,7 @@ func TestCompleteMultipartUpload(ts *TestState) { ts.Run(CompleteMultipartUpload_invalid_ETag) ts.Run(CompleteMultipartUpload_small_upload_size) ts.Run(CompleteMultipartUpload_empty_parts) + ts.Run(CompleteMultipartUpload_missing_part_fields) ts.Run(CompleteMultipartUpload_incorrect_parts_order) ts.Run(CompleteMultipartUpload_mpu_object_size) ts.Run(CompleteMultipartUpload_conditional_writes) @@ -969,6 +970,7 @@ func TestScoutfs(ts *TestState) { ts.Run(CompleteMultipartUpload_invalid_ETag) ts.Run(CompleteMultipartUpload_small_upload_size) ts.Run(CompleteMultipartUpload_empty_parts) + ts.Run(CompleteMultipartUpload_missing_part_fields) ts.Run(CompleteMultipartUpload_incorrect_parts_order) ts.Run(CompleteMultipartUpload_mpu_object_size) ts.Run(CompleteMultipartUpload_invalid_checksum_type) @@ -1648,6 +1650,7 @@ func GetIntTests() IntTests { "CompleteMultipartUpload_invalid_ETag": CompleteMultipartUpload_invalid_ETag, "CompleteMultipartUpload_small_upload_size": CompleteMultipartUpload_small_upload_size, "CompleteMultipartUpload_empty_parts": CompleteMultipartUpload_empty_parts, + "CompleteMultipartUpload_missing_part_fields": CompleteMultipartUpload_missing_part_fields, "CompleteMultipartUpload_incorrect_part_number": CompleteMultipartUpload_incorrect_part_number, "CompleteMultipartUpload_incorrect_parts_order": CompleteMultipartUpload_incorrect_parts_order, "CompleteMultipartUpload_mpu_object_size": CompleteMultipartUpload_mpu_object_size, diff --git a/tests/integration/iam.go b/tests/integration/iam.go index f1bfe695..28aae11d 100644 --- a/tests/integration/iam.go +++ b/tests/integration/iam.go @@ -189,6 +189,7 @@ func IAM_CreateBucket_empty_owner_header(s *S3Conf) error { s.awsSecret, "s3", s.awsRegion, + "", nil, time.Now(), nil, @@ -217,6 +218,7 @@ func IAM_CreateBucket_non_existing_user(s *S3Conf) error { s.awsSecret, "s3", s.awsRegion, + "", nil, time.Now(), map[string]string{ @@ -263,6 +265,7 @@ func IAM_CreateBucket_success(s *S3Conf) error { s.awsSecret, "s3", s.awsRegion, + "", body, time.Now(), map[string]string{ diff --git a/tests/integration/presigned_urls.go b/tests/integration/presigned_urls.go index ac9597ca..933b117f 100644 --- a/tests/integration/presigned_urls.go +++ b/tests/integration/presigned_urls.go @@ -20,6 +20,7 @@ import ( "io" "net/http" "net/url" + "strconv" "strings" "time" @@ -200,7 +201,7 @@ func PresignedAuth_malformed_creds_invalid_parts(s *S3Conf) error { return err } - return checkHTTPResponseApiErr(resp, s3err.QueryAuthErrors.MalformedCredential()) + return checkHTTPResponseApiErr(resp, s3err.QueryAuthErrors.MalformedCredential("")) }) } @@ -229,7 +230,7 @@ func PresignedAuth_creds_invalid_terminal(s *S3Conf) error { return err } - return checkHTTPResponseApiErr(resp, s3err.QueryAuthErrors.IncorrectTerminal("aws5_request")) + return checkHTTPResponseApiErr(resp, s3err.QueryAuthErrors.IncorrectTerminal("", "aws5_request")) }) } @@ -258,7 +259,7 @@ func PresignedAuth_creds_incorrect_service(s *S3Conf) error { return err } - return checkHTTPResponseApiErr(resp, s3err.QueryAuthErrors.IncorrectService("sns")) + return checkHTTPResponseApiErr(resp, s3err.QueryAuthErrors.IncorrectService("", "sns")) }) } @@ -321,13 +322,14 @@ func PresignedAuth_creds_invalid_date(s *S3Conf) error { return err } - return checkHTTPResponseApiErr(resp, s3err.QueryAuthErrors.InvalidDateFormat("32234Z34")) + return checkHTTPResponseApiErr(resp, s3err.QueryAuthErrors.InvalidDateFormat("", "32234Z34")) }) } func PresignedAuth_non_existing_access_key_id(s *S3Conf) error { testName := "PresignedAuth_non_existing_access_key_id" return presignedAuthHandler(s, testName, func(client *s3.PresignClient, bucket string) error { + accessKeyID := "a_rarely_existing_access_key_id890asd6f807as6ydf870say" ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: &bucket}) cancel() @@ -335,7 +337,7 @@ func PresignedAuth_non_existing_access_key_id(s *S3Conf) error { return err } - uri, err := changeAuthCred(v4req.URL, "a_rarely_existing_access_key_id890asd6f807as6ydf870say", credAccess) + uri, err := changeAuthCred(v4req.URL, accessKeyID, credAccess) if err != nil { return err } @@ -350,7 +352,7 @@ func PresignedAuth_non_existing_access_key_id(s *S3Conf) error { return err } - return checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrInvalidAccessKeyID)) + return checkHTTPResponseApiErr(resp, s3err.GetInvalidAccessKeyIdErr(accessKeyID)) }) } @@ -609,6 +611,10 @@ func PresignedAuth_expired_request(s *S3Conf) error { queries := urlParsed.Query() queries.Set("X-Amz-Date", expDate) urlParsed.RawQuery = queries.Encode() + xAmzExpires, err := strconv.Atoi(queries.Get("X-Amz-Expires")) + if err != nil { + return err + } uri, err := changeAuthCred(urlParsed.String(), expDate[:8], credDate) if err != nil { @@ -625,7 +631,7 @@ func PresignedAuth_expired_request(s *S3Conf) error { return err } - if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrExpiredPresignRequest)); err != nil { + if err := checkHTTPResponseApiErr(resp, s3err.GetExpiredPresignedURLError(xAmzExpires, "", "")); err != nil { return err } diff --git a/tests/integration/public_bucket.go b/tests/integration/public_bucket.go index f5e474c6..7482f9ff 100644 --- a/tests/integration/public_bucket.go +++ b/tests/integration/public_bucket.go @@ -2475,13 +2475,14 @@ func PublicBucket_incorrect_sha256_hash(s *S3Conf) error { // in anonymous requests the sha256 hash validity is not checked // so for any invalid values, the server calculates the hash // and compares with the provided one - req.Header.Add("x-amz-content-sha256", "incorrect_hash") + const incorrectPayloadHash = "incorrect_hash" + req.Header.Add("x-amz-content-sha256", incorrectPayloadHash) resp, err := s.httpClient.Do(req) if err != nil { return err } - return checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrContentSHA256Mismatch)) + return checkHTTPResponseApiErr(resp, s3err.GetContentSHA256MismatchErr(incorrectPayloadHash, emptySHA256Hash)) }) } diff --git a/tests/integration/server.go b/tests/integration/server.go index 887387dd..bd5e557b 100644 --- a/tests/integration/server.go +++ b/tests/integration/server.go @@ -27,7 +27,7 @@ import ( 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{ + 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 { diff --git a/tests/integration/sigv4_auth.go b/tests/integration/sigv4_auth.go index 835f1e6f..a600753c 100644 --- a/tests/integration/sigv4_auth.go +++ b/tests/integration/sigv4_auth.go @@ -15,6 +15,8 @@ package integration import ( + "crypto/md5" + "encoding/base64" "encoding/xml" "fmt" "io" @@ -42,7 +44,7 @@ func Authentication_invalid_auth_header(s *S3Conf) error { return err } - return checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrInvalidAuthHeader)) + return checkHTTPResponseApiErr(resp, s3err.GetInvalidArgumentErr(s3err.InvalidArgAuthHeader, "invalid_header")) }) } @@ -64,7 +66,7 @@ func Authentication_unsupported_signature_version(s *S3Conf) error { return err } - return checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrUnsupportedAuthorizationType)) + return checkHTTPResponseApiErr(resp, s3err.GetInvalidArgumentErr(s3err.InvalidArgAuthorizationType, "AWS2-HMAC-SHA1")) }) } @@ -192,7 +194,7 @@ func Authentication_malformed_credential(s *S3Conf) error { return err } - return checkHTTPResponseApiErr(resp, s3err.MalformedAuth.MalformedCredential()) + return checkHTTPResponseApiErr(resp, s3err.MalformedAuth.MalformedCredential("")) }) } @@ -215,7 +217,7 @@ func Authentication_credentials_invalid_terminal(s *S3Conf) error { return err } - return checkHTTPResponseApiErr(resp, s3err.MalformedAuth.IncorrectTerminal("aws_request")) + return checkHTTPResponseApiErr(resp, s3err.MalformedAuth.IncorrectTerminal("", "aws_request")) }) } @@ -238,7 +240,7 @@ func Authentication_credentials_incorrect_service(s *S3Conf) error { return err } - return checkHTTPResponseApiErr(resp, s3err.MalformedAuth.IncorrectService("ec2")) + return checkHTTPResponseApiErr(resp, s3err.MalformedAuth.IncorrectService("", "ec2")) }) } @@ -285,7 +287,7 @@ func Authentication_credentials_invalid_date(s *S3Conf) error { return err } - return checkHTTPResponseApiErr(resp, s3err.MalformedAuth.InvalidDateFormat("3223423234")) + return checkHTTPResponseApiErr(resp, s3err.MalformedAuth.InvalidDateFormat("", "3223423234")) }) } @@ -309,7 +311,7 @@ func Authentication_credentials_future_date(s *S3Conf) error { return err } - var errResp s3err.APIErrorResponse + var errResp APIErrorResponse err = xml.Unmarshal(body, &errResp) if err != nil { return err @@ -346,7 +348,7 @@ func Authentication_credentials_past_date(s *S3Conf) error { return err } - var errResp s3err.APIErrorResponse + var errResp APIErrorResponse err = xml.Unmarshal(body, &errResp) if err != nil { return err @@ -372,9 +374,10 @@ func Authentication_credentials_non_existing_access_key(s *S3Conf) error { service: "s3", date: time.Now(), }, func(req *http.Request) error { + accessKeyID := "a_rarely_existing_access_key_id_a7s86df78as6df89790a8sd7f" authHdr := req.Header.Get("Authorization") regExp := regexp.MustCompile("Credential=([^/]+)") - hdr := regExp.ReplaceAllString(authHdr, "Credential=a_rarely_existing_access_key_id_a7s86df78as6df89790a8sd7f") + hdr := regExp.ReplaceAllString(authHdr, "Credential="+accessKeyID) req.Header.Set("Authorization", hdr) resp, err := s.httpClient.Do(req) @@ -382,7 +385,7 @@ func Authentication_credentials_non_existing_access_key(s *S3Conf) error { return err } - return checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrInvalidAccessKeyID)) + return checkHTTPResponseApiErr(resp, s3err.GetInvalidAccessKeyIdErr(accessKeyID)) }) } @@ -471,28 +474,28 @@ func Authentication_invalid_sha256_payload_hash(s *S3Conf) error { return err } - return checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrInvalidSHA256Paylod)) + return checkHTTPResponseApiErr(resp, s3err.GetInvalidArgumentErr(s3err.InvalidArgSHA256Payload, "invalid_sha256")) }) } func Authentication_incorrect_payload_hash(s *S3Conf) error { testName := "Authentication_incorrect_payload_hash" + const incorrectPayloadHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b854" return authHandler(s, &authConfig{ - testName: testName, - method: http.MethodPut, - body: nil, - service: "s3", - date: time.Now(), - path: "bucket/object?tagging", + testName: testName, + method: http.MethodPut, + body: nil, + service: "s3", + date: time.Now(), + path: "bucket/object?tagging", + overrideSha256: incorrectPayloadHash, }, func(req *http.Request) error { - req.Header.Set("X-Amz-Content-Sha256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b854") - resp, err := s.httpClient.Do(req) if err != nil { return err } - return checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrContentSHA256Mismatch)) + return checkHTTPResponseApiErr(resp, s3err.GetContentSHA256MismatchErr(incorrectPayloadHash, emptySHA256Hash)) }) } @@ -512,15 +515,18 @@ func Authentication_md5(s *S3Conf) error { return err } + sum := md5.Sum(nil) + emptyMd5 := base64.StdEncoding.EncodeToString(sum[:]) + for i, test := range []struct { md5 string - err s3err.APIError + err s3err.S3Error }{ - {"invalid_md5", s3err.GetAPIError(s3err.ErrInvalidDigest)}, + {"invalid_md5", s3err.GetInvalidDigestErr("invalid_md5")}, // valid base64, but invalid md5 - {"aGVsbCBzLGRham5mamFuc2Y=", s3err.GetAPIError(s3err.ErrInvalidDigest)}, + {"aGVsbCBzLGRham5mamFuc2Y=", s3err.GetInvalidDigestErr("aGVsbCBzLGRham5mamFuc2Y=")}, // valid md5, but incorrect - {"XrY7u+Ae7tCTyyK7j1rNww==", s3err.GetAPIError(s3err.ErrBadDigest)}, + {"XrY7u+Ae7tCTyyK7j1rNww==", s3err.GetBadDigestErr(emptyMd5, base64ToHexString("XrY7u+Ae7tCTyyK7j1rNww=="))}, } { req.Header.Set("Content-Md5", test.md5) @@ -554,7 +560,6 @@ func Authentication_signature_error_incorrect_secret_key(s *S3Conf) error { service: "s3", date: time.Now(), }, func(req *http.Request) error { - resp, err := s.httpClient.Do(req) if err != nil { return err diff --git a/tests/integration/utils.go b/tests/integration/utils.go index d1489702..256bc4f1 100644 --- a/tests/integration/utils.go +++ b/tests/integration/utils.go @@ -61,6 +61,8 @@ import ( "golang.org/x/sync/semaphore" ) +const emptySHA256Hash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + var ( bcktCount atomic.Uint64 adminErrorPrefix = "XAdmin" @@ -296,18 +298,19 @@ func actionHandlerNoSetup(s *S3Conf, testName string, handler func(s3client *s3. } type authConfig struct { - testName string - path string - method string - body []byte - service string - date time.Time - headers map[string]string + testName string + path string + method string + overrideSha256 string + body []byte + service string + date time.Time + headers map[string]string } func authHandler(s *S3Conf, cfg *authConfig, handler func(req *http.Request) error) error { runF(cfg.testName) - req, err := createSignedReq(cfg.method, s.endpoint, cfg.path, s.awsID, s.awsSecret, cfg.service, s.awsRegion, cfg.body, cfg.date, cfg.headers) + req, err := createSignedReq(cfg.method, s.endpoint, cfg.path, s.awsID, s.awsSecret, cfg.service, s.awsRegion, cfg.overrideSha256, cfg.body, cfg.date, cfg.headers) if err != nil { failF("%v: %v", cfg.testName, err) return fmt.Errorf("%v: %w", cfg.testName, err) @@ -348,7 +351,7 @@ func presignedAuthHandler(s *S3Conf, testName string, handler func(client *s3.Pr return nil } -func createSignedReq(method, endpoint, path, access, secret, service, region string, body []byte, date time.Time, headers map[string]string) (*http.Request, error) { +func createSignedReq(method, endpoint, path, access, secret, service, region, overrideSha256 string, body []byte, date time.Time, headers map[string]string) (*http.Request, error) { req, err := http.NewRequest(method, fmt.Sprintf("%v/%v", endpoint, path), bytes.NewReader(body)) if err != nil { return nil, fmt.Errorf("failed to send the request: %w", err) @@ -356,8 +359,11 @@ func createSignedReq(method, endpoint, path, access, secret, service, region str signer := v4.NewSigner() - hashedPayload := sha256.Sum256(body) - hexPayload := hex.EncodeToString(hashedPayload[:]) + hexPayload := overrideSha256 + if hexPayload == "" { + hashedPayload := sha256.Sum256(body) + hexPayload = hex.EncodeToString(hashedPayload[:]) + } req.Header.Set("X-Amz-Content-Sha256", hexPayload) for key, val := range headers { @@ -372,7 +378,60 @@ func createSignedReq(method, endpoint, path, access, secret, service, region str return req, nil } -func checkHTTPResponseApiErr(resp *http.Response, apiErr s3err.APIError) error { +type APIErrorResponse struct { + XMLName xml.Name `xml:"Error"` + Code string + Message string + ArgumentName string `xml:"ArgumentName,omitempty"` + ArgumentValue string `xml:"ArgumentValue,omitempty"` + Method string `xml:"Method,omitempty"` + Resource string `xml:"Resource,omitempty"` + ResourceType s3err.ResourceType + CalculatedDigest string `xml:"CalculatedDigest,omitempty"` + ExpectedDigest string `xml:"ExpectedDigest,omitempty"` + BucketName string `xml:"BucketName,omitempty"` + ClientComputedContentSHA256 string `xml:"ClientComputedContentSHA256,omitempty"` + S3ComputedContentSHA256 string `xml:"S3ComputedContentSHA256,omitempty"` + ProposedSize int64 `xml:"ProposedSize,omitempty"` + MaxSizeAllowed int64 `xml:"MaxSizeAllowed,omitempty"` + MinSizeAllowed int64 `xml:"MinSizeAllowed,omitempty"` + ServerTime string `xml:"ServerTime,omitempty"` + XAmzExpires int `xml:"X-Amz-Expires,omitempty"` + Expires string `xml:"Expires,omitempty"` + AWSAccessKeyId string `xml:"AWSAccessKeyId,omitempty"` + Chunk int `xml:"Chunk,omitempty"` + BadChunkSize int64 `xml:"BadChunkSize,omitempty"` + ContentMD5 string `xml:"Content-MD5,omitempty"` + LocationConstraint string `xml:"LocationConstraint,omitempty"` + UploadId string `xml:"UploadId,omitempty"` + PartNumber int32 `xml:"PartNumber,omitempty"` + ETag string `xml:"ETag,omitempty"` + ActualPartCount int32 `xml:"ActualPartCount,omitempty"` + PartNumberRequested int32 `xml:"PartNumberRequested,omitempty"` + RangeRequested string `xml:"RangeRequested,omitempty"` + ActualObjectSize int64 `xml:"ActualObjectSize,omitempty"` + TagKey string `xml:"TagKey,omitempty"` + TagValue string `xml:"TagValue,omitempty"` + Size int64 `xml:"Size,omitempty"` + Header string `xml:"Header,omitempty"` + AdditionalMessage s3err.NmpAdditionalMessage `xml:"additionalMessage,omitempty"` + Key string `xml:"Key,omitempty"` + VersionId string `xml:"VersionId,omitempty"` + Condition s3err.Condition `xml:"Condition,omitempty"` + RequestTime string `xml:"RequestTime,omitempty"` + MaxAllowedSkewMilliseconds int `xml:"MaxAllowedSkewMilliseconds,omitempty"` + Region string `xml:"Region,omitempty"` + StringToSign string `xml:"StringToSign,omitempty"` + SignatureProvided string `xml:"SignatureProvided,omitempty"` + StringToSignBytes string `xml:"StringToSignBytes,omitempty"` + CanonicalRequest string `xml:"CanonicalRequest,omitempty"` + CanonicalRequestBytes string `xml:"CanonicalRequestBytes,omitempty"` + RequestID string `xml:"RequestId,omitempty"` + HostID string `xml:"HostId,omitempty"` +} + +func checkHTTPResponseApiErr(resp *http.Response, expected s3err.S3Error) error { + apiErr := expected.BaseError() body, err := io.ReadAll(resp.Body) if err != nil { return err @@ -380,7 +439,7 @@ func checkHTTPResponseApiErr(resp *http.Response, apiErr s3err.APIError) error { resp.Body.Close() - var errResp s3err.APIErrorResponse + var errResp APIErrorResponse err = xml.Unmarshal(body, &errResp) if err != nil { return err @@ -389,25 +448,181 @@ func checkHTTPResponseApiErr(resp *http.Response, apiErr s3err.APIError) error { if resp.StatusCode != apiErr.HTTPStatusCode { return fmt.Errorf("expected response status code to be %v, instead got %v", apiErr.HTTPStatusCode, resp.StatusCode) } - return compareS3ApiError(apiErr, &errResp) + return compareS3ApiError(expected, &errResp) } -func compareS3ApiError(expected s3err.APIError, received *s3err.APIErrorResponse) error { +func compareS3ApiError(expected s3err.S3Error, received *APIErrorResponse) error { + apiErr := expected.BaseError() if received == nil { - return fmt.Errorf("expected %w, received nil", expected) + return fmt.Errorf("expected %w, received nil", apiErr) } - if received.Code != expected.Code { - return fmt.Errorf("expected error code to be %v, instead got %v", expected.Code, received.Code) + if received.Code != apiErr.Code { + return fmt.Errorf("expected error code to be %v, instead got %v", apiErr.Code, received.Code) } - if received.Message != expected.Description { - return fmt.Errorf("expected error message to be %v, instead got %v", expected.Description, received.Message) + if received.Message != apiErr.Description { + return fmt.Errorf("expected error message to be %v, instead got %v", apiErr.Description, received.Message) + } + + return compareS3ApiErr(expected, received) +} + +func compareS3ApiErr(expected s3err.S3Error, received *APIErrorResponse) error { + switch err := expected.(type) { + case s3err.APIError: + return nil + case s3err.AccessForbiddenError: + return compareS3ApiErrFields( + compareErrField("Method", err.Method, received.Method), + compareErrField("ResourceType", err.ResourceType, received.ResourceType), + ) + case s3err.BadDigestError: + return compareS3ApiErrFields( + compareErrField("CalculatedDigest", err.CalculatedDigest, received.CalculatedDigest), + compareErrField("ExpectedDigest", err.ExpectedDigest, received.ExpectedDigest), + ) + case s3err.BucketError: + return compareErrField("BucketName", err.BucketName, received.BucketName) + case s3err.ContentSHA256MismatchError: + return compareS3ApiErrFields( + compareErrField("ClientComputedContentSHA256", err.ClientComputedContentSHA256, received.ClientComputedContentSHA256), + compareErrField("S3ComputedContentSHA256", err.S3ComputedContentSHA256, received.S3ComputedContentSHA256), + ) + case s3err.EntityTooLargeError: + return compareS3ApiErrFields( + compareErrField("ProposedSize", err.ProposedSize, received.ProposedSize), + compareErrField("MaxSizeAllowed", err.MaxSizeAllowed, received.MaxSizeAllowed), + ) + case s3err.EntityTooSmallError: + return compareS3ApiErrFields( + compareErrField("ProposedSize", err.ProposedSize, received.ProposedSize), + compareErrField("MinSizeAllowed", err.MinSizeAllowed, received.MinSizeAllowed), + ) + case s3err.ExpiredPresignedURLError: + return compareS3ApiErrFields( + checkErrFieldEmptiness("ServerTime", received.ServerTime, true), + compareErrField("X-Amz-Expires", err.XAmzExpires, received.XAmzExpires), + checkErrFieldEmptiness("Expires", received.Expires, true), + ) + case s3err.InvalidAccessKeyIdError: + return compareErrField("AWSAccessKeyId", err.AWSAccessKeyId, received.AWSAccessKeyId) + case s3err.InvalidArgumentError: + return compareS3ApiErrFields( + compareErrField("ArgumentName", err.ArgumentName, received.ArgumentName), + compareErrField("ArgumentValue", err.ArgumentValue, received.ArgumentValue), + ) + case s3err.InvalidChunkSizeError: + return compareS3ApiErrFields( + compareErrField("Chunk", err.Chunk, received.Chunk), + compareErrField("BadChunkSize", err.BadChunkSize, received.BadChunkSize), + ) + case s3err.InvalidDigestError: + return compareErrField("Content-MD5", err.ContentMD5, received.ContentMD5) + case s3err.InvalidLocationConstraintError: + return compareErrField("LocationConstraint", err.LocationConstraint, received.LocationConstraint) + case s3err.InvalidPartError: + return compareS3ApiErrFields( + compareErrField("UploadId", err.UploadId, received.UploadId), + compareErrField("PartNumber", err.PartNumber, received.PartNumber), + compareErrField("ETag", err.ETag, received.ETag), + ) + case s3err.InvalidPartNumberRangeError: + return compareS3ApiErrFields( + compareErrField("ActualPartCount", err.ActualPartCount, received.ActualPartCount), + compareErrField("PartNumberRequested", err.PartNumberRequested, received.PartNumberRequested), + ) + case s3err.InvalidRangeError: + return compareS3ApiErrFields( + compareErrField("RangeRequested", err.RangeRequested, received.RangeRequested), + compareErrField("ActualObjectSize", err.ActualObjectSize, received.ActualObjectSize), + ) + case s3err.InvalidTagError: + return compareS3ApiErrFields( + compareErrField("TagKey", err.TagKey, received.TagKey), + compareErrField("TagValue", err.TagValue, received.TagValue), + ) + case s3err.KeyTooLongError: + return compareS3ApiErrFields( + compareErrField("Size", err.Size, received.Size), + compareErrField("MaxSizeAllowed", err.MaxSizeAllowed, received.MaxSizeAllowed), + ) + case s3err.MetadataTooLargeError: + return compareS3ApiErrFields( + compareErrField("Size", int64(err.Size), received.Size), + compareErrField("MaxSizeAllowed", int64(err.MaxSizeAllowed), received.MaxSizeAllowed), + ) + case s3err.MethodNotAllowedError: + return compareS3ApiErrFields( + compareErrField("Method", err.Method, received.Method), + compareErrField("ResourceType", err.ResourceType, received.ResourceType), + ) + case s3err.MalformedAuthError: + return compareErrField("Region", err.Region, received.Region) + case s3err.NoSuchUploadError: + return compareErrField("UploadId", err.UploadId, received.UploadId) + case s3err.NoSuchVersionError: + return compareS3ApiErrFields( + compareErrField("Key", err.Key, received.Key), + compareErrField("VersionId", err.VersionId, received.VersionId), + ) + case s3err.NotImplementedError: + return compareS3ApiErrFields( + compareErrField("Header", err.Header, received.Header), + compareErrField("additionalMessage", err.AdditionalMessage, received.AdditionalMessage), + ) + case s3err.PreconditionFailedError: + return compareErrField("Condition", err.Condition, received.Condition) + case s3err.RequestTimeTooSkewedError: + return compareS3ApiErrFields( + checkErrFieldEmptiness("RequestTime", received.RequestTime, true), + checkErrFieldEmptiness("ServerTime", received.ServerTime, true), + compareErrField("MaxAllowedSkewMilliseconds", err.MaxAllowedSkewMilliseconds, received.MaxAllowedSkewMilliseconds), + ) + case s3err.SignatureDoesNotMatchError: + return compareS3ApiErrFields( + compareErrField("AWSAccessKeyId", err.AWSAccessKeyId, received.AWSAccessKeyId), + checkErrFieldEmptiness("StringToSign", err.StringToSign, true), + compareErrField("SignatureProvided", err.SignatureProvided, received.SignatureProvided), + checkErrFieldEmptiness("StringToSignBytes", err.StringToSignBytes, true), + checkErrFieldEmptiness("CanonicalRequest", err.CanonicalRequest, true), + checkErrFieldEmptiness("CanonicalRequestBytes", err.CanonicalRequestBytes, true), + ) } return nil } -func checkApiErr(err error, apiErr s3err.APIError) error { +func compareS3ApiErrFields(errs ...error) error { + for _, err := range errs { + if err != nil { + return err + } + } + + return nil +} + +func compareErrField[T comparable](field string, expected, received T) error { + if received != expected { + return fmt.Errorf("expected error %s to be %v, instead got %v", field, expected, received) + } + + return nil +} + +func checkErrFieldEmptiness(field, received string, require bool) error { + if require && received == "" { + return fmt.Errorf("expected error %s to be non-empty", field) + } + if !require && received != "" { + return fmt.Errorf("expected error %s to be empty, instead got %s", field, received) + } + + return nil +} + +func checkApiErr(err error, expected s3err.S3Error) error { + apiErr := expected.BaseError() if err == nil { return fmt.Errorf("expected %v, instead got nil", apiErr.Code) } @@ -2478,7 +2693,7 @@ func putBucketPolicy(client *s3.Client, bucket, policy string) error { return err } -func sendSignedRequest(s *S3Conf, req *http.Request, cancel context.CancelFunc) (map[string]string, *s3err.APIErrorResponse, error) { +func sendSignedRequest(s *S3Conf, req *http.Request, cancel context.CancelFunc) (map[string]string, *APIErrorResponse, error) { signer := v4.NewSigner() signErr := signer.SignHTTP(req.Context(), aws.Credentials{AccessKeyID: s.awsID, SecretAccessKey: s.awsSecret}, req, "STREAMING-UNSIGNED-PAYLOAD-TRAILER", "s3", s.awsRegion, time.Now()) if signErr != nil { @@ -2499,7 +2714,7 @@ func sendSignedRequest(s *S3Conf, req *http.Request, cancel context.CancelFunc) return nil, nil, fmt.Errorf("failed to read the request body: %w", err) } - var errResp s3err.APIErrorResponse + var errResp APIErrorResponse err = xml.Unmarshal(bodyBytes, &errResp) if err != nil { return nil, nil, fmt.Errorf("failed to unmarshal response body: %w", err) @@ -2516,7 +2731,7 @@ func sendSignedRequest(s *S3Conf, req *http.Request, cancel context.CancelFunc) return headers, nil, nil } -func testUnsignedStreamingPayloadTrailerObjectPut(s *S3Conf, bucket, object string, body []byte, reqHeaders map[string]string) (map[string]string, *s3err.APIErrorResponse, error) { +func testUnsignedStreamingPayloadTrailerObjectPut(s *S3Conf, bucket, object string, body []byte, reqHeaders map[string]string) (map[string]string, *APIErrorResponse, error) { ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) req, err := http.NewRequestWithContext(ctx, http.MethodPut, s.endpoint+"/"+bucket+"/"+object, bytes.NewReader(body)) if err != nil { @@ -2532,7 +2747,7 @@ func testUnsignedStreamingPayloadTrailerObjectPut(s *S3Conf, bucket, object stri return sendSignedRequest(s, req, cancel) } -func testUnsignedStreamingPayloadTrailerUploadPart(s *S3Conf, bucket, object string, uploadId *string, body []byte, reqHeaders map[string]string) (map[string]string, *s3err.APIErrorResponse, error) { +func testUnsignedStreamingPayloadTrailerUploadPart(s *S3Conf, bucket, object string, uploadId *string, body []byte, reqHeaders map[string]string) (map[string]string, *APIErrorResponse, error) { if uploadId == nil { return nil, nil, fmt.Errorf("empty upload id") } @@ -2613,7 +2828,7 @@ func withTrailingChecksum(checksum string) signedReqOpt { } } -func testSignedStreamingObjectPut(s *S3Conf, bucket, object string, payload []byte, opts ...signedReqOpt) (map[string]string, *s3err.APIErrorResponse, error) { +func testSignedStreamingObjectPut(s *S3Conf, bucket, object string, payload []byte, opts ...signedReqOpt) (map[string]string, *APIErrorResponse, error) { cfg := &signedReqCfg{ chunkSize: 8192, // minimal valid chunk size } @@ -2707,7 +2922,7 @@ func testSignedStreamingObjectPut(s *S3Conf, bucket, object string, payload []by return nil, nil, fmt.Errorf("failed to read the response body: %w", err) } - var errResp s3err.APIErrorResponse + var errResp APIErrorResponse err = xml.Unmarshal(bodyBytes, &errResp) if err != nil { return nil, nil, fmt.Errorf("failed to unmarshal response body: %w", err) @@ -3065,6 +3280,15 @@ type PostRequestConfig struct { // sendPostObject sends a POST multipart/form-data request to /{bucket}. // Returns the raw *http.Response for flexible per-test assertions. func sendPostObject(input PostRequestConfig) (*http.Response, error) { + req, _, err := newPostObjectRequest(input) + if err != nil { + return nil, err + } + + return input.s3Conf.httpClient.Do(req) +} + +func newPostObjectRequest(input PostRequestConfig) (*http.Request, map[string]string, error) { if input.date.IsZero() { input.date = time.Now().UTC() } @@ -3088,7 +3312,7 @@ func sendPostObject(input PostRequestConfig) (*http.Response, error) { var err error policy, err = encodePostPolicy(input.policyConditions, input.policyExpiration, fields, input.omitPolicyConditions) if err != nil { - return nil, err + return nil, nil, err } } else { policy = *input.rawPolicy @@ -3099,14 +3323,14 @@ func sendPostObject(input PostRequestConfig) (*http.Response, error) { body, boundary, err := buildPostObjectBody(fields, input.extraFields, input.fileContent) if err != nil { - return nil, err + return nil, nil, err } endpoint := fmt.Sprintf("%s/%s", input.s3Conf.endpoint, input.bucket) if input.s3Conf.hostStyle { u, err := url.Parse(input.s3Conf.endpoint) if err != nil { - return nil, err + return nil, nil, err } u.Host = input.bucket + "." + u.Host @@ -3115,13 +3339,13 @@ func sendPostObject(input PostRequestConfig) (*http.Response, error) { req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(body)) if err != nil { - return nil, err + return nil, nil, err } req.Header.Set("Content-Type", fmt.Sprintf("multipart/form-data; boundary=%s", boundary)) req.ContentLength = int64(len(body)) - return input.s3Conf.httpClient.Do(req) + return req, fields, nil } func getEtagBytes(etag string) ([]byte, error) { @@ -3132,3 +3356,20 @@ func md5String(data []byte) string { sum := md5.Sum(data) return hex.EncodeToString(sum[:]) } + +func base64ToHexString(s string) string { + data, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return "" + } + return hex.EncodeToString(data) +} + +func hexBytes(s string) string { + data := []byte(s) + parts := make([]string, len(data)) + for i, b := range data { + parts[i] = fmt.Sprintf("%02x", b) + } + return strings.Join(parts, " ") +} diff --git a/tests/integration/versioning.go b/tests/integration/versioning.go index 30fd5816..70e52e6d 100644 --- a/tests/integration/versioning.go +++ b/tests/integration/versioning.go @@ -239,7 +239,7 @@ func Versioning_CopyObject_invalid_versionId(s *S3Conf) error { CopySource: getPtr(fmt.Sprintf("%v/%v?versionId=invalid_versionId", bucket, srcObj)), }) cancel() - return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidVersionId)) + return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "invalid_versionId")) }, withVersioning(types.BucketVersioningStatusEnabled)) } @@ -714,7 +714,7 @@ func Versioning_GetObject_invalid_versionId(s *S3Conf) error { VersionId: getPtr("invalid_version_id"), }) cancel() - return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidVersionId)) + return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "invalid_version_id")) }, withVersioning(types.BucketVersioningStatusEnabled)) } @@ -994,7 +994,7 @@ func Versioning_GetObjectAttributes_invalid_versionId(s *S3Conf) error { }, }) cancel() - return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidVersionId)) + return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "invalid_versionId")) }) } @@ -1112,7 +1112,7 @@ func Versioning_DeleteObject_invalid_versionId(s *S3Conf) error { VersionId: getPtr("invalid_versionId"), }) cancel() - return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidVersionId)) + return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "invalid_versionId")) }) } @@ -1184,7 +1184,7 @@ func Versioning_DeleteObject_non_existing_object(s *S3Conf) error { VersionId: getPtr("non_existing_version_id"), }) canel() - if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidVersionId)); err != nil { + if err := checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "non_existing_version_id")); err != nil { return err } @@ -1893,7 +1893,7 @@ func Versioning_UploadPartCopy_invalid_versionId(s *S3Conf) error { bucket, srcObj)), }) cancel() - return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidVersionId)) + return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "invalid_versionId")) }) } @@ -2099,7 +2099,7 @@ func Versioning_PutObjectRetention_invalid_versionId(s *S3Conf) error { }, }) cancel() - return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidVersionId)) + return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "invalid_version_id")) }, withLock()) } @@ -2148,7 +2148,7 @@ func Versioning_GetObjectRetention_invalid_versionId(s *S3Conf) error { VersionId: getPtr("invalid_versionId"), }) cancel() - return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidVersionId)) + return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "invalid_versionId")) }, withLock()) } @@ -2292,7 +2292,7 @@ func Versioning_PutObjectLegalHold_invalid_versionId(s *S3Conf) error { }, }) cancel() - return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidVersionId)) + return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "invalid_version_id")) }, withLock()) } @@ -2339,7 +2339,7 @@ func Versioning_GetObjectLegalHold_invalid_versionId(s *S3Conf) error { VersionId: getPtr("invalid_version_id"), }) cancel() - return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidVersionId)) + return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "invalid_version_id")) }, withLock()) } @@ -3666,7 +3666,7 @@ func Versioning_GetObjectTagging_invalid_versionId(s *S3Conf) error { VersionId: getPtr("invalid_versionId"), }) cancel() - return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidVersionId)) + return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "invalid_versionId")) }) } @@ -3780,7 +3780,7 @@ func Versioning_PutObjectTagging_invalid_versionId(s *S3Conf) error { VersionId: getPtr("invalid_versionId"), }) cancel() - return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidVersionId)) + return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "invalid_versionId")) }, withVersioning(types.BucketVersioningStatusEnabled)) } @@ -3826,7 +3826,7 @@ func Versioning_DeleteObjectTagging_invalid_versionId(s *S3Conf) error { VersionId: getPtr("invalid_versionId"), }) cancel() - return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidVersionId)) + return checkApiErr(err, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, "invalid_versionId")) }) }