package handler import ( "context" "errors" "io" "net/http" "net/url" "strconv" "time" "git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/logs" "git.frostfs.info/TrueCloudLab/frostfs-http-gw/response" "git.frostfs.info/TrueCloudLab/frostfs-http-gw/tree" "git.frostfs.info/TrueCloudLab/frostfs-http-gw/utils" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" "github.com/valyala/fasthttp" "go.uber.org/zap" ) // max bytes needed to detect content type according to http.DetectContentType docs. const sizeToDetectType = 512 const ( hdrObjectID = "X-Object-Id" hdrOwnerID = "X-Owner-Id" hdrContainerID = "X-Container-Id" ) func (h *Handler) headObject(ctx context.Context, req request, objectAddress oid.Address) { var start = time.Now() btoken := bearerToken(ctx) prm := PrmObjectHead{ PrmAuth: PrmAuth{ BearerToken: btoken, }, Address: objectAddress, } obj, err := h.frostfs.HeadObject(ctx, prm) if err != nil { req.handleFrostFSErr(err, start) return } req.Response.Header.Set(fasthttp.HeaderContentLength, strconv.FormatUint(obj.PayloadSize(), 10)) var contentType string for _, attr := range obj.Attributes() { key := attr.Key() val := attr.Value() if !isValidToken(key) || !isValidValue(val) { continue } key = utils.BackwardTransformIfSystem(key) req.Response.Header.Set(utils.UserAttributeHeaderPrefix+key, val) switch key { case object.AttributeTimestamp: value, err := strconv.ParseInt(val, 10, 64) if err != nil { req.log.Info(logs.CouldntParseCreationDate, zap.String("key", key), zap.String("val", val), zap.Error(err)) continue } req.Response.Header.Set(fasthttp.HeaderLastModified, time.Unix(value, 0).UTC().Format(http.TimeFormat)) case object.AttributeContentType: contentType = val } } idsToResponse(&req.Response, obj) if len(contentType) == 0 { contentType, _, err = readContentType(obj.PayloadSize(), func(sz uint64) (io.Reader, error) { prmRange := PrmObjectRange{ PrmAuth: PrmAuth{ BearerToken: btoken, }, Address: objectAddress, PayloadRange: [2]uint64{0, sz}, } return h.frostfs.RangeObject(ctx, prmRange) }) if err != nil && err != io.EOF { req.handleFrostFSErr(err, start) return } } req.SetContentType(contentType) } func idsToResponse(resp *fasthttp.Response, obj *object.Object) { objID, _ := obj.ID() cnrID, _ := obj.ContainerID() resp.Header.Set(hdrObjectID, objID.String()) resp.Header.Set(hdrOwnerID, obj.OwnerID().String()) resp.Header.Set(hdrContainerID, cnrID.String()) } // HeadByAddressOrBucketName handles head requests using simple cid/oid or bucketname/key format. func (h *Handler) HeadByAddressOrBucketName(c *fasthttp.RequestCtx) { cidParam, _ := c.UserValue("cid").(string) oidParam, _ := c.UserValue("oid").(string) ctx := utils.GetContextFromRequest(c) log := utils.GetReqLogOrDefault(ctx, h.log).With( zap.String("cid", cidParam), zap.String("oid", oidParam), ) bktInfo, err := h.getBucketInfo(ctx, cidParam, log) if err != nil { logAndSendBucketError(c, log, err) return } foundOid, checkErr := h.checkS3ObjectExist(ctx, bktInfo, oidParam) if checkErr != nil && !errors.Is(checkErr, tree.ErrNodeNotFound) { logAndSendBucketError(c, log, checkErr) return } var objID oid.ID if foundOid != nil { // Head object via S3 addr := newAddress(bktInfo.CID, foundOid.OID) h.headObject(ctx, h.newRequest(c, log), addr) } else if err = objID.DecodeString(oidParam); err == nil { // Head object via native protocol addr := newAddress(bktInfo.CID, objID) h.headObject(ctx, h.newRequest(c, log), addr) } else { logAndSendBucketError(c, log, checkErr) return } } // HeadByAttribute handles attribute-based head requests. func (h *Handler) HeadByAttribute(c *fasthttp.RequestCtx) { cidParam, _ := c.UserValue("cid").(string) key, _ := c.UserValue("attr_key").(string) val, _ := c.UserValue("attr_val").(string) ctx := utils.GetContextFromRequest(c) log := utils.GetReqLogOrDefault(ctx, h.log) key, err := url.QueryUnescape(key) if err != nil { log.Error(logs.FailedToUnescapeQuery, zap.String("cid", cidParam), zap.String("attr_key", key), zap.Error(err)) response.Error(c, "could not unescape attr_key: "+err.Error(), fasthttp.StatusBadRequest) return } val, err = url.QueryUnescape(val) if err != nil { log.Error(logs.FailedToUnescapeQuery, zap.String("cid", cidParam), zap.String("attr_val", val), zap.Error(err)) response.Error(c, "could not unescape attr_val: "+err.Error(), fasthttp.StatusBadRequest) return } log = log.With(zap.String("cid", cidParam), zap.String("attr_key", key), zap.String("attr_val", val)) bktInfo, err := h.getBucketInfo(ctx, cidParam, log) if err != nil { logAndSendBucketError(c, log, err) return } res, err := h.search(ctx, bktInfo.CID, key, val, object.MatchStringEqual) if err != nil { log.Error(logs.CouldNotSearchForObjects, zap.Error(err)) response.Error(c, "could not search for objects: "+err.Error(), fasthttp.StatusBadRequest) return } defer res.Close() buf := make([]oid.ID, 1) n, err := res.Read(buf) if n == 0 { if errors.Is(err, io.EOF) { log.Error(logs.ObjectNotFound, zap.Error(err)) response.Error(c, "object not found", fasthttp.StatusNotFound) return } log.Error(logs.ReadObjectListFailed, zap.Error(err)) response.Error(c, "read object list failed: "+err.Error(), fasthttp.StatusBadRequest) return } var addr oid.Address addr.SetContainer(bktInfo.CID) addr.SetObject(buf[0]) h.headObject(ctx, h.newRequest(c, log), addr) }