mirror of
https://github.com/versity/versitygw.git
synced 2026-09-23 16:34:18 +00:00
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.
219 lines
7.1 KiB
Go
219 lines
7.1 KiB
Go
// Copyright 2026 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 (
|
|
"bytes"
|
|
"errors"
|
|
"mime"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v3"
|
|
"github.com/versity/versitygw/auth"
|
|
"github.com/versity/versitygw/debuglogger"
|
|
"github.com/versity/versitygw/internal/sigv4auth"
|
|
"github.com/versity/versitygw/s3api/utils"
|
|
"github.com/versity/versitygw/s3err"
|
|
)
|
|
|
|
const (
|
|
formFieldPolicy = "policy"
|
|
formFieldAlgorithm = "x-amz-algorithm"
|
|
formFieldCredential = "x-amz-credential"
|
|
formFieldDate = "x-amz-date"
|
|
formFieldSignature = "x-amz-signature"
|
|
formFieldSecurityToken = "x-amz-security-token"
|
|
|
|
aws4HMACSHA256 = "AWS4-HMAC-SHA256"
|
|
hourSeconds = 60 * 60
|
|
)
|
|
|
|
type PostObjectResult struct {
|
|
ContentLength int64
|
|
// FileRdr streams the file payload. Length() reports the exact number of
|
|
// file-content bytes read after the backend has consumed the body.
|
|
FileRdr utils.MpFileReader
|
|
Fields map[string]string
|
|
}
|
|
|
|
func AuthorizePostObject(root RootUserConfig, iam auth.IAMService, region string) fiber.Handler {
|
|
rootAccount := auth.Account{Access: root.Access, Secret: root.Secret, Role: auth.RoleAdmin}
|
|
|
|
return func(ctx fiber.Ctx) error {
|
|
contentLengthStr := ctx.Get("Content-Length")
|
|
reqContentLength, err := strconv.ParseInt(contentLengthStr, 10, 64)
|
|
if err != nil {
|
|
debuglogger.Logf("invalid POST object Content-Length %q: %v", contentLengthStr, err)
|
|
return s3err.GetAPIError(s3err.ErrInvalidRequest)
|
|
}
|
|
|
|
mediaType, params, err := mime.ParseMediaType(ctx.Get("Content-Type"))
|
|
if err != nil || mediaType != fiber.MIMEMultipartForm {
|
|
debuglogger.Logf("invalid POST object Content-Type %q: mediaType=%q err=%v", ctx.Get("Content-Type"), mediaType, err)
|
|
return s3err.GetPreconditionFailedErr(s3err.ConditionPostBucket)
|
|
}
|
|
|
|
boundary := params["boundary"]
|
|
if boundary == "" {
|
|
debuglogger.Logf("missing multipart boundary in POST object request")
|
|
return s3err.GetAPIError(s3err.ErrMalformedPOSTRequest)
|
|
}
|
|
|
|
bodyRdr := requestBodyStream(ctx)
|
|
if bodyRdr == nil {
|
|
bodyRdr = bytes.NewReader(ctx.BodyRaw())
|
|
}
|
|
|
|
mpParser, err := utils.NewMultipartParser(bodyRdr, boundary, reqContentLength)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
result, err := mpParser.Parse()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fields := result.Fields
|
|
|
|
if fields["key"] == "" {
|
|
debuglogger.Logf("missing object key")
|
|
return s3err.PostAuth.MissingField("key")
|
|
}
|
|
|
|
if !utils.IsObjectNameValid(fields["key"]) {
|
|
debuglogger.Logf("invalid POST object key: %q", fields["key"])
|
|
return s3err.GetAPIError(s3err.ErrBadRequest)
|
|
}
|
|
|
|
policyB64 := fields[formFieldPolicy]
|
|
algorithm := fields[formFieldAlgorithm]
|
|
credentialStr := fields[formFieldCredential]
|
|
amzDate := fields[formFieldDate]
|
|
signatureHex := fields[formFieldSignature]
|
|
|
|
// Determine if the request carries form-based credentials.
|
|
// A request is considered signed if ANY of the five auth fields is
|
|
// present; in that case ALL of them are required.
|
|
var hasAnyAuthField bool
|
|
|
|
for _, field := range []string{
|
|
formFieldPolicy,
|
|
formFieldAlgorithm,
|
|
formFieldCredential,
|
|
formFieldDate,
|
|
formFieldSignature,
|
|
} {
|
|
if _, ok := fields[field]; ok {
|
|
hasAnyAuthField = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if hasAnyAuthField {
|
|
// Signed POST Object — validate every required auth field.
|
|
for _, field := range []string{
|
|
formFieldPolicy,
|
|
formFieldAlgorithm,
|
|
formFieldCredential,
|
|
formFieldDate,
|
|
formFieldSignature,
|
|
} {
|
|
if _, ok := fields[field]; !ok {
|
|
debuglogger.Logf("missing required POST object field: %s", field)
|
|
return s3err.PostAuth.MissingField(field)
|
|
}
|
|
}
|
|
|
|
if algorithm != aws4HMACSHA256 {
|
|
debuglogger.Logf("unsupported POST object signing algorithm: %s", algorithm)
|
|
return s3err.GetInvalidArgumentErr(s3err.InvalidArgOnlyAws4HmacSha256, algorithm)
|
|
}
|
|
|
|
// Parse the date and check the date validity
|
|
tdate, err := time.Parse(iso8601Format, amzDate)
|
|
if err != nil {
|
|
debuglogger.Logf("invalid POST object x-amz-date %q: %v", amzDate, err)
|
|
return s3err.GetInvalidArgumentErr(s3err.InvalidArgDateHeader, amzDate)
|
|
}
|
|
|
|
// the signing date can't be older than an hour
|
|
// any future signing date is considered as valid
|
|
if time.Now().UTC().Unix()-tdate.UTC().Unix() > hourSeconds {
|
|
debuglogger.Logf("expired POST object x-amz-date: %q", amzDate)
|
|
return s3err.InvalidPolicyDocument.PolicyExpired()
|
|
}
|
|
|
|
creds, err := utils.ParseCredentials(credentialStr, s3err.PostAuth)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if region != creds.Region {
|
|
debuglogger.Logf("incorrect POST object credential region: got %q want %q", creds.Region, region)
|
|
return s3err.PostAuth.IncorrectRegion(credentialStr, region, creds.Region)
|
|
}
|
|
|
|
derivedKey, account, err := auth.ResolveDerivedKey(iam, rootAccount, creds.Access, fields[formFieldSecurityToken], creds.Date, creds.Region, sigv4auth.ServiceS3)
|
|
if err == auth.ErrNoSuchUser {
|
|
debuglogger.Logf("POST object access key not found: %s", creds.Access)
|
|
return s3err.GetInvalidAccessKeyIdErr(creds.Access)
|
|
}
|
|
if errors.Is(err, auth.ErrInvalidSessionToken) {
|
|
debuglogger.Logf("invalid POST object security token for access key %s", creds.Access)
|
|
return s3err.GetAPIError(s3err.ErrInvalidToken)
|
|
}
|
|
if err != nil {
|
|
debuglogger.Logf("failed to resolve POST object account %q: %v", creds.Access, err)
|
|
return err
|
|
}
|
|
|
|
utils.ContextKeyAccount.Set(ctx, account)
|
|
utils.ContextKeyIsRoot.Set(ctx, account.Access == root.Access)
|
|
|
|
expectedSig, err := utils.SignPostPolicy(policyB64, derivedKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !sigv4auth.SecureCompare(expectedSig, signatureHex) {
|
|
debuglogger.Logf("POST object signature mismatch: expected %s got %s", expectedSig, signatureHex)
|
|
// The String to sign for POST request is the base64 encoded policy
|
|
// For POST incorrect signature no canonical request and canonical request bytes are returned
|
|
// as the calculation is not based on canonical request string
|
|
return s3err.GetSignatureDoesNotMatchErr(account.Access, policyB64, signatureHex, utils.HexBytes(policyB64), "", "")
|
|
}
|
|
|
|
// Mark this request as authenticated so that
|
|
// AuthorizePublicBucketAccess (running after this middleware)
|
|
// skips its anonymous-access check.
|
|
utils.ContextKeyAuthenticated.Set(ctx, true)
|
|
}
|
|
// else: anonymous POST Object — no credentials in form fields.
|
|
// AuthorizePublicBucketAccess will verify public bucket access next.
|
|
|
|
utils.ContextKeyObjectPostResult.Set(ctx,
|
|
PostObjectResult{
|
|
Fields: fields,
|
|
FileRdr: result.FileRdr,
|
|
ContentLength: result.ContentLength,
|
|
},
|
|
)
|
|
|
|
return nil
|
|
}
|
|
}
|