mirror of
https://github.com/versity/versitygw.git
synced 2026-08-18 21:26:27 +00:00
feat: support x-amz-website-redirect-location
Integrate x-amz-website-redirect-location across object metadata flows so uploads, copies, multipart creation, HEAD, and GET preserve and return redirect locations, and website hosting applies object-level redirects from the stored value.
This commit is contained in:
+63
-42
@@ -68,6 +68,7 @@ const (
|
||||
keyObjRetention key = "Objectretention"
|
||||
keyObjLegalHold key = "Objectlegalhold"
|
||||
keyExpires key = "Vgwexpires"
|
||||
keyWebsiteRedirect key = "Vgwwebsiteredirect"
|
||||
onameAttr key = "Objname"
|
||||
onameAttrLower key = "objname"
|
||||
metaTmpMultipartPrefix key = ".sgwtmp" + "/multipart"
|
||||
@@ -84,18 +85,19 @@ const (
|
||||
|
||||
func (key) Table() map[string]struct{} {
|
||||
return map[string]struct{}{
|
||||
"acl": {},
|
||||
"ownership": {},
|
||||
"tags": {},
|
||||
"policy": {},
|
||||
"bucketlock": {},
|
||||
"website": {},
|
||||
"objectretention": {},
|
||||
"vgwexpires": {},
|
||||
"objectlegalhold": {},
|
||||
"objname": {},
|
||||
".sgwtmp/multipart": {},
|
||||
"mpmetadata": {},
|
||||
"acl": {},
|
||||
"ownership": {},
|
||||
"tags": {},
|
||||
"policy": {},
|
||||
"bucketlock": {},
|
||||
"website": {},
|
||||
"objectretention": {},
|
||||
"vgwexpires": {},
|
||||
"vgwwebsiteredirect": {},
|
||||
"objectlegalhold": {},
|
||||
"objname": {},
|
||||
".sgwtmp/multipart": {},
|
||||
"mpmetadata": {},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,6 +374,15 @@ func (az *Azure) PutObject(ctx context.Context, po s3response.PutObjectInput) (s
|
||||
metadata[string(keyExpires)] = po.Expires
|
||||
}
|
||||
}
|
||||
if getString(po.WebsiteRedirectLocation) != "" {
|
||||
if metadata == nil {
|
||||
metadata = map[string]*string{
|
||||
string(keyWebsiteRedirect): po.WebsiteRedirectLocation,
|
||||
}
|
||||
} else {
|
||||
metadata[string(keyWebsiteRedirect)] = po.WebsiteRedirectLocation
|
||||
}
|
||||
}
|
||||
|
||||
opts := &blockblob.UploadStreamOptions{
|
||||
Metadata: metadata,
|
||||
@@ -571,22 +582,23 @@ func (az *Azure) GetObject(ctx context.Context, input *s3.GetObjectInput) (*s3.G
|
||||
}
|
||||
|
||||
return &s3.GetObjectOutput{
|
||||
AcceptRanges: backend.GetPtrFromString("bytes"),
|
||||
ContentLength: blobDownloadResponse.ContentLength,
|
||||
ContentEncoding: blobDownloadResponse.ContentEncoding,
|
||||
ContentType: blobDownloadResponse.ContentType,
|
||||
ContentDisposition: blobDownloadResponse.ContentDisposition,
|
||||
ContentLanguage: blobDownloadResponse.ContentLanguage,
|
||||
CacheControl: blobDownloadResponse.CacheControl,
|
||||
ExpiresString: blobDownloadResponse.Metadata[string(keyExpires)],
|
||||
ETag: backend.GetPtrFromString(convertAzureEtag(blobDownloadResponse.ETag)),
|
||||
LastModified: blobDownloadResponse.LastModified,
|
||||
Metadata: parseAndFilterAzMetadata(blobDownloadResponse.Metadata),
|
||||
TagCount: &tagcount,
|
||||
ContentRange: contentRange,
|
||||
Body: blobDownloadResponse.Body,
|
||||
StorageClass: types.StorageClassStandard,
|
||||
PartsCount: partsCount,
|
||||
AcceptRanges: backend.GetPtrFromString("bytes"),
|
||||
ContentLength: blobDownloadResponse.ContentLength,
|
||||
ContentEncoding: blobDownloadResponse.ContentEncoding,
|
||||
ContentType: blobDownloadResponse.ContentType,
|
||||
ContentDisposition: blobDownloadResponse.ContentDisposition,
|
||||
ContentLanguage: blobDownloadResponse.ContentLanguage,
|
||||
CacheControl: blobDownloadResponse.CacheControl,
|
||||
ExpiresString: blobDownloadResponse.Metadata[string(keyExpires)],
|
||||
WebsiteRedirectLocation: blobDownloadResponse.Metadata[string(keyWebsiteRedirect)],
|
||||
ETag: backend.GetPtrFromString(convertAzureEtag(blobDownloadResponse.ETag)),
|
||||
LastModified: blobDownloadResponse.LastModified,
|
||||
Metadata: parseAndFilterAzMetadata(blobDownloadResponse.Metadata),
|
||||
TagCount: &tagcount,
|
||||
ContentRange: contentRange,
|
||||
Body: blobDownloadResponse.Body,
|
||||
StorageClass: types.StorageClassStandard,
|
||||
PartsCount: partsCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -670,20 +682,21 @@ func (az *Azure) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3
|
||||
}
|
||||
|
||||
result := &s3.HeadObjectOutput{
|
||||
ContentRange: contentRange,
|
||||
AcceptRanges: backend.GetPtrFromString("bytes"),
|
||||
ContentLength: &length,
|
||||
PartsCount: partsCount,
|
||||
ContentType: resp.ContentType,
|
||||
ContentEncoding: resp.ContentEncoding,
|
||||
ContentLanguage: resp.ContentLanguage,
|
||||
ContentDisposition: resp.ContentDisposition,
|
||||
CacheControl: resp.CacheControl,
|
||||
ExpiresString: resp.Metadata[string(keyExpires)],
|
||||
ETag: backend.GetPtrFromString(convertAzureEtag(resp.ETag)),
|
||||
LastModified: resp.LastModified,
|
||||
Metadata: parseAndFilterAzMetadata(resp.Metadata),
|
||||
StorageClass: types.StorageClassStandard,
|
||||
ContentRange: contentRange,
|
||||
AcceptRanges: backend.GetPtrFromString("bytes"),
|
||||
ContentLength: &length,
|
||||
PartsCount: partsCount,
|
||||
ContentType: resp.ContentType,
|
||||
ContentEncoding: resp.ContentEncoding,
|
||||
ContentLanguage: resp.ContentLanguage,
|
||||
ContentDisposition: resp.ContentDisposition,
|
||||
CacheControl: resp.CacheControl,
|
||||
ExpiresString: resp.Metadata[string(keyExpires)],
|
||||
WebsiteRedirectLocation: resp.Metadata[string(keyWebsiteRedirect)],
|
||||
ETag: backend.GetPtrFromString(convertAzureEtag(resp.ETag)),
|
||||
LastModified: resp.LastModified,
|
||||
Metadata: parseAndFilterAzMetadata(resp.Metadata),
|
||||
StorageClass: types.StorageClassStandard,
|
||||
}
|
||||
|
||||
status, ok := resp.Metadata[string(keyObjLegalHold)]
|
||||
@@ -1193,6 +1206,9 @@ func (az *Azure) CopyObject(ctx context.Context, input s3response.CopyObjectInpu
|
||||
if getString(input.Expires) != "" {
|
||||
meta[string(keyExpires)] = *input.Expires
|
||||
}
|
||||
if getString(input.WebsiteRedirectLocation) != "" {
|
||||
meta[string(keyWebsiteRedirect)] = *input.WebsiteRedirectLocation
|
||||
}
|
||||
// Set object metadata
|
||||
_, err = dstClient.SetMetadata(ctx, parseMetadata(meta), nil)
|
||||
if err != nil {
|
||||
@@ -1273,6 +1289,7 @@ func (az *Azure) CopyObject(ctx context.Context, input s3response.CopyObjectInpu
|
||||
ContentLanguage: input.ContentLanguage,
|
||||
CacheControl: input.CacheControl,
|
||||
Expires: input.Expires,
|
||||
WebsiteRedirectLocation: input.WebsiteRedirectLocation,
|
||||
Metadata: input.Metadata,
|
||||
ObjectLockRetainUntilDate: input.ObjectLockRetainUntilDate,
|
||||
ObjectLockMode: input.ObjectLockMode,
|
||||
@@ -1288,6 +1305,7 @@ func (az *Azure) CopyObject(ctx context.Context, input s3response.CopyObjectInpu
|
||||
pInput.ContentLanguage = downloadResp.ContentLanguage
|
||||
pInput.ContentType = downloadResp.ContentType
|
||||
pInput.Metadata = parseAzMetadata(downloadResp.Metadata)
|
||||
delete(pInput.Metadata, string(keyWebsiteRedirect))
|
||||
}
|
||||
|
||||
if input.TaggingDirective == types.TaggingDirectiveReplace {
|
||||
@@ -1397,6 +1415,9 @@ func (az *Azure) CreateMultipartUpload(ctx context.Context, input s3response.Cre
|
||||
if getString(input.Expires) != "" {
|
||||
meta[string(keyExpires)] = input.Expires
|
||||
}
|
||||
if getString(input.WebsiteRedirectLocation) != "" {
|
||||
meta[string(keyWebsiteRedirect)] = input.WebsiteRedirectLocation
|
||||
}
|
||||
|
||||
// parse object tags
|
||||
tags, err := backend.ParseObjectTags(getString(input.Tagging))
|
||||
|
||||
+1
-1
@@ -531,7 +531,7 @@ func DecompressData(data []byte) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
if closeErr != nil {
|
||||
return nil, err
|
||||
return nil, closeErr
|
||||
}
|
||||
|
||||
return decompressed, nil
|
||||
|
||||
+118
-82
@@ -122,6 +122,7 @@ const (
|
||||
contentDispHdr = "content-disposition"
|
||||
cacheCtrlHdr = "cache-control"
|
||||
expiresHdr = "expires"
|
||||
websiteRedirectHdr = "website-redirect-location"
|
||||
emptyMD5 = "\"d41d8cd98f00b204e9800998ecf8427e\""
|
||||
aclkey = "acl"
|
||||
ownershipkey = "ownership"
|
||||
@@ -1549,13 +1550,14 @@ func (p *Posix) CreateMultipartUpload(ctx context.Context, mpu s3response.Create
|
||||
|
||||
err = p.storeObjectMetaProperties(nil, bucket, filepath.Join(objdir, uploadID),
|
||||
metaProperties{
|
||||
ContentType: mpu.ContentType,
|
||||
ContentEncoding: mpu.ContentEncoding,
|
||||
ContentDisposition: mpu.ContentDisposition,
|
||||
ContentLanguage: mpu.ContentLanguage,
|
||||
CacheControl: mpu.CacheControl,
|
||||
Expires: mpu.Expires,
|
||||
Metadata: mpu.Metadata,
|
||||
ContentType: mpu.ContentType,
|
||||
ContentEncoding: mpu.ContentEncoding,
|
||||
ContentDisposition: mpu.ContentDisposition,
|
||||
ContentLanguage: mpu.ContentLanguage,
|
||||
CacheControl: mpu.CacheControl,
|
||||
Expires: mpu.Expires,
|
||||
WebsiteRedirectLocation: mpu.WebsiteRedirectLocation,
|
||||
Metadata: mpu.Metadata,
|
||||
})
|
||||
if err != nil {
|
||||
// cleanup object if returning error
|
||||
@@ -2403,13 +2405,14 @@ func (p *Posix) checkUploadIDExists(bucket, object, uploadID string) ([32]byte,
|
||||
}
|
||||
|
||||
type metaProperties struct {
|
||||
ContentType *string
|
||||
ContentEncoding *string
|
||||
ContentDisposition *string
|
||||
ContentLanguage *string
|
||||
CacheControl *string
|
||||
Expires *string
|
||||
Metadata map[string]string
|
||||
ContentType *string
|
||||
ContentEncoding *string
|
||||
ContentDisposition *string
|
||||
ContentLanguage *string
|
||||
CacheControl *string
|
||||
Expires *string
|
||||
WebsiteRedirectLocation *string
|
||||
Metadata map[string]string
|
||||
}
|
||||
|
||||
// loadObjectMetadata loads the given object metadata, if it fails to load
|
||||
@@ -2508,6 +2511,11 @@ func (p *Posix) loadObjectMetaProperties(f *os.File, bucket, object string, fi *
|
||||
result.Expires = backend.GetPtrFromString(string(b))
|
||||
}
|
||||
|
||||
b, err = p.meta.RetrieveAttribute(f, bucket, object, websiteRedirectHdr)
|
||||
if err == nil {
|
||||
result.WebsiteRedirectLocation = backend.GetPtrFromString(string(b))
|
||||
}
|
||||
|
||||
result.Metadata = p.loadObjectMetadata(f, bucket, object)
|
||||
|
||||
return result
|
||||
@@ -2587,6 +2595,12 @@ func (p *Posix) storeObjectMetaProperties(f *os.File, bucket, object string, m m
|
||||
return fmt.Errorf("set expires: %w", err)
|
||||
}
|
||||
}
|
||||
if getString(m.WebsiteRedirectLocation) != "" {
|
||||
err := p.meta.StoreAttribute(f, bucket, object, websiteRedirectHdr, []byte(*m.WebsiteRedirectLocation))
|
||||
if err != nil {
|
||||
return fmt.Errorf("set website-redirect-location: %w", err)
|
||||
}
|
||||
}
|
||||
if m.Metadata != nil {
|
||||
err := p.storeObjectMetadata(f, bucket, object, m.Metadata)
|
||||
if err != nil {
|
||||
@@ -3682,6 +3696,14 @@ func (p *Posix) PutObjectWithPostFunc(ctx context.Context, po s3response.PutObje
|
||||
return s3response.PutObjectOutput{}, fmt.Errorf("set content-type attr: %w", err)
|
||||
}
|
||||
|
||||
if getString(po.WebsiteRedirectLocation) != "" {
|
||||
err = p.meta.StoreAttribute(nil, *po.Bucket, *po.Key, websiteRedirectHdr,
|
||||
[]byte(*po.WebsiteRedirectLocation))
|
||||
if err != nil {
|
||||
return s3response.PutObjectOutput{}, fmt.Errorf("set website-redirect-location attr: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
expectedSum := getEmptyChecksumValue(checksumAlgorithm)
|
||||
if checksumValue != "" && expectedSum != checksumValue {
|
||||
return s3response.PutObjectOutput{}, s3err.GetChecksumBadDigestErr(checksumAlgorithm)
|
||||
@@ -3879,13 +3901,14 @@ func (p *Posix) PutObjectWithPostFunc(ctx context.Context, po s3response.PutObje
|
||||
|
||||
err = p.storeObjectMetaProperties(f.File(), *po.Bucket, *po.Key,
|
||||
metaProperties{
|
||||
ContentType: po.ContentType,
|
||||
ContentEncoding: po.ContentEncoding,
|
||||
ContentLanguage: po.ContentLanguage,
|
||||
ContentDisposition: po.ContentDisposition,
|
||||
CacheControl: po.CacheControl,
|
||||
Expires: po.Expires,
|
||||
Metadata: po.Metadata,
|
||||
ContentType: po.ContentType,
|
||||
ContentEncoding: po.ContentEncoding,
|
||||
ContentLanguage: po.ContentLanguage,
|
||||
ContentDisposition: po.ContentDisposition,
|
||||
CacheControl: po.CacheControl,
|
||||
Expires: po.Expires,
|
||||
WebsiteRedirectLocation: po.WebsiteRedirectLocation,
|
||||
Metadata: po.Metadata,
|
||||
})
|
||||
if err != nil {
|
||||
return s3response.PutObjectOutput{}, err
|
||||
@@ -4583,32 +4606,33 @@ func (p *Posix) GetObject(ctx context.Context, input *s3.GetObjectInput) (*s3.Ge
|
||||
|
||||
var length int64 = 0
|
||||
return &s3.GetObjectOutput{
|
||||
ChecksumCRC32: checksums.CRC32,
|
||||
ChecksumCRC32C: checksums.CRC32C,
|
||||
ChecksumSHA1: checksums.SHA1,
|
||||
ChecksumSHA256: checksums.SHA256,
|
||||
ChecksumCRC64NVME: checksums.CRC64NVME,
|
||||
ChecksumSHA512: checksums.SHA512,
|
||||
ChecksumMD5: checksums.MD5,
|
||||
ChecksumXXHASH64: checksums.XXHASH64,
|
||||
ChecksumXXHASH3: checksums.XXHASH3,
|
||||
ChecksumXXHASH128: checksums.XXHASH128,
|
||||
ChecksumType: checksums.Type,
|
||||
AcceptRanges: backend.GetPtrFromString("bytes"),
|
||||
ContentLength: &length,
|
||||
ContentEncoding: objMeta.ContentEncoding,
|
||||
ContentType: objMeta.ContentType,
|
||||
ContentLanguage: objMeta.ContentLanguage,
|
||||
ContentDisposition: objMeta.ContentDisposition,
|
||||
CacheControl: objMeta.CacheControl,
|
||||
ExpiresString: objMeta.Expires,
|
||||
ETag: &etag,
|
||||
LastModified: backend.GetTimePtr(fid.ModTime()),
|
||||
Metadata: objMeta.Metadata,
|
||||
TagCount: tagCount,
|
||||
ContentRange: nil,
|
||||
StorageClass: types.StorageClassStandard,
|
||||
VersionId: &versionId,
|
||||
ChecksumCRC32: checksums.CRC32,
|
||||
ChecksumCRC32C: checksums.CRC32C,
|
||||
ChecksumSHA1: checksums.SHA1,
|
||||
ChecksumSHA256: checksums.SHA256,
|
||||
ChecksumCRC64NVME: checksums.CRC64NVME,
|
||||
ChecksumSHA512: checksums.SHA512,
|
||||
ChecksumMD5: checksums.MD5,
|
||||
ChecksumXXHASH64: checksums.XXHASH64,
|
||||
ChecksumXXHASH3: checksums.XXHASH3,
|
||||
ChecksumXXHASH128: checksums.XXHASH128,
|
||||
ChecksumType: checksums.Type,
|
||||
AcceptRanges: backend.GetPtrFromString("bytes"),
|
||||
ContentLength: &length,
|
||||
ContentEncoding: objMeta.ContentEncoding,
|
||||
ContentType: objMeta.ContentType,
|
||||
ContentLanguage: objMeta.ContentLanguage,
|
||||
ContentDisposition: objMeta.ContentDisposition,
|
||||
CacheControl: objMeta.CacheControl,
|
||||
ExpiresString: objMeta.Expires,
|
||||
WebsiteRedirectLocation: objMeta.WebsiteRedirectLocation,
|
||||
ETag: &etag,
|
||||
LastModified: backend.GetTimePtr(fid.ModTime()),
|
||||
Metadata: objMeta.Metadata,
|
||||
TagCount: tagCount,
|
||||
ContentRange: nil,
|
||||
StorageClass: types.StorageClassStandard,
|
||||
VersionId: &versionId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -4735,34 +4759,35 @@ func (p *Posix) GetObject(ctx context.Context, input *s3.GetObjectInput) (*s3.Ge
|
||||
}
|
||||
|
||||
return &s3.GetObjectOutput{
|
||||
AcceptRanges: backend.GetPtrFromString("bytes"),
|
||||
ContentLength: &length,
|
||||
ContentEncoding: objMeta.ContentEncoding,
|
||||
ContentType: objMeta.ContentType,
|
||||
ContentDisposition: objMeta.ContentDisposition,
|
||||
ContentLanguage: objMeta.ContentLanguage,
|
||||
CacheControl: objMeta.CacheControl,
|
||||
ExpiresString: objMeta.Expires,
|
||||
ETag: &etag,
|
||||
LastModified: backend.GetTimePtr(fi.ModTime()),
|
||||
Metadata: objMeta.Metadata,
|
||||
TagCount: tagCount,
|
||||
ContentRange: contentRange,
|
||||
StorageClass: types.StorageClassStandard,
|
||||
VersionId: &versionId,
|
||||
Body: body,
|
||||
ChecksumCRC32: checksums.CRC32,
|
||||
ChecksumCRC32C: checksums.CRC32C,
|
||||
ChecksumSHA1: checksums.SHA1,
|
||||
ChecksumSHA256: checksums.SHA256,
|
||||
ChecksumCRC64NVME: checksums.CRC64NVME,
|
||||
ChecksumSHA512: checksums.SHA512,
|
||||
ChecksumMD5: checksums.MD5,
|
||||
ChecksumXXHASH64: checksums.XXHASH64,
|
||||
ChecksumXXHASH3: checksums.XXHASH3,
|
||||
ChecksumXXHASH128: checksums.XXHASH128,
|
||||
ChecksumType: checksums.Type,
|
||||
PartsCount: partsCount,
|
||||
AcceptRanges: backend.GetPtrFromString("bytes"),
|
||||
ContentLength: &length,
|
||||
ContentEncoding: objMeta.ContentEncoding,
|
||||
ContentType: objMeta.ContentType,
|
||||
ContentDisposition: objMeta.ContentDisposition,
|
||||
ContentLanguage: objMeta.ContentLanguage,
|
||||
CacheControl: objMeta.CacheControl,
|
||||
ExpiresString: objMeta.Expires,
|
||||
WebsiteRedirectLocation: objMeta.WebsiteRedirectLocation,
|
||||
ETag: &etag,
|
||||
LastModified: backend.GetTimePtr(fi.ModTime()),
|
||||
Metadata: objMeta.Metadata,
|
||||
TagCount: tagCount,
|
||||
ContentRange: contentRange,
|
||||
StorageClass: types.StorageClassStandard,
|
||||
VersionId: &versionId,
|
||||
Body: body,
|
||||
ChecksumCRC32: checksums.CRC32,
|
||||
ChecksumCRC32C: checksums.CRC32C,
|
||||
ChecksumSHA1: checksums.SHA1,
|
||||
ChecksumSHA256: checksums.SHA256,
|
||||
ChecksumCRC64NVME: checksums.CRC64NVME,
|
||||
ChecksumSHA512: checksums.SHA512,
|
||||
ChecksumMD5: checksums.MD5,
|
||||
ChecksumXXHASH64: checksums.XXHASH64,
|
||||
ChecksumXXHASH3: checksums.XXHASH3,
|
||||
ChecksumXXHASH128: checksums.XXHASH128,
|
||||
ChecksumType: checksums.Type,
|
||||
PartsCount: partsCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -5012,6 +5037,7 @@ func (p *Posix) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3.
|
||||
ContentLanguage: objMeta.ContentLanguage,
|
||||
CacheControl: objMeta.CacheControl,
|
||||
ExpiresString: objMeta.Expires,
|
||||
WebsiteRedirectLocation: objMeta.WebsiteRedirectLocation,
|
||||
ETag: &etag,
|
||||
LastModified: backend.GetTimePtr(fi.ModTime()),
|
||||
Metadata: objMeta.Metadata,
|
||||
@@ -5326,17 +5352,26 @@ func (p *Posix) CopyObject(ctx context.Context, input s3response.CopyObjectInput
|
||||
// Store the provided object meta properties
|
||||
err = p.storeObjectMetaProperties(nil, dstBucket, dstObject,
|
||||
metaProperties{
|
||||
ContentType: input.ContentType,
|
||||
ContentEncoding: input.ContentEncoding,
|
||||
ContentLanguage: input.ContentLanguage,
|
||||
ContentDisposition: input.ContentDisposition,
|
||||
CacheControl: input.CacheControl,
|
||||
Expires: input.Expires,
|
||||
Metadata: input.Metadata,
|
||||
ContentType: input.ContentType,
|
||||
ContentEncoding: input.ContentEncoding,
|
||||
ContentLanguage: input.ContentLanguage,
|
||||
ContentDisposition: input.ContentDisposition,
|
||||
CacheControl: input.CacheControl,
|
||||
Expires: input.Expires,
|
||||
WebsiteRedirectLocation: input.WebsiteRedirectLocation,
|
||||
Metadata: input.Metadata,
|
||||
})
|
||||
if err != nil {
|
||||
return s3response.CopyObjectOutput{}, err
|
||||
}
|
||||
// explicitly delete the website redirect location, as if it's not
|
||||
// provided as CopyObject input, it should not be copied
|
||||
if getString(input.WebsiteRedirectLocation) == "" {
|
||||
err := p.meta.DeleteAttribute(dstBucket, dstObject, websiteRedirectHdr)
|
||||
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
||||
return s3response.CopyObjectOutput{}, fmt.Errorf("delete website-redirect-location: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if input.TaggingDirective == types.TaggingDirectiveReplace {
|
||||
tags, err := backend.ParseObjectTags(getString(input.Tagging))
|
||||
@@ -5375,6 +5410,7 @@ func (p *Posix) CopyObject(ctx context.Context, input s3response.CopyObjectInput
|
||||
ContentLanguage: input.ContentLanguage,
|
||||
CacheControl: input.CacheControl,
|
||||
Expires: input.Expires,
|
||||
WebsiteRedirectLocation: input.WebsiteRedirectLocation,
|
||||
Metadata: input.Metadata,
|
||||
ObjectLockRetainUntilDate: input.ObjectLockRetainUntilDate,
|
||||
ObjectLockMode: input.ObjectLockMode,
|
||||
|
||||
@@ -111,6 +111,7 @@ func (c S3ApiController) POSTObject(ctx *fiber.Ctx) (*Response, error) {
|
||||
contentLanguage := parsed.Fields["content-language"]
|
||||
cacheControl := parsed.Fields["cache-control"]
|
||||
expires := parsed.Fields["expires"]
|
||||
websiteRedirectLocation := parsed.Fields["x-amz-website-redirect-location"]
|
||||
|
||||
key := parsed.Fields["key"]
|
||||
|
||||
@@ -196,29 +197,39 @@ func (c S3ApiController) POSTObject(ctx *fiber.Ctx) (*Response, error) {
|
||||
}, err
|
||||
}
|
||||
|
||||
err = utils.ValidateWebsiteRedirectLocation(websiteRedirectLocation)
|
||||
if err != nil {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
res, err := c.be.PutObject(ctx.Context(), s3response.PutObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &key,
|
||||
ContentType: &contentType,
|
||||
ContentEncoding: &contentEncoding,
|
||||
ContentDisposition: &contentDisposition,
|
||||
ContentLanguage: &contentLanguage,
|
||||
CacheControl: &cacheControl,
|
||||
Expires: &expires,
|
||||
Body: parsed.FileRdr,
|
||||
ContentLength: &parsed.ContentLength,
|
||||
Tagging: &tagging,
|
||||
Metadata: metadata,
|
||||
ChecksumCRC32: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc32]),
|
||||
ChecksumCRC32C: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc32c]),
|
||||
ChecksumSHA1: utils.GetStringPtr(checksums[types.ChecksumAlgorithmSha1]),
|
||||
ChecksumSHA256: utils.GetStringPtr(checksums[types.ChecksumAlgorithmSha256]),
|
||||
ChecksumCRC64NVME: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc64nvme]),
|
||||
ChecksumSHA512: utils.GetStringPtr(checksums[types.ChecksumAlgorithmSha512]),
|
||||
ChecksumMD5: utils.GetStringPtr(checksums[types.ChecksumAlgorithmMd5]),
|
||||
ChecksumXXHASH64: utils.GetStringPtr(checksums[types.ChecksumAlgorithmXxhash64]),
|
||||
ChecksumXXHASH3: utils.GetStringPtr(checksums[types.ChecksumAlgorithmXxhash3]),
|
||||
ChecksumXXHASH128: utils.GetStringPtr(checksums[types.ChecksumAlgorithmXxhash128]),
|
||||
Bucket: &bucket,
|
||||
Key: &key,
|
||||
ContentType: &contentType,
|
||||
ContentEncoding: &contentEncoding,
|
||||
ContentDisposition: &contentDisposition,
|
||||
ContentLanguage: &contentLanguage,
|
||||
CacheControl: &cacheControl,
|
||||
Expires: &expires,
|
||||
WebsiteRedirectLocation: &websiteRedirectLocation,
|
||||
Body: parsed.FileRdr,
|
||||
ContentLength: &parsed.ContentLength,
|
||||
Tagging: &tagging,
|
||||
Metadata: metadata,
|
||||
ChecksumCRC32: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc32]),
|
||||
ChecksumCRC32C: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc32c]),
|
||||
ChecksumSHA1: utils.GetStringPtr(checksums[types.ChecksumAlgorithmSha1]),
|
||||
ChecksumSHA256: utils.GetStringPtr(checksums[types.ChecksumAlgorithmSha256]),
|
||||
ChecksumCRC64NVME: utils.GetStringPtr(checksums[types.ChecksumAlgorithmCrc64nvme]),
|
||||
ChecksumSHA512: utils.GetStringPtr(checksums[types.ChecksumAlgorithmSha512]),
|
||||
ChecksumMD5: utils.GetStringPtr(checksums[types.ChecksumAlgorithmMd5]),
|
||||
ChecksumXXHASH64: utils.GetStringPtr(checksums[types.ChecksumAlgorithmXxhash64]),
|
||||
ChecksumXXHASH3: utils.GetStringPtr(checksums[types.ChecksumAlgorithmXxhash3]),
|
||||
ChecksumXXHASH128: utils.GetStringPtr(checksums[types.ChecksumAlgorithmXxhash128]),
|
||||
})
|
||||
if err != nil {
|
||||
return &Response{
|
||||
|
||||
@@ -551,6 +551,7 @@ func (c S3ApiController) GetObject(ctx *fiber.Ctx) (*Response, error) {
|
||||
"Content-Language": utils.ApplyOverride(res.ContentLanguage, responseOverrides["Content-Language"]),
|
||||
"Cache-Control": utils.ApplyOverride(res.CacheControl, responseOverrides["Cache-Control"]),
|
||||
"Expires": utils.ApplyOverride(res.ExpiresString, responseOverrides["Expires"]),
|
||||
"x-amz-website-redirect-location": res.WebsiteRedirectLocation,
|
||||
"x-amz-checksum-crc32": res.ChecksumCRC32,
|
||||
"x-amz-checksum-crc64nvme": res.ChecksumCRC64NVME,
|
||||
"x-amz-checksum-crc32c": res.ChecksumCRC32C,
|
||||
|
||||
@@ -794,6 +794,7 @@ func TestS3ApiController_GetObject(t *testing.T) {
|
||||
"Content-Language": nil,
|
||||
"Cache-Control": nil,
|
||||
"Expires": nil,
|
||||
"x-amz-website-redirect-location": nil,
|
||||
"x-amz-checksum-crc32": nil,
|
||||
"x-amz-checksum-crc64nvme": nil,
|
||||
"x-amz-checksum-crc32c": nil,
|
||||
@@ -844,6 +845,7 @@ func TestS3ApiController_GetObject(t *testing.T) {
|
||||
"Content-Language": nil,
|
||||
"Cache-Control": nil,
|
||||
"Expires": nil,
|
||||
"x-amz-website-redirect-location": nil,
|
||||
"x-amz-checksum-crc32": nil,
|
||||
"x-amz-checksum-crc64nvme": nil,
|
||||
"x-amz-checksum-crc32c": nil,
|
||||
|
||||
@@ -179,6 +179,7 @@ func (c S3ApiController) HeadObject(ctx *fiber.Ctx) (*Response, error) {
|
||||
"Content-Length": utils.ConvertPtrToStringPtr(res.ContentLength),
|
||||
"Content-Type": utils.ApplyOverride(res.ContentType, responseOverrides["Content-Type"]),
|
||||
"Expires": utils.ApplyOverride(res.ExpiresString, responseOverrides["Expires"]),
|
||||
"x-amz-website-redirect-location": res.WebsiteRedirectLocation,
|
||||
"ETag": res.ETag,
|
||||
"Last-Modified": utils.FormatDatePtrToString(res.LastModified, timefmt),
|
||||
"x-amz-restore": res.Restore,
|
||||
|
||||
@@ -184,6 +184,7 @@ func TestS3ApiController_HeadObject(t *testing.T) {
|
||||
"Content-Language": nil,
|
||||
"Cache-Control": nil,
|
||||
"Expires": nil,
|
||||
"x-amz-website-redirect-location": nil,
|
||||
"x-amz-checksum-crc32": nil,
|
||||
"x-amz-checksum-crc64nvme": nil,
|
||||
"x-amz-checksum-crc32c": nil,
|
||||
@@ -233,6 +234,7 @@ func TestS3ApiController_HeadObject(t *testing.T) {
|
||||
"Content-Language": nil,
|
||||
"Cache-Control": nil,
|
||||
"Expires": nil,
|
||||
"x-amz-website-redirect-location": nil,
|
||||
"x-amz-checksum-crc32": nil,
|
||||
"x-amz-checksum-crc64nvme": nil,
|
||||
"x-amz-checksum-crc32c": nil,
|
||||
|
||||
@@ -154,6 +154,7 @@ func (c S3ApiController) CreateMultipartUpload(ctx *fiber.Ctx) (*Response, error
|
||||
contentEncoding := ctx.Get("Content-Encoding")
|
||||
tagging := ctx.Get("X-Amz-Tagging")
|
||||
expires := ctx.Get("Expires")
|
||||
websiteRedirectLocation := ctx.Get("X-Amz-Website-Redirect-Location")
|
||||
legalHoldHdr := ctx.Get("X-Amz-Object-Lock-Legal-Hold")
|
||||
lockModeHdr := ctx.Get("X-Amz-Object-Lock-Mode")
|
||||
objLockDate := ctx.Get("X-Amz-Object-Lock-Retain-Until-Date")
|
||||
@@ -203,6 +204,15 @@ func (c S3ApiController) CreateMultipartUpload(ctx *fiber.Ctx) (*Response, error
|
||||
}, err
|
||||
}
|
||||
|
||||
err = utils.ValidateWebsiteRedirectLocation(websiteRedirectLocation)
|
||||
if err != nil {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
objLockState, err := utils.ParsObjectLockHdrs(ctx)
|
||||
if err != nil {
|
||||
return &Response{
|
||||
@@ -232,6 +242,7 @@ func (c S3ApiController) CreateMultipartUpload(ctx *fiber.Ctx) (*Response, error
|
||||
ContentLanguage: &contentLanguage,
|
||||
CacheControl: &cacheControl,
|
||||
Expires: &expires,
|
||||
WebsiteRedirectLocation: &websiteRedirectLocation,
|
||||
ObjectLockRetainUntilDate: &objLockState.RetainUntilDate,
|
||||
ObjectLockMode: objLockState.ObjectLockMode,
|
||||
ObjectLockLegalHoldStatus: objLockState.LegalHoldStatus,
|
||||
|
||||
@@ -494,6 +494,7 @@ func (c S3ApiController) CopyObject(ctx *fiber.Ctx) (*Response, error) {
|
||||
contentLanguage := ctx.Get("Content-Language")
|
||||
cacheControl := ctx.Get("Cache-Control")
|
||||
expires := ctx.Get("Expires")
|
||||
websiteRedirectLocation := ctx.Get("X-Amz-Website-Redirect-Location")
|
||||
tagging := ctx.Get("x-amz-tagging")
|
||||
storageClass := ctx.Get("X-Amz-Storage-Class")
|
||||
legalHoldHdr := ctx.Get("X-Amz-Object-Lock-Legal-Hold")
|
||||
@@ -578,6 +579,15 @@ func (c S3ApiController) CopyObject(ctx *fiber.Ctx) (*Response, error) {
|
||||
}, s3err.GetInvalidArgumentErr(s3err.InvalidArgTaggingDirective, string(taggingDirective))
|
||||
}
|
||||
|
||||
err = utils.ValidateWebsiteRedirectLocation(websiteRedirectLocation)
|
||||
if err != nil {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
checksumAlgorithm := types.ChecksumAlgorithm(ctx.Get("x-amz-checksum-algorithm"))
|
||||
err = utils.IsChecksumAlgorithmValid(checksumAlgorithm)
|
||||
if err != nil {
|
||||
@@ -618,6 +628,7 @@ func (c S3ApiController) CopyObject(ctx *fiber.Ctx) (*Response, error) {
|
||||
ContentLanguage: &contentLanguage,
|
||||
CacheControl: &cacheControl,
|
||||
Expires: &expires,
|
||||
WebsiteRedirectLocation: &websiteRedirectLocation,
|
||||
Tagging: &tagging,
|
||||
TaggingDirective: taggingDirective,
|
||||
CopySource: ©Source,
|
||||
@@ -665,6 +676,7 @@ func (c S3ApiController) PutObject(ctx *fiber.Ctx) (*Response, error) {
|
||||
contentLanguage := ctx.Get("Content-Language")
|
||||
cacheControl := ctx.Get("Cache-Control")
|
||||
expires := ctx.Get("Expires")
|
||||
websiteRedirectLocation := ctx.Get("X-Amz-Website-Redirect-Location")
|
||||
tagging := ctx.Get("x-amz-tagging")
|
||||
legalHoldHdr := ctx.Get("X-Amz-Object-Lock-Legal-Hold")
|
||||
lockModeHdr := ctx.Get("X-Amz-Object-Lock-Mode")
|
||||
@@ -729,6 +741,15 @@ func (c S3ApiController) PutObject(ctx *fiber.Ctx) (*Response, error) {
|
||||
}, err
|
||||
}
|
||||
|
||||
err = utils.ValidateWebsiteRedirectLocation(websiteRedirectLocation)
|
||||
if err != nil {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
err = auth.CheckObjectAccess(ctx.Context(), bucket, acct.Access, []types.ObjectIdentifier{{Key: &key}}, true, IsBucketPublic, c.be, true)
|
||||
if err != nil {
|
||||
return &Response{
|
||||
@@ -787,6 +808,7 @@ func (c S3ApiController) PutObject(ctx *fiber.Ctx) (*Response, error) {
|
||||
ContentLanguage: &contentLanguage,
|
||||
CacheControl: &cacheControl,
|
||||
Expires: &expires,
|
||||
WebsiteRedirectLocation: &websiteRedirectLocation,
|
||||
Metadata: metadata,
|
||||
Body: body,
|
||||
Tagging: &tagging,
|
||||
|
||||
@@ -59,6 +59,16 @@ func SetBucketNameValidationStrict(strict bool) {
|
||||
// object metadata combined, excluded the 'x-amz-meta-' prefix
|
||||
const maxMetadataSize = 2048
|
||||
|
||||
func ValidateWebsiteRedirectLocation(location string) error {
|
||||
if location == "" || strings.HasPrefix(location, "http://") ||
|
||||
strings.HasPrefix(location, "https://") || strings.HasPrefix(location, "/") {
|
||||
return nil
|
||||
}
|
||||
|
||||
debuglogger.Logf("invalid website redirect location: %q", location)
|
||||
return s3err.GetAPIError(s3err.ErrInvalidRedirectLocation)
|
||||
}
|
||||
|
||||
// GetUserMetaData extracts user metadata from headers with the "x-amz-meta-" prefix.
|
||||
// Keys are normalized to lowercase and duplicate headers are merged as
|
||||
// comma-separated values. The total metadata size is validated against the
|
||||
|
||||
@@ -173,6 +173,7 @@ const (
|
||||
ErrCORSIsNotEnabled
|
||||
ErrNoSuchWebsiteConfiguration
|
||||
ErrInvalidWebsiteRedirectProtocol
|
||||
ErrInvalidRedirectLocation
|
||||
ErrBothReplaceKeyAndPrefix
|
||||
ErrMaxMessageLengthExceeded
|
||||
ErrNotModified
|
||||
@@ -660,6 +661,11 @@ var errorCodeResponse = map[ErrorCode]APIError{
|
||||
Description: "Invalid protocol, protocol can be http or https. If not defined the protocol will be selected automatically.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidRedirectLocation: {
|
||||
Code: "InvalidRedirectLocation",
|
||||
Description: "The website redirect location must have a prefix of 'http://' or 'https://' or '/'.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrBothReplaceKeyAndPrefix: {
|
||||
Code: "InvalidRequest",
|
||||
Description: "You can only define ReplaceKeyPrefix or ReplaceKey but not both.",
|
||||
|
||||
+19
-1
@@ -54,7 +54,7 @@ type RedirectAllRequestsTo struct {
|
||||
// RoutingRule specifies a redirect rule with an optional condition.
|
||||
type RoutingRule struct {
|
||||
Condition *RoutingRuleCondition `xml:"Condition,omitempty"`
|
||||
Redirect Redirect `xml:"Redirect"`
|
||||
Redirect *Redirect `xml:"Redirect"`
|
||||
}
|
||||
|
||||
// RoutingRuleCondition specifies when a routing rule applies.
|
||||
@@ -139,10 +139,28 @@ func (c *RoutingRuleCondition) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.HttpErrorCodeReturnedEquals == "" && c.KeyPrefixEquals == "" {
|
||||
debuglogger.Logf("website routing rule condition is empty")
|
||||
return s3err.GetAPIError(s3err.ErrMalformedXML)
|
||||
}
|
||||
|
||||
return isValidHTTPCode(c.HttpErrorCodeReturnedEquals, validateErrorCode)
|
||||
}
|
||||
|
||||
func (r *Redirect) Validate() error {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if r.HostName == "" &&
|
||||
r.HttpRedirectCode == "" &&
|
||||
r.Protocol == "" &&
|
||||
r.ReplaceKeyPrefixWith == "" &&
|
||||
r.ReplaceKeyWith == "" {
|
||||
debuglogger.Logf("website routing rule redirect is empty")
|
||||
return s3err.GetAPIError(s3err.ErrMalformedXML)
|
||||
}
|
||||
|
||||
if r.ReplaceKeyWith != "" && r.ReplaceKeyPrefixWith != "" {
|
||||
debuglogger.Logf("website redirect has both key replacements")
|
||||
return s3err.GetAPIError(s3err.ErrBothReplaceKeyAndPrefix)
|
||||
|
||||
@@ -59,7 +59,7 @@ func TestWebsiteConfiguration_Validate(t *testing.T) {
|
||||
Condition: &RoutingRuleCondition{
|
||||
KeyPrefixEquals: "docs/",
|
||||
},
|
||||
Redirect: Redirect{
|
||||
Redirect: &Redirect{
|
||||
ReplaceKeyPrefixWith: "documents/",
|
||||
},
|
||||
},
|
||||
@@ -122,7 +122,7 @@ func TestWebsiteConfiguration_Validate(t *testing.T) {
|
||||
IndexDocument: &IndexDocument{Suffix: "index.html"},
|
||||
RoutingRules: []RoutingRule{
|
||||
{
|
||||
Redirect: Redirect{
|
||||
Redirect: &Redirect{
|
||||
ReplaceKeyWith: "newkey",
|
||||
ReplaceKeyPrefixWith: "newprefix/",
|
||||
},
|
||||
@@ -132,13 +132,45 @@ func TestWebsiteConfiguration_Validate(t *testing.T) {
|
||||
wantErr: true,
|
||||
errCode: "InvalidRequest",
|
||||
},
|
||||
{
|
||||
name: "routing rule with empty condition",
|
||||
config: WebsiteConfiguration{
|
||||
IndexDocument: &IndexDocument{Suffix: "index.html"},
|
||||
RoutingRules: []RoutingRule{
|
||||
{
|
||||
Condition: &RoutingRuleCondition{},
|
||||
Redirect: &Redirect{
|
||||
HostName: "example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errCode: "MalformedXML",
|
||||
},
|
||||
{
|
||||
name: "routing rule with empty redirect",
|
||||
config: WebsiteConfiguration{
|
||||
IndexDocument: &IndexDocument{Suffix: "index.html"},
|
||||
RoutingRules: []RoutingRule{
|
||||
{
|
||||
Condition: &RoutingRuleCondition{
|
||||
KeyPrefixEquals: "docs/",
|
||||
},
|
||||
Redirect: &Redirect{},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
errCode: "MalformedXML",
|
||||
},
|
||||
{
|
||||
name: "routing rule with invalid redirect code",
|
||||
config: WebsiteConfiguration{
|
||||
IndexDocument: &IndexDocument{Suffix: "index.html"},
|
||||
RoutingRules: []RoutingRule{
|
||||
{
|
||||
Redirect: Redirect{
|
||||
Redirect: &Redirect{
|
||||
HttpRedirectCode: "200",
|
||||
},
|
||||
},
|
||||
@@ -153,7 +185,7 @@ func TestWebsiteConfiguration_Validate(t *testing.T) {
|
||||
IndexDocument: &IndexDocument{Suffix: "index.html"},
|
||||
RoutingRules: []RoutingRule{
|
||||
{
|
||||
Redirect: Redirect{
|
||||
Redirect: &Redirect{
|
||||
HttpRedirectCode: "301",
|
||||
HostName: "example.com",
|
||||
},
|
||||
@@ -203,7 +235,7 @@ func TestWebsiteConfiguration_MatchPrefetchRoutingRuleUsesPrefixOnlyRules(t *tes
|
||||
Condition: &RoutingRuleCondition{
|
||||
HttpErrorCodeReturnedEquals: "404",
|
||||
},
|
||||
Redirect: Redirect{
|
||||
Redirect: &Redirect{
|
||||
HostName: "error.example.com",
|
||||
},
|
||||
},
|
||||
@@ -212,7 +244,7 @@ func TestWebsiteConfiguration_MatchPrefetchRoutingRuleUsesPrefixOnlyRules(t *tes
|
||||
KeyPrefixEquals: "old/",
|
||||
HttpErrorCodeReturnedEquals: "404",
|
||||
},
|
||||
Redirect: Redirect{
|
||||
Redirect: &Redirect{
|
||||
HostName: "both.example.com",
|
||||
},
|
||||
},
|
||||
@@ -220,7 +252,7 @@ func TestWebsiteConfiguration_MatchPrefetchRoutingRuleUsesPrefixOnlyRules(t *tes
|
||||
Condition: &RoutingRuleCondition{
|
||||
KeyPrefixEquals: "old/",
|
||||
},
|
||||
Redirect: Redirect{
|
||||
Redirect: &Redirect{
|
||||
HostName: "prefix.example.com",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -666,21 +666,23 @@ func CopyObject_should_copy_meta_props(s *S3Conf) error {
|
||||
|
||||
cType, cEnc, cDesp, cLang, cLength := "application/json", "base64", "test-desp", "us", int64(100)
|
||||
cacheControl, expires := "no-cache", time.Now().Add(time.Hour*10)
|
||||
redirectLocation := "/source-redirect"
|
||||
meta := map[string]string{
|
||||
"foo": "bar",
|
||||
"baz": "quxx",
|
||||
}
|
||||
|
||||
_, err := putObjectWithData(cLength, &s3.PutObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &srcObj,
|
||||
ContentDisposition: &cDesp,
|
||||
ContentEncoding: &cEnc,
|
||||
ContentLanguage: &cLang,
|
||||
ContentType: &cType,
|
||||
CacheControl: &cacheControl,
|
||||
Expires: &expires,
|
||||
Metadata: meta,
|
||||
Bucket: &bucket,
|
||||
Key: &srcObj,
|
||||
ContentDisposition: &cDesp,
|
||||
ContentEncoding: &cEnc,
|
||||
ContentLanguage: &cLang,
|
||||
ContentType: &cType,
|
||||
CacheControl: &cacheControl,
|
||||
Expires: &expires,
|
||||
WebsiteRedirectLocation: &redirectLocation,
|
||||
Metadata: meta,
|
||||
}, s3client)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -697,7 +699,7 @@ func CopyObject_should_copy_meta_props(s *S3Conf) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return checkObjectMetaProps(s3client, bucket, dstObj, ObjectMetaProps{
|
||||
if err := checkObjectMetaProps(s3client, bucket, dstObj, ObjectMetaProps{
|
||||
ContentLength: cLength,
|
||||
ContentType: cType,
|
||||
ContentEncoding: cEnc,
|
||||
@@ -706,7 +708,24 @@ func CopyObject_should_copy_meta_props(s *S3Conf) error {
|
||||
CacheControl: cacheControl,
|
||||
ExpiresString: expires.UTC().Format(timefmt),
|
||||
Metadata: meta,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
||||
out, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &dstObj,
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if got := getString(out.WebsiteRedirectLocation); got != "" {
|
||||
return fmt.Errorf("expected WebsiteRedirectLocation not to be copied, got %v", got)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -736,6 +755,7 @@ func CopyObject_should_replace_meta_props(s *S3Conf) error {
|
||||
|
||||
cType, cEnc, cDesp, cLang := "application/binary", "hex", "desp", "mex"
|
||||
cacheControl, expires := "no-cache", time.Now().Add(time.Hour*10)
|
||||
redirectLocation := "https://example.com/replaced"
|
||||
meta := map[string]string{
|
||||
"foo": "bar",
|
||||
"baz": "quxx",
|
||||
@@ -744,17 +764,18 @@ func CopyObject_should_replace_meta_props(s *S3Conf) error {
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err = s3client.CopyObject(ctx, &s3.CopyObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &dstObj,
|
||||
CopySource: getPtr(bucket + "/" + srcObj),
|
||||
MetadataDirective: types.MetadataDirectiveReplace,
|
||||
ContentDisposition: &cDesp,
|
||||
ContentEncoding: &cEnc,
|
||||
ContentLanguage: &cLang,
|
||||
ContentType: &cType,
|
||||
CacheControl: &cacheControl,
|
||||
Expires: &expires,
|
||||
Metadata: meta,
|
||||
Bucket: &bucket,
|
||||
Key: &dstObj,
|
||||
CopySource: getPtr(bucket + "/" + srcObj),
|
||||
MetadataDirective: types.MetadataDirectiveReplace,
|
||||
ContentDisposition: &cDesp,
|
||||
ContentEncoding: &cEnc,
|
||||
ContentLanguage: &cLang,
|
||||
ContentType: &cType,
|
||||
CacheControl: &cacheControl,
|
||||
Expires: &expires,
|
||||
WebsiteRedirectLocation: &redirectLocation,
|
||||
Metadata: meta,
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
@@ -762,18 +783,44 @@ func CopyObject_should_replace_meta_props(s *S3Conf) error {
|
||||
}
|
||||
|
||||
return checkObjectMetaProps(s3client, bucket, dstObj, ObjectMetaProps{
|
||||
ContentLength: contentLength,
|
||||
ContentType: cType,
|
||||
ContentEncoding: cEnc,
|
||||
ContentDisposition: cDesp,
|
||||
ContentLanguage: cLang,
|
||||
CacheControl: cacheControl,
|
||||
ExpiresString: expires.UTC().Format(timefmt),
|
||||
Metadata: meta,
|
||||
ContentLength: contentLength,
|
||||
ContentType: cType,
|
||||
ContentEncoding: cEnc,
|
||||
ContentDisposition: cDesp,
|
||||
ContentLanguage: cLang,
|
||||
CacheControl: cacheControl,
|
||||
ExpiresString: expires.UTC().Format(timefmt),
|
||||
WebsiteRedirectLocation: redirectLocation,
|
||||
Metadata: meta,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func CopyObject_invalid_website_redirect_location(s *S3Conf) error {
|
||||
testName := "CopyObject_invalid_website_redirect_location"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
srcObj, dstObj := "source-object", "dest-object"
|
||||
|
||||
_, err := putObjectWithData(100, &s3.PutObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &srcObj,
|
||||
}, s3client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err = s3client.CopyObject(ctx, &s3.CopyObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &dstObj,
|
||||
CopySource: getPtr(bucket + "/" + srcObj),
|
||||
WebsiteRedirectLocation: getPtr("ftp://example.com"),
|
||||
})
|
||||
cancel()
|
||||
return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidRedirectLocation))
|
||||
})
|
||||
}
|
||||
|
||||
func CopyObject_default_content_type_with_replace_metadata(s *S3Conf) error {
|
||||
testName := "CopyObject_default_content_type_with_replace_metadata"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
|
||||
@@ -67,18 +67,20 @@ func CreateMultipartUpload_with_metadata(s *S3Conf) error {
|
||||
}
|
||||
cType, cEnc, cDesp, cLang := "application/text", "testenc", "testdesp", "sp"
|
||||
cacheControl, expires := "no-cache", time.Now().Add(time.Hour*5)
|
||||
redirectLocation := "/multipart-redirect"
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
out, err := s3client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
|
||||
Bucket: &bucket,
|
||||
Key: &obj,
|
||||
Metadata: meta,
|
||||
ContentType: &cType,
|
||||
ContentEncoding: &cEnc,
|
||||
ContentDisposition: &cDesp,
|
||||
ContentLanguage: &cLang,
|
||||
CacheControl: &cacheControl,
|
||||
Expires: &expires,
|
||||
Bucket: &bucket,
|
||||
Key: &obj,
|
||||
Metadata: meta,
|
||||
ContentType: &cType,
|
||||
ContentEncoding: &cEnc,
|
||||
ContentDisposition: &cDesp,
|
||||
ContentLanguage: &cLang,
|
||||
CacheControl: &cacheControl,
|
||||
Expires: &expires,
|
||||
WebsiteRedirectLocation: &redirectLocation,
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
@@ -151,11 +153,29 @@ func CreateMultipartUpload_with_metadata(s *S3Conf) error {
|
||||
return fmt.Errorf("expected uploaded object content-encoding to be %v, instead got %v",
|
||||
expires.UTC().Format(timefmt), getString(resp.ExpiresString))
|
||||
}
|
||||
if getString(resp.WebsiteRedirectLocation) != redirectLocation {
|
||||
return fmt.Errorf("expected uploaded object website redirect location to be %v, instead got %v",
|
||||
redirectLocation, getString(resp.WebsiteRedirectLocation))
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func CreateMultipartUpload_invalid_website_redirect_location(s *S3Conf) error {
|
||||
testName := "CreateMultipartUpload_invalid_website_redirect_location"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err := s3client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
|
||||
Bucket: &bucket,
|
||||
Key: getPtr("my-obj"),
|
||||
WebsiteRedirectLocation: getPtr("example.com"),
|
||||
})
|
||||
cancel()
|
||||
return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidRedirectLocation))
|
||||
})
|
||||
}
|
||||
|
||||
func CreateMultipartUpload_with_object_lock(s *S3Conf) error {
|
||||
testName := "CreateMultipartUpload_with_object_lock"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
|
||||
@@ -574,22 +574,24 @@ func GetObject_success(s *S3Conf) error {
|
||||
dataLength, obj := int64(1234567), "my-obj"
|
||||
ctype, cDisp, cEnc, cLang := defaultContentType, "cont-desp", "json", "eng"
|
||||
cacheControl, expires := "cache-ctrl", time.Now().Add(time.Hour*2)
|
||||
redirectLocation := "/get-object-redirect"
|
||||
meta := map[string]string{
|
||||
"foo": "bar",
|
||||
"baz": "quxx",
|
||||
}
|
||||
|
||||
r, err := putObjectWithData(dataLength, &s3.PutObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &obj,
|
||||
ContentType: &ctype,
|
||||
ContentDisposition: &cDisp,
|
||||
ContentEncoding: &cEnc,
|
||||
ContentLanguage: &cLang,
|
||||
Expires: &expires,
|
||||
CacheControl: &cacheControl,
|
||||
Metadata: meta,
|
||||
Tagging: getPtr("key=value&key1=val1"),
|
||||
Bucket: &bucket,
|
||||
Key: &obj,
|
||||
ContentType: &ctype,
|
||||
ContentDisposition: &cDisp,
|
||||
ContentEncoding: &cEnc,
|
||||
ContentLanguage: &cLang,
|
||||
Expires: &expires,
|
||||
CacheControl: &cacheControl,
|
||||
WebsiteRedirectLocation: &redirectLocation,
|
||||
Metadata: meta,
|
||||
Tagging: getPtr("key=value&key1=val1"),
|
||||
}, s3client)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -632,6 +634,10 @@ func GetObject_success(s *S3Conf) error {
|
||||
return fmt.Errorf("expected Cache-Control %v, instead got %v",
|
||||
cacheControl, getString(out.CacheControl))
|
||||
}
|
||||
if getString(out.WebsiteRedirectLocation) != redirectLocation {
|
||||
return fmt.Errorf("expected WebsiteRedirectLocation %v, instead got %v",
|
||||
redirectLocation, getString(out.WebsiteRedirectLocation))
|
||||
}
|
||||
if out.StorageClass != types.StorageClassStandard {
|
||||
return fmt.Errorf("expected the storage class to be %v, instead got %v",
|
||||
types.StorageClassStandard, out.StorageClass)
|
||||
|
||||
@@ -569,18 +569,20 @@ func HeadObject_success(s *S3Conf) error {
|
||||
}
|
||||
ctype, cDisp, cEnc, cLang := defaultContentType, "cont-desp", "json", "eng"
|
||||
cacheControl, expires := "cache-ctrl", time.Now().Add(time.Hour*2)
|
||||
redirectLocation := "/head-object-redirect"
|
||||
|
||||
_, err := putObjectWithData(dataLen, &s3.PutObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &obj,
|
||||
Metadata: meta,
|
||||
ContentType: &ctype,
|
||||
ContentDisposition: &cDisp,
|
||||
ContentEncoding: &cEnc,
|
||||
ContentLanguage: &cLang,
|
||||
CacheControl: &cacheControl,
|
||||
Expires: &expires,
|
||||
Tagging: getPtr("key=value"),
|
||||
Bucket: &bucket,
|
||||
Key: &obj,
|
||||
Metadata: meta,
|
||||
ContentType: &ctype,
|
||||
ContentDisposition: &cDisp,
|
||||
ContentEncoding: &cEnc,
|
||||
ContentLanguage: &cLang,
|
||||
CacheControl: &cacheControl,
|
||||
Expires: &expires,
|
||||
WebsiteRedirectLocation: &redirectLocation,
|
||||
Tagging: getPtr("key=value"),
|
||||
}, s3client)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -631,6 +633,10 @@ func HeadObject_success(s *S3Conf) error {
|
||||
return fmt.Errorf("expected Cache-Control %v, instead got %v",
|
||||
cacheControl, getString(out.CacheControl))
|
||||
}
|
||||
if getString(out.WebsiteRedirectLocation) != redirectLocation {
|
||||
return fmt.Errorf("expected WebsiteRedirectLocation %v, instead got %v",
|
||||
redirectLocation, getString(out.WebsiteRedirectLocation))
|
||||
}
|
||||
if out.StorageClass != types.StorageClassStandard {
|
||||
return fmt.Errorf("expected the storage class to be %v, instead got %v",
|
||||
types.StorageClassStandard, out.StorageClass)
|
||||
|
||||
@@ -887,6 +887,7 @@ func PostObject_success_with_meta_properties(s *S3Conf) error {
|
||||
cLanguage := "en-US"
|
||||
cDisposition := "inline"
|
||||
cEncoding := "gzip"
|
||||
redirectLocation := "/post-object-redirect"
|
||||
|
||||
resp, err := sendPostObject(PostRequestConfig{
|
||||
bucket: bucket,
|
||||
@@ -900,18 +901,20 @@ func PostObject_success_with_meta_properties(s *S3Conf) error {
|
||||
[]any{"eq", "$Content-Encoding", cEncoding},
|
||||
[]any{"eq", "$Cache-Control", cacheControl},
|
||||
[]any{"eq", "$Expires", expires},
|
||||
[]any{"eq", "$x-amz-website-redirect-location", redirectLocation},
|
||||
[]any{"eq", "$x-amz-meta-foo", "bar"},
|
||||
[]any{"eq", "$x-amz-meta-baz", "quxx"},
|
||||
},
|
||||
extraFields: map[string]string{
|
||||
"Content-Type": cType,
|
||||
"Cache-Control": cacheControl,
|
||||
"Expires": expires,
|
||||
"Content-Language": cLanguage,
|
||||
"Content-Disposition": cDisposition,
|
||||
"Content-Encoding": cEncoding,
|
||||
"x-amz-meta-foo": "bar",
|
||||
"x-amz-meta-baz": "quxx",
|
||||
"Content-Type": cType,
|
||||
"Cache-Control": cacheControl,
|
||||
"Expires": expires,
|
||||
"Content-Language": cLanguage,
|
||||
"Content-Disposition": cDisposition,
|
||||
"Content-Encoding": cEncoding,
|
||||
"x-amz-website-redirect-location": redirectLocation,
|
||||
"x-amz-meta-foo": "bar",
|
||||
"x-amz-meta-baz": "quxx",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -955,6 +958,10 @@ func PostObject_success_with_meta_properties(s *S3Conf) error {
|
||||
return fmt.Errorf("expected Cache-Control %s, instead got %s",
|
||||
cacheControl, getString(out.CacheControl))
|
||||
}
|
||||
if getString(out.WebsiteRedirectLocation) != redirectLocation {
|
||||
return fmt.Errorf("expected WebsiteRedirectLocation %s, instead got %s",
|
||||
redirectLocation, getString(out.WebsiteRedirectLocation))
|
||||
}
|
||||
|
||||
expectedMeta := map[string]string{
|
||||
"foo": "bar",
|
||||
@@ -969,6 +976,30 @@ func PostObject_success_with_meta_properties(s *S3Conf) error {
|
||||
})
|
||||
}
|
||||
|
||||
func PostObject_invalid_website_redirect_location(s *S3Conf) error {
|
||||
testName := "PostObject_invalid_website_redirect_location"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
redirectLocation := "ftp://example.com"
|
||||
resp, err := sendPostObject(PostRequestConfig{
|
||||
bucket: bucket,
|
||||
key: "test-object",
|
||||
s3Conf: s,
|
||||
fileContent: []byte("data"),
|
||||
policyConditions: []any{
|
||||
[]any{"eq", "$x-amz-website-redirect-location", redirectLocation},
|
||||
},
|
||||
extraFields: map[string]string{
|
||||
"x-amz-website-redirect-location": redirectLocation,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrInvalidRedirectLocation))
|
||||
})
|
||||
}
|
||||
|
||||
func PostObject_invalid_tagging(s *S3Conf) error {
|
||||
testName := "PostObject_invalid_tagging"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
|
||||
@@ -180,6 +180,56 @@ func PutBucketWebsite_invalid_routing_rule_protocol(s *S3Conf) error {
|
||||
})
|
||||
}
|
||||
|
||||
func PutBucketWebsite_empty_routing_rule_condition(s *S3Conf) error {
|
||||
testName := "PutBucketWebsite_empty_routing_rule_condition"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err := s3client.PutBucketWebsite(ctx, &s3.PutBucketWebsiteInput{
|
||||
Bucket: &bucket,
|
||||
WebsiteConfiguration: &types.WebsiteConfiguration{
|
||||
IndexDocument: &types.IndexDocument{
|
||||
Suffix: getPtr("index.html"),
|
||||
},
|
||||
RoutingRules: []types.RoutingRule{
|
||||
{
|
||||
Condition: &types.Condition{},
|
||||
Redirect: &types.Redirect{
|
||||
HostName: getPtr("example.com"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
cancel()
|
||||
return checkApiErr(err, s3err.GetAPIError(s3err.ErrMalformedXML))
|
||||
})
|
||||
}
|
||||
|
||||
func PutBucketWebsite_empty_routing_rule_redirect(s *S3Conf) error {
|
||||
testName := "PutBucketWebsite_empty_routing_rule_redirect"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
_, err := s3client.PutBucketWebsite(ctx, &s3.PutBucketWebsiteInput{
|
||||
Bucket: &bucket,
|
||||
WebsiteConfiguration: &types.WebsiteConfiguration{
|
||||
IndexDocument: &types.IndexDocument{
|
||||
Suffix: getPtr("index.html"),
|
||||
},
|
||||
RoutingRules: []types.RoutingRule{
|
||||
{
|
||||
Condition: &types.Condition{
|
||||
KeyPrefixEquals: getPtr("docs/"),
|
||||
},
|
||||
Redirect: &types.Redirect{},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
cancel()
|
||||
return checkApiErr(err, s3err.GetAPIError(s3err.ErrMalformedXML))
|
||||
})
|
||||
}
|
||||
|
||||
func PutBucketWebsite_empty_error_document_key(s *S3Conf) error {
|
||||
testName := "PutBucketWebsite_empty_error_document_key"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
|
||||
@@ -629,6 +629,19 @@ func PutObject_with_metadata(s *S3Conf) error {
|
||||
})
|
||||
}
|
||||
|
||||
func PutObject_invalid_website_redirect_location(s *S3Conf) error {
|
||||
testName := "PutObject_invalid_website_redirect_location"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
obj := "my-obj"
|
||||
_, err := putObjectWithData(10, &s3.PutObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &obj,
|
||||
WebsiteRedirectLocation: getPtr("ftp://example.com"),
|
||||
}, s3client)
|
||||
return checkApiErr(err, s3err.GetAPIError(s3err.ErrInvalidRedirectLocation))
|
||||
})
|
||||
}
|
||||
|
||||
func PutObject_checksum_algorithm_and_header_mismatch(s *S3Conf) error {
|
||||
testName := "PutObject_checksum_algorithm_and_header_mismatch"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
|
||||
@@ -122,6 +122,44 @@ func WebsiteHosting_no_error_document(s *S3Conf) error {
|
||||
})
|
||||
}
|
||||
|
||||
// WebsiteHosting_no_bucket_in_request_location tests that website endpoint
|
||||
// requests that cannot resolve a bucket still include a useful Location header.
|
||||
func WebsiteHosting_no_bucket_in_request_location(s *S3Conf) error {
|
||||
testName := "WebsiteHosting_no_bucket_in_request_location"
|
||||
return actionHandlerNoSetup(s, testName, func(_ *s3.Client, _ string) error {
|
||||
_, domain, port := websiteEndpointParts(s)
|
||||
badHost := "nested.bucket." + domain
|
||||
baseHost := domain
|
||||
if port != "" {
|
||||
badHost = fmt.Sprintf("%s:%s", badHost, port)
|
||||
baseHost = fmt.Sprintf("%s:%s", baseHost, port)
|
||||
}
|
||||
|
||||
reqURL, err := websiteAbsoluteURL(s, badHost, "/")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodGet, reqURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create website request: %w", err)
|
||||
}
|
||||
resp, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wantLocation, err := websiteAbsoluteURL(s, baseHost, "/")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if got := resp.Header.Get("Location"); got != wantLocation {
|
||||
return fmt.Errorf("expected Location %q, got %q", wantLocation, got)
|
||||
}
|
||||
|
||||
return checkWebsiteErrorResponse(resp, s3err.GetAPIError(s3err.ErrNoBucketInRequest))
|
||||
})
|
||||
}
|
||||
|
||||
// WebsiteHosting_private_object_and_error_document tests that website hosting
|
||||
// does not serve either the requested object or the configured error document
|
||||
// unless public object access has been granted.
|
||||
@@ -203,7 +241,7 @@ func WebsiteHosting_routing_rule_post_request_redirect(s *S3Conf) error {
|
||||
if got := resp.Header.Get("Location"); got != wantLocation {
|
||||
return fmt.Errorf("expected Location %q, got %q", wantLocation, got)
|
||||
}
|
||||
return checkWebsiteResponse(resp, http.StatusFound, []byte(http.StatusText(http.StatusFound)))
|
||||
return checkWebsiteResponse(resp, http.StatusFound, nil)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -245,7 +283,7 @@ func WebsiteHosting_routing_rule_pre_request_redirect(s *S3Conf) error {
|
||||
if got := resp.Header.Get("Location"); got != wantLocation {
|
||||
return fmt.Errorf("expected Location %q, got %q", wantLocation, got)
|
||||
}
|
||||
return checkWebsiteResponse(resp, http.StatusMovedPermanently, []byte(http.StatusText(http.StatusMovedPermanently)))
|
||||
return checkWebsiteResponse(resp, http.StatusMovedPermanently, nil)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -294,7 +332,7 @@ func WebsiteHosting_routing_rule_prefix_and_error_redirect(s *S3Conf) error {
|
||||
if got := resp.Header.Get("Location"); got != wantLocation {
|
||||
return fmt.Errorf("expected Location %q, got %q", wantLocation, got)
|
||||
}
|
||||
return checkWebsiteResponse(resp, http.StatusTemporaryRedirect, []byte(http.StatusText(http.StatusTemporaryRedirect)))
|
||||
return checkWebsiteResponse(resp, http.StatusTemporaryRedirect, nil)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -373,7 +411,49 @@ func WebsiteHosting_redirect_all_requests(s *S3Conf) error {
|
||||
if got, want := resp.Header.Get("Location"), "https://www.example.com/any/path/here?tracking=1"; got != want {
|
||||
return fmt.Errorf("expected Location %q, got %q", want, got)
|
||||
}
|
||||
return checkWebsiteResponse(resp, http.StatusMovedPermanently, []byte(http.StatusText(http.StatusMovedPermanently)))
|
||||
return checkWebsiteResponse(resp, http.StatusMovedPermanently, nil)
|
||||
})
|
||||
}
|
||||
|
||||
// WebsiteHosting_object_redirect_location tests that an object-level website
|
||||
// redirect emits a 301 with the stored Location after object fetch succeeds.
|
||||
func WebsiteHosting_object_redirect_location(s *S3Conf) error {
|
||||
testName := "WebsiteHosting_object_redirect_location"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
err := putBucketWebsiteConfig(s3client, bucket, &types.WebsiteConfiguration{
|
||||
IndexDocument: &types.IndexDocument{
|
||||
Suffix: getPtr("index.html"),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := grantPublicBucketPolicy(s3client, bucket, policyTypeObject); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
redirectLocation := "/new-page.html"
|
||||
objectBody := "old"
|
||||
_, err = putObjectWithData(int64(len(objectBody)), &s3.PutObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: getPtr("old-page.html"),
|
||||
Body: strings.NewReader(objectBody),
|
||||
ContentType: getPtr("text/html"),
|
||||
WebsiteRedirectLocation: &redirectLocation,
|
||||
}, s3client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := websiteGet(s, bucket, "old-page.html", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if got := resp.Header.Get("Location"); got != redirectLocation {
|
||||
return fmt.Errorf("expected Location %q, got %q", redirectLocation, got)
|
||||
}
|
||||
return checkWebsiteResponse(resp, http.StatusMovedPermanently, nil)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -521,7 +601,7 @@ func WebsiteHosting_index_error_document_and_routing_rules(s *S3Conf) error {
|
||||
preResp.Body.Close()
|
||||
return fmt.Errorf("expected pre-rule Location %q, got %q", wantPreLocation, got)
|
||||
}
|
||||
if err := checkWebsiteResponse(preResp, http.StatusMovedPermanently, []byte(http.StatusText(http.StatusMovedPermanently))); err != nil {
|
||||
if err := checkWebsiteResponse(preResp, http.StatusMovedPermanently, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -538,7 +618,7 @@ func WebsiteHosting_index_error_document_and_routing_rules(s *S3Conf) error {
|
||||
postResp.Body.Close()
|
||||
return fmt.Errorf("expected post-rule Location %q, got %q", wantPostLocation, got)
|
||||
}
|
||||
if err := checkWebsiteResponse(postResp, http.StatusFound, []byte(http.StatusText(http.StatusFound))); err != nil {
|
||||
if err := checkWebsiteResponse(postResp, http.StatusFound, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -584,7 +664,7 @@ func WebsiteHosting_options_preflight_access_granted(s *S3Conf) error {
|
||||
Origin: "https://client.example",
|
||||
Methods: "GET, HEAD",
|
||||
AllowHeaders: "content-type, x-amz-date",
|
||||
ExposeHeaders: "Content-Length, ETag",
|
||||
ExposeHeaders: "Content-Length",
|
||||
MaxAge: "42",
|
||||
AllowCredentials: "true",
|
||||
Vary: "Origin, Access-Control-Request-Headers, Access-Control-Request-Method",
|
||||
|
||||
@@ -203,6 +203,7 @@ func TestPutObject(ts *TestState) {
|
||||
ts.Run(PutObject_invalid_object_names)
|
||||
ts.Run(PutObject_object_acl_not_supported)
|
||||
ts.Run(PutObject_long_metadata)
|
||||
ts.Run(PutObject_invalid_website_redirect_location)
|
||||
}
|
||||
|
||||
func TestHeadObject(ts *TestState) {
|
||||
@@ -370,6 +371,7 @@ func TestCopyObject(ts *TestState) {
|
||||
ts.Run(CopyObject_non_existing_dir_object)
|
||||
ts.Run(CopyObject_should_copy_meta_props)
|
||||
ts.Run(CopyObject_should_replace_meta_props)
|
||||
ts.Run(CopyObject_invalid_website_redirect_location)
|
||||
ts.Run(CopyObject_default_content_type_with_replace_metadata)
|
||||
ts.Run(CopyObject_missing_bucket_lock)
|
||||
ts.Run(CopyObject_invalid_legal_hold)
|
||||
@@ -420,6 +422,7 @@ func TestCreateMultipartUpload(ts *TestState) {
|
||||
ts.Run(CreateMultipartUpload_non_existing_bucket)
|
||||
ts.Run(CreateMultipartUpload_long_metadata)
|
||||
ts.Run(CreateMultipartUpload_with_metadata)
|
||||
ts.Run(CreateMultipartUpload_invalid_website_redirect_location)
|
||||
ts.Run(CreateMultipartUpload_with_tagging)
|
||||
ts.Run(CreateMultipartUpload_with_object_lock)
|
||||
ts.Run(CreateMultipartUpload_with_object_lock_not_enabled)
|
||||
@@ -670,6 +673,8 @@ func TestPutBucketWebsite(ts *TestState) {
|
||||
ts.Run(PutBucketWebsite_invalid_redirect_protocol)
|
||||
ts.Run(PutBucketWebsite_redirectAll_index_error_routingRules)
|
||||
ts.Run(PutBucketWebsite_invalid_routing_rule_protocol)
|
||||
ts.Run(PutBucketWebsite_empty_routing_rule_condition)
|
||||
ts.Run(PutBucketWebsite_empty_routing_rule_redirect)
|
||||
ts.Run(PutBucketWebsite_empty_error_document_key)
|
||||
ts.Run(PutBucketWebsite_too_many_routing_rules)
|
||||
ts.Run(PutBucketWebsite_routing_rule_replace_key_and_prefix)
|
||||
@@ -696,12 +701,14 @@ func TestWebsiteHosting(ts *TestState) {
|
||||
ts.Run(WebsiteHosting_error_document_served)
|
||||
ts.Run(WebsiteHosting_error_document_not_found)
|
||||
ts.Run(WebsiteHosting_no_error_document)
|
||||
ts.Run(WebsiteHosting_no_bucket_in_request_location)
|
||||
ts.Run(WebsiteHosting_private_object_and_error_document)
|
||||
ts.Run(WebsiteHosting_routing_rule_post_request_redirect)
|
||||
ts.Run(WebsiteHosting_routing_rule_pre_request_redirect)
|
||||
ts.Run(WebsiteHosting_routing_rule_prefix_and_error_redirect)
|
||||
ts.Run(WebsiteHosting_routing_rule_no_match_serves_error_document)
|
||||
ts.Run(WebsiteHosting_redirect_all_requests)
|
||||
ts.Run(WebsiteHosting_object_redirect_location)
|
||||
ts.Run(WebsiteHosting_index_document)
|
||||
ts.Run(WebsiteHosting_index_error_document_and_routing_rules)
|
||||
ts.Run(WebsiteHosting_get_cors_headers)
|
||||
@@ -1265,6 +1272,7 @@ func TestPostObject(ts *TestState) {
|
||||
ts.Run(PostObject_success_status_201)
|
||||
ts.Run(PostObject_should_ignore_anything_after_file)
|
||||
ts.Run(PostObject_success_with_meta_properties)
|
||||
ts.Run(PostObject_invalid_website_redirect_location)
|
||||
ts.Run(PostObject_invalid_tagging)
|
||||
ts.Run(PostObject_success_with_tagging)
|
||||
ts.Run(PostObject_success_double_dash_boundary)
|
||||
@@ -1396,6 +1404,7 @@ func GetIntTests() IntTests {
|
||||
"PutObject_should_combine_metadata": PutObject_should_combine_metadata,
|
||||
"PutObject_long_metadata": PutObject_long_metadata,
|
||||
"PutObject_with_metadata": PutObject_with_metadata,
|
||||
"PutObject_invalid_website_redirect_location": PutObject_invalid_website_redirect_location,
|
||||
"PutObject_invalid_credentials": PutObject_invalid_credentials,
|
||||
"PutObject_checksum_algorithm_and_header_mismatch": PutObject_checksum_algorithm_and_header_mismatch,
|
||||
"PutObject_multiple_checksum_headers": PutObject_multiple_checksum_headers,
|
||||
@@ -1600,6 +1609,7 @@ func GetIntTests() IntTests {
|
||||
"CopyObject_non_existing_dir_object": CopyObject_non_existing_dir_object,
|
||||
"CopyObject_should_copy_meta_props": CopyObject_should_copy_meta_props,
|
||||
"CopyObject_should_replace_meta_props": CopyObject_should_replace_meta_props,
|
||||
"CopyObject_invalid_website_redirect_location": CopyObject_invalid_website_redirect_location,
|
||||
"CopyObject_default_content_type_with_replace_metadata": CopyObject_default_content_type_with_replace_metadata,
|
||||
"CopyObject_missing_bucket_lock": CopyObject_missing_bucket_lock,
|
||||
"CopyObject_invalid_legal_hold": CopyObject_invalid_legal_hold,
|
||||
@@ -1634,6 +1644,7 @@ func GetIntTests() IntTests {
|
||||
"CreateMultipartUpload_non_existing_bucket": CreateMultipartUpload_non_existing_bucket,
|
||||
"CreateMultipartUpload_long_metadata": CreateMultipartUpload_long_metadata,
|
||||
"CreateMultipartUpload_with_metadata": CreateMultipartUpload_with_metadata,
|
||||
"CreateMultipartUpload_invalid_website_redirect_location": CreateMultipartUpload_invalid_website_redirect_location,
|
||||
"CreateMultipartUpload_with_tagging": CreateMultipartUpload_with_tagging,
|
||||
"CreateMultipartUpload_with_object_lock": CreateMultipartUpload_with_object_lock,
|
||||
"CreateMultipartUpload_with_object_lock_not_enabled": CreateMultipartUpload_with_object_lock_not_enabled,
|
||||
@@ -1813,6 +1824,8 @@ func GetIntTests() IntTests {
|
||||
"PutBucketWebsite_invalid_redirect_protocol": PutBucketWebsite_invalid_redirect_protocol,
|
||||
"PutBucketWebsite_redirectAll_index_error_routingRules": PutBucketWebsite_redirectAll_index_error_routingRules,
|
||||
"PutBucketWebsite_invalid_routing_rule_protocol": PutBucketWebsite_invalid_routing_rule_protocol,
|
||||
"PutBucketWebsite_empty_routing_rule_condition": PutBucketWebsite_empty_routing_rule_condition,
|
||||
"PutBucketWebsite_empty_routing_rule_redirect": PutBucketWebsite_empty_routing_rule_redirect,
|
||||
"PutBucketWebsite_empty_error_document_key": PutBucketWebsite_empty_error_document_key,
|
||||
"PutBucketWebsite_too_many_routing_rules": PutBucketWebsite_too_many_routing_rules,
|
||||
"PutBucketWebsite_routing_rule_replace_key_and_prefix": PutBucketWebsite_routing_rule_replace_key_and_prefix,
|
||||
@@ -1830,12 +1843,14 @@ func GetIntTests() IntTests {
|
||||
"WebsiteHosting_error_document_served": WebsiteHosting_error_document_served,
|
||||
"WebsiteHosting_error_document_not_found": WebsiteHosting_error_document_not_found,
|
||||
"WebsiteHosting_no_error_document": WebsiteHosting_no_error_document,
|
||||
"WebsiteHosting_no_bucket_in_request_location": WebsiteHosting_no_bucket_in_request_location,
|
||||
"WebsiteHosting_private_object_and_error_document": WebsiteHosting_private_object_and_error_document,
|
||||
"WebsiteHosting_routing_rule_post_request_redirect": WebsiteHosting_routing_rule_post_request_redirect,
|
||||
"WebsiteHosting_routing_rule_pre_request_redirect": WebsiteHosting_routing_rule_pre_request_redirect,
|
||||
"WebsiteHosting_routing_rule_prefix_and_error_redirect": WebsiteHosting_routing_rule_prefix_and_error_redirect,
|
||||
"WebsiteHosting_routing_rule_no_match_serves_error_document": WebsiteHosting_routing_rule_no_match_serves_error_document,
|
||||
"WebsiteHosting_redirect_all_requests": WebsiteHosting_redirect_all_requests,
|
||||
"WebsiteHosting_object_redirect_location": WebsiteHosting_object_redirect_location,
|
||||
"WebsiteHosting_index_document": WebsiteHosting_index_document,
|
||||
"WebsiteHosting_index_error_document_and_routing_rules": WebsiteHosting_index_error_document_and_routing_rules,
|
||||
"WebsiteHosting_get_cors_headers": WebsiteHosting_get_cors_headers,
|
||||
@@ -2147,6 +2162,7 @@ func GetIntTests() IntTests {
|
||||
"PostObject_success_status_201": PostObject_success_status_201,
|
||||
"PostObject_should_ignore_anything_after_file": PostObject_should_ignore_anything_after_file,
|
||||
"PostObject_success_with_meta_properties": PostObject_success_with_meta_properties,
|
||||
"PostObject_invalid_website_redirect_location": PostObject_invalid_website_redirect_location,
|
||||
"PostObject_invalid_tagging": PostObject_invalid_tagging,
|
||||
"PostObject_success_with_tagging": PostObject_success_with_tagging,
|
||||
"PostObject_invalid_checksum_value": PostObject_invalid_checksum_value,
|
||||
|
||||
@@ -2136,14 +2136,15 @@ func compareDelMarkers(d1, d2 []types.DeleteMarkerEntry) bool {
|
||||
}
|
||||
|
||||
type ObjectMetaProps struct {
|
||||
ContentLength int64
|
||||
ContentType string
|
||||
ContentEncoding string
|
||||
ContentDisposition string
|
||||
ContentLanguage string
|
||||
CacheControl string
|
||||
ExpiresString string
|
||||
Metadata map[string]string
|
||||
ContentLength int64
|
||||
ContentType string
|
||||
ContentEncoding string
|
||||
ContentDisposition string
|
||||
ContentLanguage string
|
||||
CacheControl string
|
||||
ExpiresString string
|
||||
WebsiteRedirectLocation string
|
||||
Metadata map[string]string
|
||||
}
|
||||
|
||||
func checkObjectMetaProps(client *s3.Client, bucket, object string, o ObjectMetaProps) error {
|
||||
@@ -2186,6 +2187,9 @@ func checkObjectMetaProps(client *s3.Client, bucket, object string, o ObjectMeta
|
||||
if o.ExpiresString != "" && getString(out.ExpiresString) != o.ExpiresString {
|
||||
return fmt.Errorf("expected Expires %v, instead got %v", o.ExpiresString, getString(out.ExpiresString))
|
||||
}
|
||||
if o.WebsiteRedirectLocation != "" && getString(out.WebsiteRedirectLocation) != o.WebsiteRedirectLocation {
|
||||
return fmt.Errorf("expected WebsiteRedirectLocation %v, instead got %v", o.WebsiteRedirectLocation, getString(out.WebsiteRedirectLocation))
|
||||
}
|
||||
if out.StorageClass != types.StorageClassStandard {
|
||||
return fmt.Errorf("expected the storage class to be %v, instead got %v", types.StorageClassStandard, out.StorageClass)
|
||||
}
|
||||
|
||||
+70
-38
@@ -134,7 +134,7 @@ func registerWebsiteRoutes(app *fiber.App, be backend.Backend, domain string) {
|
||||
func setCORSPreflightHeaders(ctx *fiber.Ctx, allowConfig *auth.CORSAllowanceConfig) {
|
||||
ctx.Set("Access-Control-Allow-Origin", allowConfig.Origin)
|
||||
ctx.Set("Access-Control-Allow-Methods", allowConfig.Methods)
|
||||
ctx.Set("Access-Control-Expose-Headers", corsExposeHeaders(allowConfig.ExposedHeaders))
|
||||
ctx.Set("Access-Control-Expose-Headers", allowConfig.ExposedHeaders)
|
||||
ctx.Set("Access-Control-Allow-Credentials", allowConfig.AllowCredentials)
|
||||
ctx.Set("Access-Control-Allow-Headers", allowConfig.AllowHeaders)
|
||||
ctx.Set("Vary", middlewares.VaryHdr)
|
||||
@@ -143,24 +143,6 @@ func setCORSPreflightHeaders(ctx *fiber.Ctx, allowConfig *auth.CORSAllowanceConf
|
||||
}
|
||||
}
|
||||
|
||||
func corsExposeHeaders(exposed string) string {
|
||||
exposed = strings.TrimSpace(exposed)
|
||||
if exposed == "" {
|
||||
return "ETag"
|
||||
}
|
||||
if exposed == "*" {
|
||||
return exposed
|
||||
}
|
||||
|
||||
for part := range strings.SplitSeq(exposed, ",") {
|
||||
if strings.EqualFold(strings.TrimSpace(part), "ETag") {
|
||||
return exposed
|
||||
}
|
||||
}
|
||||
|
||||
return exposed + ", ETag"
|
||||
}
|
||||
|
||||
func (c *websiteController) MethodNotAllowed(ctx *fiber.Ctx) error {
|
||||
return sendError(ctx, s3err.GetMethodNotAllowedErr(ctx.Method(), s3err.ResourceTypeObject, websiteAllowedMethods))
|
||||
}
|
||||
@@ -188,7 +170,7 @@ func (c *websiteController) serve(ctx *fiber.Ctx, readObject websiteObjectReader
|
||||
}
|
||||
|
||||
if rule := req.config.MatchPrefetchRoutingRule(req.key); rule != nil {
|
||||
return applyRedirect(ctx, &rule.Redirect, rule.Condition, req.key)
|
||||
return applyRedirect(ctx, rule.Redirect, rule.Condition, req.key)
|
||||
}
|
||||
|
||||
resolvedKey := resolveIndexKey(req.key, req.config)
|
||||
@@ -201,7 +183,7 @@ func (c *websiteController) serve(ctx *fiber.Ctx, readObject websiteObjectReader
|
||||
}
|
||||
|
||||
if rule := req.config.MatchPostErrorRoutingRule(req.key, result.StatusCode); rule != nil {
|
||||
return applyRedirect(ctx, &rule.Redirect, rule.Condition, req.key)
|
||||
return applyRedirect(ctx, rule.Redirect, rule.Condition, req.key)
|
||||
}
|
||||
|
||||
return serveWebsiteResult(ctx, req.bucket, req.config, result, readObject)
|
||||
@@ -213,6 +195,8 @@ func (c *websiteController) resolveRequest(ctx *fiber.Ctx) (*websiteRequestInfo,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fmt.Println(bucket)
|
||||
|
||||
key := strings.TrimPrefix(ctx.Path(), "/")
|
||||
if err := validateWebsiteNames(bucket, key); err != nil {
|
||||
return nil, err
|
||||
@@ -260,14 +244,13 @@ func validateWebsiteNames(bucket, key string) error {
|
||||
func (c *websiteController) resolveBucket(ctx *fiber.Ctx) (string, error) {
|
||||
host := ctx.Hostname()
|
||||
if host == "" {
|
||||
ctx.Set("Location", c.noBucketLocation(ctx, host))
|
||||
return "", s3err.GetAPIError(s3err.ErrNoBucketInRequest)
|
||||
}
|
||||
|
||||
// Strip port from host if present. Be careful with IPv6: only strip if the
|
||||
// last colon is not inside brackets.
|
||||
if idx := strings.LastIndex(host, ":"); idx != -1 && !strings.Contains(host[idx:], "]") {
|
||||
host = host[:idx]
|
||||
}
|
||||
host = stripHostPort(host)
|
||||
|
||||
if c.domain == "" {
|
||||
return host, nil
|
||||
@@ -286,9 +269,43 @@ func (c *websiteController) resolveBucket(ctx *fiber.Ctx) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Set("Location", c.noBucketLocation(ctx, ctx.Hostname()))
|
||||
return "", s3err.GetAPIError(s3err.ErrNoBucketInRequest)
|
||||
}
|
||||
|
||||
func (c *websiteController) noBucketLocation(ctx *fiber.Ctx, host string) string {
|
||||
locationHost := c.domain
|
||||
if locationHost == "" {
|
||||
locationHost = stripHostPort(host)
|
||||
}
|
||||
if locationHost == "" {
|
||||
return "/"
|
||||
}
|
||||
if c.domain != "" {
|
||||
if port := hostPort(host); port != "" {
|
||||
locationHost += ":" + port
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s://%s/", ctx.Protocol(), locationHost)
|
||||
}
|
||||
|
||||
func stripHostPort(host string) string {
|
||||
if idx := strings.LastIndex(host, ":"); idx != -1 && !strings.Contains(host[idx:], "]") {
|
||||
return host[:idx]
|
||||
}
|
||||
|
||||
return host
|
||||
}
|
||||
|
||||
func hostPort(host string) string {
|
||||
if idx := strings.LastIndex(host, ":"); idx != -1 && !strings.Contains(host[idx:], "]") {
|
||||
return host[idx+1:]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
type websiteResult struct {
|
||||
Key string
|
||||
StatusCode int
|
||||
@@ -297,9 +314,10 @@ type websiteResult struct {
|
||||
}
|
||||
|
||||
type websiteObject struct {
|
||||
Body io.ReadCloser
|
||||
Headers map[string]*string
|
||||
Metadata map[string]string
|
||||
Body io.ReadCloser
|
||||
Headers map[string]*string
|
||||
Metadata map[string]string
|
||||
WebsiteRedirectLocation *string
|
||||
}
|
||||
|
||||
func resolveIndexKey(key string, config *s3response.WebsiteConfiguration) string {
|
||||
@@ -337,15 +355,16 @@ func (c *websiteController) getObject(ctx *fiber.Ctx, bucket, key string) websit
|
||||
Key: key,
|
||||
StatusCode: http.StatusOK,
|
||||
Object: websiteObject{
|
||||
Body: result.Body,
|
||||
Headers: getObjectHeaders(result),
|
||||
Metadata: result.Metadata,
|
||||
Body: result.Body,
|
||||
Headers: getObjectHeaders(result),
|
||||
Metadata: result.Metadata,
|
||||
WebsiteRedirectLocation: result.WebsiteRedirectLocation,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *websiteController) headObject(ctx *fiber.Ctx, bucket, key string) websiteResult {
|
||||
if err := auth.VerifyPublicAccess(ctx.Context(), c.be, auth.ListBucketAction, auth.PermissionRead, bucket, key); err != nil {
|
||||
if err := auth.VerifyPublicAccess(ctx.Context(), c.be, auth.GetObjectAction, auth.PermissionRead, bucket, key); err != nil {
|
||||
return websiteResult{
|
||||
Key: key,
|
||||
StatusCode: statusCodeFromError(err),
|
||||
@@ -369,8 +388,9 @@ func (c *websiteController) headObject(ctx *fiber.Ctx, bucket, key string) websi
|
||||
Key: key,
|
||||
StatusCode: http.StatusOK,
|
||||
Object: websiteObject{
|
||||
Headers: headObjectHeaders(result),
|
||||
Metadata: result.Metadata,
|
||||
Headers: headObjectHeaders(result),
|
||||
Metadata: result.Metadata,
|
||||
WebsiteRedirectLocation: result.WebsiteRedirectLocation,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -388,16 +408,14 @@ func statusCodeFromError(err error) int {
|
||||
func handleRedirectAll(ctx *fiber.Ctx, redirect *s3response.RedirectAllRequestsTo, key string) error {
|
||||
protocol := redirect.Protocol
|
||||
if protocol == "" {
|
||||
protocol = "http"
|
||||
protocol = ctx.Protocol()
|
||||
}
|
||||
|
||||
location := fmt.Sprintf("%s://%s/%s", protocol, redirect.HostName, key)
|
||||
if query := string(ctx.Request().URI().QueryString()); query != "" {
|
||||
location += "?" + query
|
||||
}
|
||||
ctx.Set("Location", location)
|
||||
_, _ = utils.EnsureRequestIDs(ctx)
|
||||
return ctx.SendStatus(http.StatusMovedPermanently)
|
||||
return sendRedirect(ctx, http.StatusMovedPermanently, location)
|
||||
}
|
||||
|
||||
// applyRedirect constructs and sends a redirect response from a routing rule.
|
||||
@@ -430,8 +448,14 @@ func applyRedirect(ctx *fiber.Ctx, redirect *s3response.Redirect, condition *s3r
|
||||
if query := string(ctx.Request().URI().QueryString()); query != "" {
|
||||
location += "?" + query
|
||||
}
|
||||
return sendRedirect(ctx, httpCode, location)
|
||||
}
|
||||
|
||||
func sendRedirect(ctx *fiber.Ctx, statusCode int, location string) error {
|
||||
ctx.Set("Location", location)
|
||||
return ctx.SendStatus(httpCode)
|
||||
_, _ = utils.EnsureRequestIDs(ctx)
|
||||
ctx.Status(statusCode)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getObjectHeaders(result *s3.GetObjectOutput) map[string]*string {
|
||||
@@ -472,6 +496,14 @@ func headObjectHeaders(result *s3.HeadObjectOutput) map[string]*string {
|
||||
|
||||
func serveWebsiteResult(ctx *fiber.Ctx, bucket string, config *s3response.WebsiteConfiguration, result websiteResult, readObject websiteObjectReader) error {
|
||||
if result.Err == nil {
|
||||
// Precedence: RedirectAllRequestsTo, pre-fetch routing rules, object
|
||||
// redirect metadata, then post-error routing/error documents.
|
||||
if location := backend.GetStringFromPtr(result.Object.WebsiteRedirectLocation); location != "" {
|
||||
if result.Object.Body != nil {
|
||||
_ = result.Object.Body.Close()
|
||||
}
|
||||
return sendRedirect(ctx, http.StatusMovedPermanently, location)
|
||||
}
|
||||
return serveObject(ctx, result.Object, http.StatusOK)
|
||||
}
|
||||
|
||||
|
||||
+125
-29
@@ -36,13 +36,14 @@ import (
|
||||
type websiteTestBackend struct {
|
||||
backend.BackendUnsupported
|
||||
|
||||
websiteConfig []byte
|
||||
corsConfig []byte
|
||||
corsErr error
|
||||
objects map[string]string
|
||||
objectErrors map[string]error
|
||||
public bool
|
||||
calls []string
|
||||
websiteConfig []byte
|
||||
corsConfig []byte
|
||||
corsErr error
|
||||
objects map[string]string
|
||||
objectRedirects map[string]string
|
||||
objectErrors map[string]error
|
||||
public bool
|
||||
calls []string
|
||||
}
|
||||
|
||||
func (b *websiteTestBackend) record(call string) {
|
||||
@@ -105,9 +106,11 @@ func (b *websiteTestBackend) HeadObject(_ context.Context, input *s3.HeadObjectI
|
||||
|
||||
length := int64(len(body))
|
||||
contentType := "text/html"
|
||||
redirectLocation := redirectPtr(b.objectRedirects[*input.Key])
|
||||
return &s3.HeadObjectOutput{
|
||||
ContentLength: &length,
|
||||
ContentType: &contentType,
|
||||
ContentLength: &length,
|
||||
ContentType: &contentType,
|
||||
WebsiteRedirectLocation: redirectLocation,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -126,13 +129,22 @@ func (b *websiteTestBackend) GetObject(_ context.Context, input *s3.GetObjectInp
|
||||
|
||||
length := int64(len(body))
|
||||
contentType := "text/html"
|
||||
redirectLocation := redirectPtr(b.objectRedirects[*input.Key])
|
||||
return &s3.GetObjectOutput{
|
||||
Body: io.NopCloser(strings.NewReader(body)),
|
||||
ContentLength: &length,
|
||||
ContentType: &contentType,
|
||||
Body: io.NopCloser(strings.NewReader(body)),
|
||||
ContentLength: &length,
|
||||
ContentType: &contentType,
|
||||
WebsiteRedirectLocation: redirectLocation,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func redirectPtr(location string) *string {
|
||||
if location == "" {
|
||||
return nil
|
||||
}
|
||||
return &location
|
||||
}
|
||||
|
||||
func TestWebsiteHandlerRoutingRuleOrder(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -147,7 +159,7 @@ func TestWebsiteHandlerRoutingRuleOrder(t *testing.T) {
|
||||
Condition: &s3response.RoutingRuleCondition{
|
||||
KeyPrefixEquals: "old/",
|
||||
},
|
||||
Redirect: s3response.Redirect{
|
||||
Redirect: &s3response.Redirect{
|
||||
ReplaceKeyPrefixWith: "new/",
|
||||
HttpRedirectCode: "301",
|
||||
},
|
||||
@@ -156,7 +168,7 @@ func TestWebsiteHandlerRoutingRuleOrder(t *testing.T) {
|
||||
Condition: &s3response.RoutingRuleCondition{
|
||||
HttpErrorCodeReturnedEquals: "404",
|
||||
},
|
||||
Redirect: s3response.Redirect{
|
||||
Redirect: &s3response.Redirect{
|
||||
ReplaceKeyWith: "error.html",
|
||||
HttpRedirectCode: "302",
|
||||
},
|
||||
@@ -172,7 +184,7 @@ func TestWebsiteHandlerRoutingRuleOrder(t *testing.T) {
|
||||
Condition: &s3response.RoutingRuleCondition{
|
||||
HttpErrorCodeReturnedEquals: "404",
|
||||
},
|
||||
Redirect: s3response.Redirect{
|
||||
Redirect: &s3response.Redirect{
|
||||
ReplaceKeyWith: "error.html",
|
||||
HttpRedirectCode: "302",
|
||||
},
|
||||
@@ -181,7 +193,7 @@ func TestWebsiteHandlerRoutingRuleOrder(t *testing.T) {
|
||||
Condition: &s3response.RoutingRuleCondition{
|
||||
KeyPrefixEquals: "old/",
|
||||
},
|
||||
Redirect: s3response.Redirect{
|
||||
Redirect: &s3response.Redirect{
|
||||
ReplaceKeyPrefixWith: "new/",
|
||||
HttpRedirectCode: "301",
|
||||
},
|
||||
@@ -224,7 +236,7 @@ func TestWebsiteHandlerRoutingRuleBothConditions(t *testing.T) {
|
||||
KeyPrefixEquals: "old/",
|
||||
HttpErrorCodeReturnedEquals: "404",
|
||||
},
|
||||
Redirect: s3response.Redirect{
|
||||
Redirect: &s3response.Redirect{
|
||||
ReplaceKeyPrefixWith: "new/",
|
||||
HttpRedirectCode: "302",
|
||||
},
|
||||
@@ -277,6 +289,61 @@ func TestWebsiteHandlerRoutingRuleBothConditions(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestWebsiteHandlerObjectRedirectLocation(t *testing.T) {
|
||||
be := newWebsiteTestBackend(t, s3response.WebsiteConfiguration{
|
||||
IndexDocument: &s3response.IndexDocument{Suffix: "index.html"},
|
||||
}, map[string]string{
|
||||
"old.html": "old",
|
||||
}, true)
|
||||
be.objectRedirects["old.html"] = "/new.html"
|
||||
|
||||
resp := websiteRequest(t, be, "/old.html")
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusMovedPermanently {
|
||||
t.Fatalf("status = %d, want %d", resp.StatusCode, http.StatusMovedPermanently)
|
||||
}
|
||||
if got := resp.Header.Get("Location"); got != "/new.html" {
|
||||
t.Fatalf("Location = %q, want %q", got, "/new.html")
|
||||
}
|
||||
if got := readBody(t, resp); got != "" {
|
||||
t.Fatalf("body = %q, want empty body", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebsiteHandlerPrefetchRoutingPrecedesObjectRedirect(t *testing.T) {
|
||||
be := newWebsiteTestBackend(t, s3response.WebsiteConfiguration{
|
||||
IndexDocument: &s3response.IndexDocument{Suffix: "index.html"},
|
||||
RoutingRules: []s3response.RoutingRule{
|
||||
{
|
||||
Condition: &s3response.RoutingRuleCondition{
|
||||
KeyPrefixEquals: "old/",
|
||||
},
|
||||
Redirect: &s3response.Redirect{
|
||||
ReplaceKeyPrefixWith: "new/",
|
||||
HttpRedirectCode: "302",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, map[string]string{
|
||||
"old/page.html": "old",
|
||||
}, true)
|
||||
be.objectRedirects["old/page.html"] = "/object-redirect.html"
|
||||
|
||||
resp := websiteRequest(t, be, "/old/page.html")
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusFound {
|
||||
t.Fatalf("status = %d, want %d", resp.StatusCode, http.StatusFound)
|
||||
}
|
||||
if got := resp.Header.Get("Location"); got != "http://site.test/new/page.html" {
|
||||
t.Fatalf("Location = %q", got)
|
||||
}
|
||||
if containsCall(be.calls, "GetObject") {
|
||||
t.Fatal("GetObject was called before pre-fetch routing completed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebsiteHandlerRedirectConstruction(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -290,7 +357,7 @@ func TestWebsiteHandlerRedirectConstruction(t *testing.T) {
|
||||
Condition: &s3response.RoutingRuleCondition{
|
||||
HttpErrorCodeReturnedEquals: "404",
|
||||
},
|
||||
Redirect: s3response.Redirect{
|
||||
Redirect: &s3response.Redirect{
|
||||
ReplaceKeyWith: "error.html",
|
||||
},
|
||||
},
|
||||
@@ -303,7 +370,7 @@ func TestWebsiteHandlerRedirectConstruction(t *testing.T) {
|
||||
Condition: &s3response.RoutingRuleCondition{
|
||||
KeyPrefixEquals: "old/",
|
||||
},
|
||||
Redirect: s3response.Redirect{
|
||||
Redirect: &s3response.Redirect{
|
||||
ReplaceKeyPrefixWith: "new/",
|
||||
},
|
||||
},
|
||||
@@ -316,7 +383,7 @@ func TestWebsiteHandlerRedirectConstruction(t *testing.T) {
|
||||
Condition: &s3response.RoutingRuleCondition{
|
||||
KeyPrefixEquals: "old/",
|
||||
},
|
||||
Redirect: s3response.Redirect{
|
||||
Redirect: &s3response.Redirect{
|
||||
HostName: "example.com",
|
||||
Protocol: "https",
|
||||
ReplaceKeyPrefixWith: "new/",
|
||||
@@ -331,7 +398,7 @@ func TestWebsiteHandlerRedirectConstruction(t *testing.T) {
|
||||
Condition: &s3response.RoutingRuleCondition{
|
||||
KeyPrefixEquals: "old/",
|
||||
},
|
||||
Redirect: s3response.Redirect{
|
||||
Redirect: &s3response.Redirect{
|
||||
ReplaceKeyPrefixWith: "new/",
|
||||
},
|
||||
},
|
||||
@@ -369,7 +436,7 @@ func TestWebsiteHandlerPostErrorRoutingUsesOriginalKeyBeforeIndexExpansion(t *te
|
||||
KeyPrefixEquals: "blog/",
|
||||
HttpErrorCodeReturnedEquals: "404",
|
||||
},
|
||||
Redirect: s3response.Redirect{
|
||||
Redirect: &s3response.Redirect{
|
||||
ReplaceKeyPrefixWith: "archive/",
|
||||
HttpRedirectCode: "302",
|
||||
},
|
||||
@@ -400,7 +467,7 @@ func TestWebsiteHandlerObjectStore5xxBypassesRoutingAndErrorDocument(t *testing.
|
||||
Condition: &s3response.RoutingRuleCondition{
|
||||
HttpErrorCodeReturnedEquals: "500",
|
||||
},
|
||||
Redirect: s3response.Redirect{
|
||||
Redirect: &s3response.Redirect{
|
||||
ReplaceKeyWith: "elsewhere.html",
|
||||
HttpRedirectCode: "302",
|
||||
},
|
||||
@@ -438,7 +505,7 @@ func TestWebsiteHandlerPublicAccessDeniedPreventsObjectReadAndCanRoute(t *testin
|
||||
Condition: &s3response.RoutingRuleCondition{
|
||||
HttpErrorCodeReturnedEquals: "403",
|
||||
},
|
||||
Redirect: s3response.Redirect{
|
||||
Redirect: &s3response.Redirect{
|
||||
ReplaceKeyWith: "denied.html",
|
||||
HttpRedirectCode: "302",
|
||||
},
|
||||
@@ -544,6 +611,28 @@ func TestWebsiteHandlerGetValidatesBucketName(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebsiteHandlerNoBucketInRequestSetsLocation(t *testing.T) {
|
||||
be := newWebsiteTestBackend(t, s3response.WebsiteConfiguration{
|
||||
IndexDocument: &s3response.IndexDocument{Suffix: "index.html"},
|
||||
}, nil, true)
|
||||
|
||||
resp := websiteRequestWithDomainHostAndHeaders(t, be, "site.test", http.MethodGet, "wrong.test:8080", "/", nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusMovedPermanently {
|
||||
t.Fatalf("status = %d, want %d", resp.StatusCode, http.StatusMovedPermanently)
|
||||
}
|
||||
if got := resp.Header.Get("Location"); got != "http://site.test:8080/" {
|
||||
t.Fatalf("Location = %q, want %q", got, "http://site.test:8080/")
|
||||
}
|
||||
if got := resp.Header.Get("x-amz-error-code"); got != "WebsiteRedirect" {
|
||||
t.Fatalf("x-amz-error-code = %q, want %q", got, "WebsiteRedirect")
|
||||
}
|
||||
if len(be.calls) != 0 {
|
||||
t.Fatalf("request without bucket should not call backend, got calls: %v", be.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebsiteHandlerHeadValidatesObjectName(t *testing.T) {
|
||||
be := newWebsiteTestBackend(t, s3response.WebsiteConfiguration{
|
||||
IndexDocument: &s3response.IndexDocument{Suffix: "index.html"},
|
||||
@@ -707,7 +796,7 @@ func TestWebsiteHandlerOptionsAccessGranted(t *testing.T) {
|
||||
if got := resp.Header.Get("Access-Control-Allow-Headers"); got != "content-type, x-amz-date" {
|
||||
t.Fatalf("Access-Control-Allow-Headers = %q", got)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Expose-Headers"); got != "Content-Length, ETag" {
|
||||
if got := resp.Header.Get("Access-Control-Expose-Headers"); got != "Content-Length" {
|
||||
t.Fatalf("Access-Control-Expose-Headers = %q", got)
|
||||
}
|
||||
if got := resp.Header.Get("Access-Control-Max-Age"); got != "42" {
|
||||
@@ -893,9 +982,10 @@ func newWebsiteTestBackend(t *testing.T, config s3response.WebsiteConfiguration,
|
||||
}
|
||||
|
||||
return &websiteTestBackend{
|
||||
websiteConfig: data,
|
||||
objects: objects,
|
||||
public: public,
|
||||
websiteConfig: data,
|
||||
objects: objects,
|
||||
objectRedirects: map[string]string{},
|
||||
public: public,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -920,10 +1010,16 @@ func websiteRequestWithHeaders(t *testing.T, be backend.Backend, method, path st
|
||||
func websiteRequestWithHostAndHeaders(t *testing.T, be backend.Backend, method, host, path string, headers map[string]string) *http.Response {
|
||||
t.Helper()
|
||||
|
||||
return websiteRequestWithDomainHostAndHeaders(t, be, "", method, host, path, headers)
|
||||
}
|
||||
|
||||
func websiteRequestWithDomainHostAndHeaders(t *testing.T, be backend.Backend, domain, method, host, path string, headers map[string]string) *http.Response {
|
||||
t.Helper()
|
||||
|
||||
app := fiber.New(fiber.Config{
|
||||
ServerHeader: "VERSITYGW",
|
||||
})
|
||||
registerWebsiteRoutes(app, be, "")
|
||||
registerWebsiteRoutes(app, be, domain)
|
||||
|
||||
req := httptest.NewRequest(method, path, nil)
|
||||
req.Host = host
|
||||
|
||||
Reference in New Issue
Block a user