Files
versitygw/s3api/utils/request_ids.go
T
niksis02 9debca3eb7 fix: echo the client ETag in InvalidPart errors and stop duplicating request ID headers
Fixes #2525

`CompleteMultipartUpload` returned the stored part's ETag in `InvalidPart` errors instead of the ETag the client sent: posix on an ETag mismatch, and azure on a zero-byte part mismatch, where it returned the expected block ID. Azure also returned an empty ETag for a part that was never uploaded. All three now return the client's ETag. Like S3, `GetInvalidPartErr` removes every `"` from it, inner quotes included, and keeps surrounding whitespace.

`SetResponseHeaders` turns off header name normalization and leaves it off, so response header names stay lowercase. Before it runs, the `RequestIDs` middleware stored `X-Amz-Request-Id` and `X-Amz-Id-2`. Afterwards, the `EnsureRequestIDs` call in `ProcessController` stored `x-amz-request-id` and `x-amz-id-2`. Every response with a headers map, success responses included, therefore sent both pairs. `EnsureRequestIDs`, `RequestID` and `HostID` now use `SetCanonical` and `PeekCanonical` with the lowercase names, so the normalization setting doesn't affect them, and each header is sent once in lowercase as S3 does.
2026-10-19 12:17:19 +04:00

118 lines
3.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 utils
import (
"crypto/rand"
"encoding/base64"
"github.com/gofiber/fiber/v3"
"github.com/versity/versitygw/debuglogger"
)
const (
HeaderAmzRequestID = "x-amz-request-id"
HeaderAmzID2 = "x-amz-id-2"
s3RequestIDAlphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
s3RequestIDLength = 16
s3HostIDBytes = 65
)
// NewS3RequestID returns a request ID, for example
// "5MRQJ97RHWJ4FMX9".
func NewS3RequestID() string {
return randomBase36(s3RequestIDLength)
}
func randomBase36(length int) string {
const maxUnbiasedByte = byte(252) // 36 * 7
out := make([]byte, length)
buf := make([]byte, length*2)
for i := 0; i < length; {
mustReadRandom(buf)
for _, b := range buf {
if b >= maxUnbiasedByte {
continue
}
out[i] = s3RequestIDAlphabet[int(b)%len(s3RequestIDAlphabet)]
i++
if i == length {
break
}
}
}
return string(out)
}
// NewS3HostID generates a new s3-style host ID
func NewS3HostID() string {
b := make([]byte, s3HostIDBytes)
mustReadRandom(b)
return base64.StdEncoding.EncodeToString(b)
}
// EnsureRequestIDs makes sure the request-local IDs exist and are present
// in the response headers. Existing local values are reused so headers and XML
// bodies stay consistent throughout the request.
func EnsureRequestIDs(ctx fiber.Ctx) (requestID, hostID string) {
requestID = RequestID(ctx)
if requestID == "" {
requestID = NewS3RequestID()
ContextKeyRequestID.Set(ctx, requestID)
}
hostID = HostID(ctx)
if hostID == "" {
hostID = NewS3HostID()
ContextKeyHostID.Set(ctx, hostID)
}
// The canonical variants bypass the response's header name
// normalization flag, which SetResponseHeaders turns off part way
// through a request. Going through Set would store these names in a
// different case before and after that point and emit each twice.
ctx.Response().Header.SetCanonical([]byte(HeaderAmzRequestID), []byte(requestID))
ctx.Response().Header.SetCanonical([]byte(HeaderAmzID2), []byte(hostID))
return requestID, hostID
}
func RequestID(ctx fiber.Ctx) string {
requestID, _ := ContextKeyRequestID.Get(ctx).(string)
if requestID != "" {
return requestID
}
return string(ctx.Response().Header.PeekCanonical([]byte(HeaderAmzRequestID)))
}
func HostID(ctx fiber.Ctx) string {
hostID, _ := ContextKeyHostID.Get(ctx).(string)
if hostID != "" {
return hostID
}
return string(ctx.Response().Header.PeekCanonical([]byte(HeaderAmzID2)))
}
func mustReadRandom(b []byte) {
if _, err := rand.Read(b); err != nil {
debuglogger.Logf("randomize ID bytes: %v", err)
}
}