mirror of
https://github.com/versity/versitygw.git
synced 2026-09-20 15:04:27 +00:00
Add WebsiteConfiguration types, validation, and S3 error codes
Add S3 bucket website configuration types with XML serialization support in s3response/website.go. Includes IndexDocument, ErrorDocument, RedirectAllRequestsTo, and RoutingRules with full validation matching AWS S3 behavior. Add corresponding S3 error codes: ErrNoSuchWebsiteConfiguration, ErrInvalidWebsiteConfiguration, ErrInvalidWebsiteSuffix, and ErrInvalidWebsiteRedirectCode. Unit tests cover validation logic, XML round-trip, and parsing. Signed-off-by: Marc Singer <marc@singer.gg> Add website backend interface and implementations for posix and s3proxy Add PutBucketWebsite, GetBucketWebsite, and DeleteBucketWebsite methods to the Backend interface with BackendUnsupported stubs that return ErrNotImplemented. Posix backend stores website config as a metadata attribute (key: 'website') following the same pattern as CORS. ScoutFS inherits via embedding. S3Proxy backend stores website config in the metadata bucket with prefix 'vgw-meta-website-', consistent with existing ACL/policy/CORS metadata storage. Returns ErrNoSuchWebsiteConfiguration when not found. Signed-off-by: Marc Singer <marc@singer.gg> Add website API controllers and wire into router Add PutBucketWebsite, GetBucketWebsite, and DeleteBucketWebsite controller methods following the same pattern as CORS. Controllers parse and validate WebsiteConfiguration XML, check IAM authorization, and delegate to the backend. Replace the three HandleErrorRoute(ErrNotImplemented) stubs in the router with the new controller methods. Regenerate the backend mock to include the new interface methods. Signed-off-by: Marc Singer <marc@singer.gg> Add website index document middleware and wire into router Add ResolveWebsiteIndex middleware that rewrites directory-like object keys (empty or ending with /) to include the IndexDocument suffix when website hosting is enabled. Also handles RedirectAllRequestsTo by returning 301. Wire the middleware into both GetObject and HeadObject handler chains in the router, positioned after BucketObjectNameValidator and before auth. Signed-off-by: Marc Singer <marc@singer.gg>
This commit is contained in:
@@ -56,6 +56,9 @@ var _ backend.Backend = &BackendMock{}
|
||||
// DeleteBucketTaggingFunc: func(contextMoqParam context.Context, bucket string) error {
|
||||
// panic("mock out the DeleteBucketTagging method")
|
||||
// },
|
||||
// DeleteBucketWebsiteFunc: func(contextMoqParam context.Context, bucket string) error {
|
||||
// panic("mock out the DeleteBucketWebsite method")
|
||||
// },
|
||||
// DeleteObjectFunc: func(contextMoqParam context.Context, deleteObjectInput *s3.DeleteObjectInput) (*s3.DeleteObjectOutput, error) {
|
||||
// panic("mock out the DeleteObject method")
|
||||
// },
|
||||
@@ -83,6 +86,9 @@ var _ backend.Backend = &BackendMock{}
|
||||
// GetBucketVersioningFunc: func(contextMoqParam context.Context, bucket string) (s3response.GetBucketVersioningOutput, error) {
|
||||
// panic("mock out the GetBucketVersioning method")
|
||||
// },
|
||||
// GetBucketWebsiteFunc: func(contextMoqParam context.Context, bucket string) ([]byte, error) {
|
||||
// panic("mock out the GetBucketWebsite method")
|
||||
// },
|
||||
// GetObjectFunc: func(contextMoqParam context.Context, getObjectInput *s3.GetObjectInput) (*s3.GetObjectOutput, error) {
|
||||
// panic("mock out the GetObject method")
|
||||
// },
|
||||
@@ -152,6 +158,9 @@ var _ backend.Backend = &BackendMock{}
|
||||
// PutBucketVersioningFunc: func(contextMoqParam context.Context, bucket string, status types.BucketVersioningStatus) error {
|
||||
// panic("mock out the PutBucketVersioning method")
|
||||
// },
|
||||
// PutBucketWebsiteFunc: func(contextMoqParam context.Context, bucket string, website []byte) error {
|
||||
// panic("mock out the PutBucketWebsite method")
|
||||
// },
|
||||
// PutObjectFunc: func(contextMoqParam context.Context, putObjectInput s3response.PutObjectInput) (s3response.PutObjectOutput, error) {
|
||||
// panic("mock out the PutObject method")
|
||||
// },
|
||||
@@ -228,6 +237,9 @@ type BackendMock struct {
|
||||
// DeleteBucketTaggingFunc mocks the DeleteBucketTagging method.
|
||||
DeleteBucketTaggingFunc func(contextMoqParam context.Context, bucket string) error
|
||||
|
||||
// DeleteBucketWebsiteFunc mocks the DeleteBucketWebsite method.
|
||||
DeleteBucketWebsiteFunc func(contextMoqParam context.Context, bucket string) error
|
||||
|
||||
// DeleteObjectFunc mocks the DeleteObject method.
|
||||
DeleteObjectFunc func(contextMoqParam context.Context, deleteObjectInput *s3.DeleteObjectInput) (*s3.DeleteObjectOutput, error)
|
||||
|
||||
@@ -255,6 +267,9 @@ type BackendMock struct {
|
||||
// GetBucketVersioningFunc mocks the GetBucketVersioning method.
|
||||
GetBucketVersioningFunc func(contextMoqParam context.Context, bucket string) (s3response.GetBucketVersioningOutput, error)
|
||||
|
||||
// GetBucketWebsiteFunc mocks the GetBucketWebsite method.
|
||||
GetBucketWebsiteFunc func(contextMoqParam context.Context, bucket string) ([]byte, error)
|
||||
|
||||
// GetObjectFunc mocks the GetObject method.
|
||||
GetObjectFunc func(contextMoqParam context.Context, getObjectInput *s3.GetObjectInput) (*s3.GetObjectOutput, error)
|
||||
|
||||
@@ -324,6 +339,9 @@ type BackendMock struct {
|
||||
// PutBucketVersioningFunc mocks the PutBucketVersioning method.
|
||||
PutBucketVersioningFunc func(contextMoqParam context.Context, bucket string, status types.BucketVersioningStatus) error
|
||||
|
||||
// PutBucketWebsiteFunc mocks the PutBucketWebsite method.
|
||||
PutBucketWebsiteFunc func(contextMoqParam context.Context, bucket string, website []byte) error
|
||||
|
||||
// PutObjectFunc mocks the PutObject method.
|
||||
PutObjectFunc func(contextMoqParam context.Context, putObjectInput s3response.PutObjectInput) (s3response.PutObjectOutput, error)
|
||||
|
||||
@@ -443,6 +461,13 @@ type BackendMock struct {
|
||||
// Bucket is the bucket argument value.
|
||||
Bucket string
|
||||
}
|
||||
// DeleteBucketWebsite holds details about calls to the DeleteBucketWebsite method.
|
||||
DeleteBucketWebsite []struct {
|
||||
// ContextMoqParam is the contextMoqParam argument value.
|
||||
ContextMoqParam context.Context
|
||||
// Bucket is the bucket argument value.
|
||||
Bucket string
|
||||
}
|
||||
// DeleteObject holds details about calls to the DeleteObject method.
|
||||
DeleteObject []struct {
|
||||
// ContextMoqParam is the contextMoqParam argument value.
|
||||
@@ -510,6 +535,13 @@ type BackendMock struct {
|
||||
// Bucket is the bucket argument value.
|
||||
Bucket string
|
||||
}
|
||||
// GetBucketWebsite holds details about calls to the GetBucketWebsite method.
|
||||
GetBucketWebsite []struct {
|
||||
// ContextMoqParam is the contextMoqParam argument value.
|
||||
ContextMoqParam context.Context
|
||||
// Bucket is the bucket argument value.
|
||||
Bucket string
|
||||
}
|
||||
// GetObject holds details about calls to the GetObject method.
|
||||
GetObject []struct {
|
||||
// ContextMoqParam is the contextMoqParam argument value.
|
||||
@@ -693,6 +725,15 @@ type BackendMock struct {
|
||||
// Status is the status argument value.
|
||||
Status types.BucketVersioningStatus
|
||||
}
|
||||
// PutBucketWebsite holds details about calls to the PutBucketWebsite method.
|
||||
PutBucketWebsite []struct {
|
||||
// ContextMoqParam is the contextMoqParam argument value.
|
||||
ContextMoqParam context.Context
|
||||
// Bucket is the bucket argument value.
|
||||
Bucket string
|
||||
// Website is the website argument value.
|
||||
Website []byte
|
||||
}
|
||||
// PutObject holds details about calls to the PutObject method.
|
||||
PutObject []struct {
|
||||
// ContextMoqParam is the contextMoqParam argument value.
|
||||
@@ -801,6 +842,7 @@ type BackendMock struct {
|
||||
lockDeleteBucketOwnershipControls sync.RWMutex
|
||||
lockDeleteBucketPolicy sync.RWMutex
|
||||
lockDeleteBucketTagging sync.RWMutex
|
||||
lockDeleteBucketWebsite sync.RWMutex
|
||||
lockDeleteObject sync.RWMutex
|
||||
lockDeleteObjectTagging sync.RWMutex
|
||||
lockDeleteObjects sync.RWMutex
|
||||
@@ -810,6 +852,7 @@ type BackendMock struct {
|
||||
lockGetBucketPolicy sync.RWMutex
|
||||
lockGetBucketTagging sync.RWMutex
|
||||
lockGetBucketVersioning sync.RWMutex
|
||||
lockGetBucketWebsite sync.RWMutex
|
||||
lockGetObject sync.RWMutex
|
||||
lockGetObjectAcl sync.RWMutex
|
||||
lockGetObjectAttributes sync.RWMutex
|
||||
@@ -833,6 +876,7 @@ type BackendMock struct {
|
||||
lockPutBucketPolicy sync.RWMutex
|
||||
lockPutBucketTagging sync.RWMutex
|
||||
lockPutBucketVersioning sync.RWMutex
|
||||
lockPutBucketWebsite sync.RWMutex
|
||||
lockPutObject sync.RWMutex
|
||||
lockPutObjectAcl sync.RWMutex
|
||||
lockPutObjectLegalHold sync.RWMutex
|
||||
@@ -1251,6 +1295,42 @@ func (mock *BackendMock) DeleteBucketTaggingCalls() []struct {
|
||||
return calls
|
||||
}
|
||||
|
||||
// DeleteBucketWebsite calls DeleteBucketWebsiteFunc.
|
||||
func (mock *BackendMock) DeleteBucketWebsite(contextMoqParam context.Context, bucket string) error {
|
||||
if mock.DeleteBucketWebsiteFunc == nil {
|
||||
panic("BackendMock.DeleteBucketWebsiteFunc: method is nil but Backend.DeleteBucketWebsite was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
ContextMoqParam context.Context
|
||||
Bucket string
|
||||
}{
|
||||
ContextMoqParam: contextMoqParam,
|
||||
Bucket: bucket,
|
||||
}
|
||||
mock.lockDeleteBucketWebsite.Lock()
|
||||
mock.calls.DeleteBucketWebsite = append(mock.calls.DeleteBucketWebsite, callInfo)
|
||||
mock.lockDeleteBucketWebsite.Unlock()
|
||||
return mock.DeleteBucketWebsiteFunc(contextMoqParam, bucket)
|
||||
}
|
||||
|
||||
// DeleteBucketWebsiteCalls gets all the calls that were made to DeleteBucketWebsite.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedBackend.DeleteBucketWebsiteCalls())
|
||||
func (mock *BackendMock) DeleteBucketWebsiteCalls() []struct {
|
||||
ContextMoqParam context.Context
|
||||
Bucket string
|
||||
} {
|
||||
var calls []struct {
|
||||
ContextMoqParam context.Context
|
||||
Bucket string
|
||||
}
|
||||
mock.lockDeleteBucketWebsite.RLock()
|
||||
calls = mock.calls.DeleteBucketWebsite
|
||||
mock.lockDeleteBucketWebsite.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// DeleteObject calls DeleteObjectFunc.
|
||||
func (mock *BackendMock) DeleteObject(contextMoqParam context.Context, deleteObjectInput *s3.DeleteObjectInput) (*s3.DeleteObjectOutput, error) {
|
||||
if mock.DeleteObjectFunc == nil {
|
||||
@@ -1583,6 +1663,42 @@ func (mock *BackendMock) GetBucketVersioningCalls() []struct {
|
||||
return calls
|
||||
}
|
||||
|
||||
// GetBucketWebsite calls GetBucketWebsiteFunc.
|
||||
func (mock *BackendMock) GetBucketWebsite(contextMoqParam context.Context, bucket string) ([]byte, error) {
|
||||
if mock.GetBucketWebsiteFunc == nil {
|
||||
panic("BackendMock.GetBucketWebsiteFunc: method is nil but Backend.GetBucketWebsite was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
ContextMoqParam context.Context
|
||||
Bucket string
|
||||
}{
|
||||
ContextMoqParam: contextMoqParam,
|
||||
Bucket: bucket,
|
||||
}
|
||||
mock.lockGetBucketWebsite.Lock()
|
||||
mock.calls.GetBucketWebsite = append(mock.calls.GetBucketWebsite, callInfo)
|
||||
mock.lockGetBucketWebsite.Unlock()
|
||||
return mock.GetBucketWebsiteFunc(contextMoqParam, bucket)
|
||||
}
|
||||
|
||||
// GetBucketWebsiteCalls gets all the calls that were made to GetBucketWebsite.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedBackend.GetBucketWebsiteCalls())
|
||||
func (mock *BackendMock) GetBucketWebsiteCalls() []struct {
|
||||
ContextMoqParam context.Context
|
||||
Bucket string
|
||||
} {
|
||||
var calls []struct {
|
||||
ContextMoqParam context.Context
|
||||
Bucket string
|
||||
}
|
||||
mock.lockGetBucketWebsite.RLock()
|
||||
calls = mock.calls.GetBucketWebsite
|
||||
mock.lockGetBucketWebsite.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// GetObject calls GetObjectFunc.
|
||||
func (mock *BackendMock) GetObject(contextMoqParam context.Context, getObjectInput *s3.GetObjectInput) (*s3.GetObjectOutput, error) {
|
||||
if mock.GetObjectFunc == nil {
|
||||
@@ -2455,6 +2571,46 @@ func (mock *BackendMock) PutBucketVersioningCalls() []struct {
|
||||
return calls
|
||||
}
|
||||
|
||||
// PutBucketWebsite calls PutBucketWebsiteFunc.
|
||||
func (mock *BackendMock) PutBucketWebsite(contextMoqParam context.Context, bucket string, website []byte) error {
|
||||
if mock.PutBucketWebsiteFunc == nil {
|
||||
panic("BackendMock.PutBucketWebsiteFunc: method is nil but Backend.PutBucketWebsite was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
ContextMoqParam context.Context
|
||||
Bucket string
|
||||
Website []byte
|
||||
}{
|
||||
ContextMoqParam: contextMoqParam,
|
||||
Bucket: bucket,
|
||||
Website: website,
|
||||
}
|
||||
mock.lockPutBucketWebsite.Lock()
|
||||
mock.calls.PutBucketWebsite = append(mock.calls.PutBucketWebsite, callInfo)
|
||||
mock.lockPutBucketWebsite.Unlock()
|
||||
return mock.PutBucketWebsiteFunc(contextMoqParam, bucket, website)
|
||||
}
|
||||
|
||||
// PutBucketWebsiteCalls gets all the calls that were made to PutBucketWebsite.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedBackend.PutBucketWebsiteCalls())
|
||||
func (mock *BackendMock) PutBucketWebsiteCalls() []struct {
|
||||
ContextMoqParam context.Context
|
||||
Bucket string
|
||||
Website []byte
|
||||
} {
|
||||
var calls []struct {
|
||||
ContextMoqParam context.Context
|
||||
Bucket string
|
||||
Website []byte
|
||||
}
|
||||
mock.lockPutBucketWebsite.RLock()
|
||||
calls = mock.calls.PutBucketWebsite
|
||||
mock.lockPutBucketWebsite.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// PutObject calls PutObjectFunc.
|
||||
func (mock *BackendMock) PutObject(contextMoqParam context.Context, putObjectInput s3response.PutObjectInput) (s3response.PutObjectOutput, error) {
|
||||
if mock.PutObjectFunc == nil {
|
||||
|
||||
@@ -162,6 +162,42 @@ func (c S3ApiController) DeleteBucketCors(ctx *fiber.Ctx) (*Response, error) {
|
||||
}, err
|
||||
}
|
||||
|
||||
func (c S3ApiController) DeleteBucketWebsite(ctx *fiber.Ctx) (*Response, error) {
|
||||
bucket := ctx.Params("bucket")
|
||||
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
|
||||
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
|
||||
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
|
||||
IsBucketPublic := utils.ContextKeyPublicBucket.IsSet(ctx)
|
||||
|
||||
err := auth.VerifyAccess(ctx.Context(), c.be,
|
||||
auth.AccessOptions{
|
||||
Readonly: c.readonly,
|
||||
Acl: parsedAcl,
|
||||
AclPermission: auth.PermissionWrite,
|
||||
IsRoot: isRoot,
|
||||
Acc: acct,
|
||||
Bucket: bucket,
|
||||
Action: auth.PutBucketWebsiteAction,
|
||||
IsPublicRequest: IsBucketPublic,
|
||||
DisableACL: c.disableACL,
|
||||
})
|
||||
if err != nil {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
err = c.be.DeleteBucketWebsite(ctx.Context(), bucket)
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
Status: http.StatusNoContent,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
func (c S3ApiController) DeleteBucket(ctx *fiber.Ctx) (*Response, error) {
|
||||
bucket := ctx.Params("bucket")
|
||||
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
|
||||
|
||||
@@ -206,6 +206,50 @@ func (c S3ApiController) GetBucketCors(ctx *fiber.Ctx) (*Response, error) {
|
||||
}, err
|
||||
}
|
||||
|
||||
func (c S3ApiController) GetBucketWebsite(ctx *fiber.Ctx) (*Response, error) {
|
||||
bucket := ctx.Params("bucket")
|
||||
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
|
||||
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
|
||||
isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
|
||||
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
|
||||
|
||||
err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{
|
||||
Readonly: c.readonly,
|
||||
Acl: parsedAcl,
|
||||
AclPermission: auth.PermissionRead,
|
||||
IsRoot: isRoot,
|
||||
Acc: acct,
|
||||
Bucket: bucket,
|
||||
Action: auth.GetBucketWebsiteAction,
|
||||
IsPublicRequest: isPublicBucket,
|
||||
DisableACL: c.disableACL,
|
||||
})
|
||||
if err != nil {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
data, err := c.be.GetBucketWebsite(ctx.Context(), bucket)
|
||||
if err != nil {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
output, err := s3response.ParseWebsiteConfigOutput(data)
|
||||
return &Response{
|
||||
Data: output,
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
func (c S3ApiController) GetBucketPolicy(ctx *fiber.Ctx) (*Response, error) {
|
||||
bucket := ctx.Params("bucket")
|
||||
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
|
||||
|
||||
@@ -287,6 +287,62 @@ func (c S3ApiController) PutBucketCors(ctx *fiber.Ctx) (*Response, error) {
|
||||
}, err
|
||||
}
|
||||
|
||||
func (c S3ApiController) PutBucketWebsite(ctx *fiber.Ctx) (*Response, error) {
|
||||
bucket := ctx.Params("bucket")
|
||||
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
|
||||
acct := utils.ContextKeyAccount.Get(ctx).(auth.Account)
|
||||
isRoot := utils.ContextKeyIsRoot.Get(ctx).(bool)
|
||||
isPublicBucket := utils.ContextKeyPublicBucket.IsSet(ctx)
|
||||
|
||||
err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{
|
||||
Readonly: c.readonly,
|
||||
Acl: parsedAcl,
|
||||
AclPermission: auth.PermissionWrite,
|
||||
IsRoot: isRoot,
|
||||
Acc: acct,
|
||||
Bucket: bucket,
|
||||
Action: auth.PutBucketWebsiteAction,
|
||||
IsPublicRequest: isPublicBucket,
|
||||
DisableACL: c.disableACL,
|
||||
})
|
||||
if err != nil {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
body := ctx.Body()
|
||||
|
||||
var websiteConfig s3response.WebsiteConfiguration
|
||||
err = xml.Unmarshal(body, &websiteConfig)
|
||||
if err != nil {
|
||||
debuglogger.Logf("invalid website config request body: %v", err)
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, s3err.GetAPIError(s3err.ErrMalformedXML)
|
||||
}
|
||||
|
||||
err = websiteConfig.Validate()
|
||||
if err != nil {
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
err = c.be.PutBucketWebsite(ctx.Context(), bucket, body)
|
||||
return &Response{
|
||||
MetaOpts: &MetaOptions{
|
||||
BucketOwner: parsedAcl.Owner,
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
func (c S3ApiController) PutBucketPolicy(ctx *fiber.Ctx) (*Response, error) {
|
||||
bucket := ctx.Params("bucket")
|
||||
parsedAcl := utils.ContextKeyParsedAcl.Get(ctx).(auth.ACL)
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// 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 middlewares
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/versity/versitygw/backend"
|
||||
"github.com/versity/versitygw/s3api/utils"
|
||||
"github.com/versity/versitygw/s3response"
|
||||
)
|
||||
|
||||
// ResolveWebsiteIndex rewrites directory-like object keys to include the
|
||||
// configured IndexDocument suffix when website hosting is enabled for the
|
||||
// bucket. It also handles RedirectAllRequestsTo by returning a 301 redirect.
|
||||
//
|
||||
// This middleware should be placed in the GetObject handler chain before
|
||||
// authentication and the controller.
|
||||
func ResolveWebsiteIndex(be backend.Backend) fiber.Handler {
|
||||
return func(ctx *fiber.Ctx) error {
|
||||
if utils.ContextKeySkip.IsSet(ctx) {
|
||||
return ctx.Next()
|
||||
}
|
||||
|
||||
bucket := ctx.Params("bucket")
|
||||
if bucket == "" {
|
||||
return ctx.Next()
|
||||
}
|
||||
|
||||
key := ctx.Params("*1")
|
||||
|
||||
// Only process directory-like keys (empty or ending with /)
|
||||
if key != "" && !strings.HasSuffix(key, "/") {
|
||||
return ctx.Next()
|
||||
}
|
||||
|
||||
// Reject path traversal attempts
|
||||
if strings.Contains(key, "..") {
|
||||
return ctx.Next()
|
||||
}
|
||||
|
||||
data, err := be.GetBucketWebsite(ctx.Context(), bucket)
|
||||
if err != nil {
|
||||
// No website config: pass through to normal handling
|
||||
return ctx.Next()
|
||||
}
|
||||
|
||||
var config s3response.WebsiteConfiguration
|
||||
if xmlErr := xml.Unmarshal(data, &config); xmlErr != nil {
|
||||
return ctx.Next()
|
||||
}
|
||||
|
||||
// Handle RedirectAllRequestsTo
|
||||
if config.RedirectAllRequestsTo != nil {
|
||||
return redirectAll(ctx, config.RedirectAllRequestsTo, key)
|
||||
}
|
||||
|
||||
// Rewrite directory-like keys to include index document suffix
|
||||
if config.IndexDocument != nil && config.IndexDocument.Suffix != "" {
|
||||
newKey := key + config.IndexDocument.Suffix
|
||||
newPath := fmt.Sprintf("/%s/%s", bucket, newKey)
|
||||
ctx.Request().URI().SetPath(newPath)
|
||||
}
|
||||
|
||||
return ctx.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func redirectAll(ctx *fiber.Ctx, redirect *s3response.RedirectAllRequestsTo, key string) error {
|
||||
protocol := redirect.Protocol
|
||||
if protocol == "" {
|
||||
protocol = "https"
|
||||
}
|
||||
|
||||
location := fmt.Sprintf("%s://%s/%s", protocol, redirect.HostName, key)
|
||||
ctx.Set("Location", location)
|
||||
return ctx.SendStatus(http.StatusMovedPermanently)
|
||||
}
|
||||
+5
-3
@@ -444,7 +444,7 @@ func (sa *S3ApiRouter) Init() {
|
||||
bucketRouter.Put("",
|
||||
middlewares.MatchQueryArgs("website"),
|
||||
controllers.ProcessHandlers(
|
||||
ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)),
|
||||
ctrl.PutBucketWebsite,
|
||||
metrics.ActionPutBucketWebsite,
|
||||
services,
|
||||
middlewares.BucketObjectNameValidator(),
|
||||
@@ -665,7 +665,7 @@ func (sa *S3ApiRouter) Init() {
|
||||
bucketRouter.Delete("",
|
||||
middlewares.MatchQueryArgs("website"),
|
||||
controllers.ProcessHandlers(
|
||||
ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)),
|
||||
ctrl.DeleteBucketWebsite,
|
||||
metrics.ActionDeleteBucketWebsite,
|
||||
services,
|
||||
middlewares.BucketObjectNameValidator(),
|
||||
@@ -1056,7 +1056,7 @@ func (sa *S3ApiRouter) Init() {
|
||||
bucketRouter.Get("",
|
||||
middlewares.MatchQueryArgs("website"),
|
||||
controllers.ProcessHandlers(
|
||||
ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrNotImplemented)),
|
||||
ctrl.GetBucketWebsite,
|
||||
metrics.ActionGetBucketWebsite,
|
||||
services,
|
||||
middlewares.BucketObjectNameValidator(),
|
||||
@@ -1152,6 +1152,7 @@ func (sa *S3ApiRouter) Init() {
|
||||
metrics.ActionHeadObject,
|
||||
services,
|
||||
middlewares.BucketObjectNameValidator(),
|
||||
middlewares.ResolveWebsiteIndex(sa.be),
|
||||
middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionHeadObject, auth.GetObjectAction, auth.PermissionRead, sa.region, false),
|
||||
middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false),
|
||||
middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, false, false),
|
||||
@@ -1269,6 +1270,7 @@ func (sa *S3ApiRouter) Init() {
|
||||
metrics.ActionGetObject,
|
||||
services,
|
||||
middlewares.BucketObjectNameValidator(),
|
||||
middlewares.ResolveWebsiteIndex(sa.be),
|
||||
middlewares.AuthorizePublicBucketAccess(sa.be, metrics.ActionGetObject, auth.GetObjectAction, auth.PermissionRead, sa.region, false),
|
||||
middlewares.VerifyPresignedV4Signature(sa.root, sa.iam, sa.region, false),
|
||||
middlewares.VerifyV4Signature(sa.root, sa.iam, sa.region, false, true, false),
|
||||
|
||||
Reference in New Issue
Block a user