package handler import ( "io" "net/http" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "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" "go.uber.org/zap" ) const sizeToDetectType = 512 func getRangeToDetectContentType(maxSize uint64) *layer.RangeParams { end := maxSize if sizeToDetectType < end { end = sizeToDetectType } return &layer.RangeParams{ Start: 0, End: end - 1, } } func (h *handler) HeadObjectHandler(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 } conditional := parseConditionalHeaders(r.Header, h.reqLogger(ctx)) p := &layer.HeadObjectParams{ BktInfo: bktInfo, Object: reqInfo.ObjectName, VersionID: reqInfo.URL.Query().Get(api.QueryVersionID), } extendedInfo, err := h.obj.GetExtendedObjectInfo(ctx, p) if err != nil { h.logAndSendError(ctx, w, "could not find object", reqInfo, err) return } info := extendedInfo.ObjectInfo encryptionParams, err := h.formEncryptionParams(r) if err != nil { h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err) return } if err = encryptionParams.MatchObjectEncryption(layer.FormEncryptionInfo(info.Headers)); err != nil { h.logAndSendError(ctx, w, "encryption doesn't match object", reqInfo, errors.GetAPIError(errors.ErrBadRequest), zap.Error(err)) return } bktSettings, err := h.obj.GetBucketSettings(ctx, bktInfo) if err != nil { h.logAndSendError(ctx, w, "could not get bucket settings", reqInfo, err) return } t := &data.ObjectVersion{ BktInfo: bktInfo, ObjectName: info.Name, VersionID: info.VersionID(), } tagSet, lockInfo, err := h.obj.GetObjectTaggingAndLock(ctx, t, extendedInfo.NodeVersion) if err != nil && !errors.IsS3Error(err, errors.ErrNoSuchKey) { h.logAndSendError(ctx, w, "could not get object meta data", reqInfo, err) return } if err = checkPreconditions(info, conditional, h.cfg.MD5Enabled()); err != nil { if errors.IsS3Error(err, errors.ErrNotModified) { writeNotModifiedHeaders(w.Header(), extendedInfo, len(tagSet), bktSettings.Unversioned(), h.cfg.MD5Enabled()) } h.logAndSendError(ctx, w, "precondition failed", reqInfo, err) return } if len(info.ContentType) == 0 { if info.ContentType = layer.MimeByFilePath(info.Name); len(info.ContentType) == 0 { getParams := &layer.GetObjectParams{ ObjectInfo: info, Versioned: p.Versioned(), Range: getRangeToDetectContentType(info.Size), BucketInfo: bktInfo, } objPayload, err := h.obj.GetObject(ctx, getParams) if err != nil { h.logAndSendError(ctx, w, "could not get object", reqInfo, err, zap.Stringer("oid", info.ID)) return } buffer, err := io.ReadAll(objPayload) if err != nil { h.logAndSendError(ctx, w, "could not partly read payload to detect content type", reqInfo, err, zap.Stringer("oid", info.ID)) return } info.ContentType = http.DetectContentType(buffer) } } if err = h.setLockingHeaders(bktInfo, lockInfo, w.Header()); err != nil { h.logAndSendError(ctx, w, "could not get locking info", reqInfo, err) return } writeHeaders(w.Header(), r.Header, extendedInfo, len(tagSet), bktSettings.Unversioned(), h.cfg.MD5Enabled()) w.WriteHeader(http.StatusOK) } func (h *handler) HeadBucketHandler(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 } w.Header().Set(api.OwnerID, bktInfo.Owner.EncodeToString()) w.Header().Set(api.ContainerID, bktInfo.CID.EncodeToString()) w.Header().Set(api.AmzBucketRegion, bktInfo.LocationConstraint) if isAvailableToResolve(bktInfo.Zone, h.cfg.ResolveZoneList(), h.cfg.IsResolveListAllow()) { w.Header().Set(api.ContainerName, bktInfo.Name) w.Header().Set(api.ContainerZone, bktInfo.Zone) } if err = middleware.WriteResponse(w, http.StatusOK, nil, middleware.MimeNone); err != nil { h.logAndSendError(ctx, w, "write response", reqInfo, err) return } } func (h *handler) setLockingHeaders(bktInfo *data.BucketInfo, lockInfo data.LockInfo, header http.Header) error { if !bktInfo.ObjectLockEnabled { return nil } legalHold := &data.LegalHold{Status: legalHoldOff} retention := &data.Retention{Mode: governanceMode} if lockInfo.IsLegalHoldSet() { legalHold.Status = legalHoldOn } if lockInfo.IsRetentionSet() { retention.RetainUntilDate = lockInfo.UntilDate() if lockInfo.IsCompliance() { retention.Mode = complianceMode } } writeLockHeaders(header, legalHold, retention) return nil } func writeLockHeaders(h http.Header, legalHold *data.LegalHold, retention *data.Retention) { h.Set(api.AmzObjectLockLegalHold, legalHold.Status) if retention.RetainUntilDate != "" { h.Set(api.AmzObjectLockRetainUntilDate, retention.RetainUntilDate) h.Set(api.AmzObjectLockMode, retention.Mode) } } func isAvailableToResolve(zone string, list []string, isAllowList bool) bool { // empty zone means container doesn't have proper system name, // so we don't have to resolve it if len(zone) == 0 { return false } var zoneInList bool for _, t := range list { if t == zone { zoneInList = true break } } // InList | IsAllowList | Result // 0 0 1 // 0 1 0 // 1 0 0 // 1 1 1 return zoneInList == isAllowList }