11package main
22
33import (
4+ "bytes"
45 "context"
56 "crypto/tls"
67 "errors"
@@ -28,8 +29,10 @@ import (
2829 "github.com/terion-name/air3/internal/mtls"
2930 "github.com/terion-name/air3/internal/natsclient"
3031 "github.com/terion-name/air3/internal/pending"
32+ "github.com/terion-name/air3/internal/s3api"
3133 "github.com/terion-name/air3/internal/s3fetch"
3234 "github.com/terion-name/air3/internal/tickets"
35+ "github.com/terion-name/air3/internal/uploadsource"
3336)
3437
3538const ingestTransportBufferBytes = 256 * 1024
@@ -42,6 +45,41 @@ type ingestSender interface {
4245 Send (context.Context , tickets.Ticket , ingestMetadata , io.Reader ) error
4346}
4447
48+ type uploadSourceOpener interface {
49+ Open (context.Context , tickets.Ticket ) (* openedUploadSource , error )
50+ }
51+
52+ type openedUploadSource struct {
53+ Body io.ReadCloser
54+ ContentLength int64
55+ }
56+
57+ type httpUploadSourceOpener struct {
58+ client * http.Client
59+ }
60+
61+ func (o httpUploadSourceOpener ) Open (ctx context.Context , ticket tickets.Ticket ) (* openedUploadSource , error ) {
62+ req , err := http .NewRequestWithContext (ctx , http .MethodGet , ticket .UploadSourceURL , nil )
63+ if err != nil {
64+ return nil , fmt .Errorf ("create upload source request: %w" , err )
65+ }
66+ req .Header .Set (uploadsource .TokenHeader , ticket .UploadToken )
67+
68+ client := o .client
69+ if client == nil {
70+ client = http .DefaultClient
71+ }
72+ resp , err := client .Do (req )
73+ if err != nil {
74+ return nil , fmt .Errorf ("get upload source: %w" , err )
75+ }
76+ if resp .StatusCode < 200 || resp .StatusCode >= 300 {
77+ _ = resp .Body .Close ()
78+ return nil , fmt .Errorf ("upload source returned status %d" , resp .StatusCode )
79+ }
80+ return & openedUploadSource {Body : resp .Body , ContentLength : resp .ContentLength }, nil
81+ }
82+
4583type closeableIngestSender interface {
4684 Close () error
4785}
@@ -55,11 +93,12 @@ func closeIngestSender(sender any) error {
5593}
5694
5795type connector struct {
58- cfg config.ConnectorConfig
59- fetcher objectFetcher
60- sender ingestSender
61- logger * slog.Logger
62- now func () time.Time
96+ cfg config.ConnectorConfig
97+ fetcher objectFetcher
98+ sender ingestSender
99+ uploadOpener uploadSourceOpener
100+ logger * slog.Logger
101+ now func () time.Time
63102}
64103
65104var errTicketWorkerPoolClosed = errors .New ("ticket worker pool is closed" )
@@ -193,7 +232,16 @@ func run(ctx context.Context, logger *slog.Logger) error {
193232 logger .Warn ("ingest sender close failed" , "error" , safeLogError (err ))
194233 }
195234 }()
196- worker := newConnector (cfg , fetcher , sender , logger )
235+ uploadClient , err := ingestHTTPClient (cfg .MTLS , cfg .Timeouts .StreamTimeout , cfg .IngestDisableHTTP2 , cfg .IngestPoolSize )
236+ if err != nil {
237+ return err
238+ }
239+ defer func () {
240+ if err := closeHTTPClient (uploadClient ); err != nil {
241+ logger .Warn ("upload source client close failed" , "error" , safeLogError (err ))
242+ }
243+ }()
244+ worker := newConnectorWithUploadSourceOpener (cfg , fetcher , sender , httpUploadSourceOpener {client : uploadClient }, logger )
197245
198246 natsCtx , cancelNATS := context .WithTimeout (ctx , 10 * time .Second )
199247 defer cancelNATS ()
@@ -228,13 +276,20 @@ func run(ctx context.Context, logger *slog.Logger) error {
228276}
229277
230278func newConnector (cfg config.ConnectorConfig , fetcher objectFetcher , sender ingestSender , logger * slog.Logger ) * connector {
279+ return newConnectorWithUploadSourceOpener (cfg , fetcher , sender , httpUploadSourceOpener {client : http .DefaultClient }, logger )
280+ }
281+
282+ func newConnectorWithUploadSourceOpener (cfg config.ConnectorConfig , fetcher objectFetcher , sender ingestSender , uploadOpener uploadSourceOpener , logger * slog.Logger ) * connector {
231283 if logger == nil {
232284 logger = slog .New (slog .NewTextHandler (io .Discard , nil ))
233285 }
234286 if sender == nil {
235287 sender = httpIngestSender {client : http .DefaultClient }
236288 }
237- return & connector {cfg : cfg , fetcher : fetcher , sender : sender , logger : logger , now : time .Now }
289+ if uploadOpener == nil {
290+ uploadOpener = httpUploadSourceOpener {client : http .DefaultClient }
291+ }
292+ return & connector {cfg : cfg , fetcher : fetcher , sender : sender , uploadOpener : uploadOpener , logger : logger , now : time .Now }
238293}
239294
240295func (c * connector ) handleTicket (ctx context.Context , ticket tickets.Ticket ) error {
@@ -245,7 +300,30 @@ func (c *connector) handleTicket(ctx context.Context, ticket tickets.Ticket) err
245300 ticketCtx , cancel := context .WithDeadline (ctx , deadline )
246301 defer cancel ()
247302
248- obj , err := c .fetcher .Fetch (ticketCtx , s3fetch.Request {Method : ticket .Method , Operation : ticket .Operation , Bucket : ticket .Bucket , Key : ticket .Key , Range : ticket .Range , List : ticket .List })
303+ op , err := tickets .ResolveOperation (ticket .Method , ticket .Operation )
304+ if err != nil {
305+ return err
306+ }
307+ if isMutationOperation (op ) && ! c .cfg .MutationsEnabled {
308+ return c .sendS3Error (ticketCtx , ticket , http .StatusMethodNotAllowed , "MethodNotAllowed" , "mutations are disabled" )
309+ }
310+
311+ req := s3fetch.Request {Method : ticket .Method , Operation : op , Bucket : ticket .Bucket , Key : ticket .Key , Range : ticket .Range , List : ticket .List }
312+ if op == tickets .OperationPutObject {
313+ upload , err := c .uploadOpener .Open (ticketCtx , ticket )
314+ if err != nil || upload == nil || upload .Body == nil {
315+ return c .sendS3Error (ticketCtx , ticket , http .StatusServiceUnavailable , "ServiceUnavailable" , "upload source is unavailable" )
316+ }
317+ defer upload .Body .Close ()
318+ if upload .ContentLength >= 0 && ticket .ContentLength != nil && upload .ContentLength != * ticket .ContentLength {
319+ return c .sendS3Error (ticketCtx , ticket , http .StatusBadRequest , "InvalidRequest" , "upload source content length does not match ticket content length" )
320+ }
321+ req .Body = upload .Body
322+ req .ContentLength = ticket .ContentLength
323+ req .ContentType = ticket .ContentType
324+ }
325+
326+ obj , err := c .fetcher .Fetch (ticketCtx , req )
249327 if err != nil {
250328 status := statusForFetchError (err )
251329 return c .sender .Send (ticketCtx , ticket , metadataForStatus (status ), http .NoBody )
@@ -254,7 +332,7 @@ func (c *connector) handleTicket(ctx context.Context, ticket tickets.Ticket) err
254332
255333 metadata := metadataForObject (obj )
256334 body := obj .Body
257- if ticket . Method == http . MethodHead {
335+ if op == tickets . OperationHeadObject {
258336 body = http .NoBody
259337 }
260338 return c .sender .Send (ticketCtx , ticket , metadata , body )
@@ -275,15 +353,53 @@ func (c *connector) validateTicket(ticket tickets.Ticket) error {
275353 if ! bucketAllowed (ticket .Bucket , c .cfg .AllowedBuckets ) || ! bucketAllowed (ticket .Bucket , c .cfg .S3 .AllowedBuckets ) {
276354 return errors .New ("ticket bucket is not allowed" )
277355 }
278- if ticket .Method != http .MethodGet && ticket .Method != http .MethodHead {
279- return errors .New ("ticket method is not supported" )
356+ op , err := tickets .ResolveOperation (ticket .Method , ticket .Operation )
357+ if err != nil {
358+ return err
359+ }
360+ if ! operationAllowed (op ) {
361+ return errors .New ("ticket operation is not supported" )
280362 }
281363 if err := validateIngestURL (ticket .IngestURL ); err != nil {
282364 return err
283365 }
284366 return nil
285367}
286368
369+ func operationAllowed (op tickets.Operation ) bool {
370+ switch op {
371+ case tickets .OperationGetObject , tickets .OperationHeadObject , tickets .OperationListObjectsV2 , tickets .OperationPutObject , tickets .OperationDeleteObject :
372+ return true
373+ default :
374+ return false
375+ }
376+ }
377+
378+ func isMutationOperation (op tickets.Operation ) bool {
379+ return op == tickets .OperationPutObject || op == tickets .OperationDeleteObject
380+ }
381+
382+ func (c * connector ) sendS3Error (ctx context.Context , ticket tickets.Ticket , status int , code , message string ) error {
383+ body , err := s3api .RenderErrorXML (s3api.ErrorResponse {
384+ Code : code ,
385+ Message : message ,
386+ Resource : s3ErrorResource (ticket ),
387+ RequestID : ticket .RequestID ,
388+ })
389+ if err != nil {
390+ return fmt .Errorf ("render s3 error xml: %w" , err )
391+ }
392+ metadata := ingestMetadata {StatusCode : status , ContentType : "application/xml" , ContentLength : int64 (len (body ))}
393+ return c .sender .Send (ctx , ticket , metadata , bytes .NewReader (body ))
394+ }
395+
396+ func s3ErrorResource (ticket tickets.Ticket ) string {
397+ if ticket .Key == "" {
398+ return "/" + ticket .Bucket
399+ }
400+ return "/" + ticket .Bucket + "/" + ticket .Key
401+ }
402+
287403type httpIngestSender struct {
288404 client * http.Client
289405}
0 commit comments