mirror of
https://github.com/versity/versitygw.git
synced 2026-07-20 06:52:21 +00:00
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:
+10
-11
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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...)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -433,7 +433,7 @@ func TestS3ApiController_POSTObject(t *testing.T) {
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
err: s3err.GetAPIError(s3err.ErrMetadataTooLarge),
|
||||
err: s3err.GetMetadataTooLargeErr(2053, 2048),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -96,7 +96,7 @@ func TestS3ApiController_HeadObject(t *testing.T) {
|
||||
BucketOwner: "root",
|
||||
},
|
||||
},
|
||||
err: s3err.GetAPIError(s3err.ErrInvalidPartNumber),
|
||||
err: s3err.GetInvalidArgumentErr(s3err.InvalidArgPartNumber, "-4"),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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, ""),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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},
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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.")
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user