mirror of
https://github.com/versity/versitygw.git
synced 2026-10-22 14:22:40 +00:00
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.
118 lines
3.1 KiB
Go
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)
|
|
}
|
|
}
|