github.com/ethersphere/bee/v2@v2.2.0/pkg/api/api.go (about) 1 // Copyright 2020 The Swarm Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package api provides the functionality of the Bee 6 // client-facing HTTP API. 7 package api 8 9 import ( 10 "context" 11 "crypto/ecdsa" 12 "encoding/base64" 13 "encoding/hex" 14 "errors" 15 "fmt" 16 "io" 17 "math" 18 "math/big" 19 "mime" 20 "net/http" 21 "reflect" 22 "strconv" 23 "strings" 24 "sync" 25 "time" 26 "unicode/utf8" 27 28 "github.com/ethereum/go-ethereum/common" 29 "github.com/ethersphere/bee/v2/pkg/accesscontrol" 30 "github.com/ethersphere/bee/v2/pkg/accounting" 31 "github.com/ethersphere/bee/v2/pkg/crypto" 32 "github.com/ethersphere/bee/v2/pkg/feeds" 33 "github.com/ethersphere/bee/v2/pkg/file/pipeline" 34 "github.com/ethersphere/bee/v2/pkg/file/pipeline/builder" 35 "github.com/ethersphere/bee/v2/pkg/file/redundancy" 36 "github.com/ethersphere/bee/v2/pkg/jsonhttp" 37 "github.com/ethersphere/bee/v2/pkg/log" 38 "github.com/ethersphere/bee/v2/pkg/p2p" 39 "github.com/ethersphere/bee/v2/pkg/pingpong" 40 "github.com/ethersphere/bee/v2/pkg/postage" 41 "github.com/ethersphere/bee/v2/pkg/postage/postagecontract" 42 "github.com/ethersphere/bee/v2/pkg/pss" 43 "github.com/ethersphere/bee/v2/pkg/resolver" 44 "github.com/ethersphere/bee/v2/pkg/resolver/client/ens" 45 "github.com/ethersphere/bee/v2/pkg/sctx" 46 "github.com/ethersphere/bee/v2/pkg/settlement" 47 "github.com/ethersphere/bee/v2/pkg/settlement/swap" 48 "github.com/ethersphere/bee/v2/pkg/settlement/swap/chequebook" 49 "github.com/ethersphere/bee/v2/pkg/settlement/swap/erc20" 50 "github.com/ethersphere/bee/v2/pkg/status" 51 "github.com/ethersphere/bee/v2/pkg/steward" 52 storage "github.com/ethersphere/bee/v2/pkg/storage" 53 "github.com/ethersphere/bee/v2/pkg/storageincentives" 54 "github.com/ethersphere/bee/v2/pkg/storageincentives/staking" 55 storer "github.com/ethersphere/bee/v2/pkg/storer" 56 "github.com/ethersphere/bee/v2/pkg/swarm" 57 "github.com/ethersphere/bee/v2/pkg/topology" 58 "github.com/ethersphere/bee/v2/pkg/topology/lightnode" 59 "github.com/ethersphere/bee/v2/pkg/tracing" 60 "github.com/ethersphere/bee/v2/pkg/transaction" 61 "github.com/go-playground/validator/v10" 62 "github.com/gorilla/mux" 63 "github.com/hashicorp/go-multierror" 64 "github.com/prometheus/client_golang/prometheus" 65 "golang.org/x/sync/semaphore" 66 ) 67 68 // loggerName is the tree path name of the logger for this package. 69 const loggerName = "api" 70 71 const ( 72 SwarmPinHeader = "Swarm-Pin" 73 SwarmTagHeader = "Swarm-Tag" 74 SwarmEncryptHeader = "Swarm-Encrypt" 75 SwarmIndexDocumentHeader = "Swarm-Index-Document" 76 SwarmErrorDocumentHeader = "Swarm-Error-Document" 77 SwarmFeedIndexHeader = "Swarm-Feed-Index" 78 SwarmFeedIndexNextHeader = "Swarm-Feed-Index-Next" 79 SwarmCollectionHeader = "Swarm-Collection" 80 SwarmPostageBatchIdHeader = "Swarm-Postage-Batch-Id" 81 SwarmPostageStampHeader = "Swarm-Postage-Stamp" 82 SwarmDeferredUploadHeader = "Swarm-Deferred-Upload" 83 SwarmRedundancyLevelHeader = "Swarm-Redundancy-Level" 84 SwarmRedundancyStrategyHeader = "Swarm-Redundancy-Strategy" 85 SwarmRedundancyFallbackModeHeader = "Swarm-Redundancy-Fallback-Mode" 86 SwarmChunkRetrievalTimeoutHeader = "Swarm-Chunk-Retrieval-Timeout" 87 SwarmLookAheadBufferSizeHeader = "Swarm-Lookahead-Buffer-Size" 88 SwarmActHeader = "Swarm-Act" 89 SwarmActTimestampHeader = "Swarm-Act-Timestamp" 90 SwarmActPublisherHeader = "Swarm-Act-Publisher" 91 SwarmActHistoryAddressHeader = "Swarm-Act-History-Address" 92 93 ImmutableHeader = "Immutable" 94 GasPriceHeader = "Gas-Price" 95 GasLimitHeader = "Gas-Limit" 96 ETagHeader = "ETag" 97 98 AuthorizationHeader = "Authorization" 99 AcceptEncodingHeader = "Accept-Encoding" 100 ContentTypeHeader = "Content-Type" 101 ContentDispositionHeader = "Content-Disposition" 102 ContentLengthHeader = "Content-Length" 103 RangeHeader = "Range" 104 OriginHeader = "Origin" 105 ) 106 107 const ( 108 multiPartFormData = "multipart/form-data" 109 contentTypeTar = "application/x-tar" 110 boolHeaderSetValue = "true" 111 ) 112 113 var ( 114 errInvalidNameOrAddress = errors.New("invalid name or bzz address") 115 errNoResolver = errors.New("no resolver connected") 116 errInvalidRequest = errors.New("could not validate request") 117 errInvalidContentType = errors.New("invalid content-type") 118 errDirectoryStore = errors.New("could not store directory") 119 errFileStore = errors.New("could not store file") 120 errInvalidPostageBatch = errors.New("invalid postage batch id") 121 errBatchUnusable = errors.New("batch not usable") 122 errUnsupportedDevNodeOperation = errors.New("operation not supported in dev mode") 123 errOperationSupportedOnlyInFullMode = errors.New("operation is supported only in full mode") 124 errActDownload = errors.New("act download failed") 125 errActUpload = errors.New("act upload failed") 126 errActGranteeList = errors.New("failed to create or update grantee list") 127 128 batchIdOrStampSig = fmt.Sprintf("Either '%s' or '%s' header must be set in the request", SwarmPostageStampHeader, SwarmPostageBatchIdHeader) 129 ) 130 131 // Storer interface provides the functionality required from the local storage 132 // component of the node. 133 type Storer interface { 134 storer.UploadStore 135 storer.PinStore 136 storer.CacheStore 137 storer.NetStore 138 storer.LocalStore 139 storer.RadiusChecker 140 storer.Debugger 141 } 142 143 type PinIntegrity interface { 144 Check(ctx context.Context, logger log.Logger, pin string, out chan storer.PinStat) 145 } 146 147 type Service struct { 148 storer Storer 149 resolver resolver.Interface 150 pss pss.Interface 151 steward steward.Interface 152 logger log.Logger 153 loggerV1 log.Logger 154 tracer *tracing.Tracer 155 feedFactory feeds.Factory 156 signer crypto.Signer 157 post postage.Service 158 accesscontrol accesscontrol.Controller 159 postageContract postagecontract.Interface 160 probe *Probe 161 metricsRegistry *prometheus.Registry 162 stakingContract staking.Contract 163 Options 164 165 http.Handler 166 router *mux.Router 167 168 metrics metrics 169 170 wsWg sync.WaitGroup // wait for all websockets to close on exit 171 quit chan struct{} 172 173 overlay *swarm.Address 174 publicKey ecdsa.PublicKey 175 pssPublicKey ecdsa.PublicKey 176 ethereumAddress common.Address 177 chequebookEnabled bool 178 swapEnabled bool 179 180 topologyDriver topology.Driver 181 p2p p2p.DebugService 182 accounting accounting.Interface 183 chequebook chequebook.Service 184 pseudosettle settlement.Interface 185 pingpong pingpong.Interface 186 187 batchStore postage.Storer 188 stamperStore storage.Store 189 pinIntegrity PinIntegrity 190 191 syncStatus func() (bool, error) 192 193 swap swap.Interface 194 transaction transaction.Service 195 lightNodes *lightnode.Container 196 blockTime time.Duration 197 198 statusSem *semaphore.Weighted 199 postageSem *semaphore.Weighted 200 stakingSem *semaphore.Weighted 201 cashOutChequeSem *semaphore.Weighted 202 beeMode BeeNodeMode 203 204 chainBackend transaction.Backend 205 erc20Service erc20.Service 206 chainID int64 207 208 whitelistedWithdrawalAddress []common.Address 209 210 preMapHooks map[string]func(v string) (string, error) 211 validate *validator.Validate 212 213 redistributionAgent *storageincentives.Agent 214 215 statusService *status.Service 216 } 217 218 func (s *Service) SetP2P(p2p p2p.DebugService) { 219 if s != nil { 220 s.p2p = p2p 221 } 222 } 223 224 func (s *Service) SetSwarmAddress(addr *swarm.Address) { 225 if s != nil { 226 s.overlay = addr 227 } 228 } 229 230 func (s *Service) SetRedistributionAgent(redistributionAgent *storageincentives.Agent) { 231 if s != nil { 232 s.redistributionAgent = redistributionAgent 233 } 234 } 235 236 type Options struct { 237 CORSAllowedOrigins []string 238 WsPingPeriod time.Duration 239 } 240 241 type ExtraOptions struct { 242 Pingpong pingpong.Interface 243 TopologyDriver topology.Driver 244 LightNodes *lightnode.Container 245 Accounting accounting.Interface 246 Pseudosettle settlement.Interface 247 Swap swap.Interface 248 Chequebook chequebook.Service 249 BlockTime time.Duration 250 Storer Storer 251 Resolver resolver.Interface 252 Pss pss.Interface 253 FeedFactory feeds.Factory 254 Post postage.Service 255 AccessControl accesscontrol.Controller 256 PostageContract postagecontract.Interface 257 Staking staking.Contract 258 Steward steward.Interface 259 SyncStatus func() (bool, error) 260 NodeStatus *status.Service 261 PinIntegrity PinIntegrity 262 } 263 264 func New( 265 publicKey, pssPublicKey ecdsa.PublicKey, 266 ethereumAddress common.Address, 267 whitelistedWithdrawalAddress []string, 268 logger log.Logger, 269 transaction transaction.Service, 270 batchStore postage.Storer, 271 beeMode BeeNodeMode, 272 chequebookEnabled bool, 273 swapEnabled bool, 274 chainBackend transaction.Backend, 275 cors []string, 276 stamperStore storage.Store, 277 ) *Service { 278 s := new(Service) 279 280 s.CORSAllowedOrigins = cors 281 s.beeMode = beeMode 282 s.logger = logger.WithName(loggerName).Register() 283 s.loggerV1 = s.logger.V(1).Register() 284 s.chequebookEnabled = chequebookEnabled 285 s.swapEnabled = swapEnabled 286 s.publicKey = publicKey 287 s.pssPublicKey = pssPublicKey 288 s.ethereumAddress = ethereumAddress 289 s.transaction = transaction 290 s.batchStore = batchStore 291 s.chainBackend = chainBackend 292 s.metricsRegistry = newDebugMetrics() 293 s.preMapHooks = map[string]func(v string) (string, error){ 294 "mimeMediaType": func(v string) (string, error) { 295 typ, _, err := mime.ParseMediaType(v) 296 return typ, err 297 }, 298 "decBase64url": func(v string) (string, error) { 299 buf, err := base64.URLEncoding.DecodeString(v) 300 return string(buf), err 301 }, 302 "decHex": func(v string) (string, error) { 303 buf, err := hex.DecodeString(v) 304 return string(buf), err 305 }, 306 } 307 s.validate = validator.New() 308 s.validate.RegisterTagNameFunc(func(fld reflect.StructField) string { 309 name := strings.SplitN(fld.Tag.Get(mapStructureTagName), ",", 2)[0] 310 if name == "-" { 311 return "" 312 } 313 return name 314 }) 315 s.stamperStore = stamperStore 316 317 for _, v := range whitelistedWithdrawalAddress { 318 s.whitelistedWithdrawalAddress = append(s.whitelistedWithdrawalAddress, common.HexToAddress(v)) 319 } 320 321 return s 322 } 323 324 // Configure will create a and initialize a new API service. 325 func (s *Service) Configure(signer crypto.Signer, tracer *tracing.Tracer, o Options, e ExtraOptions, chainID int64, erc20 erc20.Service) { 326 s.signer = signer 327 s.Options = o 328 s.tracer = tracer 329 s.metrics = newMetrics() 330 331 s.quit = make(chan struct{}) 332 333 s.storer = e.Storer 334 s.resolver = e.Resolver 335 s.pss = e.Pss 336 s.feedFactory = e.FeedFactory 337 s.post = e.Post 338 s.accesscontrol = e.AccessControl 339 s.postageContract = e.PostageContract 340 s.steward = e.Steward 341 s.stakingContract = e.Staking 342 343 s.pingpong = e.Pingpong 344 s.topologyDriver = e.TopologyDriver 345 s.accounting = e.Accounting 346 s.chequebook = e.Chequebook 347 s.swap = e.Swap 348 s.lightNodes = e.LightNodes 349 s.pseudosettle = e.Pseudosettle 350 s.blockTime = e.BlockTime 351 352 s.statusSem = semaphore.NewWeighted(1) 353 s.postageSem = semaphore.NewWeighted(1) 354 s.stakingSem = semaphore.NewWeighted(1) 355 s.cashOutChequeSem = semaphore.NewWeighted(1) 356 357 s.chainID = chainID 358 s.erc20Service = erc20 359 s.syncStatus = e.SyncStatus 360 361 s.statusService = e.NodeStatus 362 363 s.preMapHooks["resolve"] = func(v string) (string, error) { 364 switch addr, err := s.resolveNameOrAddress(v); { 365 case err == nil: 366 return addr.String(), nil 367 case errors.Is(err, ens.ErrNotImplemented): 368 return v, nil 369 default: 370 return "", err 371 } 372 } 373 374 s.pinIntegrity = e.PinIntegrity 375 } 376 377 func (s *Service) SetProbe(probe *Probe) { 378 s.probe = probe 379 } 380 381 // Close hangs up running websockets on shutdown. 382 func (s *Service) Close() error { 383 s.logger.Info("api shutting down") 384 close(s.quit) 385 386 done := make(chan struct{}) 387 go func() { 388 defer close(done) 389 s.wsWg.Wait() 390 }() 391 392 select { 393 case <-done: 394 case <-time.After(1 * time.Second): 395 return errors.New("api shutting down with open websockets") 396 } 397 398 return nil 399 } 400 401 // getOrCreateSessionID attempts to get the session if an tag id is supplied, and returns an error 402 // if it does not exist. If no id is supplied, it will attempt to create a new session and return it. 403 func (s *Service) getOrCreateSessionID(tagUid uint64) (uint64, error) { 404 var ( 405 tag storer.SessionInfo 406 err error 407 ) 408 // if tag ID is not supplied, create a new tag 409 if tagUid == 0 { 410 tag, err = s.storer.NewSession() 411 } else { 412 tag, err = s.storer.Session(tagUid) 413 } 414 return tag.TagID, err 415 } 416 417 func (s *Service) resolveNameOrAddress(str string) (swarm.Address, error) { 418 // Try and mapStructure the name as a bzz address. 419 addr, err := swarm.ParseHexAddress(str) 420 if err == nil { 421 s.loggerV1.Debug("resolve name: parsing bzz address successful", "string", str, "address", addr) 422 return addr, nil 423 } 424 425 // If no resolver is not available, return an error. 426 if s.resolver == nil { 427 return swarm.ZeroAddress, errNoResolver 428 } 429 430 // Try and resolve the name using the provided resolver. 431 s.logger.Debug("resolve name: attempting to resolve string to address", "string", str) 432 addr, err = s.resolver.Resolve(str) 433 if err == nil { 434 s.loggerV1.Debug("resolve name: address resolved successfully", "string", str, "address", addr) 435 return addr, nil 436 } 437 438 return swarm.ZeroAddress, fmt.Errorf("%w: %w", errInvalidNameOrAddress, err) 439 } 440 441 func (s *Service) newTracingHandler(spanName string) func(h http.Handler) http.Handler { 442 return func(h http.Handler) http.Handler { 443 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 444 ctx, err := s.tracer.WithContextFromHTTPHeaders(r.Context(), r.Header) 445 if err != nil && !errors.Is(err, tracing.ErrContextNotFound) { 446 s.logger.Debug("extract tracing context failed", "span_name", spanName, "error", err) 447 // ignore 448 } 449 450 span, _, ctx := s.tracer.StartSpanFromContext(ctx, spanName, s.logger) 451 defer span.Finish() 452 453 err = s.tracer.AddContextHTTPHeader(ctx, r.Header) 454 if err != nil { 455 s.logger.Debug("inject tracing context failed", "span_name", spanName, "error", err) 456 // ignore 457 } 458 459 h.ServeHTTP(w, r.WithContext(ctx)) 460 }) 461 } 462 } 463 464 func (s *Service) contentLengthMetricMiddleware() func(h http.Handler) http.Handler { 465 return func(h http.Handler) http.Handler { 466 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 467 now := time.Now() 468 h.ServeHTTP(w, r) 469 switch r.Method { 470 case http.MethodGet: 471 hdr := w.Header().Get(ContentLengthHeader) 472 if hdr == "" { 473 s.logger.Debug("content length header not found") 474 return 475 } 476 477 contentLength, err := strconv.Atoi(hdr) 478 if err != nil { 479 s.logger.Debug("int conversion failed", "content_length", hdr, "error", err) 480 return 481 } 482 if contentLength > 0 { 483 s.metrics.ContentApiDuration.WithLabelValues(strconv.FormatInt(toFileSizeBucket(int64(contentLength)), 10), r.Method).Observe(time.Since(now).Seconds()) 484 } 485 case http.MethodPost: 486 if r.ContentLength > 0 { 487 s.metrics.ContentApiDuration.WithLabelValues(strconv.FormatInt(toFileSizeBucket(r.ContentLength), 10), r.Method).Observe(time.Since(now).Seconds()) 488 } 489 } 490 }) 491 } 492 } 493 494 // gasConfigMiddleware can be used by the APIs that allow block chain transactions to set 495 // gas price and gas limit through the HTTP API headers. 496 func (s *Service) gasConfigMiddleware(handlerName string) func(h http.Handler) http.Handler { 497 return func(h http.Handler) http.Handler { 498 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 499 logger := s.logger.WithName(handlerName).Build() 500 501 headers := struct { 502 GasPrice *big.Int `map:"Gas-Price"` 503 GasLimit uint64 `map:"Gas-Limit"` 504 }{} 505 if response := s.mapStructure(r.Header, &headers); response != nil { 506 response("invalid header params", logger, w) 507 return 508 } 509 ctx := r.Context() 510 ctx = sctx.SetGasPrice(ctx, headers.GasPrice) 511 ctx = sctx.SetGasLimit(ctx, headers.GasLimit) 512 513 h.ServeHTTP(w, r.WithContext(ctx)) 514 }) 515 } 516 } 517 518 // corsHandler sets CORS headers to HTTP response if allowed origins are configured. 519 func (s *Service) corsHandler(h http.Handler) http.Handler { 520 allowedHeaders := []string{ 521 "User-Agent", "Accept", "X-Requested-With", "Access-Control-Request-Headers", "Access-Control-Request-Method", "Accept-Ranges", "Content-Encoding", 522 AuthorizationHeader, AcceptEncodingHeader, ContentTypeHeader, ContentDispositionHeader, RangeHeader, OriginHeader, 523 SwarmTagHeader, SwarmPinHeader, SwarmEncryptHeader, SwarmIndexDocumentHeader, SwarmErrorDocumentHeader, SwarmCollectionHeader, SwarmPostageBatchIdHeader, SwarmPostageStampHeader, SwarmDeferredUploadHeader, SwarmRedundancyLevelHeader, SwarmRedundancyStrategyHeader, SwarmRedundancyFallbackModeHeader, SwarmChunkRetrievalTimeoutHeader, SwarmLookAheadBufferSizeHeader, SwarmFeedIndexHeader, SwarmFeedIndexNextHeader, GasPriceHeader, GasLimitHeader, ImmutableHeader, 524 } 525 allowedHeadersStr := strings.Join(allowedHeaders, ", ") 526 527 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 528 if o := r.Header.Get(OriginHeader); o != "" && s.checkOrigin(r) { 529 w.Header().Set("Access-Control-Allow-Credentials", "true") 530 w.Header().Set("Access-Control-Allow-Origin", o) 531 w.Header().Set("Access-Control-Allow-Headers", allowedHeadersStr) 532 w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS, POST, PUT, DELETE") 533 w.Header().Set("Access-Control-Max-Age", "3600") 534 } 535 h.ServeHTTP(w, r) 536 }) 537 } 538 539 // checkOrigin returns true if the origin is not set or is equal to the request host. 540 func (s *Service) checkOrigin(r *http.Request) bool { 541 origin := r.Header[OriginHeader] 542 if len(origin) == 0 { 543 return true 544 } 545 scheme := "http" 546 if r.TLS != nil { 547 scheme = "https" 548 } 549 hosts := append(s.CORSAllowedOrigins, scheme+"://"+r.Host) 550 for _, v := range hosts { 551 if equalASCIIFold(origin[0], v) || v == "*" { 552 return true 553 } 554 } 555 556 return false 557 } 558 559 // validationError is a custom error type for validation errors. 560 type validationError struct { 561 Entry string 562 Value interface{} 563 Cause error 564 } 565 566 // Error implements the error interface. 567 func (e *validationError) Error() string { 568 return fmt.Sprintf("`%s=%v`: %v", e.Entry, e.Value, e.Cause) 569 } 570 571 // mapStructure maps the input into output struct and validates the output. 572 // It's a helper method for the handlers, which reduces the chattiness 573 // of the code. 574 func (s *Service) mapStructure(input, output interface{}) func(string, log.Logger, http.ResponseWriter) { 575 // response unifies the response format for parsing and validation errors. 576 response := func(err error) func(string, log.Logger, http.ResponseWriter) { 577 return func(msg string, logger log.Logger, w http.ResponseWriter) { 578 var merr *multierror.Error 579 if !errors.As(err, &merr) { 580 logger.Debug("mapping and validation failed", "error", err) 581 logger.Error(err, "mapping and validation failed") 582 jsonhttp.InternalServerError(w, err) 583 return 584 } 585 586 logger.Debug(msg, "error", err) 587 logger.Error(err, msg) 588 589 resp := jsonhttp.StatusResponse{ 590 Message: msg, 591 Code: http.StatusBadRequest, 592 } 593 for _, err := range merr.Errors { 594 var perr *parseError 595 if errors.As(err, &perr) { 596 resp.Reasons = append(resp.Reasons, jsonhttp.Reason{ 597 Field: perr.Entry, 598 Error: perr.Cause.Error(), 599 }) 600 } 601 var verr *validationError 602 if errors.As(err, &verr) { 603 resp.Reasons = append(resp.Reasons, jsonhttp.Reason{ 604 Field: verr.Entry, 605 Error: verr.Cause.Error(), 606 }) 607 } 608 } 609 jsonhttp.BadRequest(w, resp) 610 } 611 } 612 613 if err := mapStructure(input, output, s.preMapHooks); err != nil { 614 return response(err) 615 } 616 617 if err := s.validate.Struct(output); err != nil { 618 var errs validator.ValidationErrors 619 if !errors.As(err, &errs) { 620 return response(err) 621 } 622 623 vErrs := &multierror.Error{ErrorFormat: flattenErrorsFormat} 624 for _, err := range errs { 625 val := err.Value() 626 switch v := err.Value().(type) { 627 case []byte: 628 val = string(v) 629 } 630 vErrs = multierror.Append(vErrs, 631 &validationError{ 632 Entry: strings.ToLower(err.Field()), 633 Value: val, 634 Cause: fmt.Errorf("want %s:%s", err.Tag(), err.Param()), 635 }) 636 } 637 return response(vErrs.ErrorOrNil()) 638 } 639 640 return nil 641 } 642 643 // equalASCIIFold returns true if s is equal to t with ASCII case folding as 644 // defined in RFC 4790. 645 func equalASCIIFold(s, t string) bool { 646 for s != "" && t != "" { 647 sr, size := utf8.DecodeRuneInString(s) 648 s = s[size:] 649 tr, size := utf8.DecodeRuneInString(t) 650 t = t[size:] 651 if sr == tr { 652 continue 653 } 654 if 'A' <= sr && sr <= 'Z' { 655 sr = sr + 'a' - 'A' 656 } 657 if 'A' <= tr && tr <= 'Z' { 658 tr = tr + 'a' - 'A' 659 } 660 if sr != tr { 661 return false 662 } 663 } 664 return s == t 665 } 666 667 type putterOptions struct { 668 BatchID []byte 669 TagID uint64 670 Deferred bool 671 Pin bool 672 } 673 674 type putterSessionWrapper struct { 675 storer.PutterSession 676 stamper postage.Stamper 677 save func() error 678 } 679 680 func (p *putterSessionWrapper) Put(ctx context.Context, chunk swarm.Chunk) error { 681 stamp, err := p.stamper.Stamp(chunk.Address()) 682 if err != nil { 683 return err 684 } 685 return p.PutterSession.Put(ctx, chunk.WithStamp(stamp)) 686 } 687 688 func (p *putterSessionWrapper) Done(ref swarm.Address) error { 689 return errors.Join(p.PutterSession.Done(ref), p.save()) 690 } 691 692 func (p *putterSessionWrapper) Cleanup() error { 693 return errors.Join(p.PutterSession.Cleanup(), p.save()) 694 } 695 696 func (s *Service) getStamper(batchID []byte) (postage.Stamper, func() error, error) { 697 exists, err := s.batchStore.Exists(batchID) 698 if err != nil { 699 return nil, nil, fmt.Errorf("batch exists: %w", err) 700 } 701 702 issuer, save, err := s.post.GetStampIssuer(batchID) 703 if err != nil { 704 return nil, nil, fmt.Errorf("stamp issuer: %w", err) 705 } 706 707 if usable := exists && s.post.IssuerUsable(issuer); !usable { 708 return nil, nil, errBatchUnusable 709 } 710 711 return postage.NewStamper(s.stamperStore, issuer, s.signer), save, nil 712 } 713 714 func (s *Service) newStamperPutter(ctx context.Context, opts putterOptions) (storer.PutterSession, error) { 715 if !opts.Deferred && s.beeMode == DevMode { 716 return nil, errUnsupportedDevNodeOperation 717 } 718 719 stamper, save, err := s.getStamper(opts.BatchID) 720 if err != nil { 721 return nil, fmt.Errorf("get stamper: %w", err) 722 } 723 724 var session storer.PutterSession 725 if opts.Deferred || opts.Pin { 726 session, err = s.storer.Upload(ctx, opts.Pin, opts.TagID) 727 } else { 728 session = s.storer.DirectUpload() 729 } 730 731 if err != nil { 732 return nil, fmt.Errorf("failed creating session: %w", err) 733 } 734 735 return &putterSessionWrapper{ 736 PutterSession: session, 737 stamper: stamper, 738 save: save, 739 }, nil 740 } 741 742 func (s *Service) newStampedPutter(ctx context.Context, opts putterOptions, stamp *postage.Stamp) (storer.PutterSession, error) { 743 if !opts.Deferred && s.beeMode == DevMode { 744 return nil, errUnsupportedDevNodeOperation 745 } 746 747 storedBatch, err := s.batchStore.Get(stamp.BatchID()) 748 if err != nil { 749 return nil, errInvalidPostageBatch 750 } 751 752 var session storer.PutterSession 753 if opts.Deferred || opts.Pin { 754 session, err = s.storer.Upload(ctx, opts.Pin, opts.TagID) 755 if err != nil { 756 return nil, fmt.Errorf("failed creating session: %w", err) 757 } 758 } else { 759 session = s.storer.DirectUpload() 760 } 761 762 stamper := postage.NewPresignedStamper(stamp, storedBatch.Owner) 763 764 return &putterSessionWrapper{ 765 PutterSession: session, 766 stamper: stamper, 767 save: func() error { return nil }, 768 }, nil 769 } 770 771 type pipelineFunc func(context.Context, io.Reader) (swarm.Address, error) 772 773 func requestPipelineFn(s storage.Putter, encrypt bool, rLevel redundancy.Level) pipelineFunc { 774 return func(ctx context.Context, r io.Reader) (swarm.Address, error) { 775 pipe := builder.NewPipelineBuilder(ctx, s, encrypt, rLevel) 776 return builder.FeedPipeline(ctx, pipe, r) 777 } 778 } 779 780 func requestPipelineFactory(ctx context.Context, s storage.Putter, encrypt bool, rLevel redundancy.Level) func() pipeline.Interface { 781 return func() pipeline.Interface { 782 return builder.NewPipelineBuilder(ctx, s, encrypt, rLevel) 783 } 784 } 785 786 type cleanupOnErrWriter struct { 787 http.ResponseWriter 788 logger log.Logger 789 onErr func() error 790 } 791 792 func (r *cleanupOnErrWriter) WriteHeader(statusCode int) { 793 // if there is an error status returned, cleanup. 794 if statusCode >= http.StatusBadRequest { 795 err := r.onErr() 796 if err != nil { 797 r.logger.Debug("failed cleaning up", "err", err) 798 } 799 } 800 r.ResponseWriter.WriteHeader(statusCode) 801 } 802 803 // CalculateNumberOfChunks calculates the number of chunks in an arbitrary 804 // content length. 805 func CalculateNumberOfChunks(contentLength int64, isEncrypted bool) int64 { 806 if contentLength <= swarm.ChunkSize { 807 return 1 808 } 809 branchingFactor := swarm.Branches 810 if isEncrypted { 811 branchingFactor = swarm.EncryptedBranches 812 } 813 814 dataChunks := math.Ceil(float64(contentLength) / float64(swarm.ChunkSize)) 815 totalChunks := dataChunks 816 intermediate := dataChunks / float64(branchingFactor) 817 818 for intermediate > 1 { 819 totalChunks += math.Ceil(intermediate) 820 intermediate = intermediate / float64(branchingFactor) 821 } 822 823 return int64(totalChunks) + 1 824 } 825 826 // defaultUploadMethod returns true for deferred when the deferred header is not present. 827 func defaultUploadMethod(deferred *bool) bool { 828 if deferred == nil { 829 return true 830 } 831 832 return *deferred 833 }