Files
versitygw/s3api/middlewares/public-bucket.go
T
niksis02 d383e1f32f fix: track how the request body ended before closing the connection
The body stream is now wrapped in a `bodyStreamTracker` before the handler touches it, which remembers the stream's first terminal result rather than asking fasthttp a second, unsafe question. `io.EOF` means the body was read out in full and the connection is still in sync; no terminal result means the handler stopped partway, so the leftovers are drained the way a
`Content-Length` body already was; a framing error means nothing decodable is left and the connection cannot carry another request.

`fasthttp.Request.SetBodyStream` cannot install the wrapper, as it releases the current `*requestStream` back to its pool, so `requestBodyStream` is now the accessor every body reader takes the stream from.

Broken framing no longer gives up on draining either. The connection is closed either way, so a bounded read off the raw socket costs nothing and lets the client finish its write and read the S3 error instead of a reset.
2026-09-09 23:30:32 +04:00

137 lines
4.5 KiB
Go

// Copyright 2023 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package middlewares
import (
"crypto/sha256"
"encoding/hex"
"io"
"strings"
"github.com/gofiber/fiber/v3"
"github.com/versity/versitygw/auth"
"github.com/versity/versitygw/backend"
"github.com/versity/versitygw/metrics"
"github.com/versity/versitygw/s3api/utils"
"github.com/versity/versitygw/s3err"
)
// AuthorizePublicBucketAccess checks if the bucket grants public
// access to anonymous requesters
func AuthorizePublicBucketAccess(be backend.Backend, s3action string, policyPermission auth.Action, permission auth.Permission, region string, streamBody bool) fiber.Handler {
return func(ctx fiber.Ctx) error {
// skip for authenticated requests
if utils.IsPresignedURLAuth(ctx) || ctx.Get("Authorization") != "" || utils.ContextKeyAuthenticated.IsSet(ctx) {
return nil
}
switch s3action {
case metrics.ActionListAllMyBuckets:
return s3err.GetAPIError(s3err.ErrAccessDenied)
case metrics.ActionGetBucketOwnershipControls:
return s3err.GetAPIError(s3err.ErrAnonymousGetBucketOwnership)
case metrics.ActionPutBucketOwnershipControls, metrics.ActionDeleteBucketOwnershipControls:
return s3err.GetAPIError(s3err.ErrAnonymousPutBucketOwnership)
case metrics.ActionPutBucketAcl, metrics.ActionPutObjectAcl, metrics.ActionSelectObjectContent, metrics.ActionCreateBucket:
return s3err.GetAPIError(s3err.ErrAnonymousRequest)
case metrics.ActionCopyObject:
return s3err.GetAPIError(s3err.ErrAnonymousCopyObject)
case metrics.ActionCreateMultipartUpload:
return s3err.GetAPIError(s3err.ErrAnonymousCreateMp)
case metrics.ActionUploadPartCopy, metrics.ActionDeleteObjects:
// TODO: should be fixed with https://github.com/versity/versitygw/issues/1327
// TODO: should be fixed with https://github.com/versity/versitygw/issues/1338
return s3err.GetAPIError(s3err.ErrAccessDenied)
}
bucket, object := parsePath(ctx.Path())
err := auth.VerifyPublicAccess(ctx, be, policyPermission, permission, bucket, object)
if err != nil {
if s3action == metrics.ActionHeadBucket {
// add the bucket region header for HeadBucket
// if anonymous access is denied
ctx.Response().Header.Add("x-amz-bucket-region", region)
}
return err
}
// at this point the bucket is considered as public
// as public access is granted
utils.ContextKeyPublicBucket.Set(ctx, true)
payloadHash := ctx.Get("X-Amz-Content-Sha256")
err = utils.IsAnonymousPayloadHashSupported(payloadHash)
if err != nil {
return err
}
if streamBody {
if utils.IsUnsignedStreamingPayload(payloadHash) {
cLength, err := utils.ParseDecodedContentLength(ctx)
if err != nil {
return err
}
// stack an unsigned streaming payload reader
checksumType, err := utils.ExtractChecksumType(ctx)
if err != nil {
return err
}
wrapBodyReader(ctx, func(r io.Reader) io.Reader {
var cr io.Reader
cr, err = utils.NewUnsignedChunkReader(r, checksumType, cLength)
return cr
})
return err
} else if utils.IsUnsignedPaylod(payloadHash) {
// for UNSIGNED-PAYLOD simply store the body reader in context locals
utils.ContextKeyBodyReader.Set(ctx, requestBodyStream(ctx))
return nil
} else {
// stack a hash reader to calculated the payload sha256 hash
wrapBodyReader(ctx, func(r io.Reader) io.Reader {
var cr io.Reader
cr, err = utils.NewHashReader(r, payloadHash, utils.HashTypeSha256Hex)
return cr
})
return err
}
}
if payloadHash != "" {
// Calculate the hash of the request payload
hashedPayload := sha256.Sum256(ctx.BodyRaw())
hexPayload := hex.EncodeToString(hashedPayload[:])
// Compare the calculated hash with the hash provided
if payloadHash != hexPayload {
return s3err.GetContentSHA256MismatchErr(payloadHash, hexPayload)
}
}
return nil
}
}
// parsePath extracts the bucket and object names from the path
func parsePath(path string) (string, string) {
p := strings.TrimPrefix(path, "/")
bucket, object, _ := strings.Cut(p, "/")
return bucket, object
}