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" ) 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, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrMalformedXML), err.Error())) 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("number of rules cannot be greater than %d", maxRules) } ids := make(map[string]struct{}, len(cfg.Rules)) for i, rule := range cfg.Rules { if _, ok := ids[rule.ID]; ok && rule.ID != "" { return fmt.Errorf("duplicate 'ID': %s", rule.ID) } ids[rule.ID] = struct{}{} if len(rule.ID) > maxRuleIDLen { return fmt.Errorf("'ID' value cannot be longer than %d characters", maxRuleIDLen) } if rule.Status != data.LifecycleStatusEnabled && rule.Status != data.LifecycleStatusDisabled { return fmt.Errorf("invalid lifecycle status: %s", rule.Status) } if rule.AbortIncompleteMultipartUpload == nil && rule.Expiration == nil && rule.NonCurrentVersionExpiration == nil { return fmt.Errorf("at least one action needs to be specified in a rule") } if rule.AbortIncompleteMultipartUpload != nil { if rule.AbortIncompleteMultipartUpload.DaysAfterInitiation != nil && *rule.AbortIncompleteMultipartUpload.DaysAfterInitiation <= 0 { return fmt.Errorf("days after initiation must be a positive integer: %d", *rule.AbortIncompleteMultipartUpload.DaysAfterInitiation) } if rule.Filter != nil && (rule.Filter.Tag != nil || (rule.Filter.And != nil && len(rule.Filter.And.Tags) > 0)) { return fmt.Errorf("abort incomplete multipart upload cannot be specified with tags") } } if rule.Expiration != nil { if rule.Expiration.ExpiredObjectDeleteMarker != nil { if rule.Expiration.Days != nil || rule.Expiration.Date != "" { return fmt.Errorf("expired object delete marker cannot be specified with days or date") } if rule.Filter != nil && (rule.Filter.Tag != nil || (rule.Filter.And != nil && len(rule.Filter.And.Tags) > 0)) { return fmt.Errorf("expired object delete marker cannot be specified with tags") } } if rule.Expiration.Days != nil && *rule.Expiration.Days <= 0 { return fmt.Errorf("expiration days must be a positive integer: %d", *rule.Expiration.Days) } if rule.Expiration.Date != "" { parsedTime, err := time.Parse("2006-01-02T15:04:05Z", rule.Expiration.Date) if err != nil { return fmt.Errorf("invalid value of expiration date: %s", 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.NewerNonCurrentVersions > maxNewerNoncurrentVersions || *rule.NonCurrentVersionExpiration.NewerNonCurrentVersions <= 0) { return fmt.Errorf("invalid value of newer noncurrent versions: %d", *rule.NonCurrentVersionExpiration.NewerNonCurrentVersions) } if rule.NonCurrentVersionExpiration.NonCurrentDays != nil && *rule.NonCurrentVersionExpiration.NonCurrentDays <= 0 { return fmt.Errorf("invalid value of noncurrent days: %d", *rule.NonCurrentVersionExpiration.NonCurrentDays) } } 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.ObjectSizeGreaterThan != nil && filter.And.ObjectSizeLessThan != nil && *filter.And.ObjectSizeLessThan <= *filter.And.ObjectSizeGreaterThan { return fmt.Errorf("the maximum object size must be larger than the minimum object size") } } if filter.ObjectSizeGreaterThan != nil { fields++ } if filter.ObjectSizeLessThan != nil { 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("filter cannot have more than one field") } 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 }