Files
versitygw/tests/integration/utils.go
niksis02 068b04ec62 fix: fixes PutObjectRetention error cases and object lock error code/message.
Fixes #1559
Fixes #1330

This PR focuses on three main changes:

1. **Fix object lock error codes and descriptions**
   When an object was WORM-protected and delete/overwrite was disallowed due to object lock configurations, the gateway incorrectly returned the `s3.ErrObjectLocked` error code and description. These have now been corrected.
2. **Update `PutObjectRetention` behavior**
   Previously, when an object already had a retention mode set, the gateway only allowed modifications if the mode was changed from `GOVERNANCE` to `COMPLIANCE`, and only when the user had the `s3:BypassGovernanceRetention` permission.
   The logic has been updated: if the existing retention mode is the same as the one being applied, the operation is now allowed regardless of other factors.
3. **Fix error checks in integration tests (AWS SDK regression)**
   Due to an AWS SDK regression, integration tests were previously limited to checking partial error descriptions. This issue seems to be resolved for some actions (though the ticket is still open: https://github.com/aws/aws-sdk-go-v2/issues/2921). Error checks have been reverted back to full description comparisons where possible.
2025-09-25 01:41:41 +04:00

1838 lines
47 KiB
Go

// Copyright 2023 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package integration
import (
"bytes"
"context"
"crypto/md5"
"crypto/rand"
"crypto/sha1"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/xml"
"errors"
"fmt"
"hash"
"hash/crc32"
"hash/crc64"
"io"
"math/big"
"math/bits"
rnd "math/rand"
"net/http"
"net/url"
"os/exec"
"slices"
"sort"
"strings"
"time"
"unicode"
"github.com/aws/aws-sdk-go-v2/aws"
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/aws/smithy-go"
"github.com/versity/versitygw/s3err"
)
var (
bcktCount = 0
adminErrorPrefix = "XAdmin"
)
type user struct {
access string
secret string
role string
}
var (
testuser1 user = user{
access: "grt1",
secret: "grt1secret",
role: "user",
}
testuser2 user = user{
access: "grt2",
secret: "grt2secret",
role: "user",
}
testuserplus user = user{
access: "grtplus",
secret: "grt1plussecret",
role: "userplus",
}
testadmin user = user{
access: "admin",
secret: "adminsecret",
role: "admin",
}
)
func getBucketName() string {
bcktCount++
return fmt.Sprintf("test-bucket-%v", bcktCount)
}
func setup(s *S3Conf, bucket string, opts ...setupOpt) error {
s3client := s.GetClient()
cfg := new(setupCfg)
for _, opt := range opts {
opt(cfg)
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.CreateBucket(ctx, &s3.CreateBucketInput{
Bucket: &bucket,
ObjectLockEnabledForBucket: &cfg.LockEnabled,
ObjectOwnership: cfg.Ownership,
})
cancel()
if err != nil {
return err
}
if cfg.VersioningStatus != "" {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.PutBucketVersioning(ctx, &s3.PutBucketVersioningInput{
Bucket: &bucket,
VersioningConfiguration: &types.VersioningConfiguration{
Status: cfg.VersioningStatus,
},
})
cancel()
if err != nil {
return err
}
}
return nil
}
func teardown(s *S3Conf, bucket string) error {
s3client := s.GetClient()
deleteObject := func(bucket, key, versionId *string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: bucket,
Key: key,
VersionId: versionId,
})
cancel()
if err != nil {
return fmt.Errorf("failed to delete object %v: %w", *key, err)
}
return nil
}
if s.versioningEnabled {
in := &s3.ListObjectVersionsInput{Bucket: &bucket}
for {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.ListObjectVersions(ctx, in)
cancel()
if err != nil {
return fmt.Errorf("failed to list objects: %w", err)
}
for _, item := range out.Versions {
err = deleteObject(&bucket, item.Key, item.VersionId)
if err != nil {
return err
}
}
for _, item := range out.DeleteMarkers {
err = deleteObject(&bucket, item.Key, item.VersionId)
if err != nil {
return err
}
}
if out.IsTruncated != nil && *out.IsTruncated {
in.KeyMarker = out.KeyMarker
in.VersionIdMarker = out.NextVersionIdMarker
} else {
break
}
}
} else {
for {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
Bucket: &bucket,
})
cancel()
if err != nil {
return fmt.Errorf("failed to list objects: %w", err)
}
for _, item := range out.Contents {
err = deleteObject(&bucket, item.Key, nil)
if err != nil {
return err
}
}
if out.IsTruncated != nil && *out.IsTruncated {
continue
}
break
}
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := s3client.DeleteBucket(ctx, &s3.DeleteBucketInput{
Bucket: &bucket,
})
cancel()
return err
}
type setupCfg struct {
LockEnabled bool
VersioningStatus types.BucketVersioningStatus
Ownership types.ObjectOwnership
Anonymous bool
SkipTearDown bool
}
type setupOpt func(*setupCfg)
func withLock() setupOpt {
return func(s *setupCfg) { s.LockEnabled = true }
}
func withOwnership(o types.ObjectOwnership) setupOpt {
return func(s *setupCfg) { s.Ownership = o }
}
func withVersioning(v types.BucketVersioningStatus) setupOpt {
return func(s *setupCfg) { s.VersioningStatus = v }
}
func withAnonymousClient() setupOpt {
return func(s *setupCfg) { s.Anonymous = true }
}
func withSkipTearDown() setupOpt {
return func(s *setupCfg) { s.SkipTearDown = true }
}
func actionHandler(s *S3Conf, testName string, handler func(s3client *s3.Client, bucket string) error, opts ...setupOpt) error {
runF(testName)
bucketName := getBucketName()
cfg := new(setupCfg)
for _, opt := range opts {
opt(cfg)
}
err := setup(s, bucketName, opts...)
if err != nil {
failF("%v: failed to create a bucket: %v", testName, err)
return fmt.Errorf("%v: failed to create a bucket: %w", testName, err)
}
var client *s3.Client
if cfg.Anonymous {
client = s.GetAnonymousClient()
} else {
client = s.GetClient()
}
handlerErr := handler(client, bucketName)
if handlerErr != nil {
failF("%v: %v", testName, handlerErr)
}
if !cfg.SkipTearDown {
err = teardown(s, bucketName)
if err != nil {
fmt.Printf(colorRed+"%v: failed to delete the bucket: %v", testName, err)
if handlerErr == nil {
return fmt.Errorf("%v: failed to delete the bucket: %w", testName, err)
}
}
}
if handlerErr == nil {
passF(testName)
}
return handlerErr
}
func actionHandlerNoSetup(s *S3Conf, testName string, handler func(s3client *s3.Client, bucket string) error, _ ...setupOpt) error {
runF(testName)
client := s.GetClient()
handlerErr := handler(client, "")
if handlerErr != nil {
failF("%v: %v", testName, handlerErr)
}
if handlerErr == nil {
passF(testName)
}
return handlerErr
}
type authConfig struct {
testName string
path string
method string
body []byte
service string
date time.Time
}
func authHandler(s *S3Conf, cfg *authConfig, handler func(req *http.Request) error) error {
runF(cfg.testName)
req, err := createSignedReq(cfg.method, s.endpoint, cfg.path, s.awsID, s.awsSecret, cfg.service, s.awsRegion, cfg.body, cfg.date, nil)
if err != nil {
failF("%v: %v", cfg.testName, err)
return fmt.Errorf("%v: %w", cfg.testName, err)
}
err = handler(req)
if err != nil {
failF("%v: %v", cfg.testName, err)
return fmt.Errorf("%v: %w", cfg.testName, err)
}
passF(cfg.testName)
return nil
}
func presignedAuthHandler(s *S3Conf, testName string, handler func(client *s3.PresignClient, bucket string) error) error {
runF(testName)
bucket := getBucketName()
err := setup(s, bucket)
if err != nil {
failF("%v: %v", testName, err)
return fmt.Errorf("%v: %w", testName, err)
}
clt := s.GetPresignClient()
err = handler(clt, bucket)
if err != nil {
failF("%v: %v", testName, err)
return fmt.Errorf("%v: %w", testName, err)
}
err = teardown(s, bucket)
if err != nil {
failF("%v: %v", testName, err)
return fmt.Errorf("%v: %w", testName, err)
}
passF(testName)
return nil
}
func createSignedReq(method, endpoint, path, access, secret, service, region string, body []byte, date time.Time, headers map[string]string) (*http.Request, error) {
req, err := http.NewRequest(method, fmt.Sprintf("%v/%v", endpoint, path), bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("failed to send the request: %w", err)
}
signer := v4.NewSigner()
hashedPayload := sha256.Sum256(body)
hexPayload := hex.EncodeToString(hashedPayload[:])
req.Header.Set("X-Amz-Content-Sha256", hexPayload)
for key, val := range headers {
req.Header.Add(key, val)
}
signErr := signer.SignHTTP(req.Context(), aws.Credentials{AccessKeyID: access, SecretAccessKey: secret}, req, hexPayload, service, region, date)
if signErr != nil {
return nil, fmt.Errorf("failed to sign the request: %w", signErr)
}
return req, nil
}
func checkHTTPResponseApiErr(resp *http.Response, apiErr s3err.APIError) error {
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
resp.Body.Close()
var errResp s3err.APIErrorResponse
err = xml.Unmarshal(body, &errResp)
if err != nil {
return err
}
if resp.StatusCode != apiErr.HTTPStatusCode {
return fmt.Errorf("expected response status code to be %v, instead got %v", apiErr.HTTPStatusCode, resp.StatusCode)
}
if errResp.Code != apiErr.Code {
return fmt.Errorf("expected error code to be %v, instead got %v", apiErr.Code, errResp.Code)
}
if errResp.Message != apiErr.Description {
return fmt.Errorf("expected error message to be %v, instead got %v", apiErr.Description, errResp.Message)
}
return nil
}
func checkApiErr(err error, apiErr s3err.APIError) error {
if err == nil {
return fmt.Errorf("expected %v, instead got nil", apiErr.Code)
}
var ae smithy.APIError
if errors.As(err, &ae) {
if ae.ErrorCode() != apiErr.Code {
return fmt.Errorf("expected error code to be %v, instead got %v", apiErr.Code, ae.ErrorCode())
}
if ae.ErrorMessage() != apiErr.Description {
return fmt.Errorf("expected error message to be %v, instead got %v", apiErr.Description, ae.ErrorMessage())
}
return nil
}
return fmt.Errorf("expected aws api error, instead got: %w", err)
}
func checkSdkApiErr(err error, code string) error {
var ae smithy.APIError
if errors.As(err, &ae) {
if ae.ErrorCode() != code {
return fmt.Errorf("expected %v, instead got %v", code, ae.ErrorCode())
}
return nil
}
return err
}
func putObjects(client *s3.Client, objs []string, bucket string) ([]types.Object, error) {
var contents []types.Object
var size int64
for _, key := range objs {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := client.PutObject(ctx, &s3.PutObjectInput{
Key: &key,
Bucket: &bucket,
})
cancel()
if err != nil {
return nil, err
}
k := key
contents = append(contents, types.Object{
Key: &k,
ETag: res.ETag,
StorageClass: types.ObjectStorageClassStandard,
Size: &size,
})
}
sort.SliceStable(contents, func(i, j int) bool {
return *contents[i].Key < *contents[j].Key
})
return contents, nil
}
func listObjects(client *s3.Client, bucket, prefix, delimiter string, maxKeys int32) ([]types.Object, []types.CommonPrefix, error) {
var contents []types.Object
var commonPrefixes []types.CommonPrefix
var continuationToken *string
for {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
Bucket: &bucket,
ContinuationToken: continuationToken,
Prefix: &prefix,
Delimiter: &delimiter,
MaxKeys: &maxKeys,
})
cancel()
if err != nil {
return nil, nil, err
}
contents = append(contents, res.Contents...)
commonPrefixes = append(commonPrefixes, res.CommonPrefixes...)
continuationToken = res.NextContinuationToken
if !*res.IsTruncated {
break
}
}
return contents, commonPrefixes, nil
}
func hasObjNames(objs []types.Object, names []string) bool {
if len(objs) != len(names) {
return false
}
for _, obj := range objs {
if slices.Contains(names, *obj.Key) {
continue
}
return false
}
return true
}
func hasPrefixName(prefixes []types.CommonPrefix, names []string) bool {
if len(prefixes) != len(names) {
return false
}
for _, prefix := range prefixes {
if slices.Contains(names, *prefix.Prefix) {
continue
}
return false
}
return true
}
type putObjectOutput struct {
csum [32]byte
data []byte
res *s3.PutObjectOutput
}
func putObjectWithData(lgth int64, input *s3.PutObjectInput, client *s3.Client) (*putObjectOutput, error) {
var csum [32]byte
var data []byte
if input.Body == nil {
data = make([]byte, lgth)
rand.Read(data)
csum = sha256.Sum256(data)
r := bytes.NewReader(data)
input.Body = r
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := client.PutObject(ctx, input)
cancel()
if err != nil {
return nil, err
}
return &putObjectOutput{
csum: csum,
data: data,
res: res,
}, nil
}
type mpCfg struct {
checksumAlgorithm types.ChecksumAlgorithm
checksumType types.ChecksumType
}
type mpOpt func(*mpCfg)
func withChecksum(algo types.ChecksumAlgorithm) mpOpt {
return func(mc *mpCfg) { mc.checksumAlgorithm = algo }
}
func withChecksumType(t types.ChecksumType) mpOpt {
return func(mc *mpCfg) { mc.checksumType = t }
}
func createMp(s3client *s3.Client, bucket, key string, opts ...mpOpt) (*s3.CreateMultipartUploadOutput, error) {
cfg := new(mpCfg)
for _, opt := range opts {
opt(cfg)
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
Bucket: &bucket,
Key: &key,
ChecksumAlgorithm: cfg.checksumAlgorithm,
ChecksumType: cfg.checksumType,
})
cancel()
return out, err
}
func isSameData(a, b []byte) bool {
return bytes.Equal(a, b)
}
func compareMultipartUploads(list1, list2 []types.MultipartUpload) bool {
if len(list1) != len(list2) {
return false
}
for i, item := range list1 {
if *item.Key != *list2[i].Key {
return false
}
if *item.UploadId != *list2[i].UploadId {
return false
}
if item.StorageClass != list2[i].StorageClass {
return false
}
if item.ChecksumAlgorithm != list2[i].ChecksumAlgorithm {
return false
}
if item.ChecksumType != list2[i].ChecksumType {
return false
}
}
return true
}
func compareParts(parts1, parts2 []types.Part) bool {
if len(parts1) != len(parts2) {
fmt.Printf("list length are not equal: %v != %v\n", len(parts1), len(parts2))
return false
}
for i, prt := range parts1 {
if *prt.PartNumber != *parts2[i].PartNumber {
fmt.Printf("partNumbers are not equal, %v != %v\n", *prt.PartNumber, *parts2[i].PartNumber)
return false
}
if *prt.ETag != *parts2[i].ETag {
fmt.Printf("etags are not equal, %v != %v\n", *prt.ETag, *parts2[i].ETag)
return false
}
if *prt.Size != *parts2[i].Size {
fmt.Printf("sizes are not equal, %v != %v\n", *prt.Size, *parts2[i].Size)
return false
}
if prt.ChecksumCRC32 != nil {
if *prt.ChecksumCRC32 != getString(parts2[i].ChecksumCRC32) {
fmt.Printf("crc32 checksums are not equal, %v != %v\n", *prt.ChecksumCRC32, getString(parts2[i].ChecksumCRC32))
return false
}
}
if prt.ChecksumCRC32C != nil {
if *prt.ChecksumCRC32C != getString(parts2[i].ChecksumCRC32C) {
fmt.Printf("crc32c checksums are not equal, %v != %v\n", *prt.ChecksumCRC32C, getString(parts2[i].ChecksumCRC32C))
return false
}
}
if prt.ChecksumSHA1 != nil {
if *prt.ChecksumSHA1 != getString(parts2[i].ChecksumSHA1) {
fmt.Printf("sha1 checksums are not equal, %v != %v\n", *prt.ChecksumSHA1, getString(parts2[i].ChecksumSHA1))
return false
}
}
if prt.ChecksumSHA256 != nil {
if *prt.ChecksumSHA256 != getString(parts2[i].ChecksumSHA256) {
fmt.Printf("sha256 checksums are not equal, %v != %v\n", *prt.ChecksumSHA256, getString(parts2[i].ChecksumSHA256))
return false
}
}
if prt.ChecksumCRC64NVME != nil {
if *prt.ChecksumCRC64NVME != getString(parts2[i].ChecksumCRC64NVME) {
fmt.Printf("crc64nvme checksums are not equal, %v != %v\n", *prt.ChecksumCRC64NVME, getString(parts2[i].ChecksumCRC64NVME))
return false
}
}
}
return true
}
func areTagsSame(tags1, tags2 []types.Tag) bool {
if len(tags1) != len(tags2) {
return false
}
for _, tag := range tags1 {
if !containsTag(tag, tags2) {
return false
}
}
return true
}
func containsTag(tag types.Tag, list []types.Tag) bool {
for _, item := range list {
if *item.Key == *tag.Key && *item.Value == *tag.Value {
return true
}
}
return false
}
func compareGrants(grts1, grts2 []types.Grant) bool {
if len(grts1) != len(grts2) {
return false
}
for i, grt := range grts1 {
if grt.Permission != grts2[i].Permission {
return false
}
if *grt.Grantee.ID != *grts2[i].Grantee.ID {
return false
}
if grt.Grantee.Type != grts2[i].Grantee.Type {
return false
}
}
return true
}
func execCommand(args ...string) ([]byte, error) {
cmd := exec.Command("./versitygw", args...)
return cmd.CombinedOutput()
}
func getString(str *string) string {
if str == nil {
return ""
}
return *str
}
func getPtr[T any](str T) *T {
return &str
}
// mp1 needs to be the response from the server
// mp2 needs to be the expected values
// The keys from the server are always converted to lowercase
func areMapsSame(mp1, mp2 map[string]string) bool {
if len(mp1) != len(mp2) {
return false
}
for key, val := range mp2 {
if mp1[strings.ToLower(key)] != val {
return false
}
}
return true
}
func compareBuckets(list1 []types.Bucket, list2 []types.Bucket) bool {
if len(list1) != len(list2) {
return false
}
for i, elem := range list1 {
if *elem.Name != *list2[i].Name {
fmt.Printf("bucket names are not equal: %s != %s\n", *elem.Name, *list2[i].Name)
return false
}
if *elem.BucketRegion != *list2[i].BucketRegion {
fmt.Printf("bucket regions are not equal: %s != %s\n", *elem.BucketRegion, *list2[i].BucketRegion)
return false
}
}
return true
}
func compareObjects(list1, list2 []types.Object) bool {
if len(list1) != len(list2) {
fmt.Println("list lengths are not equal")
return false
}
for i, obj := range list1 {
if *obj.Key != *list2[i].Key {
fmt.Printf("keys are not equal: %q != %q\n", *obj.Key, *list2[i].Key)
return false
}
if *obj.ETag != *list2[i].ETag {
fmt.Printf("etags are not equal: (%q %q) %q != %q\n",
*obj.Key, *list2[i].Key, *obj.ETag, *list2[i].ETag)
return false
}
if *obj.Size != *list2[i].Size {
fmt.Printf("sizes are not equal: (%q %q) %v != %v\n",
*obj.Key, *list2[i].Key, *obj.Size, *list2[i].Size)
return false
}
if obj.StorageClass != list2[i].StorageClass {
fmt.Printf("storage classes are not equal: (%q %q) %v != %v\n",
*obj.Key, *list2[i].Key, obj.StorageClass, list2[i].StorageClass)
return false
}
if len(obj.ChecksumAlgorithm) != 0 {
if obj.ChecksumAlgorithm[0] != list2[i].ChecksumAlgorithm[0] {
fmt.Printf("checksum algorithms are not equal: (%q %q) %v != %v\n",
*obj.Key, *list2[i].Key, obj.ChecksumAlgorithm[0], list2[i].ChecksumAlgorithm[0])
return false
}
}
if obj.ChecksumType != "" {
if obj.ChecksumType[0] != list2[i].ChecksumType[0] {
fmt.Printf("checksum types are not equal: (%q %q) %v != %v\n",
*obj.Key, *list2[i].Key, obj.ChecksumType, list2[i].ChecksumType)
return false
}
}
if obj.Owner != nil {
if *obj.Owner.ID != *list2[i].Owner.ID {
fmt.Printf("object owner IDs not equal: (%q %q) %v != %v\n",
*obj.Key, *list2[i].Key, *obj.Owner.ID, *list2[i].Owner.ID)
}
}
}
return true
}
func comparePrefixes(list1 []string, list2 []types.CommonPrefix) bool {
if len(list1) != len(list2) {
return false
}
elementMap := make(map[string]bool)
for _, elem := range list1 {
elementMap[elem] = true
}
for _, elem := range list2 {
if _, found := elementMap[*elem.Prefix]; !found {
return false
}
}
return true
}
func compareDelObjects(list1, list2 []types.DeletedObject) bool {
if len(list1) != len(list2) {
return false
}
for i, obj := range list1 {
if *obj.Key != *list2[i].Key {
return false
}
if obj.VersionId != nil {
if list2[i].VersionId == nil {
return false
}
if *obj.VersionId != *list2[i].VersionId {
return false
}
}
if obj.DeleteMarkerVersionId != nil {
if list2[i].DeleteMarkerVersionId == nil {
return false
}
if *obj.DeleteMarkerVersionId != *list2[i].DeleteMarkerVersionId {
return false
}
}
if obj.DeleteMarker != nil {
if list2[i].DeleteMarker == nil {
return false
}
if *obj.DeleteMarker != *list2[i].DeleteMarker {
return false
}
}
}
return true
}
func uploadParts(client *s3.Client, size, partCount int64, bucket, key, uploadId string, opts ...mpOpt) (parts []types.Part, csum string, err error) {
partSize := size / partCount
var hash hash.Hash
cfg := new(mpCfg)
for _, opt := range opts {
opt(cfg)
}
switch cfg.checksumAlgorithm {
case types.ChecksumAlgorithmCrc32:
hash = crc32.NewIEEE()
case types.ChecksumAlgorithmCrc32c:
hash = crc32.New(crc32.MakeTable(crc32.Castagnoli))
case types.ChecksumAlgorithmSha1:
hash = sha1.New()
case types.ChecksumAlgorithmSha256:
hash = sha256.New()
case types.ChecksumAlgorithmCrc64nvme:
hash = crc64.New(crc64.MakeTable(bits.Reverse64(0xad93d23594c93659)))
default:
hash = sha256.New()
}
for partNumber := int64(1); partNumber <= partCount; partNumber++ {
partStart := (partNumber - 1) * partSize
partEnd := partStart + partSize - 1
if partEnd > size-1 {
partEnd = size - 1
}
partBuffer := make([]byte, partEnd-partStart+1)
rand.Read(partBuffer)
hash.Write(partBuffer)
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
pn := int32(partNumber)
out, err := client.UploadPart(ctx, &s3.UploadPartInput{
Bucket: &bucket,
Key: &key,
UploadId: &uploadId,
Body: bytes.NewReader(partBuffer),
PartNumber: &pn,
ChecksumAlgorithm: cfg.checksumAlgorithm,
})
cancel()
if err != nil {
return parts, "", err
}
part := types.Part{
ETag: out.ETag,
PartNumber: &pn,
Size: &partSize,
}
switch cfg.checksumAlgorithm {
case types.ChecksumAlgorithmCrc32:
part.ChecksumCRC32 = out.ChecksumCRC32
case types.ChecksumAlgorithmCrc32c:
part.ChecksumCRC32C = out.ChecksumCRC32C
case types.ChecksumAlgorithmSha1:
part.ChecksumSHA1 = out.ChecksumSHA1
case types.ChecksumAlgorithmSha256:
part.ChecksumSHA256 = out.ChecksumSHA256
case types.ChecksumAlgorithmCrc64nvme:
part.ChecksumCRC64NVME = out.ChecksumCRC64NVME
}
parts = append(parts, part)
}
sum := hash.Sum(nil)
if cfg.checksumAlgorithm == "" {
csum = hex.EncodeToString(sum[:])
} else {
csum = base64.StdEncoding.EncodeToString(sum[:])
}
return parts, csum, err
}
func createUsers(s *S3Conf, users []user) error {
for _, usr := range users {
err := deleteUser(s, usr.access)
if err != nil {
return err
}
out, err := execCommand(s.getAdminCommand("-a", s.awsID, "-s", s.awsSecret, "-er", s.endpoint, "create-user", "-a", usr.access, "-s", usr.secret, "-r", usr.role)...)
if err != nil {
return err
}
if strings.Contains(string(out), adminErrorPrefix) {
return fmt.Errorf("failed to create user account: %s", out)
}
}
return nil
}
func deleteUser(s *S3Conf, access string) error {
out, err := execCommand(s.getAdminCommand("-a", s.awsID, "-s", s.awsSecret, "-er", s.endpoint, "delete-user", "-a", access)...)
if err != nil {
return err
}
if strings.Contains(string(out), adminErrorPrefix) {
return fmt.Errorf("failed to delete the user account, %s", out)
}
return nil
}
func changeBucketsOwner(s *S3Conf, buckets []string, owner string) error {
for _, bucket := range buckets {
out, err := execCommand(s.getAdminCommand("-a", s.awsID, "-s", s.awsSecret, "-er", s.endpoint, "change-bucket-owner", "-b", bucket, "-o", owner)...)
if err != nil {
return err
}
if strings.Contains(string(out), adminErrorPrefix) {
return fmt.Errorf("failed to change the bucket owner: %s", out)
}
}
return nil
}
func listBuckets(s *S3Conf) error {
out, err := execCommand(s.getAdminCommand("-a", s.awsID, "-s", s.awsSecret, "-er", s.endpoint, "list-buckets")...)
if err != nil {
return err
}
if strings.Contains(string(out), adminErrorPrefix) {
return fmt.Errorf("failed to list buckets, %s", out)
}
return nil
}
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
func genRandString(length int) string {
source := rnd.NewSource(time.Now().UnixNano())
random := rnd.New(source)
result := make([]byte, length)
for i := range result {
result[i] = charset[random.Intn(len(charset))]
}
return string(result)
}
const (
credAccess int = iota
credDate
credRegion
credService
credTerminator
)
func changeAuthCred(uri, newVal string, index int) (string, error) {
urlParsed, err := url.Parse(uri)
if err != nil {
return "", err
}
queries := urlParsed.Query()
creds := strings.Split(queries.Get("X-Amz-Credential"), "/")
creds[index] = newVal
queries.Set("X-Amz-Credential", strings.Join(creds, "/"))
urlParsed.RawQuery = queries.Encode()
return urlParsed.String(), nil
}
func genPolicyDoc(effect, principal, action, resource string) string {
jsonTemplate := `{
"Statement": [
{
"Effect": "%s",
"Principal": %s,
"Action": %s,
"Resource": %s
}
]
}
`
return fmt.Sprintf(jsonTemplate, effect, principal, action, resource)
}
type policyType string
const (
policyTypeBucket policyType = "bucket"
policyTypeObject policyType = "object"
policyTypeFull policyType = "full"
)
func grantPublicBucketPolicy(client *s3.Client, bucket string, tp policyType) error {
var doc string
switch tp {
case policyTypeBucket:
doc = genPolicyDoc("Allow", `"*"`, `"s3:*"`, fmt.Sprintf(`"arn:aws:s3:::%s"`, bucket))
case policyTypeObject:
doc = genPolicyDoc("Allow", `"*"`, `"s3:*"`, fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket))
case policyTypeFull:
template := `{
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:*",
"Resource": "arn:aws:s3:::%s"
},
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:*",
"Resource": "arn:aws:s3:::%s/*"
}
]
}
`
doc = fmt.Sprintf(template, bucket, bucket)
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: &bucket,
Policy: &doc,
})
cancel()
return err
}
func getMalformedPolicyError(msg string) s3err.APIError {
return s3err.APIError{
Code: "MalformedPolicy",
Description: msg,
HTTPStatusCode: http.StatusBadRequest,
}
}
// if true enables, otherwise disables
func changeBucketObjectLockStatus(client *s3.Client, bucket string, status bool) error {
cfg := types.ObjectLockConfiguration{}
if status {
cfg.ObjectLockEnabled = types.ObjectLockEnabledEnabled
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{
Bucket: &bucket,
ObjectLockConfiguration: &cfg,
})
cancel()
if err != nil {
return err
}
return nil
}
func putBucketVersioningStatus(client *s3.Client, bucket string, status types.BucketVersioningStatus) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := client.PutBucketVersioning(ctx, &s3.PutBucketVersioningInput{
Bucket: &bucket,
VersioningConfiguration: &types.VersioningConfiguration{
Status: status,
},
})
cancel()
return err
}
func checkWORMProtection(client *s3.Client, bucket, object string) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := client.PutObject(ctx, &s3.PutObjectInput{
Bucket: &bucket,
Key: &object,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLocked)); err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &bucket,
Key: &object,
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLocked)); err != nil {
return err
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
_, err = client.DeleteObjects(ctx, &s3.DeleteObjectsInput{
Bucket: &bucket,
Delete: &types.Delete{
Objects: []types.ObjectIdentifier{
{
Key: &object,
},
},
},
})
cancel()
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLocked)); err != nil {
return err
}
return nil
}
func objStrings(objs []types.Object) []string {
objStrs := make([]string, len(objs))
for i, obj := range objs {
objStrs[i] = *obj.Key
}
return objStrs
}
func pfxStrings(pfxs []types.CommonPrefix) []string {
pfxStrs := make([]string, len(pfxs))
for i, pfx := range pfxs {
pfxStrs[i] = *pfx.Prefix
}
return pfxStrs
}
type versCfg struct {
checksumAlgorithm types.ChecksumAlgorithm
}
type versOpt func(*versCfg)
func withChecksumAlgo(algo types.ChecksumAlgorithm) versOpt {
return func(vc *versCfg) { vc.checksumAlgorithm = algo }
}
func createObjVersions(client *s3.Client, bucket, object string, count int, opts ...versOpt) ([]types.ObjectVersion, error) {
cfg := new(versCfg)
for _, o := range opts {
o(cfg)
}
versions := []types.ObjectVersion{}
for i := range count {
rNumber, err := rand.Int(rand.Reader, big.NewInt(100000))
dataLength := rNumber.Int64()
if err != nil {
return nil, err
}
r, err := putObjectWithData(dataLength, &s3.PutObjectInput{
Bucket: &bucket,
Key: &object,
}, client)
if err != nil {
return nil, err
}
isLatest := i == count-1
version := types.ObjectVersion{
ETag: r.res.ETag,
IsLatest: &isLatest,
Key: &object,
Size: &dataLength,
VersionId: r.res.VersionId,
StorageClass: types.ObjectVersionStorageClassStandard,
ChecksumType: r.res.ChecksumType,
}
switch {
case r.res.ChecksumCRC32 != nil:
version.ChecksumAlgorithm = []types.ChecksumAlgorithm{
types.ChecksumAlgorithmCrc32,
}
case r.res.ChecksumCRC32C != nil:
version.ChecksumAlgorithm = []types.ChecksumAlgorithm{
types.ChecksumAlgorithmCrc32c,
}
case r.res.ChecksumCRC64NVME != nil:
version.ChecksumAlgorithm = []types.ChecksumAlgorithm{
types.ChecksumAlgorithmCrc64nvme,
}
case r.res.ChecksumSHA1 != nil:
version.ChecksumAlgorithm = []types.ChecksumAlgorithm{
types.ChecksumAlgorithmSha1,
}
case r.res.ChecksumSHA256 != nil:
version.ChecksumAlgorithm = []types.ChecksumAlgorithm{
types.ChecksumAlgorithmSha256,
}
}
versions = append(versions, version)
}
versions = reverseSlice(versions)
return versions, nil
}
// ReverseSlice reverses a slice of any type
func reverseSlice[T any](s []T) []T {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
return s
}
func compareVersions(v1, v2 []types.ObjectVersion) bool {
if len(v1) != len(v2) {
return false
}
for i, version := range v1 {
if version.Key == nil || v2[i].Key == nil {
return false
}
if *version.Key != *v2[i].Key {
return false
}
if version.VersionId == nil || v2[i].VersionId == nil {
return false
}
if *version.VersionId != *v2[i].VersionId {
return false
}
if version.IsLatest == nil || v2[i].IsLatest == nil {
return false
}
if *version.IsLatest != *v2[i].IsLatest {
return false
}
if version.Size == nil || v2[i].Size == nil {
return false
}
if *version.Size != *v2[i].Size {
return false
}
if version.ETag == nil || v2[i].ETag == nil {
return false
}
if *version.ETag != *v2[i].ETag {
return false
}
if version.StorageClass != v2[i].StorageClass {
return false
}
if version.ChecksumType != "" {
if version.ChecksumType != v2[i].ChecksumType {
return false
}
}
if len(version.ChecksumAlgorithm) != 0 {
if len(v2[i].ChecksumAlgorithm) == 0 {
return false
}
if version.ChecksumAlgorithm[0] != v2[i].ChecksumAlgorithm[0] {
return false
}
}
}
return true
}
func compareDelMarkers(d1, d2 []types.DeleteMarkerEntry) bool {
if len(d1) != len(d2) {
return false
}
for i, dEntry := range d1 {
if dEntry.Key == nil || d2[i].Key == nil {
return false
}
if *dEntry.Key != *d2[i].Key {
return false
}
if dEntry.IsLatest == nil || d2[i].IsLatest == nil {
return false
}
if *dEntry.IsLatest != *d2[i].IsLatest {
return false
}
if dEntry.VersionId == nil || d2[i].VersionId == nil {
return false
}
if *dEntry.VersionId != *d2[i].VersionId {
return false
}
}
return true
}
type ObjectMetaProps struct {
ContentLength int64
ContentType string
ContentEncoding string
ContentDisposition string
ContentLanguage string
CacheControl string
ExpiresString string
Metadata map[string]string
}
func checkObjectMetaProps(client *s3.Client, bucket, object string, o ObjectMetaProps) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &object,
})
cancel()
if err != nil {
return err
}
if o.Metadata != nil {
if !areMapsSame(out.Metadata, o.Metadata) {
return fmt.Errorf("expected the object metadata to be %v, instead got %v", o.Metadata, out.Metadata)
}
}
if out.ContentLength == nil {
return fmt.Errorf("expected Content-Length %v, instead got nil", o.ContentLength)
}
if *out.ContentLength != o.ContentLength {
return fmt.Errorf("expected Content-Length %v, instead got %v", o.ContentLength, *out.ContentLength)
}
if o.ContentType != "" && getString(out.ContentType) != o.ContentType {
return fmt.Errorf("expected Content-Type %v, instead got %v", o.ContentType, getString(out.ContentType))
}
if o.ContentDisposition != "" && getString(out.ContentDisposition) != o.ContentDisposition {
return fmt.Errorf("expected Content-Disposition %v, instead got %v", o.ContentDisposition, getString(out.ContentDisposition))
}
if o.ContentEncoding != "" && getString(out.ContentEncoding) != o.ContentEncoding {
return fmt.Errorf("expected Content-Encoding %v, instead got %v", o.ContentEncoding, getString(out.ContentEncoding))
}
if o.ContentLanguage != "" && getString(out.ContentLanguage) != o.ContentLanguage {
return fmt.Errorf("expected Content-Language %v, instead got %v", o.ContentLanguage, getString(out.ContentLanguage))
}
if o.CacheControl != "" && getString(out.CacheControl) != o.CacheControl {
return fmt.Errorf("expected Cache-Control %v, instead got %v", o.CacheControl, getString(out.CacheControl))
}
if o.ExpiresString != "" && getString(out.ExpiresString) != o.ExpiresString {
return fmt.Errorf("expected Expires %v, instead got %v", o.ExpiresString, getString(out.ExpiresString))
}
if out.StorageClass != types.StorageClassStandard {
return fmt.Errorf("expected the storage class to be %v, instead got %v", types.StorageClassStandard, out.StorageClass)
}
return nil
}
func getBoolPtr(b bool) *bool {
return &b
}
type PublicBucketTestCase struct {
Action string
Call func(ctx context.Context) error
ExpectedErr error
}
// randomizeCase randomizes the provided string latters case
func randomizeCase(s string) string {
var b strings.Builder
for _, ch := range s {
if rnd.Intn(2) == 0 {
b.WriteRune(unicode.ToLower(ch))
} else {
b.WriteRune(unicode.ToUpper(ch))
}
}
return b.String()
}
func headObject_zero_len_with_range_helper(testName, obj string, s *S3Conf) error {
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
objLength := int64(0)
_, err := putObjectWithData(objLength, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
testRange := func(rg, contentRange string, cLength int64, expectErr bool) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &bucket,
Key: &obj,
Range: &rg,
})
cancel()
if err == nil && expectErr {
return fmt.Errorf("%v: expected err 'RequestedRangeNotSatisfiable' error, instead got nil", rg)
}
if err != nil {
if !expectErr {
return err
}
var ae smithy.APIError
if errors.As(err, &ae) {
if ae.ErrorCode() != "RequestedRangeNotSatisfiable" {
return fmt.Errorf("%v: expected RequestedRangeNotSatisfiable, instead got %v", rg, ae.ErrorCode())
}
if ae.ErrorMessage() != "Requested Range Not Satisfiable" {
return fmt.Errorf("%v: expected the error message to be 'Requested Range Not Satisfiable', instead got %v", rg, ae.ErrorMessage())
}
return nil
}
return fmt.Errorf("%v: invalid error got %w", rg, err)
}
if getString(res.AcceptRanges) != "bytes" {
return fmt.Errorf("%v: expected accept ranges to be 'bytes', instead got %v", rg, getString(res.AcceptRanges))
}
if res.ContentLength == nil {
return fmt.Errorf("%v: expected non nil content-length", rg)
}
if *res.ContentLength != cLength {
return fmt.Errorf("%v: expected content-length to be %v, instead got %v", rg, cLength, *res.ContentLength)
}
if getString(res.ContentRange) != contentRange {
return fmt.Errorf("%v: expected content-range to be %v, instead got %v", rg, contentRange, getString(res.ContentRange))
}
return nil
}
// Reference server expectations for a 0-byte object.
for _, el := range []struct {
objRange string
contentRange string
contentLength int64
expectedErr bool
}{
{"bytes=abc", "", objLength, false},
{"bytes=a-z", "", objLength, false},
{"bytes=,", "", objLength, false},
{"bytes=0-0,1-2", "", objLength, false},
{"foo=0-1", "", objLength, false},
{"bytes=--1", "", objLength, false},
{"bytes=0--1", "", objLength, false},
{"bytes= -1", "", objLength, false},
{"bytes=0 -1", "", objLength, false},
{"bytes=-1", "", objLength, false}, // reference server returns no error, empty Content-Range
{"bytes=00-01", "", objLength, true}, // RequestedRangeNotSatisfiable
{"bytes=-0", "", 0, true},
{"bytes=0-0", "", 0, true},
{"bytes=0-", "", 0, true},
} {
if err := testRange(el.objRange, el.contentRange, el.contentLength, el.expectedErr); err != nil {
return err
}
}
return nil
})
}
func getObject_zero_len_with_range_helper(testName, obj string, s *S3Conf) error {
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
objLength := int64(0)
res, err := putObjectWithData(objLength, &s3.PutObjectInput{
Bucket: &bucket,
Key: &obj,
}, s3client)
if err != nil {
return err
}
testGetObjectRange := func(rng, contentRange string, cLength int64, expData []byte, expErr error) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
defer cancel()
out, err := s3client.GetObject(ctx, &s3.GetObjectInput{
Bucket: &bucket,
Key: &obj,
Range: &rng,
})
if err == nil && expErr != nil {
return fmt.Errorf("%v: expected err %v, instead got nil", rng, expErr)
}
if err != nil {
if expErr == nil {
return err
}
parsedErr, ok := expErr.(s3err.APIError)
if !ok {
return fmt.Errorf("invalid error type provided, expected s3err.APIError")
}
return checkApiErr(err, parsedErr)
}
if out.ContentLength == nil {
return fmt.Errorf("%v: expected non nil content-length", rng)
}
if *out.ContentLength != cLength {
return fmt.Errorf("%v: expected content-length to be %v, instead got %v", rng, cLength, *out.ContentLength)
}
if getString(out.AcceptRanges) != "bytes" {
return fmt.Errorf("%v: expected accept-ranges to be 'bytes', instead got %v", rng, getString(out.AcceptRanges))
}
if getString(out.ContentRange) != contentRange {
return fmt.Errorf("%v: expected content-range to be %v, instead got %v", rng, contentRange, getString(out.ContentRange))
}
data, err := io.ReadAll(out.Body)
if err != nil {
return fmt.Errorf("%v: read object data: %w", rng, err)
}
out.Body.Close()
if !isSameData(data, expData) {
return fmt.Errorf("%v: incorrect data retrieved", rng)
}
return nil
}
for _, el := range []struct {
rng string
contentRange string
cLength int64
expData []byte
expErr error
}{
{"bytes=abc", "", objLength, res.data, nil},
{"bytes=a-z", "", objLength, res.data, nil},
{"bytes=,", "", objLength, res.data, nil},
{"bytes=0-0,1-2", "", objLength, res.data, nil},
{"foo=0-1", "", objLength, res.data, nil},
{"bytes=--1", "", objLength, res.data, nil},
{"bytes=0--1", "", objLength, res.data, nil},
{"bytes= -1", "", objLength, res.data, nil},
{"bytes=0 -1", "", objLength, res.data, nil},
{"bytes=-1", "", objLength, res.data, nil},
// error (RequestedRangeNotSatisfiable)
{"bytes=00-01", "", objLength, nil, s3err.GetAPIError(s3err.ErrInvalidRange)},
{"bytes=-0", "", 0, nil, s3err.GetAPIError(s3err.ErrInvalidRange)},
{"bytes=0-0", "", 0, nil, s3err.GetAPIError(s3err.ErrInvalidRange)},
{"bytes=0-", "", 0, nil, s3err.GetAPIError(s3err.ErrInvalidRange)},
} {
if err := testGetObjectRange(el.rng, el.contentRange, el.cLength, el.expData, el.expErr); err != nil {
return err
}
}
return nil
})
}
func getInt32(ptr *int32) int32 {
if ptr == nil {
return 0
}
return *ptr
}
func putBucketCors(client *s3.Client, input *s3.PutBucketCorsInput) error {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
_, err := client.PutBucketCors(ctx, input)
cancel()
return err
}
func compareCorsConfig(expected, got []types.CORSRule) error {
if expected == nil && got == nil {
return nil
}
if got == nil {
return errors.New("nil CORS config")
}
if len(expected) != len(got) {
return fmt.Errorf("expected CORS rules length to be %v, instead got %v", len(expected), len(got))
}
for i, r := range expected {
rule := got[i]
if !slices.Equal(r.AllowedOrigins, rule.AllowedOrigins) {
return fmt.Errorf("expected the allowed origins to be %v, instead got %v", r.AllowedOrigins, rule.AllowedOrigins)
}
if !slices.Equal(r.AllowedMethods, rule.AllowedMethods) {
return fmt.Errorf("expected the allowed methods to be %v, instead got %v", r.AllowedMethods, rule.AllowedMethods)
}
if !slices.Equal(r.AllowedHeaders, rule.AllowedHeaders) {
return fmt.Errorf("expected the allowed headers to be %v, instead got %v", r.AllowedHeaders, rule.AllowedHeaders)
}
if !slices.Equal(r.ExposeHeaders, rule.ExposeHeaders) {
return fmt.Errorf("expected the allowed origins to be %v, instead got %v", r.ExposeHeaders, rule.ExposeHeaders)
}
if getInt32(r.MaxAgeSeconds) != getInt32(rule.MaxAgeSeconds) {
return fmt.Errorf("expected the max age seconds to be %v, instead got %v", getInt32(r.MaxAgeSeconds), getInt32(rule.MaxAgeSeconds))
}
if getString(r.ID) != getString(rule.ID) {
return fmt.Errorf("expected ID to be %v, instead got %v", getString(r.ID), getString(rule.ID))
}
}
return nil
}
type PreflightResult struct {
Origin string
Methods string
AllowHeaders string
ExposeHeaders string
MaxAge string
AllowCredentials string
Vary string
err error
}
func extractCORSHeaders(resp *http.Response) (*PreflightResult, error) {
if resp.StatusCode >= 400 {
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read response body: %w", err)
}
var errResp smithy.GenericAPIError
err = xml.Unmarshal(body, &errResp)
if err != nil {
return nil, fmt.Errorf("unmarshal respone body: %w", err)
}
return &PreflightResult{
err: &errResp,
}, nil
}
return &PreflightResult{
Origin: resp.Header.Get("Access-Control-Allow-Origin"),
Methods: resp.Header.Get("Access-Control-Allow-Methods"),
ExposeHeaders: resp.Header.Get("Access-Control-Expose-Headers"),
MaxAge: resp.Header.Get("Access-Control-Max-Age"),
AllowHeaders: resp.Header.Get("Access-Control-Allow-Headers"),
AllowCredentials: resp.Header.Get("Access-Control-Allow-Credentials"),
Vary: resp.Header.Get("Vary"),
}, nil
}
func makeOPTIONSRequest(s *S3Conf, bucket, origin, method string, headers string) (*PreflightResult, error) {
req, err := http.NewRequest(http.MethodOptions, fmt.Sprintf("%s/%s/object", s.endpoint, bucket), nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
req.Header.Add("Origin", origin)
req.Header.Add("Access-Control-Request-Method", method)
req.Header.Add("Access-Control-Request-Headers", headers)
resp, err := s.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("send request: %w", err)
}
return extractCORSHeaders(resp)
}
func comparePreflightResult(expected, got *PreflightResult) error {
if expected == nil {
return fmt.Errorf("nil expected preflight request result")
}
if got == nil {
return fmt.Errorf("expected the preflights result to be %v, instead got nil", *expected)
}
if expected.err != nil {
if got.err == nil {
return fmt.Errorf("expected %w error, instaed got nil", expected.err)
}
apiErr, ok := expected.err.(s3err.APIError)
if !ok {
return fmt.Errorf("expected s3err.APIError, instead got %w", expected.err)
}
return checkApiErr(got.err, apiErr)
}
if got.err != nil {
return fmt.Errorf("expected no error, instaed got %w", got.err)
}
if expected.Origin != got.Origin {
return fmt.Errorf("expected the origin to be %v, instead got %v", expected.Origin, got.Origin)
}
if expected.Methods != got.Methods {
return fmt.Errorf("expected the allowed methods to be %v, instead got %v", expected.Methods, got.Methods)
}
if expected.AllowHeaders != got.AllowHeaders {
return fmt.Errorf("expected the allow headers to be %v, instead got %v", expected.AllowHeaders, got.AllowHeaders)
}
if expected.ExposeHeaders != got.ExposeHeaders {
return fmt.Errorf("expected the expose headers to be %v, instead got %v", expected.ExposeHeaders, got.ExposeHeaders)
}
if expected.MaxAge != got.MaxAge {
return fmt.Errorf("expected the max age to be %v, instead got %v", expected.MaxAge, got.MaxAge)
}
if expected.AllowCredentials != got.AllowCredentials {
return fmt.Errorf("expected the allow credentials to be %v, instead got %v", expected.AllowCredentials, got.AllowCredentials)
}
if expected.Vary != got.Vary {
return fmt.Errorf("expected the Vary header to be %v, instead got %v", expected.Vary, got.Vary)
}
return nil
}
func testOPTIONSEdnpoint(s *S3Conf, bucket, origin, method string, headers string, expected *PreflightResult) error {
result, err := makeOPTIONSRequest(s, bucket, origin, method, headers)
if err != nil {
return err
}
return comparePreflightResult(expected, result)
}
func calculateEtag(data []byte) (string, error) {
h := md5.New()
_, err := h.Write(data)
if err != nil {
return "", err
}
dataSum := h.Sum(nil)
return fmt.Sprintf("\"%s\"", hex.EncodeToString(dataSum[:])), nil
}
func sprintBuckets(buckets []types.Bucket) string {
if len(buckets) == 0 {
return ""
}
names := make([]string, len(buckets))
for i, bucket := range buckets {
names[i] = *bucket.Name
}
return strings.Join(names, ",")
}
func sprintPrefixes(cpfx []types.CommonPrefix) string {
if len(cpfx) == 0 {
return ""
}
names := make([]string, len(cpfx))
for i, pfx := range cpfx {
names[i] = *pfx.Prefix
}
return strings.Join(names, ",")
}
func sprintVersions(objects []types.ObjectVersion) string {
if len(objects) == 0 {
return ""
}
names := make([]string, len(objects))
for i, obj := range objects {
names[i] = fmt.Sprintf("%v/%v", *obj.Key, obj.VersionId)
}
return strings.Join(names, ",")
}