mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-03 10:37:35 +01:00
* s3api: hash an s3vectors body up to the Vectors API limit SigV4 verification of a non-S3 request hashed at most 10 MiB of the body and silently dropped the rest, so any S3 Vectors request over 10 MiB failed its signature check with 403, although the API accepts 32 MiB. The limit now follows the signing service, and a body over it is reported as ErrEntityTooLarge instead of being truncated; the embedded IAM answers that with InvalidInput, as it already does for a body over its own limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * s3vectors: answer an oversized request with a ValidationException A body over 32 MiB was refused as "403 AccessDenied: authentication failed". A declared Content-Length over the limit is now refused before authentication, and a streamed body over it, whether the signature check or the handler reads it, gets the same "request body exceeds 32 MiB" ValidationException. The limit is the one SigV4 verification uses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * s3vectors: log engine and catalog failures at the default level An engine error reached the client as a bare "internal error", and the cause was logged only with -v=1, so a misconfigured engine left nothing in a default log. Engine and catalog failures are now logged as errors, and a reported unavailability as a warning, with the operation, bucket and index. The response still leaves the engine's message out, since it can name internal hosts and paths, and points at the gateway log instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * s3api: hash the signed body through readRequestBody streamHashRequestBody repeated readRequestBody's read-past-the-limit check; it now reads the body with readRequestBody and hashes the bytes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
151 lines
5.4 KiB
Go
151 lines
5.4 KiB
Go
// Package s3vectors serves the AWS S3 Vectors API on top of the shared
|
|
// VectorService gRPC boundary and the S3 Tables catalog.
|
|
package s3vectors
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/vector_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables"
|
|
stats_collect "github.com/seaweedfs/seaweedfs/weed/stats"
|
|
)
|
|
|
|
// FilerClient provides access to the filer for storage operations.
|
|
type FilerClient interface {
|
|
WithFilerClient(streamingMode bool, fn func(client filer_pb.SeaweedFilerClient) error) error
|
|
}
|
|
|
|
// S3Authenticator authenticates a request, returning the caller identity.
|
|
type S3Authenticator interface {
|
|
AuthenticateRequest(r *http.Request) (string, interface{}, s3err.ErrorCode)
|
|
DefaultAllow() bool
|
|
}
|
|
|
|
// Server implements the AWS S3 Vectors API.
|
|
type Server struct {
|
|
filerClient FilerClient
|
|
tables *s3tables.Manager
|
|
internal *s3tables.Manager
|
|
vectors vector_pb.VectorServiceClient
|
|
authenticator S3Authenticator
|
|
region string
|
|
accountID string
|
|
}
|
|
|
|
// NewServer creates an S3 Vectors server over the given filer and catalog.
|
|
func NewServer(filerClient FilerClient, authenticator S3Authenticator, vectors vector_pb.VectorServiceClient) *Server {
|
|
manager := s3tables.NewManager()
|
|
if authenticator != nil {
|
|
manager.SetDefaultAllow(authenticator.DefaultAllow())
|
|
}
|
|
// Bookkeeping calls that ride along an operation the catalog already
|
|
// authorizes — format lookups, the implicit index namespace — must not
|
|
// demand read grants the API does not require.
|
|
internal := s3tables.NewManager()
|
|
internal.SetTrusted(true)
|
|
return &Server{
|
|
filerClient: filerClient,
|
|
tables: manager,
|
|
internal: internal,
|
|
vectors: vectors,
|
|
authenticator: authenticator,
|
|
}
|
|
}
|
|
|
|
// SetRegion configures the region embedded in the ARNs this server mints.
|
|
func (s *Server) SetRegion(region string) { s.region = region }
|
|
|
|
// SetAccountID configures the account id embedded in the ARNs this server mints.
|
|
func (s *Server) SetAccountID(id string) { s.accountID = id }
|
|
|
|
// SetEncryptionChecker tells CreateVectorBucket whether the gateway can
|
|
// apply the AES256 a bucket declares.
|
|
func (s *Server) SetEncryptionChecker(fn func() bool) { s.internal.SetEncryptionChecker(fn) }
|
|
|
|
// RegisterRoutes mounts POST / dispatching on the X-Amz-Target header and
|
|
// POST /{Operation} for clients that route per-path. Metrics wrap Auth like the
|
|
// S3 track middleware so rejected requests still count toward the operation.
|
|
func (s *Server) RegisterRoutes(router *mux.Router) {
|
|
router.HandleFunc("/", s.instrument(s.Auth(s.dispatch))).Methods(http.MethodPost)
|
|
router.HandleFunc("/{op}", s.instrument(s.Auth(s.dispatch))).Methods(http.MethodPost)
|
|
}
|
|
|
|
// operationFromRequest resolves the operation the way dispatch does: the last
|
|
// path segment, or the X-Amz-Target suffix when the path is bare.
|
|
func operationFromRequest(r *http.Request) string {
|
|
op := strings.Trim(r.URL.Path, "/")
|
|
if i := strings.LastIndex(op, "/"); i >= 0 {
|
|
op = op[i+1:]
|
|
}
|
|
if op == "" {
|
|
target := r.Header.Get("X-Amz-Target")
|
|
op = target[strings.LastIndex(target, ".")+1:]
|
|
}
|
|
return op
|
|
}
|
|
|
|
// instrument records in-flight, latency, and status for a known operation;
|
|
// unknown operations stay out of the metric labels.
|
|
func (s *Server) instrument(next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
op := operationFromRequest(r)
|
|
if _, ok := handlers[op]; !ok {
|
|
next(w, r)
|
|
return
|
|
}
|
|
label := "s3vectors." + op
|
|
inFlight := stats_collect.S3InFlightRequestsGauge.WithLabelValues(label)
|
|
inFlight.Inc()
|
|
defer inFlight.Dec()
|
|
recorder := stats_collect.NewStatusResponseWriter(w)
|
|
start := time.Now()
|
|
next(recorder, r)
|
|
elapsed := time.Since(start)
|
|
stats_collect.S3RequestHistogram.WithLabelValues(label, "").Observe(elapsed.Seconds())
|
|
stats_collect.S3RequestCounter.WithLabelValues(label, strconv.Itoa(recorder.Status), "").Inc()
|
|
stats_collect.RecordBucketActiveTime("")
|
|
}
|
|
}
|
|
|
|
func (s *Server) dispatch(w http.ResponseWriter, r *http.Request) {
|
|
// Clients carry the operation in the X-Amz-Target header or in the path —
|
|
// POST /QueryVectors and POST / with a target mean the same call here.
|
|
op := operationFromRequest(r)
|
|
handler, ok := handlers[op]
|
|
if !ok {
|
|
writeError(w, http.StatusBadRequest, "ValidationException", "unknown operation "+op)
|
|
return
|
|
}
|
|
handler(s, w, r)
|
|
}
|
|
|
|
// Auth authenticates the caller and puts the identity in the request context.
|
|
func (s *Server) Auth(handler http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if r.ContentLength > maxRequestBodyBytes {
|
|
writeError(w, http.StatusBadRequest, "ValidationException", errRequestBodyTooLarge.Error())
|
|
return
|
|
}
|
|
identity, identityObj, errCode := s.authenticator.AuthenticateRequest(r)
|
|
switch errCode {
|
|
case s3err.ErrNone:
|
|
case s3err.ErrEntityTooLarge:
|
|
writeError(w, http.StatusBadRequest, "ValidationException", errRequestBodyTooLarge.Error())
|
|
return
|
|
default:
|
|
writeError(w, http.StatusForbidden, "AccessDenied", "authentication failed")
|
|
return
|
|
}
|
|
r = r.WithContext(s3_constants.SetIdentityInContext(
|
|
s3_constants.SetIdentityNameInContext(r.Context(), identity), identityObj))
|
|
handler(w, r)
|
|
}
|
|
}
|