package handler import ( "bytes" "context" "crypto/md5" "encoding/base64" "fmt" "io" "net/http" "time" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/util" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap" "github.com/google/uuid" ) const ( maxRules = 1000 maxRuleIDLen = 255 maxNewerNoncurrentVersions = 100 ) func (h *handler) GetBucketLifecycleHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() reqInfo := middleware.GetReqInfo(ctx) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) if err != nil { h.logAndSendError(ctx, w, "could not get bucket info", reqInfo, err) return } cfg, err := h.obj.GetBucketLifecycleConfiguration(ctx, bktInfo) if err != nil { h.logAndSendError(ctx, w, "could not get bucket lifecycle configuration", reqInfo, err) return } if err = middleware.EncodeToResponse(w, cfg); err != nil { h.logAndSendError(ctx, w, "could not encode GetBucketLifecycle response", reqInfo, err) return } } func (h *handler) PutBucketLifecycleHandler(w http.ResponseWriter, r *http.Request) { var buf bytes.Buffer tee := io.TeeReader(r.Body, &buf) ctx := r.Context() reqInfo := middleware.GetReqInfo(ctx) // Content-Md5 is required and should be set // https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketLifecycleConfiguration.html if _, ok := r.Header[api.ContentMD5]; !ok { h.logAndSendError(ctx, w, "missing Content-MD5", reqInfo, apierr.GetAPIError(apierr.ErrMissingContentMD5)) return } headerMD5, err := base64.StdEncoding.DecodeString(r.Header.Get(api.ContentMD5)) if err != nil { h.logAndSendError(ctx, w, "invalid Content-MD5", reqInfo, apierr.GetAPIError(apierr.ErrInvalidDigest)) return } cfg := new(data.LifecycleConfiguration) if err = h.cfg.NewXMLDecoder(tee).Decode(cfg); err != nil { h.logAndSendError(ctx, w, "could not decode body", reqInfo, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrMalformedXML), err.Error())) return } bodyMD5, err := getContentMD5(&buf) if err != nil { h.logAndSendError(ctx, w, "could not get content md5", reqInfo, err) return } if !bytes.Equal(headerMD5, bodyMD5) { h.logAndSendError(ctx, w, "Content-MD5 does not match", reqInfo, apierr.GetAPIError(apierr.ErrInvalidDigest)) return } bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) if err != nil { h.logAndSendError(ctx, w, "could not get bucket info", reqInfo, err) return } networkInfo, err := h.obj.GetNetworkInfo(ctx) if err != nil { h.logAndSendError(ctx, w, "could not get network info", reqInfo, err) return } if err = checkLifecycleConfiguration(ctx, cfg, &networkInfo); err != nil { h.logAndSendError(ctx, w, "invalid lifecycle configuration", reqInfo, err) return } params := &layer.PutBucketLifecycleParams{ BktInfo: bktInfo, LifecycleCfg: cfg, } params.CopiesNumbers, err = h.pickCopiesNumbers(parseMetadata(r), reqInfo.Namespace, bktInfo.LocationConstraint) if err != nil { h.logAndSendError(ctx, w, "invalid copies number", reqInfo, err) return } if err = h.obj.PutBucketLifecycleConfiguration(ctx, params); err != nil { h.logAndSendError(ctx, w, "could not put bucket lifecycle configuration", reqInfo, err) return } } func (h *handler) DeleteBucketLifecycleHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() reqInfo := middleware.GetReqInfo(ctx) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) if err != nil { h.logAndSendError(ctx, w, "could not get bucket info", reqInfo, err) return } if err = h.obj.DeleteBucketLifecycleConfiguration(ctx, bktInfo); err != nil { h.logAndSendError(ctx, w, "could not delete bucket lifecycle configuration", reqInfo, err) return } w.WriteHeader(http.StatusNoContent) } func checkLifecycleConfiguration(ctx context.Context, cfg *data.LifecycleConfiguration, ni *netmap.NetworkInfo) error { now := layer.TimeNow(ctx) if len(cfg.Rules) > maxRules { return fmt.Errorf("%w: number of rules cannot be greater than %d", apierr.GetAPIError(apierr.ErrInvalidRequest), maxRules) } ids := make(map[string]struct{}, len(cfg.Rules)) for i, rule := range cfg.Rules { if rule.ID == "" { id, err := uuid.NewRandom() if err != nil { return fmt.Errorf("generate uuid: %w", err) } cfg.Rules[i].ID = id.String() rule.ID = id.String() } if _, ok := ids[rule.ID]; ok { return fmt.Errorf("%w: duplicate 'ID': %s", apierr.GetAPIError(apierr.ErrInvalidArgument), rule.ID) } ids[rule.ID] = struct{}{} if len(rule.ID) > maxRuleIDLen { return fmt.Errorf("%w: 'ID' value cannot be longer than %d characters", apierr.GetAPIError(apierr.ErrInvalidArgument), maxRuleIDLen) } if rule.Status != data.LifecycleStatusEnabled && rule.Status != data.LifecycleStatusDisabled { return fmt.Errorf("%w: invalid lifecycle status: %s", apierr.GetAPIError(apierr.ErrMalformedXML), rule.Status) } if rule.AbortIncompleteMultipartUpload == nil && rule.Expiration == nil && rule.NonCurrentVersionExpiration == nil { return fmt.Errorf("%w: at least one action needs to be specified in a rule", apierr.GetAPIError(apierr.ErrInvalidRequest)) } if rule.AbortIncompleteMultipartUpload != nil { if rule.AbortIncompleteMultipartUpload.DaysAfterInitiation != nil && *rule.AbortIncompleteMultipartUpload.DaysAfterInitiation <= 0 { return fmt.Errorf("%w: days after initiation must be a positive integer", apierr.GetAPIError(apierr.ErrInvalidArgument)) } if rule.Filter != nil && (rule.Filter.Tag != nil || (rule.Filter.And != nil && len(rule.Filter.And.Tags) > 0)) { return fmt.Errorf("%w: abort incomplete multipart upload cannot be specified with tags", apierr.GetAPIError(apierr.ErrInvalidRequest)) } } if rule.Expiration != nil { if rule.Expiration.ExpiredObjectDeleteMarker != nil { if rule.Expiration.Days != nil || rule.Expiration.Date != "" { return fmt.Errorf("%w: expired object delete marker cannot be specified with days or date", apierr.GetAPIError(apierr.ErrMalformedXML)) } if rule.Filter != nil && (rule.Filter.Tag != nil || (rule.Filter.And != nil && len(rule.Filter.And.Tags) > 0)) { return fmt.Errorf("%w: expired object delete marker cannot be specified with tags", apierr.GetAPIError(apierr.ErrInvalidRequest)) } } if rule.Expiration.Days != nil && *rule.Expiration.Days <= 0 { return fmt.Errorf("%w: expiration days must be a positive integer", apierr.GetAPIError(apierr.ErrInvalidArgument)) } if rule.Expiration.Date != "" { parsedTime, err := time.Parse("2006-01-02T15:04:05Z", rule.Expiration.Date) if err != nil { return fmt.Errorf("%w: invalid value of expiration date: %s", apierr.GetAPIError(apierr.ErrInvalidArgument), rule.Expiration.Date) } epoch, err := util.TimeToEpoch(ni, now, parsedTime) if err != nil { return fmt.Errorf("convert time to epoch: %w", err) } cfg.Rules[i].Expiration.Epoch = &epoch } } if rule.NonCurrentVersionExpiration != nil { if rule.NonCurrentVersionExpiration.NewerNonCurrentVersions != nil && rule.NonCurrentVersionExpiration.NonCurrentDays == nil { return fmt.Errorf("%w: newer noncurrent versions cannot be specified without noncurrent days", apierr.GetAPIError(apierr.ErrMalformedXML)) } if rule.NonCurrentVersionExpiration.NewerNonCurrentVersions != nil && (*rule.NonCurrentVersionExpiration.NewerNonCurrentVersions > maxNewerNoncurrentVersions || *rule.NonCurrentVersionExpiration.NewerNonCurrentVersions <= 0) { return fmt.Errorf("%w: newer noncurrent versions must be a positive integer up to %d", apierr.GetAPIError(apierr.ErrInvalidArgument), maxNewerNoncurrentVersions) } if rule.NonCurrentVersionExpiration.NonCurrentDays != nil && *rule.NonCurrentVersionExpiration.NonCurrentDays <= 0 { return fmt.Errorf("%w: noncurrent days must be a positive integer", apierr.GetAPIError(apierr.ErrInvalidArgument)) } } if err := checkLifecycleRuleFilter(rule.Filter); err != nil { return err } if rule.Filter != nil && rule.Filter.Prefix != "" && rule.Prefix != "" { return fmt.Errorf("%w: rule cannot have two prefixes", apierr.GetAPIError(apierr.ErrMalformedXML)) } } return nil } func checkLifecycleRuleFilter(filter *data.LifecycleRuleFilter) error { if filter == nil { return nil } var fields int if filter.And != nil { fields++ for _, tag := range filter.And.Tags { err := checkTag(tag) if err != nil { return err } } if filter.And.ObjectSizeLessThan != nil { if *filter.And.ObjectSizeLessThan == 0 { return fmt.Errorf("%w: the maximum object size must be more than 0", apierr.GetAPIError(apierr.ErrInvalidRequest)) } if filter.And.ObjectSizeGreaterThan != nil && *filter.And.ObjectSizeLessThan <= *filter.And.ObjectSizeGreaterThan { return fmt.Errorf("%w: the maximum object size must be larger than the minimum object size", apierr.GetAPIError(apierr.ErrInvalidRequest)) } } } if filter.ObjectSizeGreaterThan != nil { fields++ } if filter.ObjectSizeLessThan != nil { if *filter.ObjectSizeLessThan == 0 { return fmt.Errorf("%w: the maximum object size must be more than 0", apierr.GetAPIError(apierr.ErrInvalidRequest)) } fields++ } if filter.Prefix != "" { fields++ } if filter.Tag != nil { fields++ err := checkTag(*filter.Tag) if err != nil { return err } } if fields > 1 { return fmt.Errorf("%w: filter cannot have more than one field", apierr.GetAPIError(apierr.ErrMalformedXML)) } return nil } func getContentMD5(reader io.Reader) ([]byte, error) { hash := md5.New() _, err := io.Copy(hash, reader) if err != nil { return nil, err } return hash.Sum(nil), nil }