@@ -276,9 +276,24 @@ type s3EdgeRequest struct {
276276 rangeHeader string
277277 operation tickets.Operation
278278 list * tickets.ListRequest
279+ multipart * tickets.MultipartRequest
279280 headBucket bool
280281 contentLength * int64
281282 contentType string
283+ // body is the effective upload body for operations that stream one:
284+ // the raw request body, or an aws-chunked decoder wrapped around it.
285+ body io.ReadCloser
286+ }
287+
288+ // operationStreamsUploadBody reports whether op carries a client request body
289+ // that streams through the edge (upload source or direct backend call).
290+ func operationStreamsUploadBody (op tickets.Operation ) bool {
291+ switch op {
292+ case tickets .OperationPutObject , tickets .OperationUploadPart , tickets .OperationCompleteMultipartUpload :
293+ return true
294+ default :
295+ return false
296+ }
282297}
283298
284299func edgeS3APIMode (cfg config.EdgeConfig ) s3api.RoutingMode {
@@ -324,8 +339,11 @@ func (s *edgeServer) serveS3API(w http.ResponseWriter, r *http.Request) {
324339 return
325340 }
326341
327- ticket := tickets.Ticket {Version : tickets .Version , RequestID : reqID , Bucket : req .bucket , Key : req .key , Method : r .Method , Operation : req .operation , Range : req .rangeHeader , List : req .list , Server : req .server , DeadlineUnixMS : deadline .UnixMilli (), IngestURL : ingestURL , IngestToken : ingestToken , TraceID : reqID }
328- if req .operation == tickets .OperationPutObject {
342+ ticket := tickets.Ticket {Version : tickets .Version , RequestID : reqID , Bucket : req .bucket , Key : req .key , Method : r .Method , Operation : req .operation , Range : req .rangeHeader , List : req .list , Multipart : req .multipart , Server : req .server , DeadlineUnixMS : deadline .UnixMilli (), IngestURL : ingestURL , IngestToken : ingestToken , TraceID : reqID }
343+ if req .operation == tickets .OperationCreateMultipartUpload {
344+ ticket .ContentType = req .contentType
345+ }
346+ if operationStreamsUploadBody (req .operation ) {
329347 uploadToken , err := s .newToken ()
330348 if err != nil {
331349 writeS3Error (w , r , s3HTTPError {status : http .StatusInternalServerError , code : "InternalError" , message : "Request setup failed" })
@@ -351,13 +369,13 @@ func (s *edgeServer) serveS3API(w http.ResponseWriter, r *http.Request) {
351369 defer s .registry .Cancel (reqID , pending .ErrCanceled )
352370
353371 uploadRegistered := false
354- if req .operation == tickets . OperationPutObject {
372+ if operationStreamsUploadBody ( req .operation ) {
355373 if s .uploadSources == nil {
356374 s .registry .Cancel (reqID , uploadsource .ErrInvalidSource )
357375 writeS3Error (w , r , s3HTTPError {status : http .StatusInternalServerError , code : "InternalError" , message : "Request setup failed" })
358376 return
359377 }
360- source := uploadsource.Source {RequestID : reqID , Token : ticket .UploadToken , Body : r . Body , ContentLength : req .contentLength , ContentType : req .contentType , Deadline : deadline , Context : r .Context ()}
378+ source := uploadsource.Source {RequestID : reqID , Token : ticket .UploadToken , Body : req . body , ContentLength : req .contentLength , ContentType : req .contentType , Deadline : deadline , Context : r .Context ()}
361379 if err := s .uploadSources .Register (source ); err != nil {
362380 s .registry .Cancel (reqID , err )
363381 writeS3Error (w , r , s3ErrorForUploadSetup (err ))
@@ -438,7 +456,7 @@ func (s *edgeServer) resolveS3Request(r *http.Request) (s3EdgeRequest, error) {
438456 ValidateServer : publicpath .ValidateAlias ,
439457 })
440458 if err != nil {
441- return s3EdgeRequest {}, s3ErrorForClassify (err )
459+ return s3EdgeRequest {}, s3ErrorForClassify (err , s . cfg . MutationsEnabled )
442460 }
443461
444462 req := s3EdgeRequest {server : mapping .Server , bucket : mapping .BackendBucket , key : mapping .BackendKey }
@@ -458,22 +476,67 @@ func (s *edgeServer) resolveS3Request(r *http.Request) (s3EdgeRequest, error) {
458476 case s3api .OperationPutObject :
459477 req .operation = tickets .OperationPutObject
460478 req .contentType = r .Header .Get ("Content-Type" )
461- if r .ContentLength >= 0 {
462- contentLength := r .ContentLength
463- req .contentLength = & contentLength
464- }
465479 if err := validateS3ObjectRequest (req ); err != nil {
466480 return s3EdgeRequest {}, err
467481 }
468- if err := s .validateS3MutationRequest (r , mapping .Operation , authCtx , req ); err != nil {
482+ if err := s .validateS3MutationRequest (r , req .operation , authCtx ); err != nil {
483+ return s3EdgeRequest {}, err
484+ }
485+ if err := resolveS3UploadBody (r , authCtx , & req ); err != nil {
469486 return s3EdgeRequest {}, err
470487 }
471488 case s3api .OperationDeleteObject :
472489 req .operation = tickets .OperationDeleteObject
473490 if err := validateS3ObjectRequest (req ); err != nil {
474491 return s3EdgeRequest {}, err
475492 }
476- if err := s .validateS3MutationRequest (r , mapping .Operation , authCtx , req ); err != nil {
493+ if err := s .validateS3MutationRequest (r , req .operation , authCtx ); err != nil {
494+ return s3EdgeRequest {}, err
495+ }
496+ case s3api .OperationCreateMultipartUpload :
497+ req .operation = tickets .OperationCreateMultipartUpload
498+ req .contentType = r .Header .Get ("Content-Type" )
499+ req .multipart = multipartFromMapping (mapping )
500+ if err := validateS3ObjectRequest (req ); err != nil {
501+ return s3EdgeRequest {}, err
502+ }
503+ if err := s .validateS3MutationRequest (r , req .operation , authCtx ); err != nil {
504+ return s3EdgeRequest {}, err
505+ }
506+ case s3api .OperationUploadPart :
507+ req .operation = tickets .OperationUploadPart
508+ req .multipart = multipartFromMapping (mapping )
509+ if err := validateS3ObjectRequest (req ); err != nil {
510+ return s3EdgeRequest {}, err
511+ }
512+ if err := s .validateS3MutationRequest (r , req .operation , authCtx ); err != nil {
513+ return s3EdgeRequest {}, err
514+ }
515+ if err := resolveS3UploadBody (r , authCtx , & req ); err != nil {
516+ return s3EdgeRequest {}, err
517+ }
518+ case s3api .OperationCompleteMultipartUpload :
519+ req .operation = tickets .OperationCompleteMultipartUpload
520+ req .multipart = multipartFromMapping (mapping )
521+ if err := validateS3ObjectRequest (req ); err != nil {
522+ return s3EdgeRequest {}, err
523+ }
524+ if err := s .validateS3MutationRequest (r , req .operation , authCtx ); err != nil {
525+ return s3EdgeRequest {}, err
526+ }
527+ if err := resolveS3UploadBody (r , authCtx , & req ); err != nil {
528+ return s3EdgeRequest {}, err
529+ }
530+ if * req .contentLength > tickets .MaxCompleteMultipartBodyBytes {
531+ return s3EdgeRequest {}, s3HTTPError {status : http .StatusBadRequest , code : "InvalidRequest" , message : "Invalid request" }
532+ }
533+ case s3api .OperationAbortMultipartUpload :
534+ req .operation = tickets .OperationAbortMultipartUpload
535+ req .multipart = multipartFromMapping (mapping )
536+ if err := validateS3ObjectRequest (req ); err != nil {
537+ return s3EdgeRequest {}, err
538+ }
539+ if err := s .validateS3MutationRequest (r , req .operation , authCtx ); err != nil {
477540 return s3EdgeRequest {}, err
478541 }
479542 case s3api .OperationListObjectsV2 :
@@ -496,31 +559,64 @@ func (s *edgeServer) resolveS3Request(r *http.Request) (s3EdgeRequest, error) {
496559 return req , nil
497560}
498561
499- func (s * edgeServer ) validateS3MutationRequest (r * http.Request , operation s3api .Operation , authCtx s3api.AuthContext , req s3EdgeRequest ) error {
562+ func (s * edgeServer ) validateS3MutationRequest (r * http.Request , operation tickets .Operation , authCtx s3api.AuthContext ) error {
500563 if ! s .cfg .MutationsEnabled {
501564 return s3HTTPError {status : http .StatusMethodNotAllowed , code : "MethodNotAllowed" , message : "Method not allowed" }
502565 }
503566 if strings .TrimSpace (r .Header .Get ("Range" )) != "" {
504567 return s3HTTPError {status : http .StatusBadRequest , code : "InvalidRequest" , message : "Invalid request" }
505568 }
506- if err := s3api .ValidatePayloadHashForOperation (operation , authCtx ); err != nil {
569+ if err := s3api .ValidatePayloadHashForOperation (s3api . Operation ( operation ) , authCtx ); err != nil {
507570 return s3HTTPError {status : http .StatusBadRequest , code : "InvalidRequest" , message : "Invalid request" }
508571 }
509- if operation == s3api .OperationPutObject {
510- if req .contentLength == nil || * req .contentLength < 0 {
572+ if operationStreamsUploadBody (operation ) {
573+ // The aws-chunked markers belong to the unsigned-trailer mode, which
574+ // resolveS3UploadBody decodes; anything else must be a plain body.
575+ if authCtx .PayloadHashMode () != s3api .PayloadHashModeStreamingUnsignedTrailer && hasAWSChunkedMarkers (r , authCtx ) {
511576 return s3HTTPError {status : http .StatusBadRequest , code : "InvalidRequest" , message : "Invalid request" }
512577 }
513- if hasAWSChunkedMarkers (r , authCtx ) {
578+ return nil
579+ }
580+ if deleteHasBodyOrChunkedMarkers (r , authCtx ) {
581+ return s3HTTPError {status : http .StatusBadRequest , code : "InvalidRequest" , message : "Invalid request" }
582+ }
583+ return nil
584+ }
585+
586+ // resolveS3UploadBody sets req.body and req.contentLength to the effective
587+ // (decoded) upload body: aws-chunked framing is unwrapped for the unsigned
588+ // trailer mode, everything else streams the raw body and needs an explicit
589+ // Content-Length.
590+ func resolveS3UploadBody (r * http.Request , authCtx s3api.AuthContext , req * s3EdgeRequest ) error {
591+ if authCtx .PayloadHashMode () == s3api .PayloadHashModeStreamingUnsignedTrailer {
592+ decoded , err := strconv .ParseInt (strings .TrimSpace (r .Header .Get ("X-Amz-Decoded-Content-Length" )), 10 , 64 )
593+ if err != nil || decoded < 0 {
514594 return s3HTTPError {status : http .StatusBadRequest , code : "InvalidRequest" , message : "Invalid request" }
515595 }
596+ req .contentLength = & decoded
597+ req .body = struct {
598+ io.Reader
599+ io.Closer
600+ }{s3api .NewAWSChunkedReader (r .Body , decoded ), r .Body }
516601 return nil
517602 }
518- if deleteHasBodyOrChunkedMarkers ( r , authCtx ) {
603+ if r . ContentLength < 0 {
519604 return s3HTTPError {status : http .StatusBadRequest , code : "InvalidRequest" , message : "Invalid request" }
520605 }
606+ contentLength := r .ContentLength
607+ req .contentLength = & contentLength
608+ req .body = r .Body
521609 return nil
522610}
523611
612+ func multipartFromMapping (mapping s3api.RequestMapping ) * tickets.MultipartRequest {
613+ return & tickets.MultipartRequest {
614+ UploadID : mapping .Multipart .UploadID ,
615+ PartNumber : int32 (mapping .Multipart .PartNumber ),
616+ Rewrite : tickets.MultipartRewrite {Bucket : mapping .S3Bucket , Key : mapping .S3Key },
617+ }
618+ }
619+
524620func hasAWSChunkedMarkers (r * http.Request , authCtx s3api.AuthContext ) bool {
525621 return authCtx .PayloadHashMode () == s3api .PayloadHashModeStreaming ||
526622 headerHasToken (r .Header , "Content-Encoding" , "aws-chunked" ) ||
@@ -632,10 +728,12 @@ func (s *edgeServer) serveS3Direct(w http.ResponseWriter, r *http.Request, req s
632728 return
633729 }
634730
635- fetchReq := s3fetch.Request {Method : r .Method , Operation : req .operation , Bucket : req .bucket , Key : req .key , Range : req .rangeHeader , List : req .list }
636- if req .operation == tickets . OperationPutObject {
637- fetchReq .Body = r . Body
731+ fetchReq := s3fetch.Request {Method : r .Method , Operation : req .operation , Bucket : req .bucket , Key : req .key , Range : req .rangeHeader , List : req .list , Multipart : req . multipart }
732+ if operationStreamsUploadBody ( req .operation ) {
733+ fetchReq .Body = req . body
638734 fetchReq .ContentLength = req .contentLength
735+ }
736+ if req .operation == tickets .OperationPutObject || req .operation == tickets .OperationCreateMultipartUpload {
639737 fetchReq .ContentType = req .contentType
640738 }
641739 fetched , err := fetcher .Fetch (r .Context (), fetchReq )
@@ -651,12 +749,12 @@ func (s *edgeServer) serveS3Direct(w http.ResponseWriter, r *http.Request, req s
651749 if fetched .Body != nil {
652750 defer fetched .Body .Close ()
653751 }
654- if req .operation == tickets .OperationPutObject {
752+ if req .operation == tickets .OperationPutObject || req . operation == tickets . OperationUploadPart {
655753 setTrimmedHeader (w .Header (), "ETag" , fetched .ETag )
656754 w .WriteHeader (http .StatusOK )
657755 return
658756 }
659- if req .operation == tickets .OperationDeleteObject {
757+ if req .operation == tickets .OperationDeleteObject || req . operation == tickets . OperationAbortMultipartUpload {
660758 w .WriteHeader (http .StatusNoContent )
661759 return
662760 }
@@ -688,6 +786,7 @@ type s3HTTPError struct {
688786 status int
689787 code string
690788 message string
789+ allow string
691790}
692791
693792func (e s3HTTPError ) Error () string {
@@ -697,7 +796,11 @@ func (e s3HTTPError) Error() string {
697796func writeS3Error (w http.ResponseWriter , r * http.Request , err error ) {
698797 s3err := s3ErrorFor (err )
699798 if s3err .status == http .StatusMethodNotAllowed {
700- w .Header ().Set ("Allow" , "GET, HEAD" )
799+ allow := s3err .allow
800+ if allow == "" {
801+ allow = "GET, HEAD"
802+ }
803+ w .Header ().Set ("Allow" , allow )
701804 }
702805 w .Header ().Set ("Content-Type" , "application/xml" )
703806 w .WriteHeader (s3err .status )
@@ -731,10 +834,14 @@ func s3ErrorForAuth(err error) s3HTTPError {
731834 }
732835}
733836
734- func s3ErrorForClassify (err error ) s3HTTPError {
837+ func s3ErrorForClassify (err error , mutationsEnabled bool ) s3HTTPError {
735838 message := err .Error ()
736839 if strings .Contains (message , "unsupported method" ) {
737- return s3HTTPError {status : http .StatusMethodNotAllowed , code : "MethodNotAllowed" , message : "Method not allowed" }
840+ allow := "GET, HEAD"
841+ if mutationsEnabled {
842+ allow = "GET, HEAD, PUT, POST, DELETE"
843+ }
844+ return s3HTTPError {status : http .StatusMethodNotAllowed , code : "MethodNotAllowed" , message : "Method not allowed" , allow : allow }
738845 }
739846 return s3HTTPError {status : http .StatusBadRequest , code : "InvalidRequest" , message : "Invalid request" }
740847}
0 commit comments