Files
seaweedfs/weed/s3api/s3api_put_object_helper.go
T
Chris LuandDevin 2521c202aa s3: accept signed streaming uploads when IAM is disabled (#11766)
* s3: skip seed-signature verification in chunked reader when IAM is off

When no credentials are configured there is nothing to verify streaming
chunk signatures against. Leave the credential unset so the reader decodes
the aws-chunked body and skips per-chunk verification, matching the
unsigned-streaming path. This also fixes SigV4-signed unsigned-streaming
uploads on open gateways, which previously failed in calculateSeedSignature.

setupIam() now sets isAuthEnabled to match the credentials it installs, so
the signature-verification tests still exercise the enabled path.

* s3: decode signed streaming uploads when IAM is disabled

AWS SDK v2 clients (Trino, Spark, modern aws-cli) send PutObject bodies as
STREAMING-AWS4-HMAC-SHA256-PAYLOAD. On a gateway without S3 auth this was
rejected with ErrAuthNotSetup, breaking writes to table buckets and regular
buckets alike. The auth check already short-circuits when IAM is off, so
route both streaming payload types through the chunked reader.

* s3: cover signed trailer streaming uploads without IAM

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-16 21:10:38 -07:00

100 lines
2.9 KiB
Go

package s3api
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"hash"
"io"
"net/http"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
)
// getRequestDataReader returns the appropriate reader for the request body.
// Chunked streaming uploads are decoded regardless of whether IAM is enabled;
// with no credentials to verify against, signature checking is skipped inside
// newChunkedReader.
func getRequestDataReader(s3a *S3ApiServer, r *http.Request) (io.ReadCloser, s3err.ErrorCode) {
var s3ErrCode s3err.ErrorCode
dataReader := r.Body
rAuthType := getRequestAuthType(r)
if rAuthType == authTypeStreamingSigned || rAuthType == authTypeStreamingUnsigned {
dataReader, s3ErrCode = s3a.iam.newChunkedReader(r)
}
if s3ErrCode != s3err.ErrNone {
return nil, s3ErrCode
}
expected, valid := expectedContentSha256(r)
if !valid {
return nil, s3err.ErrInvalidArgument
}
if expected != nil && dataReader != nil {
if r.ContentLength == 0 {
// Handlers may never read an empty body, so check it now.
emptySum := sha256.Sum256(nil)
if !bytes.Equal(expected, emptySum[:]) {
return nil, s3err.ErrContentSHA256Mismatch
}
return dataReader, s3err.ErrNone
}
dataReader = &contentSha256Verifier{reader: dataReader, hasher: sha256.New(), expected: expected}
}
return dataReader, s3err.ErrNone
}
// expectedContentSha256 decodes the x-amz-content-sha256 header when it declares
// a payload hash — hex or base64 per SigV4 — so the body can be checked as it
// streams. Sentinel values (streaming, unsigned) carry no hash and are skipped;
// anything that decodes to neither returns valid=false.
func expectedContentSha256(r *http.Request) (expected []byte, valid bool) {
v := r.Header.Get("X-Amz-Content-Sha256")
if v == "" || v == unsignedPayload || v == streamingContentSHA256 ||
v == streamingContentSHA256Trailer || v == streamingUnsignedPayload {
return nil, true
}
if decoded, err := hex.DecodeString(v); err == nil && len(decoded) == sha256.Size {
return decoded, true
}
if decoded, err := base64.StdEncoding.DecodeString(v); err == nil && len(decoded) == sha256.Size {
return decoded, true
}
return nil, false
}
// contentSha256Verifier checks the decoded x-amz-content-sha256 once the stream
// is exhausted; handlers that never consume the body are unaffected.
type contentSha256Verifier struct {
reader io.ReadCloser
hasher hash.Hash
expected []byte
done bool
pending error
}
func (v *contentSha256Verifier) Read(p []byte) (int, error) {
if v.pending != nil {
return 0, v.pending
}
n, err := v.reader.Read(p)
v.hasher.Write(p[:n])
if err == io.EOF && !v.done {
v.done = true
if !bytes.Equal(v.hasher.Sum(nil), v.expected) {
v.pending = errors.New(s3err.ErrMsgContentSha256Mismatch)
err = nil
}
}
if n == 0 && v.pending != nil {
return 0, v.pending
}
return n, err
}
func (v *contentSha256Verifier) Close() error {
return v.reader.Close()
}