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.
This commit is contained in:
niksis02
2026-05-15 03:31:55 +04:00
parent eade1e3a71
commit 9f786b3c2c
132 changed files with 3511 additions and 1339 deletions
+10 -11
View File
@@ -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
+1 -1
View File
@@ -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))
}
}
+3 -3
View File
@@ -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 {
+4 -4
View File
@@ -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)
})
+3 -3
View File
@@ -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:
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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),
},
}
+18 -7
View File
@@ -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 {
+5 -5
View File
@@ -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{})
}
}
+30 -26
View File
@@ -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
+44 -53
View File
@@ -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
+6 -6
View File
@@ -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
}
+2 -2
View File
@@ -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
+189 -184
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -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
+4 -4
View File
@@ -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)
+2 -2
View File
@@ -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
}
+1 -1
View File
@@ -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 {
+2
View File
@@ -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))
+23 -20
View File
@@ -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...)
+22 -9
View File
@@ -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)
+4 -4
View File
@@ -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"),
},
},
{
+4 -4
View File
@@ -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"),
},
},
{
+1 -1
View File
@@ -433,7 +433,7 @@ func TestS3ApiController_POSTObject(t *testing.T) {
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrMetadataTooLarge),
err: s3err.GetMetadataTooLargeErr(2053, 2048),
},
},
{
+6 -11
View File
@@ -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)
}
}
}
+5 -9
View File
@@ -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"),
},
},
{
+2 -2
View File
@@ -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)
}
+4 -4
View File
@@ -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"),
},
},
{
+1 -1
View File
@@ -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 != "" {
+1 -1
View File
@@ -96,7 +96,7 @@ func TestS3ApiController_HeadObject(t *testing.T) {
BucketOwner: "root",
},
},
err: s3err.GetAPIError(s3err.ErrInvalidPartNumber),
err: s3err.GetInvalidArgumentErr(s3err.InvalidArgPartNumber, "-4"),
},
},
{
+30 -12
View File
@@ -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(`<CompleteMultipartUpload xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Part><PartNumber>1</PartNumber><ETag>ETag</ETag></Part></CompleteMultipartUpload>`)
s3cmdMpBody := []byte("<CompleteMultipartUpload><Part><PartNumber>1</PartNumber><ETag>ETag</ETag></Part></CompleteMultipartUpload>")
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) {
+4 -4
View File
@@ -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"))
+10 -10
View File
@@ -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, ""),
},
},
{
+5 -2
View File
@@ -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{
+2 -2
View File
@@ -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),
},
},
{
@@ -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)
}
+4 -4
View File
@@ -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
+29 -20
View File
@@ -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
}
}
@@ -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
+2 -2
View File
@@ -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)
+9 -6
View File
@@ -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
+2 -2
View File
@@ -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())
},
+10 -12
View File
@@ -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
+1 -1
View File
@@ -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)
}
}
+4 -1
View File
@@ -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()
+28
View File
@@ -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()
}
}
+46
View File
@@ -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)
}
+81 -12
View File
@@ -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))
}
+8 -4
View File
@@ -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))
}
+32 -89
View File
@@ -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],
+2 -2
View File
@@ -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,
+9 -9
View File
@@ -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
+3 -3
View File
@@ -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)
}
})
}
+2 -16
View File
@@ -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)
}
+11 -2
View File
@@ -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
}
+2 -1
View File
@@ -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)
+1 -1
View File
@@ -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",
+17 -46
View File
@@ -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
+114
View File
@@ -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)
}
}
+53
View File
@@ -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)))
}
+37 -33
View File
@@ -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
+3 -3
View File
@@ -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)
}
}
+52 -44
View File
@@ -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
}
+37 -37
View File
@@ -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},
+57
View File
@@ -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 <Method> and <ResourceType> 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,
}
}
+57
View File
@@ -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 <CalculatedDigest> and <ExpectedDigest> 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,
}
}
+56
View File
@@ -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 <BucketName> 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,
}
}
+58
View File
@@ -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 <ClientComputedContentSHA256> and <S3ComputedContentSHA256> 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,
}
}
+57
View File
@@ -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 <ProposedSize> and <MaxSizeAllowed> 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,
}
}
+57
View File
@@ -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 <ProposedSize> and <MinSizeAllowed> 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,
}
}
+61
View File
@@ -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 <ServerTime>, <X-Amz-Expires> and <Expires> 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,
}
}
+53
View File
@@ -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 <AWSAccessKeyId> 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,
}
}
+274
View File
@@ -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 <ArgumentName> and <ArgumentValue> 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),
}
}
+57
View File
@@ -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 <Chunk> and <BadChunkSize> 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,
}
}
+53
View File
@@ -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 <Content-MD5> 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,
}
}
@@ -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 <LocationConstraint> 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,
}
}
+63
View File
@@ -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 <UploadId>, <PartNumber>, and <ETag> 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,
}
}
+58
View File
@@ -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 <ActualPartCount> and <PartNumberRequested> 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,
}
}
+57
View File
@@ -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 <RangeRequested> and <ActualObjectSize> 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,
}
}
+59
View File
@@ -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 <TagKey> and optionally <TagValue> 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,
}
}
+57
View File
@@ -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 <Size> and <MaxSizeAllowed> 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,
}
}
+57
View File
@@ -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 <Size> and <MaxSizeAllowed> 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,
}
}
+67
View File
@@ -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 <Method> and <ResourceType> 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,
}
}
+53
View File
@@ -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 <UploadId> 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,
}
}
+57
View File
@@ -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 <Key> and <VersionId> 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,
}
}
+66
View File
@@ -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 <Header> and <additionalMessage> 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,
}
}
+58 -33
View File
@@ -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 \"<YOUR-AKID>/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.")
}
+65
View File
@@ -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 <Condition> 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,
}
}
+15 -15
View File
@@ -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 "<YOUR-AKID>/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")
}
+63
View File
@@ -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 <RequestTime>, <ServerTime>, and <MaxAllowedSkewMilliseconds> 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,
}
}
+38 -267
View File
@@ -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"
)
+73
View File
@@ -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,
}
}
+51 -17
View File
@@ -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 <Region> 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 \"<YOUR-AKID>/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.",
)
+1 -14
View File
@@ -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:
+6 -5
View File
@@ -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)
}
+5 -2
View File
@@ -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,
+6 -5
View File
@@ -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)
}
+2 -1
View File
@@ -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 {
+1 -1
View File
@@ -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
}
+72 -3
View File
@@ -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, `<CompleteMultipartUpload xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Part><PartNumber>%d</PartNumber></Part></CompleteMultipartUpload>`,
getInt32(part.PartNumber))
},
},
{
name: "missing PartNumber",
body: func(part types.Part) []byte {
return fmt.Appendf(nil, `<CompleteMultipartUpload xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Part><ETag>%s</ETag></Part></CompleteMultipartUpload>`,
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
}

Some files were not shown because too many files have changed in this diff Show More