From 45f9d8bd4615c8e4d0fd0106954b4588d862c0e2 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Mon, 23 Dec 2024 16:33:15 +0300 Subject: [PATCH 1/8] services/object: Inline intermediate DELETE service Continues b3e19e2c00dedfd5d6620a62859567ac7885458c. Other ops are going to be changed the same way in future commits. Signed-off-by: Leonard Lyubich --- cmd/neofs-node/object.go | 13 ++---- pkg/services/object/delete/v2/service.go | 59 ------------------------ pkg/services/object/delete/v2/util.go | 54 ---------------------- pkg/services/object/server.go | 42 +++++++++++++++-- pkg/services/object/server_test.go | 3 +- pkg/services/object/util/prm.go | 52 +++++++++++++++++++++ 6 files changed, 96 insertions(+), 127 deletions(-) delete mode 100644 pkg/services/object/delete/v2/service.go delete mode 100644 pkg/services/object/delete/v2/util.go diff --git a/cmd/neofs-node/object.go b/cmd/neofs-node/object.go index e1869a99f4..b17d5d6cdc 100644 --- a/cmd/neofs-node/object.go +++ b/cmd/neofs-node/object.go @@ -21,7 +21,6 @@ import ( "github.com/nspcc-dev/neofs-node/pkg/services/object/acl" v2 "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/v2" deletesvc "github.com/nspcc-dev/neofs-node/pkg/services/object/delete" - deletesvcV2 "github.com/nspcc-dev/neofs-node/pkg/services/object/delete/v2" getsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/get" getsvcV2 "github.com/nspcc-dev/neofs-node/pkg/services/object/get/v2" headsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/head" @@ -54,7 +53,7 @@ type objectSvc struct { get *getsvcV2.Service - delete *deletesvcV2.Service + delete *deletesvc.Service } func (c *cfg) MaxObjectSize() uint64 { @@ -84,8 +83,8 @@ func (s *objectSvc) Get(req *object.GetRequest, stream objectService.GetObjectSt return s.get.Get(req, stream) } -func (s *objectSvc) Delete(ctx context.Context, req *object.DeleteRequest) (*object.DeleteResponse, error) { - return s.delete.Delete(ctx, req) +func (s *objectSvc) Delete(ctx context.Context, prm deletesvc.Prm) error { + return s.delete.Delete(ctx, prm) } func (s *objectSvc) GetRange(req *object.GetRangeRequest, stream objectService.GetObjectRangeStream) error { @@ -289,15 +288,11 @@ func initObjectService(c *cfg) { deletesvc.WithKeyStorage(keyStorage), ) - sDeleteV2 := deletesvcV2.NewService( - deletesvcV2.WithInternalService(sDelete), - ) - objSvc := &objectSvc{ put: sPutV2, search: sSearchV2, get: sGetV2, - delete: sDeleteV2, + delete: sDelete, } // cachedFirstObjectsNumber is a total cached objects number; the V2 split scheme diff --git a/pkg/services/object/delete/v2/service.go b/pkg/services/object/delete/v2/service.go deleted file mode 100644 index 7b450b8a0c..0000000000 --- a/pkg/services/object/delete/v2/service.go +++ /dev/null @@ -1,59 +0,0 @@ -package deletesvc - -import ( - "context" - - objectV2 "github.com/nspcc-dev/neofs-api-go/v2/object" - deletesvc "github.com/nspcc-dev/neofs-node/pkg/services/object/delete" -) - -// Service implements Delete operation of Object service v2. -type Service struct { - *cfg -} - -// Option represents Service constructor option. -type Option func(*cfg) - -type cfg struct { - svc *deletesvc.Service -} - -// NewService constructs Service instance from provided options. -func NewService(opts ...Option) *Service { - c := new(cfg) - - for i := range opts { - opts[i](c) - } - - return &Service{ - cfg: c, - } -} - -// Delete calls internal service. -func (s *Service) Delete(ctx context.Context, req *objectV2.DeleteRequest) (*objectV2.DeleteResponse, error) { - resp := new(objectV2.DeleteResponse) - - body := new(objectV2.DeleteResponseBody) - resp.SetBody(body) - - p, err := s.toPrm(req, body) - if err != nil { - return nil, err - } - - err = s.svc.Delete(ctx, *p) - if err != nil { - return nil, err - } - - return resp, nil -} - -func WithInternalService(v *deletesvc.Service) Option { - return func(c *cfg) { - c.svc = v - } -} diff --git a/pkg/services/object/delete/v2/util.go b/pkg/services/object/delete/v2/util.go deleted file mode 100644 index bf0564508a..0000000000 --- a/pkg/services/object/delete/v2/util.go +++ /dev/null @@ -1,54 +0,0 @@ -package deletesvc - -import ( - "errors" - "fmt" - - objectV2 "github.com/nspcc-dev/neofs-api-go/v2/object" - "github.com/nspcc-dev/neofs-api-go/v2/refs" - deletesvc "github.com/nspcc-dev/neofs-node/pkg/services/object/delete" - "github.com/nspcc-dev/neofs-node/pkg/services/object/util" - oid "github.com/nspcc-dev/neofs-sdk-go/object/id" -) - -type tombstoneBodyWriter struct { - body *objectV2.DeleteResponseBody -} - -func (s *Service) toPrm(req *objectV2.DeleteRequest, respBody *objectV2.DeleteResponseBody) (*deletesvc.Prm, error) { - body := req.GetBody() - - addrV2 := body.GetAddress() - if addrV2 == nil { - return nil, errors.New("missing object address") - } - - var addr oid.Address - - err := addr.ReadFromV2(*addrV2) - if err != nil { - return nil, fmt.Errorf("invalid object address: %w", err) - } - - commonPrm, err := util.CommonPrmFromV2(req) - if err != nil { - return nil, err - } - - p := new(deletesvc.Prm) - p.SetCommonParameters(commonPrm) - - p.WithAddress(addr) - p.WithTombstoneAddressTarget(&tombstoneBodyWriter{ - body: respBody, - }) - - return p, nil -} - -func (w *tombstoneBodyWriter) SetAddress(addr oid.Address) { - var addrV2 refs.Address - addr.WriteToV2(&addrV2) - - w.body.SetTombstone(&addrV2) -} diff --git a/pkg/services/object/server.go b/pkg/services/object/server.go index 7e21d178b5..cad270a294 100644 --- a/pkg/services/object/server.go +++ b/pkg/services/object/server.go @@ -20,6 +20,8 @@ import ( "github.com/nspcc-dev/neofs-node/pkg/core/netmap" objectcore "github.com/nspcc-dev/neofs-node/pkg/core/object" aclsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/v2" + deletesvc "github.com/nspcc-dev/neofs-node/pkg/services/object/delete" + objutil "github.com/nspcc-dev/neofs-node/pkg/services/object/util" "github.com/nspcc-dev/neofs-node/pkg/services/util" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" @@ -64,7 +66,7 @@ type ServiceServer interface { Put(context.Context) (PutObjectStream, error) Head(context.Context, *v2object.HeadRequest) (*v2object.HeadResponse, error) Search(*v2object.SearchRequest, SearchStream) error - Delete(context.Context, *v2object.DeleteRequest) (*v2object.DeleteResponse, error) + Delete(context.Context, deletesvc.Prm) error GetRange(*v2object.GetRangeRequest, GetObjectRangeStream) error GetRangeHash(context.Context, *v2object.GetRangeHashRequest) (*v2object.GetRangeHashResponse, error) } @@ -290,7 +292,14 @@ func (s *server) makeStatusDeleteResponse(err error) *protoobject.DeleteResponse }) } -// Delete converts gRPC DeleteRequest message and passes it to internal Object service. +type deleteResponseBody protoobject.DeleteResponse_Body + +func (x *deleteResponseBody) SetAddress(addr oid.Address) { + var addr2 refsv2.Address + addr.WriteToV2(&addr2) + x.Tombstone = addr2.ToGRPCMessage().(*refs.Address) +} + func (s *server) Delete(ctx context.Context, req *protoobject.DeleteRequest) (*protoobject.DeleteResponse, error) { delReq := new(v2object.DeleteRequest) err := delReq.FromGRPCMessage(req) @@ -323,12 +332,37 @@ func (s *server) Delete(ctx context.Context, req *protoobject.DeleteRequest) (*p return s.makeStatusDeleteResponse(err), nil } - resp, err := s.srv.Delete(ctx, delReq) + ma := req.GetBody().GetAddress() + if ma == nil { + return s.makeStatusDeleteResponse(errors.New("missing object address")), nil + } + var addr oid.Address + var addr2 refsv2.Address + if err := addr2.FromGRPCMessage(ma); err != nil { + panic(err) + } + err = addr.ReadFromV2(addr2) + if err != nil { + return s.makeStatusDeleteResponse(fmt.Errorf("invalid object address: %w", err)), nil + } + + cp, err := objutil.CommonPrmFromRequest(req) + if err != nil { + return s.makeStatusDeleteResponse(err), nil + } + + var rb protoobject.DeleteResponse_Body + + var p deletesvc.Prm + p.SetCommonParameters(cp) + p.WithAddress(addr) + p.WithTombstoneAddressTarget((*deleteResponseBody)(&rb)) + err = s.srv.Delete(ctx, p) if err != nil { return s.makeStatusDeleteResponse(err), nil } - return s.signDeleteResponse(resp.ToGRPCMessage().(*protoobject.DeleteResponse)), nil + return s.signDeleteResponse(&protoobject.DeleteResponse{Body: &rb}), nil } func (s *server) signHeadResponse(resp *protoobject.HeadResponse) *protoobject.HeadResponse { diff --git a/pkg/services/object/server_test.go b/pkg/services/object/server_test.go index 5d581a6ac0..86298dd3d6 100644 --- a/pkg/services/object/server_test.go +++ b/pkg/services/object/server_test.go @@ -19,6 +19,7 @@ import ( objectcore "github.com/nspcc-dev/neofs-node/pkg/core/object" objectSvc "github.com/nspcc-dev/neofs-node/pkg/services/object" v2 "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/v2" + deletesvc "github.com/nspcc-dev/neofs-node/pkg/services/object/delete" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" @@ -57,7 +58,7 @@ func (x noCallObjectService) Search(*objectV2.SearchRequest, objectSvc.SearchStr panic("must not be called") } -func (x noCallObjectService) Delete(context.Context, *objectV2.DeleteRequest) (*objectV2.DeleteResponse, error) { +func (x noCallObjectService) Delete(context.Context, deletesvc.Prm) error { panic("must not be called") } diff --git a/pkg/services/object/util/prm.go b/pkg/services/object/util/prm.go index ed6ee192a6..93c62b684c 100644 --- a/pkg/services/object/util/prm.go +++ b/pkg/services/object/util/prm.go @@ -3,7 +3,9 @@ package util import ( "fmt" + apiacl "github.com/nspcc-dev/neofs-api-go/v2/acl" "github.com/nspcc-dev/neofs-api-go/v2/session" + protosession "github.com/nspcc-dev/neofs-api-go/v2/session/grpc" "github.com/nspcc-dev/neofs-sdk-go/bearer" sessionsdk "github.com/nspcc-dev/neofs-sdk-go/session" ) @@ -128,3 +130,53 @@ func CommonPrmFromV2(req interface { return prm, nil } + +// CommonPrmFromRequest is a temporary copy-paste of [CommonPrmFromV2]. +func CommonPrmFromRequest(req interface { + GetMetaHeader() *protosession.RequestMetaHeader +}) (*CommonPrm, error) { + meta := req.GetMetaHeader() + ttl := meta.GetTtl() + + // unwrap meta header to get original request meta information + for meta.GetOrigin() != nil { + meta = meta.GetOrigin() + } + + var st *sessionsdk.Object + if mt := meta.GetSessionToken(); mt != nil { + var st2 session.Token + if err := st2.FromGRPCMessage(mt); err != nil { + panic(err) + } + st = new(sessionsdk.Object) + if err := st.ReadFromV2(st2); err != nil { + return nil, fmt.Errorf("invalid session token: %w", err) + } + } + + var bt *bearer.Token + if mt := meta.GetBearerToken(); mt != nil { + var bt2 apiacl.BearerToken + if err := bt2.FromGRPCMessage(mt); err != nil { + panic(err) + } + bt = new(bearer.Token) + if err := bt.ReadFromV2(bt2); err != nil { + return nil, fmt.Errorf("invalid bearer token: %w", err) + } + } + + xHdrs := meta.GetXHeaders() + prm := &CommonPrm{ + local: ttl <= maxLocalTTL, + token: st, + bearer: bt, + ttl: ttl - 1, // decrease TTL for new requests + xhdrs: make([]string, 0, 2*len(xHdrs)), + } + for i := range xHdrs { + prm.xhdrs = append(prm.xhdrs, xHdrs[i].GetKey(), xHdrs[i].GetValue()) + } + return prm, nil +} From adba550d5c1744f80116923e8e38dc3a4349aa4f Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Mon, 23 Dec 2024 18:18:01 +0300 Subject: [PATCH 2/8] services/object: Inline intermediate SEARCH service Continues b3e19e2c00dedfd5d6620a62859567ac7885458c.. Signed-off-by: Leonard Lyubich --- cmd/neofs-node/object.go | 14 +- pkg/services/object/search/v2/service.go | 60 ------- pkg/services/object/search/v2/streamer.go | 29 ---- pkg/services/object/search/v2/util.go | 199 --------------------- pkg/services/object/server.go | 201 +++++++++++++++++++--- pkg/services/object/server_test.go | 3 +- 6 files changed, 179 insertions(+), 327 deletions(-) delete mode 100644 pkg/services/object/search/v2/service.go delete mode 100644 pkg/services/object/search/v2/streamer.go delete mode 100644 pkg/services/object/search/v2/util.go diff --git a/cmd/neofs-node/object.go b/cmd/neofs-node/object.go index b17d5d6cdc..c7f9829a21 100644 --- a/cmd/neofs-node/object.go +++ b/cmd/neofs-node/object.go @@ -27,7 +27,6 @@ import ( putsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/put" putsvcV2 "github.com/nspcc-dev/neofs-node/pkg/services/object/put/v2" searchsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/search" - searchsvcV2 "github.com/nspcc-dev/neofs-node/pkg/services/object/search/v2" "github.com/nspcc-dev/neofs-node/pkg/services/object/split" "github.com/nspcc-dev/neofs-node/pkg/services/object/tombstone" "github.com/nspcc-dev/neofs-node/pkg/services/object/util" @@ -49,7 +48,7 @@ import ( type objectSvc struct { put *putsvcV2.Service - search *searchsvcV2.Service + search *searchsvc.Service get *getsvcV2.Service @@ -75,8 +74,8 @@ func (s *objectSvc) Head(ctx context.Context, req *object.HeadRequest) (*object. return s.get.Head(ctx, req) } -func (s *objectSvc) Search(req *object.SearchRequest, stream objectService.SearchStream) error { - return s.search.Search(req, stream) +func (s *objectSvc) Search(ctx context.Context, prm searchsvc.Prm) error { + return s.search.Search(ctx, prm) } func (s *objectSvc) Get(req *object.GetRequest, stream objectService.GetObjectStream) error { @@ -247,11 +246,6 @@ func initObjectService(c *cfg) { searchsvc.WithKeyStorage(keyStorage), ) - sSearchV2 := searchsvcV2.NewService( - searchsvcV2.WithInternalService(sSearch), - searchsvcV2.WithKeyStorage(keyStorage), - ) - mNumber, err := c.shared.basics.cli.MagicNumber() fatalOnErr(err) @@ -290,7 +284,7 @@ func initObjectService(c *cfg) { objSvc := &objectSvc{ put: sPutV2, - search: sSearchV2, + search: sSearch, get: sGetV2, delete: sDelete, } diff --git a/pkg/services/object/search/v2/service.go b/pkg/services/object/search/v2/service.go deleted file mode 100644 index 2022269380..0000000000 --- a/pkg/services/object/search/v2/service.go +++ /dev/null @@ -1,60 +0,0 @@ -package searchsvc - -import ( - objectV2 "github.com/nspcc-dev/neofs-api-go/v2/object" - objectSvc "github.com/nspcc-dev/neofs-node/pkg/services/object" - searchsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/search" - objutil "github.com/nspcc-dev/neofs-node/pkg/services/object/util" -) - -// Service implements Search operation of Object service v2. -type Service struct { - *cfg -} - -// Option represents Service constructor option. -type Option func(*cfg) - -type cfg struct { - svc *searchsvc.Service - - keyStorage *objutil.KeyStorage -} - -// NewService constructs Service instance from provided options. -func NewService(opts ...Option) *Service { - c := new(cfg) - - for i := range opts { - opts[i](c) - } - - return &Service{ - cfg: c, - } -} - -// Search calls internal service and returns v2 object stream. -func (s *Service) Search(req *objectV2.SearchRequest, stream objectSvc.SearchStream) error { - p, err := s.toPrm(req, stream) - if err != nil { - return err - } - - return s.svc.Search(stream.Context(), *p) -} - -// WithInternalService returns option to set entity -// that handles request payload. -func WithInternalService(v *searchsvc.Service) Option { - return func(c *cfg) { - c.svc = v - } -} - -// WithKeyStorage returns option to set local private key storage. -func WithKeyStorage(ks *objutil.KeyStorage) Option { - return func(c *cfg) { - c.keyStorage = ks - } -} diff --git a/pkg/services/object/search/v2/streamer.go b/pkg/services/object/search/v2/streamer.go deleted file mode 100644 index a1731a28fc..0000000000 --- a/pkg/services/object/search/v2/streamer.go +++ /dev/null @@ -1,29 +0,0 @@ -package searchsvc - -import ( - "github.com/nspcc-dev/neofs-api-go/v2/object" - "github.com/nspcc-dev/neofs-api-go/v2/refs" - objectSvc "github.com/nspcc-dev/neofs-node/pkg/services/object" - oid "github.com/nspcc-dev/neofs-sdk-go/object/id" -) - -type streamWriter struct { - stream objectSvc.SearchStream -} - -func (s *streamWriter) WriteIDs(ids []oid.ID) error { - r := new(object.SearchResponse) - - body := new(object.SearchResponseBody) - r.SetBody(body) - - idsV2 := make([]refs.ObjectID, len(ids)) - - for i := range ids { - ids[i].WriteToV2(&idsV2[i]) - } - - body.SetIDList(idsV2) - - return s.stream.Send(r) -} diff --git a/pkg/services/object/search/v2/util.go b/pkg/services/object/search/v2/util.go deleted file mode 100644 index 498290a421..0000000000 --- a/pkg/services/object/search/v2/util.go +++ /dev/null @@ -1,199 +0,0 @@ -package searchsvc - -import ( - "context" - "errors" - "fmt" - "io" - "sync" - - objectV2 "github.com/nspcc-dev/neofs-api-go/v2/object" - protoobject "github.com/nspcc-dev/neofs-api-go/v2/object/grpc" - "github.com/nspcc-dev/neofs-api-go/v2/refs" - "github.com/nspcc-dev/neofs-api-go/v2/session" - "github.com/nspcc-dev/neofs-api-go/v2/signature" - "github.com/nspcc-dev/neofs-api-go/v2/status" - protostatus "github.com/nspcc-dev/neofs-api-go/v2/status/grpc" - "github.com/nspcc-dev/neofs-node/pkg/core/client" - "github.com/nspcc-dev/neofs-node/pkg/network" - objectSvc "github.com/nspcc-dev/neofs-node/pkg/services/object" - "github.com/nspcc-dev/neofs-node/pkg/services/object/internal" - searchsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/search" - "github.com/nspcc-dev/neofs-node/pkg/services/object/util" - apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" - cid "github.com/nspcc-dev/neofs-sdk-go/container/id" - "github.com/nspcc-dev/neofs-sdk-go/object" - oid "github.com/nspcc-dev/neofs-sdk-go/object/id" - "google.golang.org/grpc" -) - -func (s *Service) toPrm(req *objectV2.SearchRequest, stream objectSvc.SearchStream) (*searchsvc.Prm, error) { - body := req.GetBody() - - cnrV2 := body.GetContainerID() - if cnrV2 == nil { - return nil, errors.New("missing container ID") - } - - var id cid.ID - - err := id.ReadFromV2(*cnrV2) - if err != nil { - return nil, fmt.Errorf("invalid container ID: %w", err) - } - - meta := req.GetMetaHeader() - - commonPrm, err := util.CommonPrmFromV2(req) - if err != nil { - return nil, err - } - - p := new(searchsvc.Prm) - p.SetCommonParameters(commonPrm) - - p.SetWriter(&streamWriter{ - stream: stream, - }) - - if !commonPrm.LocalOnly() { - var onceResign sync.Once - - key, err := s.keyStorage.GetKey(nil) - if err != nil { - return nil, err - } - - p.SetRequestForwarder(groupAddressRequestForwarder(func(addr network.Address, c client.MultiAddressClient, pubkey []byte) ([]oid.ID, error) { - var err error - - // once compose and resign forwarding request - onceResign.Do(func() { - // compose meta header of the local server - metaHdr := new(session.RequestMetaHeader) - metaHdr.SetTTL(meta.GetTTL() - 1) - // TODO: #1165 think how to set the other fields - metaHdr.SetOrigin(meta) - - req.SetMetaHeader(metaHdr) - - err = signature.SignServiceMessage(key, req) - }) - - if err != nil { - return nil, err - } - - var searchStream protoobject.ObjectService_SearchClient - ctx, cancel := context.WithCancel(stream.Context()) - defer cancel() - err = c.RawForAddress(addr, func(conn *grpc.ClientConn) error { - searchStream, err = protoobject.NewObjectServiceClient(conn).Search(ctx, req.ToGRPCMessage().(*protoobject.SearchRequest)) - return err - }) - if err != nil { - return nil, err - } - - // code below is copy-pasted from c.SearchObjects implementation, - // perhaps it is worth highlighting the utility function in neofs-api-go - var searchResult []oid.ID - - for { - // receive message from server stream - resp, err := searchStream.Recv() - if err != nil { - if errors.Is(err, io.EOF) { - break - } - - return nil, fmt.Errorf("reading the response failed: %w", err) - } - - // verify response key - if err = internal.VerifyResponseKeyV2(pubkey, resp); err != nil { - return nil, err - } - - // verify response structure - resp2 := new(objectV2.SearchResponse) - if err = resp2.FromGRPCMessage(resp); err != nil { - panic(err) // can only fail on wrong type, here it's correct - } - if err := signature.VerifyServiceMessage(resp2); err != nil { - return nil, fmt.Errorf("could not verify %T: %w", resp, err) - } - - if err := checkStatus(resp.GetMetaHeader().GetStatus()); err != nil { - return nil, fmt.Errorf("remote node response: %w", err) - } - - chunk := resp.GetBody().GetIdList() - var id oid.ID - - for i := range chunk { - var id2 refs.ObjectID - if err = id2.FromGRPCMessage(chunk[i]); err != nil { - panic(err) // can only fail on wrong type, here it's correct - } - err = id.ReadFromV2(id2) - if err != nil { - return nil, fmt.Errorf("invalid object ID: %w", err) - } - - searchResult = append(searchResult, id) - } - } - - return searchResult, nil - })) - } - - p.WithContainerID(id) - p.WithSearchFilters(object.NewSearchFiltersFromV2(body.GetFilters())) - - return p, nil -} - -func checkStatus(st *protostatus.Status) error { - stV2 := new(status.Status) - if err := stV2.FromGRPCMessage(st); err != nil { - panic(err) // can only fail on wrong type, here it's correct - } - if !status.IsSuccess(stV2.Code()) { - return apistatus.ErrorFromV2(stV2) - } - - return nil -} - -func groupAddressRequestForwarder(f func(network.Address, client.MultiAddressClient, []byte) ([]oid.ID, error)) searchsvc.RequestForwarder { - return func(info client.NodeInfo, c client.MultiAddressClient) ([]oid.ID, error) { - var ( - firstErr error - res []oid.ID - - key = info.PublicKey() - ) - - info.AddressGroup().IterateAddresses(func(addr network.Address) (stop bool) { - var err error - - defer func() { - stop = err == nil - - if stop || firstErr == nil { - firstErr = err - } - - // would be nice to log otherwise - }() - - res, err = f(addr, c, key) - - return - }) - - return res, firstErr - } -} diff --git a/pkg/services/object/server.go b/pkg/services/object/server.go index cad270a294..02b46ed010 100644 --- a/pkg/services/object/server.go +++ b/pkg/services/object/server.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "sync" "time" v2object "github.com/nspcc-dev/neofs-api-go/v2/object" @@ -16,11 +17,16 @@ import ( refs "github.com/nspcc-dev/neofs-api-go/v2/refs/grpc" protosession "github.com/nspcc-dev/neofs-api-go/v2/session/grpc" "github.com/nspcc-dev/neofs-api-go/v2/signature" + "github.com/nspcc-dev/neofs-api-go/v2/status" protostatus "github.com/nspcc-dev/neofs-api-go/v2/status/grpc" + "github.com/nspcc-dev/neofs-node/pkg/core/client" "github.com/nspcc-dev/neofs-node/pkg/core/netmap" objectcore "github.com/nspcc-dev/neofs-node/pkg/core/object" + "github.com/nspcc-dev/neofs-node/pkg/network" aclsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/v2" deletesvc "github.com/nspcc-dev/neofs-node/pkg/services/object/delete" + "github.com/nspcc-dev/neofs-node/pkg/services/object/internal" + searchsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/search" objutil "github.com/nspcc-dev/neofs-node/pkg/services/object/util" "github.com/nspcc-dev/neofs-node/pkg/services/util" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" @@ -32,6 +38,7 @@ import ( "github.com/nspcc-dev/neofs-sdk-go/stat" "github.com/nspcc-dev/neofs-sdk-go/user" "github.com/nspcc-dev/neofs-sdk-go/version" + "google.golang.org/grpc" "google.golang.org/protobuf/proto" ) @@ -47,12 +54,6 @@ type GetObjectRangeStream interface { Send(*v2object.GetRangeResponse) error } -// SearchStream is an interface of NeoFS API v2 compatible search streamer. -type SearchStream interface { - util.ServerStream - Send(*v2object.SearchResponse) error -} - // PutObjectStream is an interface of NeoFS API v2 compatible client's object streamer. type PutObjectStream interface { Send(*v2object.PutRequest) error @@ -65,7 +66,7 @@ type ServiceServer interface { Get(*v2object.GetRequest, GetObjectStream) error Put(context.Context) (PutObjectStream, error) Head(context.Context, *v2object.HeadRequest) (*v2object.HeadResponse, error) - Search(*v2object.SearchRequest, SearchStream) error + Search(context.Context, searchsvc.Prm) error Delete(context.Context, deletesvc.Prm) error GetRange(*v2object.GetRangeRequest, GetObjectRangeStream) error GetRangeHash(context.Context, *v2object.GetRangeHashRequest) (*v2object.GetRangeHashResponse, error) @@ -665,20 +666,17 @@ func (s *server) sendStatusSearchResponse(stream protoobject.ObjectService_Searc }) } -type searchStreamerV2 struct { - protoobject.ObjectService_SearchServer +type searchStream struct { + base protoobject.ObjectService_SearchServer srv *server reqInfo aclsvc.RequestInfo } -func (s *searchStreamerV2) Send(resp *v2object.SearchResponse) error { - r := resp.ToGRPCMessage().(*protoobject.SearchResponse) - ids := r.GetBody().GetIdList() +func (s *searchStream) WriteIDs(ids []oid.ID) error { for len(ids) > 0 { - newResp := &protoobject.SearchResponse{ - Body: &protoobject.SearchResponse_Body{}, - MetaHeader: proto.Clone(r.GetMetaHeader()).(*protosession.ResponseMetaHeader), // TODO: can go w/o cloning? - VerifyHeader: proto.Clone(r.GetVerifyHeader()).(*protosession.ResponseVerificationHeader), + // gRPC can retain message for some time, this is why we create full new message each time + r := &protoobject.SearchResponse{ + Body: &protoobject.SearchResponse_Body{}, } cut := maxObjAddrRespAmount @@ -686,13 +684,18 @@ func (s *searchStreamerV2) Send(resp *v2object.SearchResponse) error { cut = len(ids) } - newResp.Body.IdList = ids[:cut] + r.Body.IdList = make([]*refs.ObjectID, cut) + var id2 refsv2.ObjectID + for i := range cut { + ids[i].WriteToV2(&id2) + r.Body.IdList[i] = id2.ToGRPCMessage().(*refs.ObjectID) + } // TODO: do not check response multiple times // TODO: why check it at all? if err := s.srv.aclChecker.CheckEACL(r, s.reqInfo); err != nil { return eACLErr(s.reqInfo, err) } - if err := s.srv.sendSearchResponse(s.ObjectService_SearchServer, r); err != nil { + if err := s.srv.sendSearchResponse(s.base, r); err != nil { return err } @@ -701,8 +704,6 @@ func (s *searchStreamerV2) Send(resp *v2object.SearchResponse) error { return nil } -// Search converts gRPC SearchRequest message and server-side stream and overtakes its data -// to gRPC stream. func (s *server) Search(req *protoobject.SearchRequest, gStream protoobject.ObjectService_SearchServer) error { searchReq := new(v2object.SearchRequest) err := searchReq.FromGRPCMessage(req) @@ -733,20 +734,152 @@ func (s *server) Search(req *protoobject.SearchRequest, gStream protoobject.Obje return s.sendStatusSearchResponse(gStream, err) } - err = s.srv.Search( - searchReq, - &searchStreamerV2{ - ObjectService_SearchServer: gStream, - srv: s, - reqInfo: reqInfo, - }, - ) + p, err := convertSearchPrm(gStream.Context(), s.signer, req, &searchStream{ + base: gStream, + srv: s, + reqInfo: reqInfo, + }) + if err != nil { + return s.sendStatusSearchResponse(gStream, err) + } + err = s.srv.Search(gStream.Context(), p) if err != nil { return s.sendStatusSearchResponse(gStream, err) } return nil } +// converts original request into parameters accepted by the internal handler. +// Note that the stream is untouched within this call, errors are not reported +// into it. +func convertSearchPrm(ctx context.Context, signer ecdsa.PrivateKey, req *protoobject.SearchRequest, stream *searchStream) (searchsvc.Prm, error) { + body := req.GetBody() + mc := body.GetContainerId() + if mc == nil { + return searchsvc.Prm{}, errors.New("missing container ID") + } + + var id cid.ID + var id2 refsv2.ContainerID + if err := id2.FromGRPCMessage(mc); err != nil { + panic(err) + } + if err := id.ReadFromV2(id2); err != nil { + return searchsvc.Prm{}, fmt.Errorf("invalid container ID: %w", err) + } + + cp, err := objutil.CommonPrmFromRequest(req) + if err != nil { + return searchsvc.Prm{}, err + } + + mfs := body.GetFilters() + fs2 := make([]v2object.SearchFilter, len(mfs)) + for i := range mfs { + if err := fs2[i].FromGRPCMessage(mfs[i]); err != nil { + panic(err) + } + } + + var p searchsvc.Prm + p.SetCommonParameters(cp) + p.WithContainerID(id) + p.WithSearchFilters(object.NewSearchFiltersFromV2(fs2)) + p.SetWriter(stream) + if cp.LocalOnly() { + return p, nil + } + + var onceResign sync.Once + meta := req.GetMetaHeader() + p.SetRequestForwarder(func(node client.NodeInfo, c client.MultiAddressClient) ([]oid.ID, error) { + var err error + onceResign.Do(func() { + req.MetaHeader = &protosession.RequestMetaHeader{ + // TODO: #1165 think how to set the other fields + Ttl: meta.GetTtl() - 1, // FIXME: meta can be nil + Origin: meta, + } + var req2 v2object.SearchRequest + if err := req2.FromGRPCMessage(req); err != nil { + panic(err) + } + if err = signature.SignServiceMessage(&signer, &req2); err == nil { + req = req2.ToGRPCMessage().(*protoobject.SearchRequest) + } + }) + if err != nil { + return nil, err + } + + var firstErr error + nodePub := node.PublicKey() + addrs := node.AddressGroup() + for i := range addrs { + res, err := searchOnRemoteNode(ctx, c, addrs[i], nodePub, req) + if err == nil { + return res, nil + } + if firstErr == nil { + firstErr = err + } + // TODO: log error + } + return nil, firstErr + }) + return p, nil +} + +func searchOnRemoteNode(ctx context.Context, c client.MultiAddressClient, addr network.Address, nodePub []byte, req *protoobject.SearchRequest) ([]oid.ID, error) { + var searchStream protoobject.ObjectService_SearchClient + if err := c.RawForAddress(addr, func(conn *grpc.ClientConn) error { + var err error + // FIXME: context should be cancelled on return from upper func + searchStream, err = protoobject.NewObjectServiceClient(conn).Search(ctx, req) + return err + }); err != nil { + return nil, err + } + + var res []oid.ID + for { + resp, err := searchStream.Recv() + if err != nil { + if errors.Is(err, io.EOF) { + return res, nil + } + return nil, fmt.Errorf("reading the response failed: %w", err) + } + + if err := internal.VerifyResponseKeyV2(nodePub, resp); err != nil { + return nil, err + } + resp2 := new(v2object.SearchResponse) + if err := resp2.FromGRPCMessage(resp); err != nil { + panic(err) // can only fail on wrong type, here it's correct + } + if err := signature.VerifyServiceMessage(resp2); err != nil { + return nil, fmt.Errorf("could not verify %T: %w", resp, err) + } + if err := checkStatus(resp.GetMetaHeader().GetStatus()); err != nil { + return nil, fmt.Errorf("remote node response: %w", err) + } + + chunk := resp.GetBody().GetIdList() + var id oid.ID + for i := range chunk { + var id2 refsv2.ObjectID + if err := id2.FromGRPCMessage(chunk[i]); err != nil { + panic(err) // can only fail on wrong type, here it's correct + } + if err := id.ReadFromV2(id2); err != nil { + return nil, fmt.Errorf("invalid object ID: %w", err) + } + res = append(res, id) + } + } +} + // Replicate serves neo.fs.v2.object.ObjectService/Replicate RPC. func (s *server) Replicate(_ context.Context, req *protoobject.ReplicateRequest) (*protoobject.ReplicateResponse, error) { if req.Object == nil { @@ -1007,3 +1140,15 @@ func (s *server) metaInfoSignature(o object.Object) ([]byte, error) { return res, nil } + +func checkStatus(st *protostatus.Status) error { + stV2 := new(status.Status) + if err := stV2.FromGRPCMessage(st); err != nil { + panic(err) // can only fail on wrong type, here it's correct + } + if !status.IsSuccess(stV2.Code()) { + return apistatus.ErrorFromV2(stV2) + } + + return nil +} diff --git a/pkg/services/object/server_test.go b/pkg/services/object/server_test.go index 86298dd3d6..efbc35c456 100644 --- a/pkg/services/object/server_test.go +++ b/pkg/services/object/server_test.go @@ -20,6 +20,7 @@ import ( objectSvc "github.com/nspcc-dev/neofs-node/pkg/services/object" v2 "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/v2" deletesvc "github.com/nspcc-dev/neofs-node/pkg/services/object/delete" + searchsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/search" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" @@ -54,7 +55,7 @@ func (x noCallObjectService) Head(context.Context, *objectV2.HeadRequest) (*obje panic("must not be called") } -func (x noCallObjectService) Search(*objectV2.SearchRequest, objectSvc.SearchStream) error { +func (x noCallObjectService) Search(context.Context, searchsvc.Prm) error { panic("must not be called") } From d148fefb67628040fa927c724dd74bbc852f1d07 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Mon, 23 Dec 2024 20:30:24 +0300 Subject: [PATCH 3/8] services/object: Inline intermediate PUT service Continues b3e19e2c00dedfd5d6620a62859567ac7885458c. Signed-off-by: Leonard Lyubich --- cmd/neofs-node/object.go | 12 +- pkg/services/object/put/v2/service.go | 62 -------- pkg/services/object/put/v2/streamer.go | 211 ------------------------ pkg/services/object/put/v2/util.go | 47 ------ pkg/services/object/server.go | 212 +++++++++++++++++++++++-- pkg/services/object/server_test.go | 3 +- 6 files changed, 203 insertions(+), 344 deletions(-) delete mode 100644 pkg/services/object/put/v2/service.go delete mode 100644 pkg/services/object/put/v2/streamer.go delete mode 100644 pkg/services/object/put/v2/util.go diff --git a/cmd/neofs-node/object.go b/cmd/neofs-node/object.go index c7f9829a21..23e374f0c1 100644 --- a/cmd/neofs-node/object.go +++ b/cmd/neofs-node/object.go @@ -25,7 +25,6 @@ import ( getsvcV2 "github.com/nspcc-dev/neofs-node/pkg/services/object/get/v2" headsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/head" putsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/put" - putsvcV2 "github.com/nspcc-dev/neofs-node/pkg/services/object/put/v2" searchsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/search" "github.com/nspcc-dev/neofs-node/pkg/services/object/split" "github.com/nspcc-dev/neofs-node/pkg/services/object/tombstone" @@ -46,7 +45,7 @@ import ( ) type objectSvc struct { - put *putsvcV2.Service + put *putsvc.Service search *searchsvc.Service @@ -66,7 +65,7 @@ func (c *cfg) MaxObjectSize() uint64 { return sz } -func (s *objectSvc) Put(ctx context.Context) (objectService.PutObjectStream, error) { +func (s *objectSvc) Put(ctx context.Context) (*putsvc.Streamer, error) { return s.put.Put(ctx) } @@ -265,11 +264,6 @@ func initObjectService(c *cfg) { putsvc.WithTombstoneVerifier(tombstone.NewVerifier(objectSource{sGet, sSearch})), ) - sPutV2 := putsvcV2.NewService( - putsvcV2.WithInternalService(sPut), - putsvcV2.WithKey(&c.key.PrivateKey), - ) - sDelete := deletesvc.New( deletesvc.WithLogger(c.log), deletesvc.WithPutService(sPut), @@ -283,7 +277,7 @@ func initObjectService(c *cfg) { ) objSvc := &objectSvc{ - put: sPutV2, + put: sPut, search: sSearch, get: sGetV2, delete: sDelete, diff --git a/pkg/services/object/put/v2/service.go b/pkg/services/object/put/v2/service.go deleted file mode 100644 index 11bdd8d63b..0000000000 --- a/pkg/services/object/put/v2/service.go +++ /dev/null @@ -1,62 +0,0 @@ -package putsvc - -import ( - "context" - "crypto/ecdsa" - "fmt" - - "github.com/nspcc-dev/neofs-node/pkg/services/object" - putsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/put" -) - -// Service implements Put operation of Object service v2. -type Service struct { - *cfg -} - -// Option represents Service constructor option. -type Option func(*cfg) - -type cfg struct { - svc *putsvc.Service - key *ecdsa.PrivateKey -} - -// NewService constructs Service instance from provided options. -func NewService(opts ...Option) *Service { - c := new(cfg) - - for i := range opts { - opts[i](c) - } - - return &Service{ - cfg: c, - } -} - -// Put calls internal service and returns v2 object streamer. -func (s *Service) Put(ctx context.Context) (object.PutObjectStream, error) { - stream, err := s.svc.Put(ctx) - if err != nil { - return nil, fmt.Errorf("(%T) could not open object put stream: %w", s, err) - } - - return &streamer{ - stream: stream, - key: s.key, - ctx: ctx, - }, nil -} - -func WithInternalService(v *putsvc.Service) Option { - return func(c *cfg) { - c.svc = v - } -} - -func WithKey(k *ecdsa.PrivateKey) Option { - return func(c *cfg) { - c.key = k - } -} diff --git a/pkg/services/object/put/v2/streamer.go b/pkg/services/object/put/v2/streamer.go deleted file mode 100644 index e959caa31c..0000000000 --- a/pkg/services/object/put/v2/streamer.go +++ /dev/null @@ -1,211 +0,0 @@ -package putsvc - -import ( - "context" - "crypto/ecdsa" - "fmt" - - "github.com/nspcc-dev/neofs-api-go/v2/object" - protoobject "github.com/nspcc-dev/neofs-api-go/v2/object/grpc" - sessionV2 "github.com/nspcc-dev/neofs-api-go/v2/session" - "github.com/nspcc-dev/neofs-api-go/v2/signature" - "github.com/nspcc-dev/neofs-api-go/v2/status" - protostatus "github.com/nspcc-dev/neofs-api-go/v2/status/grpc" - "github.com/nspcc-dev/neofs-node/pkg/core/client" - "github.com/nspcc-dev/neofs-node/pkg/network" - "github.com/nspcc-dev/neofs-node/pkg/services/object/internal" - internalclient "github.com/nspcc-dev/neofs-node/pkg/services/object/internal/client" - putsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/put" - apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" - "google.golang.org/grpc" -) - -type streamer struct { - stream *putsvc.Streamer - key *ecdsa.PrivateKey - saveChunks bool - init *object.PutRequest - chunks []*object.PutRequest - - *sizes // only for relay streams - ctx context.Context -} - -type sizes struct { - payloadSz uint64 // value from the header - - writtenPayload uint64 // sum size of already cached chunks -} - -func (s *streamer) Send(req *object.PutRequest) (err error) { - switch v := req.GetBody().GetObjectPart().(type) { - case *object.PutObjectPartInit: - var initPrm *putsvc.PutInitPrm - - initPrm, err = s.toInitPrm(v, req) - if err != nil { - return err - } - - if err = s.stream.Init(initPrm); err != nil { - err = fmt.Errorf("(%T) could not init object put stream: %w", s, err) - } - - s.saveChunks = v.GetSignature() != nil - if s.saveChunks { - maxSz := s.stream.MaxObjectSize() - - s.sizes = &sizes{ - payloadSz: uint64(v.GetHeader().GetPayloadLength()), - } - - // check payload size limit overflow - if s.payloadSz > maxSz { - return putsvc.ErrExceedingMaxSize - } - - s.init = req - } - case *object.PutObjectPartChunk: - if s.saveChunks { - s.writtenPayload += uint64(len(v.GetChunk())) - - // check payload size overflow - if s.writtenPayload > s.payloadSz { - return putsvc.ErrWrongPayloadSize - } - } - - if err = s.stream.SendChunk(toChunkPrm(v)); err != nil { - err = fmt.Errorf("(%T) could not send payload chunk: %w", s, err) - } - - if s.saveChunks { - s.chunks = append(s.chunks, req) - } - default: - err = fmt.Errorf("(%T) invalid object put stream part type %T", s, v) - } - - if err != nil || !s.saveChunks { - return - } - - metaHdr := new(sessionV2.RequestMetaHeader) - meta := req.GetMetaHeader() - - metaHdr.SetTTL(meta.GetTTL() - 1) - metaHdr.SetOrigin(meta) - req.SetMetaHeader(metaHdr) - - return signature.SignServiceMessage(s.key, req) -} - -func (s *streamer) CloseAndRecv() (*object.PutResponse, error) { - if s.saveChunks { - // check payload size correctness - if s.writtenPayload != s.payloadSz { - return nil, putsvc.ErrWrongPayloadSize - } - } - - resp, err := s.stream.Close() - if err != nil { - return nil, fmt.Errorf("(%T) could not object put stream: %w", s, err) - } - - return fromPutResponse(resp), nil -} - -func (s *streamer) relayRequest(info client.NodeInfo, c client.MultiAddressClient) error { - // open stream - - key := info.PublicKey() - - var firstErr error - - info.AddressGroup().IterateAddresses(func(addr network.Address) (stop bool) { - var err error - - defer func() { - stop = err == nil - - if stop || firstErr == nil { - firstErr = err - } - - // would be nice to log otherwise - }() - - var stream protoobject.ObjectService_PutClient - ctx, cancel := context.WithCancel(s.ctx) - defer cancel() - err = c.RawForAddress(addr, func(conn *grpc.ClientConn) error { - stream, err = protoobject.NewObjectServiceClient(conn).Put(ctx) - return err - }) - if err != nil { - err = fmt.Errorf("stream opening failed: %w", err) - return - } - - // send init part - err = stream.Send(s.init.ToGRPCMessage().(*protoobject.PutRequest)) - if err != nil { - internalclient.ReportError(c, err) - err = fmt.Errorf("sending the initial message to stream failed: %w", err) - return - } - - for i := range s.chunks { - if err = stream.Send(s.chunks[i].ToGRPCMessage().(*protoobject.PutRequest)); err != nil { - internalclient.ReportError(c, err) - err = fmt.Errorf("sending the chunk %d failed: %w", i, err) - return - } - } - - // close object stream and receive response from remote node - resp, err := stream.CloseAndRecv() - if err != nil { - err = fmt.Errorf("closing the stream failed: %w", err) - return - } - - // verify response key - if err = internal.VerifyResponseKeyV2(key, resp); err != nil { - return - } - - // verify response structure - resp2 := new(object.PutResponse) - if err = resp2.FromGRPCMessage(resp); err != nil { - panic(err) // can only fail on wrong type, here it's correct - } - err = signature.VerifyServiceMessage(resp2) - if err != nil { - err = fmt.Errorf("response verification failed: %w", err) - } - - err = checkStatus(resp.GetMetaHeader().GetStatus()) - if err != nil { - err = fmt.Errorf("remote node response: %w", err) - } - - return - }) - - return firstErr -} - -func checkStatus(st *protostatus.Status) error { - stV2 := new(status.Status) - if err := stV2.FromGRPCMessage(st); err != nil { - panic(err) // can only fail on wrong type, here it's correct - } - if !status.IsSuccess(stV2.Code()) { - return apistatus.ErrorFromV2(stV2) - } - - return nil -} diff --git a/pkg/services/object/put/v2/util.go b/pkg/services/object/put/v2/util.go deleted file mode 100644 index 88c8c7db9c..0000000000 --- a/pkg/services/object/put/v2/util.go +++ /dev/null @@ -1,47 +0,0 @@ -package putsvc - -import ( - objectV2 "github.com/nspcc-dev/neofs-api-go/v2/object" - refsV2 "github.com/nspcc-dev/neofs-api-go/v2/refs" - putsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/put" - "github.com/nspcc-dev/neofs-node/pkg/services/object/util" - "github.com/nspcc-dev/neofs-sdk-go/object" -) - -func (s *streamer) toInitPrm(part *objectV2.PutObjectPartInit, req *objectV2.PutRequest) (*putsvc.PutInitPrm, error) { - oV2 := new(objectV2.Object) - oV2.SetObjectID(part.GetObjectID()) - oV2.SetSignature(part.GetSignature()) - oV2.SetHeader(part.GetHeader()) - - commonPrm, err := util.CommonPrmFromV2(req) - if err != nil { - return nil, err - } - - return new(putsvc.PutInitPrm). - WithObject( - object.NewFromV2(oV2), - ). - WithRelay(s.relayRequest). - WithCommonPrm(commonPrm). - WithCopiesNumber(part.GetCopiesNumber()), nil -} - -func toChunkPrm(req *objectV2.PutObjectPartChunk) *putsvc.PutChunkPrm { - return new(putsvc.PutChunkPrm). - WithChunk(req.GetChunk()) -} - -func fromPutResponse(r *putsvc.PutResponse) *objectV2.PutResponse { - var idV2 refsV2.ObjectID - r.ObjectID().WriteToV2(&idV2) - - body := new(objectV2.PutResponseBody) - body.SetObjectID(&idV2) - - resp := new(objectV2.PutResponse) - resp.SetBody(body) - - return resp -} diff --git a/pkg/services/object/server.go b/pkg/services/object/server.go index 02b46ed010..28c608b4f4 100644 --- a/pkg/services/object/server.go +++ b/pkg/services/object/server.go @@ -26,6 +26,8 @@ import ( aclsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/v2" deletesvc "github.com/nspcc-dev/neofs-node/pkg/services/object/delete" "github.com/nspcc-dev/neofs-node/pkg/services/object/internal" + internalclient "github.com/nspcc-dev/neofs-node/pkg/services/object/internal/client" + putsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/put" searchsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/search" objutil "github.com/nspcc-dev/neofs-node/pkg/services/object/util" "github.com/nspcc-dev/neofs-node/pkg/services/util" @@ -54,17 +56,11 @@ type GetObjectRangeStream interface { Send(*v2object.GetRangeResponse) error } -// PutObjectStream is an interface of NeoFS API v2 compatible client's object streamer. -type PutObjectStream interface { - Send(*v2object.PutRequest) error - CloseAndRecv() (*v2object.PutResponse, error) -} - // ServiceServer is an interface of utility // serving v2 Object service. type ServiceServer interface { Get(*v2object.GetRequest, GetObjectStream) error - Put(context.Context) (PutObjectStream, error) + Put(context.Context) (*putsvc.Streamer, error) Head(context.Context, *v2object.HeadRequest) (*v2object.HeadResponse, error) Search(context.Context, searchsvc.Prm) error Delete(context.Context, deletesvc.Prm) error @@ -215,7 +211,191 @@ func (s *server) sendStatusPutResponse(stream protoobject.ObjectService_PutServe }) } -// Put opens internal Object service Put stream and overtakes data from gRPC stream to it. +type putStream struct { + signer ecdsa.PrivateKey + base *putsvc.Streamer + + cacheReqs bool + initReq *protoobject.PutRequest + chunkReqs []*protoobject.PutRequest + + expBytes, recvBytes uint64 // payload +} + +func newIntermediatePutStream(signer ecdsa.PrivateKey, base *putsvc.Streamer) *putStream { + return &putStream{ + signer: signer, + base: base, + } +} + +func (x *putStream) sendToRemoteNode(node client.NodeInfo, c client.MultiAddressClient) error { + var firstErr error + nodePub := node.PublicKey() + addrs := node.AddressGroup() + for i := range addrs { + err := putToRemoteNode(c, addrs[i], nodePub, x.initReq, x.chunkReqs) + if err == nil { + return nil + } + if firstErr == nil { + firstErr = err + } + // TODO: log error + } + return firstErr +} + +func putToRemoteNode(c client.MultiAddressClient, addr network.Address, nodePub []byte, + initReq *protoobject.PutRequest, chunkReqs []*protoobject.PutRequest) error { + var stream protoobject.ObjectService_PutClient + err := c.RawForAddress(addr, func(conn *grpc.ClientConn) error { + var err error + stream, err = protoobject.NewObjectServiceClient(conn).Put(context.TODO()) // FIXME: use proper context + return err + }) + if err != nil { + return fmt.Errorf("stream opening failed: %w", err) + } + + err = stream.Send(initReq) + if err != nil { + internalclient.ReportError(c, err) + return fmt.Errorf("sending the initial message to stream failed: %w", err) + } + for i := range chunkReqs { + if err := stream.Send(chunkReqs[i]); err != nil { + internalclient.ReportError(c, err) + return fmt.Errorf("sending the chunk %d failed: %w", i, err) + } + } + + resp, err := stream.CloseAndRecv() + if err != nil { + return fmt.Errorf("closing the stream failed: %w", err) + } + + if err := internal.VerifyResponseKeyV2(nodePub, resp); err != nil { + return err + } + resp2 := new(v2object.PutResponse) + if err := resp2.FromGRPCMessage(resp); err != nil { + panic(err) // can only fail on wrong type, here it's correct + } + if err := signature.VerifyServiceMessage(resp2); err != nil { + return fmt.Errorf("response verification failed: %w", err) + } + if err := checkStatus(resp.GetMetaHeader().GetStatus()); err != nil { + return fmt.Errorf("remote node response: %w", err) + } + return nil +} + +func (x *putStream) resignRequest(req *protoobject.PutRequest) (*protoobject.PutRequest, error) { + meta := req.GetMetaHeader() + req.MetaHeader = &protosession.RequestMetaHeader{ + Ttl: meta.GetTtl() - 1, + Origin: meta, + } + var req2 v2object.PutRequest + if err := req2.FromGRPCMessage(req); err != nil { + panic(err) + } + if err := signature.SignServiceMessage(&x.signer, &req2); err != nil { + return nil, err + } + return req2.ToGRPCMessage().(*protoobject.PutRequest), nil +} + +func (x *putStream) forwardRequest(req *protoobject.PutRequest) error { + switch v := req.GetBody().GetObjectPart().(type) { + default: + return fmt.Errorf("invalid object put stream part type %T", v) + case *protoobject.PutRequest_Body_Init_: + if v == nil || v.Init == nil { // TODO: seems like this is done several times, deduplicate + return errors.New("nil oneof field with heading part") + } + + cp, err := objutil.CommonPrmFromRequest(req) + if err != nil { + return err + } + + mo := &protoobject.Object{ + ObjectId: v.Init.ObjectId, + Signature: v.Init.Signature, + Header: v.Init.Header, + } + var obj2 v2object.Object + if err := obj2.FromGRPCMessage(mo); err != nil { + panic(err) + } + obj := object.NewFromV2(&obj2) + + var p putsvc.PutInitPrm + p.WithCommonPrm(cp) + p.WithObject(obj) + p.WithCopiesNumber(v.Init.CopiesNumber) + p.WithRelay(x.sendToRemoteNode) + if err = x.base.Init(&p); err != nil { + return fmt.Errorf("could not init object put stream: %w", err) + } + + if x.cacheReqs = v.Init.Signature != nil; !x.cacheReqs { + return nil + } + + x.expBytes = v.Init.Header.GetPayloadLength() + if m := x.base.MaxObjectSize(); x.expBytes > m { + return putsvc.ErrExceedingMaxSize + } + signed, err := x.resignRequest(req) // TODO: resign only when needed + if err != nil { + return err // TODO: add context + } + x.initReq = signed + case *protoobject.PutRequest_Body_Chunk: + c := v.GetChunk() + if x.cacheReqs { + if x.recvBytes += uint64(len(c)); x.recvBytes > x.expBytes { + return putsvc.ErrWrongPayloadSize + } + } + if err := x.base.SendChunk(new(putsvc.PutChunkPrm).WithChunk(c)); err != nil { + return fmt.Errorf("could not send payload chunk: %w", err) + } + if !x.cacheReqs { + return nil + } + signed, err := x.resignRequest(req) // TODO: resign only when needed + if err != nil { + return err // TODO: add context + } + x.chunkReqs = append(x.chunkReqs, signed) + } + return nil +} + +func (x *putStream) close() (*protoobject.PutResponse, error) { + if x.cacheReqs && x.recvBytes != x.expBytes { + return nil, putsvc.ErrWrongPayloadSize + } + + resp, err := x.base.Close() + if err != nil { + return nil, fmt.Errorf("could not object put stream: %w", err) + } + + id := resp.ObjectID() + var id2 refsv2.ObjectID + id.WriteToV2(&id2) + return &protoobject.PutResponse{ + Body: &protoobject.PutResponse_Body{ + ObjectId: id2.ToGRPCMessage().(*refs.ObjectID), + }, + }, nil +} + func (s *server) Put(gStream protoobject.ObjectService_PutServer) error { stream, err := s.srv.Put(gStream.Context()) if err != nil { @@ -226,15 +406,19 @@ func (s *server) Put(gStream protoobject.ObjectService_PutServer) error { defer func() { s.pushOpExecResult(stat.MethodObjectPut, err, t) }() var req *protoobject.PutRequest - var resp *v2object.PutResponse + var resp *protoobject.PutResponse + + ps := newIntermediatePutStream(s.signer, stream) for { if req, err = gStream.Recv(); err != nil { if errors.Is(err, io.EOF) { - if resp, err = stream.CloseAndRecv(); err == nil { - err = s.sendPutResponse(gStream, resp.ToGRPCMessage().(*protoobject.PutResponse)) // assign for defer - } else { - err = s.sendStatusPutResponse(gStream, err) + resp, err = ps.close() + if err != nil { + return s.sendStatusPutResponse(gStream, err) } + + err = s.sendPutResponse(gStream, resp) + return err } return err } @@ -276,7 +460,7 @@ func (s *server) Put(gStream protoobject.ObjectService_PutServer) error { } } - if err = stream.Send(putReq); err != nil { + if err = ps.forwardRequest(req); err != nil { err = s.sendStatusPutResponse(gStream, err) // assign for defer return err } diff --git a/pkg/services/object/server_test.go b/pkg/services/object/server_test.go index efbc35c456..2e31347638 100644 --- a/pkg/services/object/server_test.go +++ b/pkg/services/object/server_test.go @@ -20,6 +20,7 @@ import ( objectSvc "github.com/nspcc-dev/neofs-node/pkg/services/object" v2 "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/v2" deletesvc "github.com/nspcc-dev/neofs-node/pkg/services/object/delete" + putsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/put" searchsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/search" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" @@ -47,7 +48,7 @@ func (x noCallObjectService) Get(*objectV2.GetRequest, objectSvc.GetObjectStream panic("must not be called") } -func (x noCallObjectService) Put(context.Context) (objectSvc.PutObjectStream, error) { +func (x noCallObjectService) Put(context.Context) (*putsvc.Streamer, error) { panic("must not be called") } From f8f6b4ce83c8a68f0f153a0c93554255b69ee293 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Mon, 23 Dec 2024 21:42:54 +0300 Subject: [PATCH 4/8] services/object: Inline intermediate GET/RANGE/HEAD services Continues b3e19e2c00dedfd5d6620a62859567ac7885458c. All these ops are very similar, so refactored in one scope. Signed-off-by: Leonard Lyubich --- cmd/neofs-node/object.go | 16 +- pkg/services/object/get/v2/service.go | 63 --- pkg/services/object/get/v2/streamer.go | 62 --- pkg/services/object/get/v2/util.go | 618 --------------------- pkg/services/object/server.go | 728 ++++++++++++++++++++++--- pkg/services/object/server_test.go | 23 +- pkg/services/util/server.go | 10 - 7 files changed, 669 insertions(+), 851 deletions(-) delete mode 100644 pkg/services/object/get/v2/streamer.go delete mode 100644 pkg/services/util/server.go diff --git a/cmd/neofs-node/object.go b/cmd/neofs-node/object.go index 23e374f0c1..002ee3b6c0 100644 --- a/cmd/neofs-node/object.go +++ b/cmd/neofs-node/object.go @@ -49,7 +49,8 @@ type objectSvc struct { search *searchsvc.Service - get *getsvcV2.Service + get *getsvcV2.Service + get_ *getsvc.Service delete *deletesvc.Service } @@ -69,24 +70,24 @@ func (s *objectSvc) Put(ctx context.Context) (*putsvc.Streamer, error) { return s.put.Put(ctx) } -func (s *objectSvc) Head(ctx context.Context, req *object.HeadRequest) (*object.HeadResponse, error) { - return s.get.Head(ctx, req) +func (s *objectSvc) Head(ctx context.Context, prm getsvc.HeadPrm) error { + return s.get_.Head(ctx, prm) } func (s *objectSvc) Search(ctx context.Context, prm searchsvc.Prm) error { return s.search.Search(ctx, prm) } -func (s *objectSvc) Get(req *object.GetRequest, stream objectService.GetObjectStream) error { - return s.get.Get(req, stream) +func (s *objectSvc) Get(ctx context.Context, prm getsvc.Prm) error { + return s.get_.Get(ctx, prm) } func (s *objectSvc) Delete(ctx context.Context, prm deletesvc.Prm) error { return s.delete.Delete(ctx, prm) } -func (s *objectSvc) GetRange(req *object.GetRangeRequest, stream objectService.GetObjectRangeStream) error { - return s.get.GetRange(req, stream) +func (s *objectSvc) GetRange(ctx context.Context, prm getsvc.RangePrm) error { + return s.get_.GetRange(ctx, prm) } func (s *objectSvc) GetRangeHash(ctx context.Context, req *object.GetRangeHashRequest) (*object.GetRangeHashResponse, error) { @@ -280,6 +281,7 @@ func initObjectService(c *cfg) { put: sPut, search: sSearch, get: sGetV2, + get_: sGet, delete: sDelete, } diff --git a/pkg/services/object/get/v2/service.go b/pkg/services/object/get/v2/service.go index 63e4ebd416..23ed610a1b 100644 --- a/pkg/services/object/get/v2/service.go +++ b/pkg/services/object/get/v2/service.go @@ -2,13 +2,10 @@ package getsvc import ( "context" - "errors" objectV2 "github.com/nspcc-dev/neofs-api-go/v2/object" - objectSvc "github.com/nspcc-dev/neofs-node/pkg/services/object" getsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/get" objutil "github.com/nspcc-dev/neofs-node/pkg/services/object/util" - "github.com/nspcc-dev/neofs-sdk-go/object" ) // Service implements Get operation of Object service v2. @@ -38,44 +35,6 @@ func NewService(opts ...Option) *Service { } } -// Get calls internal service and returns v2 object stream. -func (s *Service) Get(req *objectV2.GetRequest, stream objectSvc.GetObjectStream) error { - p, err := s.toPrm(req, stream) - if err != nil { - return err - } - - err = s.svc.Get(stream.Context(), *p) - - var splitErr *object.SplitInfoError - - switch { - case errors.As(err, &splitErr): - return stream.Send(splitInfoResponse(splitErr.SplitInfo())) - default: - return err - } -} - -// GetRange calls internal service and returns v2 payload range stream. -func (s *Service) GetRange(req *objectV2.GetRangeRequest, stream objectSvc.GetObjectRangeStream) error { - p, err := s.toRangePrm(req, stream) - if err != nil { - return err - } - - err = s.svc.GetRange(stream.Context(), *p) - - var splitErr *object.SplitInfoError - - switch { - case errors.As(err, &splitErr): - return stream.Send(splitInfoRangeResponse(splitErr.SplitInfo())) - default: - return err - } -} - // GetRangeHash calls internal service and returns v2 response. func (s *Service) GetRangeHash(ctx context.Context, req *objectV2.GetRangeHashRequest) (*objectV2.GetRangeHashResponse, error) { p, err := s.toHashRangePrm(req) @@ -91,28 +50,6 @@ func (s *Service) GetRangeHash(ctx context.Context, req *objectV2.GetRangeHashRe return toHashResponse(req.GetBody().GetType(), res), nil } -// Head serves NeoFS API v2 compatible HEAD requests. -func (s *Service) Head(ctx context.Context, req *objectV2.HeadRequest) (*objectV2.HeadResponse, error) { - resp := new(objectV2.HeadResponse) - resp.SetBody(new(objectV2.HeadResponseBody)) - - p, err := s.toHeadPrm(ctx, req, resp) - if err != nil { - return nil, err - } - - err = s.svc.Head(ctx, *p) - - var splitErr *object.SplitInfoError - - if errors.As(err, &splitErr) { - setSplitInfoHeadResponse(splitErr.SplitInfo(), resp) - err = nil - } - - return resp, err -} - func WithInternalService(v *getsvc.Service) Option { return func(c *cfg) { c.svc = v diff --git a/pkg/services/object/get/v2/streamer.go b/pkg/services/object/get/v2/streamer.go deleted file mode 100644 index b502d4542a..0000000000 --- a/pkg/services/object/get/v2/streamer.go +++ /dev/null @@ -1,62 +0,0 @@ -package getsvc - -import ( - objectV2 "github.com/nspcc-dev/neofs-api-go/v2/object" - objectSvc "github.com/nspcc-dev/neofs-node/pkg/services/object" - "github.com/nspcc-dev/neofs-sdk-go/object" -) - -type streamObjectWriter struct { - objectSvc.GetObjectStream -} - -type streamObjectRangeWriter struct { - objectSvc.GetObjectRangeStream -} - -func (s *streamObjectWriter) WriteHeader(obj *object.Object) error { - p := new(objectV2.GetObjectPartInit) - - objV2 := obj.ToV2() - p.SetObjectID(objV2.GetObjectID()) - p.SetHeader(objV2.GetHeader()) - p.SetSignature(objV2.GetSignature()) - - return s.GetObjectStream.Send(newResponse(p)) -} - -func (s *streamObjectWriter) WriteChunk(chunk []byte) error { - p := new(objectV2.GetObjectPartChunk) - p.SetChunk(chunk) - - return s.GetObjectStream.Send(newResponse(p)) -} - -func newResponse(p objectV2.GetObjectPart) *objectV2.GetResponse { - r := new(objectV2.GetResponse) - - body := new(objectV2.GetResponseBody) - r.SetBody(body) - - body.SetObjectPart(p) - - return r -} - -func (s *streamObjectRangeWriter) WriteChunk(chunk []byte) error { - return s.GetObjectRangeStream.Send(newRangeResponse(chunk)) -} - -func newRangeResponse(p []byte) *objectV2.GetRangeResponse { - r := new(objectV2.GetRangeResponse) - - body := new(objectV2.GetRangeResponseBody) - r.SetBody(body) - - part := new(objectV2.GetRangePartChunk) - part.SetChunk(p) - - body.SetRangePart(part) - - return r -} diff --git a/pkg/services/object/get/v2/util.go b/pkg/services/object/get/v2/util.go index 11740b1bfb..040812afb5 100644 --- a/pkg/services/object/get/v2/util.go +++ b/pkg/services/object/get/v2/util.go @@ -7,26 +7,21 @@ import ( "errors" "fmt" "hash" - "io" "sync" objectV2 "github.com/nspcc-dev/neofs-api-go/v2/object" protoobject "github.com/nspcc-dev/neofs-api-go/v2/object/grpc" "github.com/nspcc-dev/neofs-api-go/v2/refs" - protorefs "github.com/nspcc-dev/neofs-api-go/v2/refs/grpc" "github.com/nspcc-dev/neofs-api-go/v2/session" "github.com/nspcc-dev/neofs-api-go/v2/signature" "github.com/nspcc-dev/neofs-api-go/v2/status" protostatus "github.com/nspcc-dev/neofs-api-go/v2/status/grpc" "github.com/nspcc-dev/neofs-node/pkg/core/client" "github.com/nspcc-dev/neofs-node/pkg/network" - objectSvc "github.com/nspcc-dev/neofs-node/pkg/services/object" getsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/get" "github.com/nspcc-dev/neofs-node/pkg/services/object/internal" - internalclient "github.com/nspcc-dev/neofs-node/pkg/services/object/internal/client" "github.com/nspcc-dev/neofs-node/pkg/services/object/util" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" - neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" "github.com/nspcc-dev/neofs-sdk-go/object" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" versionSDK "github.com/nspcc-dev/neofs-sdk-go/version" @@ -34,348 +29,6 @@ import ( "google.golang.org/grpc" ) -var errWrongMessageSeq = errors.New("incorrect message sequence") - -func (s *Service) toPrm(req *objectV2.GetRequest, stream objectSvc.GetObjectStream) (*getsvc.Prm, error) { - body := req.GetBody() - - addrV2 := body.GetAddress() - if addrV2 == nil { - return nil, errors.New("missing object address") - } - - var addr oid.Address - - err := addr.ReadFromV2(*addrV2) - if err != nil { - return nil, fmt.Errorf("invalid object address: %w", err) - } - - meta := req.GetMetaHeader() - - commonPrm, err := util.CommonPrmFromV2(req) - if err != nil { - return nil, err - } - - streamWrapper := &streamObjectWriter{stream} - - p := new(getsvc.Prm) - p.SetCommonParameters(commonPrm) - - p.WithAddress(addr) - p.WithRawFlag(body.GetRaw()) - p.SetObjectWriter(streamWrapper) - - if !commonPrm.LocalOnly() { - var onceResign sync.Once - - var onceHeaderSending sync.Once - var globalProgress int - - p.SetRequestForwarder(groupAddressRequestForwarder(func(ctx context.Context, addr network.Address, c client.MultiAddressClient, pubkey []byte) (*object.Object, error) { - var err error - - key, err := s.keyStorage.GetKey(nil) - if err != nil { - return nil, err - } - - // once compose and resign forwarding request - onceResign.Do(func() { - // compose meta header of the local server - metaHdr := new(session.RequestMetaHeader) - metaHdr.SetTTL(meta.GetTTL() - 1) - // TODO: #1165 think how to set the other fields - metaHdr.SetOrigin(meta) - writeCurrentVersion(metaHdr) - - req.SetMetaHeader(metaHdr) - - err = signature.SignServiceMessage(key, req) - }) - - if err != nil { - return nil, err - } - - // code below is copy-pasted from c.GetObject implementation, - // perhaps it is worth highlighting the utility function in neofs-api-go - - // open stream - var getStream protoobject.ObjectService_GetClient - ctx, cancel := context.WithCancel(ctx) - defer cancel() - err = c.RawForAddress(addr, func(conn *grpc.ClientConn) error { - getStream, err = protoobject.NewObjectServiceClient(conn).Get(ctx, req.ToGRPCMessage().(*protoobject.GetRequest)) - return err - }) - if err != nil { - return nil, fmt.Errorf("stream opening failed: %w", err) - } - - var ( - headWas bool - localProgress int - ) - - for { - // receive message from server stream - resp, err := getStream.Recv() - if err != nil { - if errors.Is(err, io.EOF) { - if !headWas { - return nil, io.ErrUnexpectedEOF - } - - break - } - - internalclient.ReportError(c, err) - return nil, fmt.Errorf("reading the response failed: %w", err) - } - - // verify response key - if err = internal.VerifyResponseKeyV2(pubkey, resp); err != nil { - return nil, err - } - - // verify response structure - resp2 := new(objectV2.GetResponse) - if err = resp2.FromGRPCMessage(resp); err != nil { - panic(err) // can only fail on wrong type, here it's correct - } - if err := signature.VerifyServiceMessage(resp2); err != nil { - return nil, fmt.Errorf("response verification failed: %w", err) - } - - if err = checkStatus(resp.GetMetaHeader().GetStatus()); err != nil { - return nil, err - } - - switch v := resp.GetBody().GetObjectPart().(type) { - default: - return nil, fmt.Errorf("unexpected object part %T", v) - case *protoobject.GetResponse_Body_Init_: - if headWas { - return nil, errWrongMessageSeq - } - - headWas = true - - if v == nil || v.Init == nil { - return nil, errors.New("nil header oneof field") - } - - m := &protoobject.Object{ - ObjectId: v.Init.ObjectId, - Signature: v.Init.Signature, - Header: v.Init.Header, - } - - obj := new(objectV2.Object) - if err := obj.FromGRPCMessage(m); err != nil { - panic(err) // can only fail on wrong type, here it's correct - } - - onceHeaderSending.Do(func() { - err = streamWrapper.WriteHeader(object.NewFromV2(obj)) - }) - if err != nil { - return nil, fmt.Errorf("could not write object header in Get forwarder: %w", err) - } - case *protoobject.GetResponse_Body_Chunk: - if !headWas { - return nil, errWrongMessageSeq - } - - origChunk := v.GetChunk() - - chunk := chunkToSend(globalProgress, localProgress, origChunk) - if len(chunk) == 0 { - localProgress += len(origChunk) - continue - } - - if err = streamWrapper.WriteChunk(chunk); err != nil { - return nil, fmt.Errorf("could not write object chunk in Get forwarder: %w", err) - } - - localProgress += len(origChunk) - globalProgress += len(chunk) - case *protoobject.GetResponse_Body_SplitInfo: - if v == nil || v.SplitInfo == nil { - return nil, errors.New("nil split info oneof field") - } - var si2 objectV2.SplitInfo - if err := si2.FromGRPCMessage(v.SplitInfo); err != nil { - panic(err) // can only fail on wrong type, here it's correct - } - si := object.NewSplitInfoFromV2(&si2) - return nil, object.NewSplitInfoError(si) - } - } - - return nil, nil - })) - } - - return p, nil -} - -func (s *Service) toRangePrm(req *objectV2.GetRangeRequest, stream objectSvc.GetObjectRangeStream) (*getsvc.RangePrm, error) { - body := req.GetBody() - - addrV2 := body.GetAddress() - if addrV2 == nil { - return nil, errors.New("missing object address") - } - - var addr oid.Address - - err := addr.ReadFromV2(*addrV2) - if err != nil { - return nil, fmt.Errorf("invalid object address: %w", err) - } - - meta := req.GetMetaHeader() - - commonPrm, err := util.CommonPrmFromV2(req) - if err != nil { - return nil, err - } - - p := new(getsvc.RangePrm) - p.SetCommonParameters(commonPrm) - - streamWrapper := &streamObjectRangeWriter{stream} - - p.WithAddress(addr) - p.WithRawFlag(body.GetRaw()) - p.SetChunkWriter(streamWrapper) - p.SetRange(object.NewRangeFromV2(body.GetRange())) - - err = p.Validate() - if err != nil { - return nil, fmt.Errorf("request params validation: %w", err) - } - - if !commonPrm.LocalOnly() { - var onceResign sync.Once - var globalProgress int - - key, err := s.keyStorage.GetKey(nil) - if err != nil { - return nil, err - } - - p.SetRequestForwarder(groupAddressRequestForwarder(func(ctx context.Context, addr network.Address, c client.MultiAddressClient, pubkey []byte) (*object.Object, error) { - var err error - - // once compose and resign forwarding request - onceResign.Do(func() { - // compose meta header of the local server - metaHdr := new(session.RequestMetaHeader) - metaHdr.SetTTL(meta.GetTTL() - 1) - // TODO: #1165 think how to set the other fields - metaHdr.SetOrigin(meta) - writeCurrentVersion(metaHdr) - - req.SetMetaHeader(metaHdr) - - err = signature.SignServiceMessage(key, req) - }) - - if err != nil { - return nil, err - } - - // code below is copy-pasted from c.ObjectPayloadRangeData implementation, - // perhaps it is worth highlighting the utility function in neofs-api-go - - // open stream - var rangeStream protoobject.ObjectService_GetRangeClient - ctx, cancel := context.WithCancel(ctx) - defer cancel() - err = c.RawForAddress(addr, func(conn *grpc.ClientConn) error { - rangeStream, err = protoobject.NewObjectServiceClient(conn).GetRange(ctx, req.ToGRPCMessage().(*protoobject.GetRangeRequest)) - return err - }) - if err != nil { - return nil, fmt.Errorf("could not create Get payload range stream: %w", err) - } - - var localProgress int - - for { - // receive message from server stream - resp, err := rangeStream.Recv() - if err != nil { - if errors.Is(err, io.EOF) { - break - } - - internalclient.ReportError(c, err) - return nil, fmt.Errorf("reading the response failed: %w", err) - } - - // verify response key - if err = internal.VerifyResponseKeyV2(pubkey, resp); err != nil { - return nil, err - } - - // verify response structure - resp2 := new(objectV2.GetRangeResponse) - if err = resp2.FromGRPCMessage(resp); err != nil { - panic(err) // can only fail on wrong type, here it's correct - } - if err := signature.VerifyServiceMessage(resp2); err != nil { - return nil, fmt.Errorf("could not verify %T: %w", resp, err) - } - - if err = checkStatus(resp.GetMetaHeader().GetStatus()); err != nil { - return nil, err - } - - switch v := resp.GetBody().GetRangePart().(type) { - case nil: - return nil, fmt.Errorf("unexpected range type %T", v) - case *protoobject.GetRangeResponse_Body_Chunk: - origChunk := v.GetChunk() - - chunk := chunkToSend(globalProgress, localProgress, origChunk) - if len(chunk) == 0 { - localProgress += len(origChunk) - continue - } - - if err = streamWrapper.WriteChunk(chunk); err != nil { - return nil, fmt.Errorf("could not write object chunk in GetRange forwarder: %w", err) - } - - localProgress += len(origChunk) - globalProgress += len(chunk) - case *protoobject.GetRangeResponse_Body_SplitInfo: - if v == nil || v.SplitInfo == nil { - return nil, errors.New("nil split info oneof field") - } - var si2 objectV2.SplitInfo - if err := si2.FromGRPCMessage(v.SplitInfo); err != nil { - panic(err) // can only fail on wrong type, here it's correct - } - si := object.NewSplitInfoFromV2(&si2) - - return nil, object.NewSplitInfoError(si) - } - } - - return nil, nil - })) - } - - return p, nil -} - func (s *Service) toHashRangePrm(req *objectV2.GetRangeHashRequest) (*getsvc.RangeHashPrm, error) { body := req.GetBody() @@ -504,239 +157,6 @@ func (s *Service) toHashRangePrm(req *objectV2.GetRangeHashRequest) (*getsvc.Ran return p, nil } -type headResponseWriter struct { - mainOnly bool - - body *objectV2.HeadResponseBody -} - -func (w *headResponseWriter) WriteHeader(hdr *object.Object) error { - if w.mainOnly { - w.body.SetHeaderPart(toShortObjectHeader(hdr)) - } else { - w.body.SetHeaderPart(toFullObjectHeader(hdr)) - } - - return nil -} - -func (s *Service) toHeadPrm(_ context.Context, req *objectV2.HeadRequest, resp *objectV2.HeadResponse) (*getsvc.HeadPrm, error) { - body := req.GetBody() - - addrV2 := body.GetAddress() - if addrV2 == nil { - return nil, errors.New("missing object address") - } - - var objAddr oid.Address - - err := objAddr.ReadFromV2(*addrV2) - if err != nil { - return nil, fmt.Errorf("invalid object address: %w", err) - } - - meta := req.GetMetaHeader() - - commonPrm, err := util.CommonPrmFromV2(req) - if err != nil { - return nil, err - } - - p := new(getsvc.HeadPrm) - p.SetCommonParameters(commonPrm) - - p.WithAddress(objAddr) - p.WithRawFlag(body.GetRaw()) - p.SetHeaderWriter(&headResponseWriter{ - mainOnly: body.GetMainOnly(), - body: resp.GetBody(), - }) - - if !commonPrm.LocalOnly() { - var onceResign sync.Once - - p.SetRequestForwarder(groupAddressRequestForwarder(func(ctx context.Context, addr network.Address, c client.MultiAddressClient, pubkey []byte) (*object.Object, error) { - var err error - - key, err := s.keyStorage.GetKey(nil) - if err != nil { - return nil, err - } - - // once compose and resign forwarding request - onceResign.Do(func() { - // compose meta header of the local server - metaHdr := new(session.RequestMetaHeader) - metaHdr.SetTTL(meta.GetTTL() - 1) - // TODO: #1165 think how to set the other fields - metaHdr.SetOrigin(meta) - writeCurrentVersion(metaHdr) - - req.SetMetaHeader(metaHdr) - - err = signature.SignServiceMessage(key, req) - }) - - if err != nil { - return nil, err - } - - // code below is copy-pasted from c.GetObjectHeader implementation, - // perhaps it is worth highlighting the utility function in neofs-api-go - - // send Head request - var headResp *protoobject.HeadResponse - err = c.RawForAddress(addr, func(conn *grpc.ClientConn) error { - headResp, err = protoobject.NewObjectServiceClient(conn).Head(ctx, req.ToGRPCMessage().(*protoobject.HeadRequest)) - return err - }) - if err != nil { - return nil, fmt.Errorf("sending the request failed: %w", err) - } - - // verify response key - if err = internal.VerifyResponseKeyV2(pubkey, headResp); err != nil { - return nil, err - } - - // verify response structure - resp2 := new(objectV2.HeadResponse) - if err = resp2.FromGRPCMessage(headResp); err != nil { - panic(err) // can only fail on wrong type, here it's correct - } - if err := signature.VerifyServiceMessage(resp2); err != nil { - return nil, fmt.Errorf("response verification failed: %w", err) - } - - if err = checkStatus(headResp.GetMetaHeader().GetStatus()); err != nil { - return nil, err - } - - var ( - hdr *protoobject.Header - idSig *protorefs.Signature - ) - - switch v := headResp.GetBody().GetHead().(type) { - case nil: - return nil, fmt.Errorf("unexpected header type %T", v) - case *protoobject.HeadResponse_Body_ShortHeader: - if !body.GetMainOnly() { - return nil, fmt.Errorf("wrong header part type: expected %T, received %T", - (*objectV2.ShortHeader)(nil), (*objectV2.HeaderWithSignature)(nil), - ) - } - - if v == nil || v.ShortHeader == nil { - return nil, errors.New("nil short header oneof field") - } - - h := v.ShortHeader - hdr = &protoobject.Header{ - Version: h.Version, - OwnerId: h.OwnerId, - CreationEpoch: h.CreationEpoch, - PayloadLength: h.PayloadLength, - PayloadHash: h.PayloadHash, - ObjectType: h.ObjectType, - HomomorphicHash: h.HomomorphicHash, - } - case *protoobject.HeadResponse_Body_Header: - if body.GetMainOnly() { - return nil, fmt.Errorf("wrong header part type: expected %T, received %T", - (*objectV2.HeaderWithSignature)(nil), (*objectV2.ShortHeader)(nil), - ) - } - - if v == nil || v.Header == nil { - return nil, errors.New("nil header oneof field") - } - - if v.Header.Header == nil { - return nil, errors.New("missing header") - } - - hdr = v.Header.Header - idSig = v.Header.Signature - - if idSig == nil { - // TODO(@cthulhu-rider): #1387 use "const" error - return nil, errors.New("missing signature") - } - - binID := objAddr.Object().Marshal() - - var sig2 refs.Signature - if err := sig2.FromGRPCMessage(idSig); err != nil { - panic(err) // can only fail on wrong type, here it's correct - } - var sig neofscrypto.Signature - if err := sig.ReadFromV2(sig2); err != nil { - return nil, fmt.Errorf("can't read signature: %w", err) - } - - if !sig.Verify(binID) { - return nil, errors.New("invalid object ID signature") - } - case *protoobject.HeadResponse_Body_SplitInfo: - if v == nil || v.SplitInfo == nil { - return nil, errors.New("nil split info oneof field") - } - var si2 objectV2.SplitInfo - if err := si2.FromGRPCMessage(v.SplitInfo); err != nil { - panic(err) // can only fail on wrong type, here it's correct - } - si := object.NewSplitInfoFromV2(&si2) - - return nil, object.NewSplitInfoError(si) - } - - mObj := &protoobject.Object{ - Signature: idSig, - Header: hdr, - } - objv2 := new(objectV2.Object) - if err := objv2.FromGRPCMessage(mObj); err != nil { - panic(err) // can only fail on wrong type, here it's correct - } - - obj := object.NewFromV2(objv2) - obj.SetID(objAddr.Object()) - - // convert the object - return obj, nil - })) - } - - return p, nil -} - -func splitInfoResponse(info *object.SplitInfo) *objectV2.GetResponse { - resp := new(objectV2.GetResponse) - - body := new(objectV2.GetResponseBody) - resp.SetBody(body) - - body.SetObjectPart(info.ToV2()) - - return resp -} - -func splitInfoRangeResponse(info *object.SplitInfo) *objectV2.GetRangeResponse { - resp := new(objectV2.GetRangeResponse) - - body := new(objectV2.GetRangeResponseBody) - resp.SetBody(body) - - body.SetRangePart(info.ToV2()) - - return resp -} - -func setSplitInfoHeadResponse(info *object.SplitInfo, resp *objectV2.HeadResponse) { - resp.GetBody().SetHeaderPart(info.ToV2()) -} - func toHashResponse(typ refs.ChecksumType, res *getsvc.RangeHashRes) *objectV2.GetRangeHashResponse { resp := new(objectV2.GetRangeHashResponse) @@ -749,31 +169,6 @@ func toHashResponse(typ refs.ChecksumType, res *getsvc.RangeHashRes) *objectV2.G return resp } -func toFullObjectHeader(hdr *object.Object) objectV2.GetHeaderPart { - obj := hdr.ToV2() - - hs := new(objectV2.HeaderWithSignature) - hs.SetHeader(obj.GetHeader()) - hs.SetSignature(obj.GetSignature()) - - return hs -} - -func toShortObjectHeader(hdr *object.Object) objectV2.GetHeaderPart { - hdrV2 := hdr.ToV2().GetHeader() - - sh := new(objectV2.ShortHeader) - sh.SetOwnerID(hdrV2.GetOwnerID()) - sh.SetCreationEpoch(hdrV2.GetCreationEpoch()) - sh.SetPayloadLength(hdrV2.GetPayloadLength()) - sh.SetVersion(hdrV2.GetVersion()) - sh.SetObjectType(hdrV2.GetObjectType()) - sh.SetHomomorphicHash(hdrV2.GetHomomorphicHash()) - sh.SetPayloadHash(hdrV2.GetPayloadHash()) - - return sh -} - func groupAddressRequestForwarder[V any](f func(context.Context, network.Address, client.MultiAddressClient, []byte) (V, error)) func(context.Context, client.NodeInfo, client.MultiAddressClient) (V, error) { return func(ctx context.Context, info client.NodeInfo, c client.MultiAddressClient) (V, error) { var ( @@ -825,16 +220,3 @@ func checkStatus(st *protostatus.Status) error { return nil } - -func chunkToSend(global, local int, chunk []byte) []byte { - if global == local { - return chunk - } - - if local+len(chunk) <= global { - // chunk has already been sent - return nil - } - - return chunk[global-local:] -} diff --git a/pkg/services/object/server.go b/pkg/services/object/server.go index 28c608b4f4..d5057ef0f8 100644 --- a/pkg/services/object/server.go +++ b/pkg/services/object/server.go @@ -25,6 +25,7 @@ import ( "github.com/nspcc-dev/neofs-node/pkg/network" aclsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/v2" deletesvc "github.com/nspcc-dev/neofs-node/pkg/services/object/delete" + getsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/get" "github.com/nspcc-dev/neofs-node/pkg/services/object/internal" internalclient "github.com/nspcc-dev/neofs-node/pkg/services/object/internal/client" putsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/put" @@ -41,30 +42,17 @@ import ( "github.com/nspcc-dev/neofs-sdk-go/user" "github.com/nspcc-dev/neofs-sdk-go/version" "google.golang.org/grpc" - "google.golang.org/protobuf/proto" ) -// GetObjectStream is an interface of NeoFS API v2 compatible object streamer. -type GetObjectStream interface { - util.ServerStream - Send(*v2object.GetResponse) error -} - -// GetObjectRangeStream is an interface of NeoFS API v2 compatible payload range streamer. -type GetObjectRangeStream interface { - util.ServerStream - Send(*v2object.GetRangeResponse) error -} - // ServiceServer is an interface of utility // serving v2 Object service. type ServiceServer interface { - Get(*v2object.GetRequest, GetObjectStream) error + Get(context.Context, getsvc.Prm) error Put(context.Context) (*putsvc.Streamer, error) - Head(context.Context, *v2object.HeadRequest) (*v2object.HeadResponse, error) + Head(context.Context, getsvc.HeadPrm) error Search(context.Context, searchsvc.Prm) error Delete(context.Context, deletesvc.Prm) error - GetRange(*v2object.GetRangeRequest, GetObjectRangeStream) error + GetRange(context.Context, getsvc.RangePrm) error GetRangeHash(context.Context, *v2object.GetRangeHashRequest) (*v2object.GetRangeHashResponse, error) } @@ -555,12 +543,21 @@ func (s *server) signHeadResponse(resp *protoobject.HeadResponse) *protoobject.H } func (s *server) makeStatusHeadResponse(err error) *protoobject.HeadResponse { + var splitErr *object.SplitInfoError + if errors.As(err, &splitErr) { + return s.signHeadResponse(&protoobject.HeadResponse{ + Body: &protoobject.HeadResponse_Body{ + Head: &protoobject.HeadResponse_Body_SplitInfo{ + SplitInfo: splitErr.SplitInfo().ToV2().ToGRPCMessage().(*protoobject.SplitInfo), + }, + }, + }) + } return s.signHeadResponse(&protoobject.HeadResponse{ MetaHeader: s.makeResponseMetaHeader(util.ToStatus(err)), }) } -// Head converts gRPC HeadRequest message and passes it to internal Object service. func (s *server) Head(ctx context.Context, req *protoobject.HeadRequest) (*protoobject.HeadResponse, error) { searchReq := new(v2object.HeadRequest) err := searchReq.FromGRPCMessage(req) @@ -593,18 +590,239 @@ func (s *server) Head(ctx context.Context, req *protoobject.HeadRequest) (*proto return s.makeStatusHeadResponse(err), nil } - resp, err := s.srv.Head(ctx, searchReq) + var resp protoobject.HeadResponse + p, err := convertHeadPrm(s.signer, req, &resp) + if err != nil { + return s.makeStatusHeadResponse(err), nil + } + err = s.srv.Head(ctx, p) if err != nil { return s.makeStatusHeadResponse(err), nil } - err = s.aclChecker.CheckEACL(resp.ToGRPCMessage().(*protoobject.HeadResponse), reqInfo) + err = s.aclChecker.CheckEACL(&resp, reqInfo) if err != nil { err = eACLErr(reqInfo, err) // defer return s.makeStatusHeadResponse(err), nil } - return s.signHeadResponse(resp.ToGRPCMessage().(*protoobject.HeadResponse)), nil + return s.signHeadResponse(&resp), nil +} + +type headResponse struct { + short bool + dst *protoobject.HeadResponse +} + +func (x *headResponse) WriteHeader(hdr *object.Object) error { + mo := hdr.ToV2().ToGRPCMessage().(*protoobject.Object) + if x.short { + mh := mo.GetHeader() + x.dst.Body = &protoobject.HeadResponse_Body{ + Head: &protoobject.HeadResponse_Body_ShortHeader{ + ShortHeader: &protoobject.ShortHeader{ + Version: mh.GetVersion(), + CreationEpoch: mh.GetCreationEpoch(), + OwnerId: mh.GetOwnerId(), + ObjectType: mh.GetObjectType(), + PayloadLength: mh.GetPayloadLength(), + PayloadHash: mh.GetPayloadHash(), + HomomorphicHash: mh.GetHomomorphicHash(), + }, + }, + } + return nil + } + x.dst.Body = &protoobject.HeadResponse_Body{ + Head: &protoobject.HeadResponse_Body_Header{ + Header: &protoobject.HeaderWithSignature{ + Header: mo.GetHeader(), + Signature: mo.GetSignature(), + }, + }, + } + return nil +} + +// converts original request into parameters accepted by the internal handler. +// Note that the response is untouched within this call. +func convertHeadPrm(signer ecdsa.PrivateKey, req *protoobject.HeadRequest, resp *protoobject.HeadResponse) (getsvc.HeadPrm, error) { + body := req.GetBody() + ma := body.GetAddress() + if ma == nil { // includes nil body + return getsvc.HeadPrm{}, errors.New("missing object address") + } + + var addr oid.Address + var addr2 refsv2.Address + if err := addr2.FromGRPCMessage(ma); err != nil { + panic(err) + } + if err := addr.ReadFromV2(addr2); err != nil { + return getsvc.HeadPrm{}, fmt.Errorf("invalid object address: %w", err) + } + + cp, err := objutil.CommonPrmFromRequest(req) + if err != nil { + return getsvc.HeadPrm{}, err + } + + var p getsvc.HeadPrm + p.SetCommonParameters(cp) + p.WithAddress(addr) + p.WithRawFlag(body.Raw) + p.SetHeaderWriter(&headResponse{ + short: body.MainOnly, + dst: resp, + }) + if cp.LocalOnly() { + return p, nil + } + + var onceResign sync.Once + meta := req.GetMetaHeader() + bID := addr.Object().Marshal() + p.SetRequestForwarder(func(ctx context.Context, node client.NodeInfo, c client.MultiAddressClient) (*object.Object, error) { + var err error + onceResign.Do(func() { + req.MetaHeader = &protosession.RequestMetaHeader{ + // TODO: #1165 think how to set the other fields + Ttl: meta.GetTtl() - 1, // FIXME: meta can be nil + Origin: meta, + } + var req2 v2object.HeadRequest + if err := req2.FromGRPCMessage(req); err != nil { + panic(err) + } + if err = signature.SignServiceMessage(&signer, &req2); err == nil { + req = req2.ToGRPCMessage().(*protoobject.HeadRequest) + } + }) + if err != nil { + return nil, err + } + + var firstErr error + nodePub := node.PublicKey() + addrs := node.AddressGroup() + for i := range addrs { + hdr, err := getHeaderFromRemoteNode(ctx, c, addrs[i], nodePub, req, bID) + if err == nil { + hdr.SetID(addr.Object()) + return hdr, nil + } + if firstErr == nil { + firstErr = err + } + // TODO: log error + } + return nil, firstErr + }) + return p, nil +} + +func getHeaderFromRemoteNode(ctx context.Context, c client.MultiAddressClient, addr network.Address, nodePub []byte, req *protoobject.HeadRequest, + bID []byte) (*object.Object, error) { + var resp *protoobject.HeadResponse + err := c.RawForAddress(addr, func(conn *grpc.ClientConn) error { + var err error + resp, err = protoobject.NewObjectServiceClient(conn).Head(ctx, req) + return err + }) + if err != nil { + return nil, fmt.Errorf("sending the request failed: %w", err) + } + + if err := internal.VerifyResponseKeyV2(nodePub, resp); err != nil { + return nil, err + } + resp2 := new(v2object.HeadResponse) + if err := resp2.FromGRPCMessage(resp); err != nil { + panic(err) // can only fail on wrong type, here it's correct + } + if err := signature.VerifyServiceMessage(resp2); err != nil { + return nil, fmt.Errorf("response verification failed: %w", err) + } + if err := checkStatus(resp.GetMetaHeader().GetStatus()); err != nil { + return nil, err + } + + var hdr *protoobject.Header + var idSig *refs.Signature + switch v := resp.GetBody().GetHead().(type) { + case nil: + return nil, fmt.Errorf("unexpected header type %T", v) + case *protoobject.HeadResponse_Body_ShortHeader: + if !req.Body.MainOnly { + return nil, fmt.Errorf("wrong header part type: expected %T, received %T", + (*protoobject.HeadResponse_Body_Header)(nil), (*protoobject.HeadResponse_Body_ShortHeader)(nil), + ) + } + if v == nil || v.ShortHeader == nil { + return nil, errors.New("nil short header oneof field") + } + h := v.ShortHeader + hdr = &protoobject.Header{ + Version: h.Version, + OwnerId: h.OwnerId, + CreationEpoch: h.CreationEpoch, + PayloadLength: h.PayloadLength, + PayloadHash: h.PayloadHash, + ObjectType: h.ObjectType, + HomomorphicHash: h.HomomorphicHash, + } + case *protoobject.HeadResponse_Body_Header: + if req.Body.MainOnly { + return nil, fmt.Errorf("wrong header part type: expected %T, received %T", + (*protoobject.HeadResponse_Body_Header)(nil), (*protoobject.HeadResponse_Body_ShortHeader)(nil), + ) + } + if v == nil || v.Header == nil { + return nil, errors.New("nil header oneof field") + } + if v.Header.Header == nil { + return nil, errors.New("missing header") + } + if v.Header.Signature == nil { + // TODO(@cthulhu-rider): #1387 use "const" error + return nil, errors.New("missing signature") + } + + var sig2 refsv2.Signature + if err := sig2.FromGRPCMessage(v.Header.Signature); err != nil { + panic(err) // can only fail on wrong type, here it's correct + } + var sig neofscrypto.Signature + if err := sig.ReadFromV2(sig2); err != nil { + return nil, fmt.Errorf("can't read signature: %w", err) + } + if !sig.Verify(bID) { + return nil, errors.New("invalid object ID signature") + } + + hdr = v.Header.Header + idSig = v.Header.Signature + case *protoobject.HeadResponse_Body_SplitInfo: + if v == nil || v.SplitInfo == nil { + return nil, errors.New("nil split info oneof field") + } + var si2 v2object.SplitInfo + if err := si2.FromGRPCMessage(v.SplitInfo); err != nil { + panic(err) // can only fail on wrong type, here it's correct + } + si := object.NewSplitInfoFromV2(&si2) + return nil, object.NewSplitInfoError(si) + } + + mObj := &protoobject.Object{ + Signature: idSig, + Header: hdr, + } + objv2 := new(v2object.Object) + if err := objv2.FromGRPCMessage(mObj); err != nil { + panic(err) // can only fail on wrong type, here it's correct + } + return object.NewFromV2(objv2), nil } func (s *server) signHashResponse(resp *protoobject.GetRangeHashResponse) *protoobject.GetRangeHashResponse { @@ -662,47 +880,61 @@ func (s *server) sendGetResponse(stream protoobject.ObjectService_GetServer, res } func (s *server) sendStatusGetResponse(stream protoobject.ObjectService_GetServer, err error) error { + var splitErr *object.SplitInfoError + if errors.As(err, &splitErr) { + return s.sendGetResponse(stream, &protoobject.GetResponse{ + Body: &protoobject.GetResponse_Body{ + ObjectPart: &protoobject.GetResponse_Body_SplitInfo{ + SplitInfo: splitErr.SplitInfo().ToV2().ToGRPCMessage().(*protoobject.SplitInfo), + }, + }, + }) + } return s.sendGetResponse(stream, &protoobject.GetResponse{ MetaHeader: s.makeResponseMetaHeader(util.ToStatus(err)), }) } -type getStreamerV2 struct { - protoobject.ObjectService_GetServer +type getStream struct { + base protoobject.ObjectService_GetServer srv *server reqInfo aclsvc.RequestInfo } -func (s *getStreamerV2) Send(resp *v2object.GetResponse) error { - r := resp.ToGRPCMessage().(*protoobject.GetResponse) - switch v := r.GetBody().GetObjectPart().(type) { - case *protoobject.GetResponse_Body_Init_: - if err := s.srv.aclChecker.CheckEACL(r, s.reqInfo); err != nil { - return eACLErr(s.reqInfo, err) - } - case *protoobject.GetResponse_Body_Chunk: - for buf := bytes.NewBuffer(v.GetChunk()); buf.Len() > 0; { - newResp := &protoobject.GetResponse{ - Body: &protoobject.GetResponse_Body{ - ObjectPart: &protoobject.GetResponse_Body_Chunk{ - Chunk: buf.Next(maxRespDataChunkSize), - }, +func (s *getStream) WriteHeader(hdr *object.Object) error { + mo := hdr.ToV2().ToGRPCMessage().(*protoobject.Object) + resp := &protoobject.GetResponse{ + Body: &protoobject.GetResponse_Body{ + ObjectPart: &protoobject.GetResponse_Body_Init_{Init: &protoobject.GetResponse_Body_Init{ + ObjectId: mo.ObjectId, + Signature: mo.Signature, + Header: mo.Header, + }}, + }, + } + if err := s.srv.aclChecker.CheckEACL(resp, s.reqInfo); err != nil { + return eACLErr(s.reqInfo, err) + } + return s.srv.sendGetResponse(s.base, resp) +} + +func (s *getStream) WriteChunk(chunk []byte) error { + for buf := bytes.NewBuffer(chunk); buf.Len() > 0; { + newResp := &protoobject.GetResponse{ + Body: &protoobject.GetResponse_Body{ + ObjectPart: &protoobject.GetResponse_Body_Chunk{ + Chunk: buf.Next(maxRespDataChunkSize), }, - MetaHeader: proto.Clone(r.GetMetaHeader()).(*protosession.ResponseMetaHeader), // TODO: can go w/o cloning? - VerifyHeader: proto.Clone(r.GetVerifyHeader()).(*protosession.ResponseVerificationHeader), - } - if err := s.srv.sendGetResponse(s.ObjectService_GetServer, newResp); err != nil { - return err - } + }, + } + if err := s.srv.sendGetResponse(s.base, newResp); err != nil { + return err } - s.srv.metrics.AddGetPayload(len(v.Chunk)) - return nil } - return s.srv.sendGetResponse(s.ObjectService_GetServer, r) + s.srv.metrics.AddGetPayload(len(chunk)) + return nil } -// Get converts gRPC GetRequest message and server-side stream and overtakes its data -// to gRPC stream. func (s *server) Get(req *protoobject.GetRequest, gStream protoobject.ObjectService_GetServer) error { getReq := new(v2object.GetRequest) err := getReq.FromGRPCMessage(req) @@ -733,69 +965,240 @@ func (s *server) Get(req *protoobject.GetRequest, gStream protoobject.ObjectServ return s.sendStatusGetResponse(gStream, err) } - err = s.srv.Get( - getReq, - &getStreamerV2{ - ObjectService_GetServer: gStream, - srv: s, - reqInfo: reqInfo, - }, - ) + p, err := convertGetPrm(s.signer, req, &getStream{ + base: gStream, + srv: s, + reqInfo: reqInfo, + }) + if err != nil { + return s.sendStatusGetResponse(gStream, err) + } + err = s.srv.Get(gStream.Context(), p) if err != nil { return s.sendStatusGetResponse(gStream, err) } return nil } +// converts original request into parameters accepted by the internal handler. +// Note that the stream is untouched within this call, errors are not reported +// into it. +func convertGetPrm(signer ecdsa.PrivateKey, req *protoobject.GetRequest, stream *getStream) (getsvc.Prm, error) { + body := req.GetBody() + ma := body.GetAddress() + if ma == nil { // includes nil body + return getsvc.Prm{}, errors.New("missing object address") + } + + var addr oid.Address + var addr2 refsv2.Address + if err := addr2.FromGRPCMessage(ma); err != nil { + panic(err) + } + if err := addr.ReadFromV2(addr2); err != nil { + return getsvc.Prm{}, fmt.Errorf("invalid object address: %w", err) + } + + cp, err := objutil.CommonPrmFromRequest(req) + if err != nil { + return getsvc.Prm{}, err + } + + var p getsvc.Prm + p.SetCommonParameters(cp) + p.WithAddress(addr) + p.WithRawFlag(body.Raw) + p.SetObjectWriter(stream) + if cp.LocalOnly() { + return p, nil + } + + var onceResign sync.Once + var onceHdr sync.Once + var respondedPayload int + meta := req.GetMetaHeader() + p.SetRequestForwarder(func(ctx context.Context, node client.NodeInfo, c client.MultiAddressClient) (*object.Object, error) { + var err error + onceResign.Do(func() { + req.MetaHeader = &protosession.RequestMetaHeader{ + // TODO: #1165 think how to set the other fields + Ttl: meta.GetTtl() - 1, // FIXME: meta can be nil + Origin: meta, + } + var req2 v2object.GetRequest + if err := req2.FromGRPCMessage(req); err != nil { + panic(err) + } + if err = signature.SignServiceMessage(&signer, &req2); err == nil { + req = req2.ToGRPCMessage().(*protoobject.GetRequest) + } + }) + if err != nil { + return nil, err + } + + var firstErr error + nodePub := node.PublicKey() + addrs := node.AddressGroup() + for i := range addrs { + err := continueGetFromRemoteNode(ctx, c, addrs[i], nodePub, req, stream, &onceHdr, &respondedPayload) + if errors.Is(err, io.EOF) { + return nil, nil + } + if firstErr == nil { + firstErr = err + } + // TODO: log error + } + return nil, firstErr + }) + return p, nil +} + +func continueGetFromRemoteNode(ctx context.Context, c client.MultiAddressClient, addr network.Address, nodePub []byte, req *protoobject.GetRequest, + stream *getStream, onceHdr *sync.Once, respondedPayload *int) error { + var getStream protoobject.ObjectService_GetClient + err := c.RawForAddress(addr, func(conn *grpc.ClientConn) error { + var err error + // FIXME: context should be cancelled on return from upper func + getStream, err = protoobject.NewObjectServiceClient(conn).Get(ctx, req) + return err + }) + if err != nil { + return fmt.Errorf("stream opening failed: %w", err) + } + + var headWas bool + var readPayload int + for { + resp, err := getStream.Recv() + if err != nil { + if errors.Is(err, io.EOF) { + if !headWas { + return io.ErrUnexpectedEOF + } + return io.EOF + } + internalclient.ReportError(c, err) + return fmt.Errorf("reading the response failed: %w", err) + } + + if err = internal.VerifyResponseKeyV2(nodePub, resp); err != nil { + return err + } + resp2 := new(v2object.GetResponse) + if err := resp2.FromGRPCMessage(resp); err != nil { + panic(err) // can only fail on wrong type, here it's correct + } + if err := signature.VerifyServiceMessage(resp2); err != nil { + return fmt.Errorf("response verification failed: %w", err) + } + if err := checkStatus(resp.GetMetaHeader().GetStatus()); err != nil { + return err + } + + switch v := resp.GetBody().GetObjectPart().(type) { + default: + return fmt.Errorf("unexpected object part %T", v) + case *protoobject.GetResponse_Body_Init_: + if headWas { + return errors.New("incorrect message sequence") + } + headWas = true + if v == nil || v.Init == nil { + return errors.New("nil header oneof field") + } + mo := &protoobject.Object{ + ObjectId: v.Init.ObjectId, + Signature: v.Init.Signature, + Header: v.Init.Header, + } + obj := new(v2object.Object) + if err := obj.FromGRPCMessage(mo); err != nil { + panic(err) // can only fail on wrong type, here it's correct + } + onceHdr.Do(func() { + err = stream.WriteHeader(object.NewFromV2(obj)) + }) + if err != nil { + return fmt.Errorf("could not write object header in Get forwarder: %w", err) + } + case *protoobject.GetResponse_Body_Chunk: + if !headWas { + return errors.New("incorrect message sequence") + } + fullChunk := v.GetChunk() + respChunk := chunkToSend(*respondedPayload, readPayload, fullChunk) + if len(respChunk) == 0 { + readPayload += len(fullChunk) + continue + } + if err := stream.WriteChunk(respChunk); err != nil { + return fmt.Errorf("could not write object chunk in Get forwarder: %w", err) + } + readPayload += len(fullChunk) + *respondedPayload += len(respChunk) + case *protoobject.GetResponse_Body_SplitInfo: + if v == nil || v.SplitInfo == nil { + return errors.New("nil split info oneof field") + } + var si2 v2object.SplitInfo + if err := si2.FromGRPCMessage(v.SplitInfo); err != nil { + panic(err) // can only fail on wrong type, here it's correct + } + si := object.NewSplitInfoFromV2(&si2) + return object.NewSplitInfoError(si) + } + } +} + func (s *server) sendRangeResponse(stream protoobject.ObjectService_GetRangeServer, resp *protoobject.GetRangeResponse) error { return stream.Send(util.SignResponse(&s.signer, resp, v2object.GetRangeResponse{})) } func (s *server) sendStatusRangeResponse(stream protoobject.ObjectService_GetRangeServer, err error) error { + var splitErr *object.SplitInfoError + if errors.As(err, &splitErr) { + return s.sendRangeResponse(stream, &protoobject.GetRangeResponse{ + Body: &protoobject.GetRangeResponse_Body{ + RangePart: &protoobject.GetRangeResponse_Body_SplitInfo{ + SplitInfo: splitErr.SplitInfo().ToV2().ToGRPCMessage().(*protoobject.SplitInfo), + }, + }, + }) + } return s.sendRangeResponse(stream, &protoobject.GetRangeResponse{ MetaHeader: s.makeResponseMetaHeader(util.ToStatus(err)), }) } -type getRangeStreamerV2 struct { - protoobject.ObjectService_GetRangeServer +type rangeStream struct { + base protoobject.ObjectService_GetRangeServer srv *server reqInfo aclsvc.RequestInfo } -func (s *getRangeStreamerV2) Send(resp *v2object.GetRangeResponse) error { - r := resp.ToGRPCMessage().(*protoobject.GetRangeResponse) - v, ok := r.GetBody().GetRangePart().(*protoobject.GetRangeResponse_Body_Chunk) - if !ok { - if err := s.srv.aclChecker.CheckEACL(r, s.reqInfo); err != nil { - return eACLErr(s.reqInfo, err) - } - return s.srv.sendRangeResponse(s.ObjectService_GetRangeServer, r) - } - for buf := bytes.NewBuffer(v.GetChunk()); buf.Len() > 0; { +func (s *rangeStream) WriteChunk(chunk []byte) error { + for buf := bytes.NewBuffer(chunk); buf.Len() > 0; { newResp := &protoobject.GetRangeResponse{ Body: &protoobject.GetRangeResponse_Body{ RangePart: &protoobject.GetRangeResponse_Body_Chunk{ Chunk: buf.Next(maxRespDataChunkSize), }, }, - MetaHeader: proto.Clone(r.GetMetaHeader()).(*protosession.ResponseMetaHeader), // TODO: can go w/o cloning? - VerifyHeader: proto.Clone(r.GetVerifyHeader()).(*protosession.ResponseVerificationHeader), } // TODO: do not check response multiple times // TODO: why check it at all? if err := s.srv.aclChecker.CheckEACL(newResp, s.reqInfo); err != nil { return eACLErr(s.reqInfo, err) } - if err := s.srv.sendRangeResponse(s.ObjectService_GetRangeServer, newResp); err != nil { + if err := s.srv.sendRangeResponse(s.base, newResp); err != nil { return err } } return nil } -// GetRange converts gRPC GetRangeRequest message and server-side stream and overtakes its data -// to gRPC stream. func (s *server) GetRange(req *protoobject.GetRangeRequest, gStream protoobject.ObjectService_GetRangeServer) error { getRngReq := new(v2object.GetRangeRequest) err := getRngReq.FromGRPCMessage(req) @@ -826,20 +1229,174 @@ func (s *server) GetRange(req *protoobject.GetRangeRequest, gStream protoobject. return s.sendStatusRangeResponse(gStream, err) } - err = s.srv.GetRange( - getRngReq, - &getRangeStreamerV2{ - ObjectService_GetRangeServer: gStream, - srv: s, - reqInfo: reqInfo, - }, - ) + p, err := convertRangePrm(s.signer, req, &rangeStream{ + base: gStream, + srv: s, + reqInfo: reqInfo, + }) + if err != nil { + return s.sendStatusRangeResponse(gStream, err) + } + err = s.srv.GetRange(gStream.Context(), p) if err != nil { return s.sendStatusRangeResponse(gStream, err) } return nil } +// converts original request into parameters accepted by the internal handler. +// Note that the stream is untouched within this call, errors are not reported +// into it. +func convertRangePrm(signer ecdsa.PrivateKey, req *protoobject.GetRangeRequest, stream *rangeStream) (getsvc.RangePrm, error) { + body := req.GetBody() + ma := body.GetAddress() + if ma == nil { // includes nil body + return getsvc.RangePrm{}, errors.New("missing object address") + } + + var addr oid.Address + var addr2 refsv2.Address + if err := addr2.FromGRPCMessage(ma); err != nil { + panic(err) + } + if err := addr.ReadFromV2(addr2); err != nil { + return getsvc.RangePrm{}, fmt.Errorf("invalid object address: %w", err) + } + + rln := body.Range.GetLength() + if rln == 0 { // includes nil range + return getsvc.RangePrm{}, errors.New("zero range length") + } + if body.Range.Offset+rln <= body.Range.Offset { + return getsvc.RangePrm{}, errors.New("range overflow") + } + + cp, err := objutil.CommonPrmFromRequest(req) + if err != nil { + return getsvc.RangePrm{}, err + } + + var p getsvc.RangePrm + p.SetCommonParameters(cp) + p.WithAddress(addr) + p.WithRawFlag(body.Raw) + p.SetChunkWriter(stream) + var rng object.Range + rng.SetOffset(body.Range.Offset) + rng.SetLength(rln) + p.SetRange(&rng) + if cp.LocalOnly() { + return p, nil + } + + var onceResign sync.Once + var respondedPayload int + meta := req.GetMetaHeader() + p.SetRequestForwarder(func(ctx context.Context, node client.NodeInfo, c client.MultiAddressClient) (*object.Object, error) { + var err error + onceResign.Do(func() { + req.MetaHeader = &protosession.RequestMetaHeader{ + // TODO: #1165 think how to set the other fields + Ttl: meta.GetTtl() - 1, // FIXME: meta can be nil + Origin: meta, + } + var req2 v2object.GetRangeRequest + if err := req2.FromGRPCMessage(req); err != nil { + panic(err) + } + if err = signature.SignServiceMessage(&signer, &req2); err == nil { + req = req2.ToGRPCMessage().(*protoobject.GetRangeRequest) + } + }) + if err != nil { + return nil, err + } + + var firstErr error + nodePub := node.PublicKey() + addrs := node.AddressGroup() + for i := range addrs { + err := continueRangeFromRemoteNode(ctx, c, addrs[i], nodePub, req, stream, &respondedPayload) + if errors.Is(err, io.EOF) { + return nil, nil + } + if firstErr == nil { + firstErr = err + } + // TODO: log error + } + return nil, firstErr + }) + return p, nil +} + +func continueRangeFromRemoteNode(ctx context.Context, c client.MultiAddressClient, addr network.Address, nodePub []byte, req *protoobject.GetRangeRequest, + stream *rangeStream, respondedPayload *int) error { + var rangeStream protoobject.ObjectService_GetRangeClient + err := c.RawForAddress(addr, func(conn *grpc.ClientConn) error { + var err error + // FIXME: context should be cancelled on return from upper func + rangeStream, err = protoobject.NewObjectServiceClient(conn).GetRange(ctx, req) + return err + }) + if err != nil { + return fmt.Errorf("stream opening failed: %w", err) + } + + var readPayload int + for { + resp, err := rangeStream.Recv() + if err != nil { + if errors.Is(err, io.EOF) { + return io.EOF + } + internalclient.ReportError(c, err) + return fmt.Errorf("reading the response failed: %w", err) + } + + if err = internal.VerifyResponseKeyV2(nodePub, resp); err != nil { + return err + } + resp2 := new(v2object.GetRangeResponse) + if err := resp2.FromGRPCMessage(resp); err != nil { + panic(err) // can only fail on wrong type, here it's correct + } + if err := signature.VerifyServiceMessage(resp2); err != nil { + return fmt.Errorf("response verification failed: %w", err) + } + if err := checkStatus(resp.GetMetaHeader().GetStatus()); err != nil { + return err + } + + switch v := resp.GetBody().GetRangePart().(type) { + default: + return fmt.Errorf("unexpected range type %T", v) + case *protoobject.GetRangeResponse_Body_Chunk: + fullChunk := v.GetChunk() + respChunk := chunkToSend(*respondedPayload, readPayload, fullChunk) + if len(respChunk) == 0 { + readPayload += len(fullChunk) + continue + } + if err := stream.WriteChunk(respChunk); err != nil { + return fmt.Errorf("could not write object chunk in Get forwarder: %w", err) + } + readPayload += len(fullChunk) + *respondedPayload += len(respChunk) + case *protoobject.GetRangeResponse_Body_SplitInfo: + if v == nil || v.SplitInfo == nil { + return errors.New("nil split info oneof field") + } + var si2 v2object.SplitInfo + if err := si2.FromGRPCMessage(v.SplitInfo); err != nil { + panic(err) // can only fail on wrong type, here it's correct + } + si := object.NewSplitInfoFromV2(&si2) + return object.NewSplitInfoError(si) + } + } +} + func (s *server) sendSearchResponse(stream protoobject.ObjectService_SearchServer, resp *protoobject.SearchResponse) error { return stream.Send(util.SignResponse(&s.signer, resp, v2object.SearchResponse{})) } @@ -1336,3 +1893,14 @@ func checkStatus(st *protostatus.Status) error { return nil } + +func chunkToSend(global, local int, chunk []byte) []byte { + if global == local { + return chunk + } + if local+len(chunk) <= global { + // chunk has already been sent + return nil + } + return chunk[global-local:] +} diff --git a/pkg/services/object/server_test.go b/pkg/services/object/server_test.go index 2e31347638..0749352aa2 100644 --- a/pkg/services/object/server_test.go +++ b/pkg/services/object/server_test.go @@ -17,9 +17,10 @@ import ( refsv2 "github.com/nspcc-dev/neofs-api-go/v2/refs" refs "github.com/nspcc-dev/neofs-api-go/v2/refs/grpc" objectcore "github.com/nspcc-dev/neofs-node/pkg/core/object" - objectSvc "github.com/nspcc-dev/neofs-node/pkg/services/object" + . "github.com/nspcc-dev/neofs-node/pkg/services/object" v2 "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/v2" deletesvc "github.com/nspcc-dev/neofs-node/pkg/services/object/delete" + getsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/get" putsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/put" searchsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/search" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" @@ -44,7 +45,7 @@ func randECDSAPrivateKey(tb testing.TB) *ecdsa.PrivateKey { type noCallObjectService struct{} -func (x noCallObjectService) Get(*objectV2.GetRequest, objectSvc.GetObjectStream) error { +func (x noCallObjectService) Get(context.Context, getsvc.Prm) error { panic("must not be called") } @@ -52,7 +53,7 @@ func (x noCallObjectService) Put(context.Context) (*putsvc.Streamer, error) { panic("must not be called") } -func (x noCallObjectService) Head(context.Context, *objectV2.HeadRequest) (*objectV2.HeadResponse, error) { +func (x noCallObjectService) Head(context.Context, getsvc.HeadPrm) error { panic("must not be called") } @@ -64,7 +65,7 @@ func (x noCallObjectService) Delete(context.Context, deletesvc.Prm) error { panic("must not be called") } -func (x noCallObjectService) GetRange(*objectV2.GetRangeRequest, objectSvc.GetObjectRangeStream) error { +func (x noCallObjectService) GetRange(context.Context, getsvc.RangePrm) error { panic("must not be called") } @@ -257,7 +258,7 @@ func TestServer_Replicate(t *testing.T) { var noCallStorage noCallTestStorage var noCallACLChecker noCallTestACLChecker var noCallReqProc noCallTestReqInfoExtractor - noCallSrv := objectSvc.New(noCallObjSvc, 0, &noCallFSChain, noCallStorage, neofscryptotest.Signer().ECDSAPrivateKey, nopMetrics{}, noCallACLChecker, noCallReqProc) + noCallSrv := New(noCallObjSvc, 0, &noCallFSChain, noCallStorage, neofscryptotest.Signer().ECDSAPrivateKey, nopMetrics{}, noCallACLChecker, noCallReqProc) clientSigner := neofscryptotest.Signer() clientPubKey := neofscrypto.PublicKeyBytes(clientSigner.Public()) serverPubKey := neofscrypto.PublicKeyBytes(neofscryptotest.Signer().Public()) @@ -421,7 +422,7 @@ func TestServer_Replicate(t *testing.T) { t.Run("apply storage policy failure", func(t *testing.T) { fsChain := newTestFSChain(t, serverPubKey, clientPubKey, cnr) - srv := objectSvc.New(noCallObjSvc, 0, fsChain, noCallStorage, neofscryptotest.Signer().ECDSAPrivateKey, nopMetrics{}, noCallACLChecker, noCallReqProc) + srv := New(noCallObjSvc, 0, fsChain, noCallStorage, neofscryptotest.Signer().ECDSAPrivateKey, nopMetrics{}, noCallACLChecker, noCallReqProc) fsChain.cnrErr = errors.New("any error") @@ -433,7 +434,7 @@ func TestServer_Replicate(t *testing.T) { t.Run("client or server mismatches object's storage policy", func(t *testing.T) { fsChain := newTestFSChain(t, serverPubKey, clientPubKey, cnr) - srv := objectSvc.New(noCallObjSvc, 0, fsChain, noCallStorage, neofscryptotest.Signer().ECDSAPrivateKey, nopMetrics{}, noCallACLChecker, noCallReqProc) + srv := New(noCallObjSvc, 0, fsChain, noCallStorage, neofscryptotest.Signer().ECDSAPrivateKey, nopMetrics{}, noCallACLChecker, noCallReqProc) fsChain.serverOutsideCnr = true fsChain.clientOutsideCnr = true @@ -454,7 +455,7 @@ func TestServer_Replicate(t *testing.T) { t.Run("local storage failure", func(t *testing.T) { fsChain := newTestFSChain(t, serverPubKey, clientPubKey, cnr) s := newTestStorage(t, req.Object) - srv := objectSvc.New(noCallObjSvc, 0, fsChain, s, neofscryptotest.Signer().ECDSAPrivateKey, nopMetrics{}, noCallACLChecker, noCallReqProc) + srv := New(noCallObjSvc, 0, fsChain, s, neofscryptotest.Signer().ECDSAPrivateKey, nopMetrics{}, noCallACLChecker, noCallReqProc) s.storeErr = errors.New("any error") @@ -470,7 +471,7 @@ func TestServer_Replicate(t *testing.T) { reqForSignature, o := anyValidRequest(t, clientSigner, cnr, objID) fsChain := newTestFSChain(t, serverPubKey, clientPubKey, cnr) s := newTestStorage(t, reqForSignature.Object) - srv := objectSvc.New(noCallObjSvc, mNumber, fsChain, s, signer.ECDSAPrivateKey, nopMetrics{}, noCallACLChecker, noCallReqProc) + srv := New(noCallObjSvc, mNumber, fsChain, s, signer.ECDSAPrivateKey, nopMetrics{}, noCallACLChecker, noCallReqProc) t.Run("signature not requested", func(t *testing.T) { resp, err := srv.Replicate(context.Background(), reqForSignature) @@ -513,7 +514,7 @@ func TestServer_Replicate(t *testing.T) { t.Run("OK", func(t *testing.T) { fsChain := newTestFSChain(t, serverPubKey, clientPubKey, cnr) s := newTestStorage(t, req.Object) - srv := objectSvc.New(noCallObjSvc, 0, fsChain, s, neofscryptotest.Signer().ECDSAPrivateKey, nopMetrics{}, noCallACLChecker, noCallReqProc) + srv := New(noCallObjSvc, 0, fsChain, s, neofscryptotest.Signer().ECDSAPrivateKey, nopMetrics{}, noCallACLChecker, noCallReqProc) resp, err := srv.Replicate(context.Background(), req) require.NoError(t, err) @@ -554,7 +555,7 @@ func BenchmarkServer_Replicate(b *testing.B) { ctx := context.Background() var fsChain nopFSChain - srv := objectSvc.New(nil, 0, fsChain, nopStorage{}, neofscryptotest.Signer().ECDSAPrivateKey, nopMetrics{}, nopACLChecker{}, nopReqInfoExtractor{}) + srv := New(nil, 0, fsChain, nopStorage{}, neofscryptotest.Signer().ECDSAPrivateKey, nopMetrics{}, nopACLChecker{}, nopReqInfoExtractor{}) for _, tc := range []struct { name string diff --git a/pkg/services/util/server.go b/pkg/services/util/server.go deleted file mode 100644 index 83ab323f71..0000000000 --- a/pkg/services/util/server.go +++ /dev/null @@ -1,10 +0,0 @@ -package util - -import ( - "context" -) - -// ServerStream is an interface of server-side stream v2. -type ServerStream interface { - Context() context.Context -} From 54070de0067801261bf04d30ed77baa92f9f98a2 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 24 Dec 2024 16:36:02 +0300 Subject: [PATCH 5/8] services/object: Inline intermediate HASH service Continues b3e19e2c00dedfd5d6620a62859567ac7885458c. This completes elimination of the recurrent architecture of object requests' handling. Signed-off-by: Leonard Lyubich --- cmd/neofs-node/object.go | 48 +++--- pkg/services/object/get/v2/service.go | 64 -------- pkg/services/object/get/v2/util.go | 222 -------------------------- pkg/services/object/server.go | 213 ++++++++++++++++++++---- pkg/services/object/server_test.go | 23 ++- pkg/services/object/util/prm.go | 47 ------ 6 files changed, 231 insertions(+), 386 deletions(-) delete mode 100644 pkg/services/object/get/v2/service.go delete mode 100644 pkg/services/object/get/v2/util.go diff --git a/cmd/neofs-node/object.go b/cmd/neofs-node/object.go index 002ee3b6c0..0d20a46397 100644 --- a/cmd/neofs-node/object.go +++ b/cmd/neofs-node/object.go @@ -3,12 +3,13 @@ package main import ( "bytes" "context" + "crypto/ecdsa" "errors" "fmt" "sync/atomic" + "github.com/google/uuid" lru "github.com/hashicorp/golang-lru/v2" - "github.com/nspcc-dev/neofs-api-go/v2/object" objectGRPC "github.com/nspcc-dev/neofs-api-go/v2/object/grpc" replicatorconfig "github.com/nspcc-dev/neofs-node/cmd/neofs-node/config/replicator" coreclient "github.com/nspcc-dev/neofs-node/pkg/core/client" @@ -22,7 +23,6 @@ import ( v2 "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/v2" deletesvc "github.com/nspcc-dev/neofs-node/pkg/services/object/delete" getsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/get" - getsvcV2 "github.com/nspcc-dev/neofs-node/pkg/services/object/get/v2" headsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/head" putsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/put" searchsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/search" @@ -49,8 +49,7 @@ type objectSvc struct { search *searchsvc.Service - get *getsvcV2.Service - get_ *getsvc.Service + get *getsvc.Service delete *deletesvc.Service } @@ -71,7 +70,7 @@ func (s *objectSvc) Put(ctx context.Context) (*putsvc.Streamer, error) { } func (s *objectSvc) Head(ctx context.Context, prm getsvc.HeadPrm) error { - return s.get_.Head(ctx, prm) + return s.get.Head(ctx, prm) } func (s *objectSvc) Search(ctx context.Context, prm searchsvc.Prm) error { @@ -79,7 +78,7 @@ func (s *objectSvc) Search(ctx context.Context, prm searchsvc.Prm) error { } func (s *objectSvc) Get(ctx context.Context, prm getsvc.Prm) error { - return s.get_.Get(ctx, prm) + return s.get.Get(ctx, prm) } func (s *objectSvc) Delete(ctx context.Context, prm deletesvc.Prm) error { @@ -87,11 +86,11 @@ func (s *objectSvc) Delete(ctx context.Context, prm deletesvc.Prm) error { } func (s *objectSvc) GetRange(ctx context.Context, prm getsvc.RangePrm) error { - return s.get_.GetRange(ctx, prm) + return s.get.GetRange(ctx, prm) } -func (s *objectSvc) GetRangeHash(ctx context.Context, req *object.GetRangeHashRequest) (*object.GetRangeHashResponse, error) { - return s.get.GetRangeHash(ctx, req) +func (s *objectSvc) GetRangeHash(ctx context.Context, prm getsvc.RangeHashPrm) (*getsvc.RangeHashRes, error) { + return s.get.GetRangeHash(ctx, prm) } type delNetInfo struct { @@ -230,11 +229,6 @@ func initObjectService(c *cfg) { *c.cfgObject.getSvc = *sGet // need smth better - sGetV2 := getsvcV2.NewService( - getsvcV2.WithInternalService(sGet), - getsvcV2.WithKeyStorage(keyStorage), - ) - cnrNodes, err := newContainerNodes(c.cfgObject.cnrSource, c.netMapSource) fatalOnErr(err) c.cfgObject.containerNodes = cnrNodes @@ -280,8 +274,7 @@ func initObjectService(c *cfg) { objSvc := &objectSvc{ put: sPut, search: sSearch, - get: sGetV2, - get_: sGet, + get: sGet, delete: sDelete, } @@ -309,7 +302,11 @@ func initObjectService(c *cfg) { SetHeaderSource(cachedHeaderSource(sGet, cachedFirstObjectsNumber, c.log)), ) - server := objectService.New(objSvc, mNumber, fsChain, (*putObjectServiceWrapper)(sPut), c.shared.basics.key.PrivateKey, c.metricsCollector, aclChecker, aclSvc) + storage := storageForObjectService{ + putSvc: sPut, + keys: keyStorage, + } + server := objectService.New(objSvc, mNumber, fsChain, storage, c.shared.basics.key.PrivateKey, c.metricsCollector, aclChecker, aclSvc) for _, srv := range c.cfgGRPC.servers { objectGRPC.RegisterObjectServiceServer(srv, server) @@ -618,10 +615,21 @@ func (x *fsChainForObjects) IsOwnPublicKey(pubKey []byte) bool { // maintenance now. func (x *fsChainForObjects) LocalNodeUnderMaintenance() bool { return x.isMaintenance.Load() } -type putObjectServiceWrapper putsvc.Service +type storageForObjectService struct { + putSvc *putsvc.Service + keys *util.KeyStorage +} + +func (x storageForObjectService) VerifyAndStoreObjectLocally(obj objectSDK.Object) error { + return x.putSvc.ValidateAndStoreObjectLocally(obj) +} -func (x *putObjectServiceWrapper) VerifyAndStoreObject(obj objectSDK.Object) error { - return (*putsvc.Service)(x).ValidateAndStoreObjectLocally(obj) +func (x storageForObjectService) GetSessionPrivateKey(usr user.ID, uid uuid.UUID) (ecdsa.PrivateKey, error) { + k, err := x.keys.GetKey(&util.SessionInfo{ID: uid, Owner: usr}) + if err != nil { + return ecdsa.PrivateKey{}, err + } + return *k, nil } type objectSource struct { diff --git a/pkg/services/object/get/v2/service.go b/pkg/services/object/get/v2/service.go deleted file mode 100644 index 23ed610a1b..0000000000 --- a/pkg/services/object/get/v2/service.go +++ /dev/null @@ -1,64 +0,0 @@ -package getsvc - -import ( - "context" - - objectV2 "github.com/nspcc-dev/neofs-api-go/v2/object" - getsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/get" - objutil "github.com/nspcc-dev/neofs-node/pkg/services/object/util" -) - -// Service implements Get operation of Object service v2. -type Service struct { - *cfg -} - -// Option represents Service constructor option. -type Option func(*cfg) - -type cfg struct { - svc *getsvc.Service - - keyStorage *objutil.KeyStorage -} - -// NewService constructs Service instance from provided options. -func NewService(opts ...Option) *Service { - c := new(cfg) - - for i := range opts { - opts[i](c) - } - - return &Service{ - cfg: c, - } -} - -// GetRangeHash calls internal service and returns v2 response. -func (s *Service) GetRangeHash(ctx context.Context, req *objectV2.GetRangeHashRequest) (*objectV2.GetRangeHashResponse, error) { - p, err := s.toHashRangePrm(req) - if err != nil { - return nil, err - } - - res, err := s.svc.GetRangeHash(ctx, *p) - if err != nil { - return nil, err - } - - return toHashResponse(req.GetBody().GetType(), res), nil -} - -func WithInternalService(v *getsvc.Service) Option { - return func(c *cfg) { - c.svc = v - } -} - -// WithKeyStorage returns option to set local private key storage. -func WithKeyStorage(ks *objutil.KeyStorage) Option { - return func(c *cfg) { - c.keyStorage = ks - } -} diff --git a/pkg/services/object/get/v2/util.go b/pkg/services/object/get/v2/util.go deleted file mode 100644 index 040812afb5..0000000000 --- a/pkg/services/object/get/v2/util.go +++ /dev/null @@ -1,222 +0,0 @@ -package getsvc - -import ( - "context" - "crypto/ecdsa" - "crypto/sha256" - "errors" - "fmt" - "hash" - "sync" - - objectV2 "github.com/nspcc-dev/neofs-api-go/v2/object" - protoobject "github.com/nspcc-dev/neofs-api-go/v2/object/grpc" - "github.com/nspcc-dev/neofs-api-go/v2/refs" - "github.com/nspcc-dev/neofs-api-go/v2/session" - "github.com/nspcc-dev/neofs-api-go/v2/signature" - "github.com/nspcc-dev/neofs-api-go/v2/status" - protostatus "github.com/nspcc-dev/neofs-api-go/v2/status/grpc" - "github.com/nspcc-dev/neofs-node/pkg/core/client" - "github.com/nspcc-dev/neofs-node/pkg/network" - getsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/get" - "github.com/nspcc-dev/neofs-node/pkg/services/object/internal" - "github.com/nspcc-dev/neofs-node/pkg/services/object/util" - apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" - "github.com/nspcc-dev/neofs-sdk-go/object" - oid "github.com/nspcc-dev/neofs-sdk-go/object/id" - versionSDK "github.com/nspcc-dev/neofs-sdk-go/version" - "github.com/nspcc-dev/tzhash/tz" - "google.golang.org/grpc" -) - -func (s *Service) toHashRangePrm(req *objectV2.GetRangeHashRequest) (*getsvc.RangeHashPrm, error) { - body := req.GetBody() - - addrV2 := body.GetAddress() - if addrV2 == nil { - return nil, errors.New("missing object address") - } - - var addr oid.Address - - err := addr.ReadFromV2(*addrV2) - if err != nil { - return nil, fmt.Errorf("invalid object address: %w", err) - } - - commonPrm, err := util.CommonPrmFromV2(req) - if err != nil { - return nil, err - } - - p := new(getsvc.RangeHashPrm) - p.SetCommonParameters(commonPrm) - - p.WithAddress(addr) - - if tok := commonPrm.SessionToken(); tok != nil { - signerKey, err := s.keyStorage.GetKey(&util.SessionInfo{ - ID: tok.ID(), - Owner: tok.Issuer(), - }) - if err != nil && errors.As(err, new(apistatus.SessionTokenNotFound)) { - commonPrm.ForgetTokens() - signerKey, err = s.keyStorage.GetKey(nil) - } - - if err != nil { - return nil, fmt.Errorf("fetching session key: %w", err) - } - - p.WithCachedSignerKey(signerKey) - } - - rngsV2 := body.GetRanges() - rngs := make([]object.Range, len(rngsV2)) - - for i := range rngsV2 { - rngs[i] = *object.NewRangeFromV2(&rngsV2[i]) - } - - p.SetRangeList(rngs) - p.SetSalt(body.GetSalt()) - - switch t := body.GetType(); t { - default: - return nil, fmt.Errorf("unknown checksum type %v", t) - case refs.SHA256: - p.SetHashGenerator(func() hash.Hash { - return sha256.New() - }) - case refs.TillichZemor: - p.SetHashGenerator(func() hash.Hash { - return tz.New() - }) - } - - if !commonPrm.LocalOnly() { - var onceResign sync.Once - var key *ecdsa.PrivateKey - - key, err = s.keyStorage.GetKey(nil) - if err != nil { - return nil, err - } - - p.SetRangeHashRequestForwarder(groupAddressRequestForwarder(func(ctx context.Context, addr network.Address, c client.MultiAddressClient, pubkey []byte) ([][]byte, error) { - meta := req.GetMetaHeader() - - // once compose and resign forwarding request - onceResign.Do(func() { - // compose meta header of the local server - metaHdr := new(session.RequestMetaHeader) - metaHdr.SetTTL(meta.GetTTL() - 1) - // TODO: #1165 think how to set the other fields - metaHdr.SetOrigin(meta) - writeCurrentVersion(metaHdr) - - req.SetMetaHeader(metaHdr) - - err = signature.SignServiceMessage(key, req) - }) - if err != nil { - return nil, err - } - - var resp *protoobject.GetRangeHashResponse - err = c.RawForAddress(addr, func(conn *grpc.ClientConn) error { - resp, err = protoobject.NewObjectServiceClient(conn).GetRangeHash(ctx, req.ToGRPCMessage().(*protoobject.GetRangeHashRequest)) - return err - }) - if err != nil { - return nil, fmt.Errorf("GetRangeHash rpc failure: %w", err) - } - - // verify response key - if err = internal.VerifyResponseKeyV2(pubkey, resp); err != nil { - return nil, err - } - - // verify response structure - resp2 := new(objectV2.GetRangeHashResponse) - if err = resp2.FromGRPCMessage(resp); err != nil { - panic(err) // can only fail on wrong type, here it's correct - } - if err := signature.VerifyServiceMessage(resp2); err != nil { - return nil, fmt.Errorf("could not verify %T: %w", resp, err) - } - - if err = checkStatus(resp.GetMetaHeader().GetStatus()); err != nil { - return nil, err - } - - return resp.GetBody().GetHashList(), nil - })) - } - - return p, nil -} - -func toHashResponse(typ refs.ChecksumType, res *getsvc.RangeHashRes) *objectV2.GetRangeHashResponse { - resp := new(objectV2.GetRangeHashResponse) - - body := new(objectV2.GetRangeHashResponseBody) - resp.SetBody(body) - - body.SetType(typ) - body.SetHashList(res.Hashes()) - - return resp -} - -func groupAddressRequestForwarder[V any](f func(context.Context, network.Address, client.MultiAddressClient, []byte) (V, error)) func(context.Context, client.NodeInfo, client.MultiAddressClient) (V, error) { - return func(ctx context.Context, info client.NodeInfo, c client.MultiAddressClient) (V, error) { - var ( - firstErr error - res V - - key = info.PublicKey() - ) - - info.AddressGroup().IterateAddresses(func(addr network.Address) (stop bool) { - var err error - - defer func() { - stop = err == nil - - if stop || firstErr == nil { - firstErr = err - } - - // would be nice to log otherwise - }() - - res, err = f(ctx, addr, c, key) - - return - }) - - return res, firstErr - } -} - -func writeCurrentVersion(metaHdr *session.RequestMetaHeader) { - versionV2 := new(refs.Version) - - apiVersion := versionSDK.Current() - apiVersion.WriteToV2(versionV2) - - metaHdr.SetVersion(versionV2) -} - -func checkStatus(st *protostatus.Status) error { - stV2 := new(status.Status) - if err := stV2.FromGRPCMessage(st); err != nil { - panic(err) // can only fail on wrong type, here it's correct - } - if !status.IsSuccess(stV2.Code()) { - return apistatus.ErrorFromV2(stV2) - } - - return nil -} diff --git a/pkg/services/object/server.go b/pkg/services/object/server.go index d5057ef0f8..8591de0f01 100644 --- a/pkg/services/object/server.go +++ b/pkg/services/object/server.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "crypto/ecdsa" + "crypto/sha256" "encoding/binary" "errors" "fmt" @@ -11,6 +12,7 @@ import ( "sync" "time" + "github.com/google/uuid" v2object "github.com/nspcc-dev/neofs-api-go/v2/object" protoobject "github.com/nspcc-dev/neofs-api-go/v2/object/grpc" refsv2 "github.com/nspcc-dev/neofs-api-go/v2/refs" @@ -41,19 +43,20 @@ import ( "github.com/nspcc-dev/neofs-sdk-go/stat" "github.com/nspcc-dev/neofs-sdk-go/user" "github.com/nspcc-dev/neofs-sdk-go/version" + "github.com/nspcc-dev/tzhash/tz" "google.golang.org/grpc" ) -// ServiceServer is an interface of utility -// serving v2 Object service. -type ServiceServer interface { +// Handlers represents storage node's internal handler Object service op +// payloads. +type Handlers interface { Get(context.Context, getsvc.Prm) error Put(context.Context) (*putsvc.Streamer, error) Head(context.Context, getsvc.HeadPrm) error Search(context.Context, searchsvc.Prm) error Delete(context.Context, deletesvc.Prm) error GetRange(context.Context, getsvc.RangePrm) error - GetRangeHash(context.Context, *v2object.GetRangeHashRequest) (*v2object.GetRangeHashResponse, error) + GetRangeHash(context.Context, getsvc.RangeHashPrm) (*getsvc.RangeHashRes, error) } // Various NeoFS protocol status codes. @@ -102,13 +105,22 @@ type FSChain interface { LocalNodeUnderMaintenance() bool } -// Storage groups ops of the node's object storage required to serve NeoFS API -// Object service. +type sessions interface { + // GetSessionPrivateKey reads private session key by user and session IDs. + // Returns [apistatus.ErrSessionTokenNotFound] if there is no data for the + // referenced session. + GetSessionPrivateKey(usr user.ID, uid uuid.UUID) (ecdsa.PrivateKey, error) +} + +// Storage groups ops of the node's storage required to serve NeoFS API Object +// service. type Storage interface { - // VerifyAndStoreObject checks whether given object has correct format and, if - // so, saves it into the Storage. StoreObject is called only when local node - // complies with the container's storage policy. - VerifyAndStoreObject(object.Object) error + sessions + + // VerifyAndStoreObjectLocally checks whether given object has correct format + // and, if so, saves it in the Storage. StoreObject is called only when local + // node complies with the container's storage policy. + VerifyAndStoreObjectLocally(object.Object) error } // ACLInfoExtractor is the interface that allows to fetch data required for ACL @@ -148,8 +160,7 @@ const ( ) type server struct { - srv ServiceServer - + handlers Handlers fsChain FSChain storage Storage signer ecdsa.PrivateKey @@ -160,9 +171,9 @@ type server struct { } // New provides protoobject.ObjectServiceServer for the given parameters. -func New(c ServiceServer, magicNumber uint32, fsChain FSChain, st Storage, signer ecdsa.PrivateKey, m MetricCollector, ac aclsvc.ACLChecker, rp ACLInfoExtractor) protoobject.ObjectServiceServer { +func New(hs Handlers, magicNumber uint32, fsChain FSChain, st Storage, signer ecdsa.PrivateKey, m MetricCollector, ac aclsvc.ACLChecker, rp ACLInfoExtractor) protoobject.ObjectServiceServer { return &server{ - srv: c, + handlers: hs, fsChain: fsChain, storage: st, signer: signer, @@ -177,12 +188,16 @@ func (s *server) pushOpExecResult(op stat.Method, err error, startedAt time.Time s.metrics.HandleOpExecResult(op, err == nil, time.Since(startedAt)) } -func (s *server) makeResponseMetaHeader(st *protostatus.Status) *protosession.ResponseMetaHeader { +func newCurrentProtoVersionMessage() *refs.Version { v := version.Current() var v2 refsv2.Version v.WriteToV2(&v2) + return v2.ToGRPCMessage().(*refs.Version) +} + +func (s *server) makeResponseMetaHeader(st *protostatus.Status) *protosession.ResponseMetaHeader { return &protosession.ResponseMetaHeader{ - Version: v2.ToGRPCMessage().(*refs.Version), + Version: newCurrentProtoVersionMessage(), Epoch: s.fsChain.CurrentEpoch(), Status: st, } @@ -385,14 +400,14 @@ func (x *putStream) close() (*protoobject.PutResponse, error) { } func (s *server) Put(gStream protoobject.ObjectService_PutServer) error { - stream, err := s.srv.Put(gStream.Context()) + t := time.Now() + stream, err := s.handlers.Put(gStream.Context()) + + defer func() { s.pushOpExecResult(stat.MethodObjectPut, err, t) }() if err != nil { return err } - t := time.Now() - defer func() { s.pushOpExecResult(stat.MethodObjectPut, err, t) }() - var req *protoobject.PutRequest var resp *protoobject.PutResponse @@ -530,7 +545,7 @@ func (s *server) Delete(ctx context.Context, req *protoobject.DeleteRequest) (*p p.SetCommonParameters(cp) p.WithAddress(addr) p.WithTombstoneAddressTarget((*deleteResponseBody)(&rb)) - err = s.srv.Delete(ctx, p) + err = s.handlers.Delete(ctx, p) if err != nil { return s.makeStatusDeleteResponse(err), nil } @@ -595,7 +610,7 @@ func (s *server) Head(ctx context.Context, req *protoobject.HeadRequest) (*proto if err != nil { return s.makeStatusHeadResponse(err), nil } - err = s.srv.Head(ctx, p) + err = s.handlers.Head(ctx, p) if err != nil { return s.makeStatusHeadResponse(err), nil } @@ -867,12 +882,154 @@ func (s *server) GetRangeHash(ctx context.Context, req *protoobject.GetRangeHash return s.makeStatusHashResponse(err), nil } - resp, err := s.srv.GetRangeHash(ctx, hashRngReq) + p, err := convertHashPrm(s.signer, s.storage, req) + if err != nil { + return s.makeStatusHashResponse(err), nil + } + res, err := s.handlers.GetRangeHash(ctx, p) if err != nil { return s.makeStatusHashResponse(err), nil } - return s.signHashResponse(resp.ToGRPCMessage().(*protoobject.GetRangeHashResponse)), nil + return s.signHashResponse(&protoobject.GetRangeHashResponse{ + Body: &protoobject.GetRangeHashResponse_Body{ + Type: req.Body.Type, + HashList: res.Hashes(), + }}), nil +} + +// converts original request into parameters accepted by the internal handler. +func convertHashPrm(signer ecdsa.PrivateKey, ss sessions, req *protoobject.GetRangeHashRequest) (getsvc.RangeHashPrm, error) { + body := req.GetBody() + ma := body.GetAddress() + if ma == nil { // includes nil body + return getsvc.RangeHashPrm{}, errors.New("missing object address") + } + + var addr oid.Address + var addr2 refsv2.Address + if err := addr2.FromGRPCMessage(ma); err != nil { + panic(err) + } + if err := addr.ReadFromV2(addr2); err != nil { + return getsvc.RangeHashPrm{}, fmt.Errorf("invalid object address: %w", err) + } + + cp, err := objutil.CommonPrmFromRequest(req) + if err != nil { + return getsvc.RangeHashPrm{}, err + } + + var p getsvc.RangeHashPrm + + switch t := body.GetType(); t { + default: + return getsvc.RangeHashPrm{}, fmt.Errorf("unknown checksum type %v", t) + case refs.ChecksumType_SHA256: + p.SetHashGenerator(sha256.New) + case refs.ChecksumType_TZ: + p.SetHashGenerator(tz.New) + } + + if tok := cp.SessionToken(); tok != nil { + signerKey, err := ss.GetSessionPrivateKey(tok.Issuer(), tok.ID()) + if err != nil { + if !errors.Is(err, apistatus.ErrSessionTokenNotFound) { + return getsvc.RangeHashPrm{}, fmt.Errorf("fetching session key: %w", err) + } + cp.ForgetTokens() + signerKey = signer + } + p.WithCachedSignerKey(&signerKey) + } + + mr := body.GetRanges() + rngs := make([]object.Range, len(mr)) + for i := range mr { + var r2 v2object.Range + if err := r2.FromGRPCMessage(mr[i]); err != nil { + panic(err) + } + rngs[i] = *object.NewRangeFromV2(&r2) + } + + p.SetCommonParameters(cp) + p.WithAddress(addr) + p.SetRangeList(rngs) + p.SetSalt(body.GetSalt()) + + if cp.LocalOnly() { + return p, nil + } + + var onceResign sync.Once + meta := req.GetMetaHeader() + p.SetRangeHashRequestForwarder(func(ctx context.Context, node client.NodeInfo, c client.MultiAddressClient) ([][]byte, error) { + var err error + onceResign.Do(func() { + req.MetaHeader = &protosession.RequestMetaHeader{ + // TODO: #1165 think how to set the other fields + Version: newCurrentProtoVersionMessage(), + Ttl: meta.GetTtl() - 1, // FIXME: meta can be nil + Origin: meta, + } + var req2 v2object.GetRangeHashRequest + if err := req2.FromGRPCMessage(req); err != nil { + panic(err) + } + if err = signature.SignServiceMessage(&signer, &req2); err == nil { + req = req2.ToGRPCMessage().(*protoobject.GetRangeHashRequest) + } + }) + if err != nil { + return nil, err + } + + var firstErr error + nodePub := node.PublicKey() + addrs := node.AddressGroup() + for i := range addrs { + hs, err := getHashesFromRemoteNode(ctx, c, addrs[i], nodePub, req) + if err == nil { + return hs, nil + } + if firstErr == nil { + firstErr = err + } + // TODO: log error + } + return nil, firstErr + }) + return p, nil +} + +func getHashesFromRemoteNode(ctx context.Context, c client.MultiAddressClient, addr network.Address, nodePub []byte, + req *protoobject.GetRangeHashRequest) ([][]byte, error) { + var resp *protoobject.GetRangeHashResponse + err := c.RawForAddress(addr, func(conn *grpc.ClientConn) error { + var err error + resp, err = protoobject.NewObjectServiceClient(conn).GetRangeHash(ctx, req) + return err + }) + if err != nil { + return nil, fmt.Errorf("GetRangeHash rpc failure: %w", err) + } + + if err := internal.VerifyResponseKeyV2(nodePub, resp); err != nil { + return nil, err + } + resp2 := new(v2object.GetRangeHashResponse) + if err := resp2.FromGRPCMessage(resp); err != nil { + panic(err) // can only fail on wrong type, here it's correct + } + if err := signature.VerifyServiceMessage(resp2); err != nil { + return nil, fmt.Errorf("response verification failed: %w", err) + } + if err := checkStatus(resp.GetMetaHeader().GetStatus()); err != nil { + return nil, err + } + // TODO: verify number of hashes + return resp.GetBody().GetHashList(), nil } func (s *server) sendGetResponse(stream protoobject.ObjectService_GetServer, resp *protoobject.GetResponse) error { @@ -973,7 +1130,7 @@ func (s *server) Get(req *protoobject.GetRequest, gStream protoobject.ObjectServ if err != nil { return s.sendStatusGetResponse(gStream, err) } - err = s.srv.Get(gStream.Context(), p) + err = s.handlers.Get(gStream.Context(), p) if err != nil { return s.sendStatusGetResponse(gStream, err) } @@ -1237,7 +1394,7 @@ func (s *server) GetRange(req *protoobject.GetRangeRequest, gStream protoobject. if err != nil { return s.sendStatusRangeResponse(gStream, err) } - err = s.srv.GetRange(gStream.Context(), p) + err = s.handlers.GetRange(gStream.Context(), p) if err != nil { return s.sendStatusRangeResponse(gStream, err) } @@ -1483,7 +1640,7 @@ func (s *server) Search(req *protoobject.SearchRequest, gStream protoobject.Obje if err != nil { return s.sendStatusSearchResponse(gStream, err) } - err = s.srv.Search(gStream.Context(), p) + err = s.handlers.Search(gStream.Context(), p) if err != nil { return s.sendStatusSearchResponse(gStream, err) } @@ -1773,7 +1930,7 @@ func (s *server) Replicate(_ context.Context, req *protoobject.ReplicateRequest) }}, nil } - err = s.storage.VerifyAndStoreObject(*obj) + err = s.storage.VerifyAndStoreObjectLocally(*obj) if err != nil { return &protoobject.ReplicateResponse{Status: &protostatus.Status{ Code: codeInternal, diff --git a/pkg/services/object/server_test.go b/pkg/services/object/server_test.go index 0749352aa2..6d7ac363a2 100644 --- a/pkg/services/object/server_test.go +++ b/pkg/services/object/server_test.go @@ -12,7 +12,7 @@ import ( "testing" "time" - objectV2 "github.com/nspcc-dev/neofs-api-go/v2/object" + "github.com/google/uuid" objectgrpc "github.com/nspcc-dev/neofs-api-go/v2/object/grpc" refsv2 "github.com/nspcc-dev/neofs-api-go/v2/refs" refs "github.com/nspcc-dev/neofs-api-go/v2/refs/grpc" @@ -23,6 +23,7 @@ import ( getsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/get" putsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/put" searchsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/search" + apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" @@ -69,7 +70,7 @@ func (x noCallObjectService) GetRange(context.Context, getsvc.RangePrm) error { panic("must not be called") } -func (x noCallObjectService) GetRangeHash(context.Context, *objectV2.GetRangeHashRequest) (*objectV2.GetRangeHashResponse, error) { +func (x noCallObjectService) GetRangeHash(context.Context, getsvc.RangeHashPrm) (*getsvc.RangeHashRes, error) { panic("must not be called") } @@ -86,7 +87,12 @@ func (*noCallTestFSChain) LocalNodeUnderMaintenance() bool { panic("must not be type noCallTestStorage struct{} -func (noCallTestStorage) VerifyAndStoreObject(object.Object) error { panic("must not be called") } +func (noCallTestStorage) VerifyAndStoreObjectLocally(object.Object) error { + panic("must not be called") +} +func (noCallTestStorage) GetSessionPrivateKey(user.ID, uuid.UUID) (ecdsa.PrivateKey, error) { + panic("implement me") +} type noCallTestACLChecker struct{} @@ -213,11 +219,15 @@ func newTestStorage(t testing.TB, obj *objectgrpc.Object) *testStorage { return &testStorage{t: t, obj: obj} } -func (x *testStorage) VerifyAndStoreObject(obj object.Object) error { +func (x *testStorage) VerifyAndStoreObjectLocally(obj object.Object) error { require.Equal(x.t, x.obj, obj.ToV2().ToGRPCMessage().(*objectgrpc.Object)) return x.storeErr } +func (x *testStorage) GetSessionPrivateKey(user.ID, uuid.UUID) (ecdsa.PrivateKey, error) { + return ecdsa.PrivateKey{}, apistatus.ErrSessionTokenNotFound +} + func anyValidRequest(tb testing.TB, signer neofscrypto.Signer, cnr cid.ID, objID oid.ID) (*objectgrpc.ReplicateRequest, object.Object) { obj := objecttest.Object() obj.SetType(object.TypeRegular) @@ -549,7 +559,10 @@ func (nopFSChain) LocalNodeUnderMaintenance() bool { return false } type nopStorage struct{} -func (nopStorage) VerifyAndStoreObject(object.Object) error { return nil } +func (nopStorage) VerifyAndStoreObjectLocally(object.Object) error { return nil } +func (nopStorage) GetSessionPrivateKey(user.ID, uuid.UUID) (ecdsa.PrivateKey, error) { + return ecdsa.PrivateKey{}, apistatus.ErrSessionTokenNotFound +} func BenchmarkServer_Replicate(b *testing.B) { ctx := context.Background() diff --git a/pkg/services/object/util/prm.go b/pkg/services/object/util/prm.go index 93c62b684c..750aa9678a 100644 --- a/pkg/services/object/util/prm.go +++ b/pkg/services/object/util/prm.go @@ -84,53 +84,6 @@ func (p *CommonPrm) ForgetTokens() { } } -func CommonPrmFromV2(req interface { - GetMetaHeader() *session.RequestMetaHeader -}) (*CommonPrm, error) { - meta := req.GetMetaHeader() - ttl := meta.GetTTL() - - // unwrap meta header to get original request meta information - for meta.GetOrigin() != nil { - meta = meta.GetOrigin() - } - - var tokenSession *sessionsdk.Object - var err error - - if tokenSessionV2 := meta.GetSessionToken(); tokenSessionV2 != nil { - tokenSession = new(sessionsdk.Object) - - err = tokenSession.ReadFromV2(*tokenSessionV2) - if err != nil { - return nil, fmt.Errorf("invalid session token: %w", err) - } - } - - xHdrs := meta.GetXHeaders() - - prm := &CommonPrm{ - local: ttl <= maxLocalTTL, - token: tokenSession, - ttl: ttl - 1, // decrease TTL for new requests - xhdrs: make([]string, 0, 2*len(xHdrs)), - } - - if tok := meta.GetBearerToken(); tok != nil { - prm.bearer = new(bearer.Token) - err = prm.bearer.ReadFromV2(*tok) - if err != nil { - return nil, fmt.Errorf("invalid bearer token: %w", err) - } - } - - for i := range xHdrs { - prm.xhdrs = append(prm.xhdrs, xHdrs[i].GetKey(), xHdrs[i].GetValue()) - } - - return prm, nil -} - // CommonPrmFromRequest is a temporary copy-paste of [CommonPrmFromV2]. func CommonPrmFromRequest(req interface { GetMetaHeader() *protosession.RequestMetaHeader From d451264bef9b6f5022c25fb93f0fca0e2874c6ad Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 29 Jan 2025 12:44:30 +0300 Subject: [PATCH 6/8] object: require proper meta header to be present Signed-off-by: Roman Khimov --- pkg/services/object/server.go | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/pkg/services/object/server.go b/pkg/services/object/server.go index 8591de0f01..04709eef2d 100644 --- a/pkg/services/object/server.go +++ b/pkg/services/object/server.go @@ -696,13 +696,16 @@ func convertHeadPrm(signer ecdsa.PrivateKey, req *protoobject.HeadRequest, resp var onceResign sync.Once meta := req.GetMetaHeader() + if meta == nil { + return getsvc.HeadPrm{}, errors.New("missing meta header") + } bID := addr.Object().Marshal() p.SetRequestForwarder(func(ctx context.Context, node client.NodeInfo, c client.MultiAddressClient) (*object.Object, error) { var err error onceResign.Do(func() { req.MetaHeader = &protosession.RequestMetaHeader{ // TODO: #1165 think how to set the other fields - Ttl: meta.GetTtl() - 1, // FIXME: meta can be nil + Ttl: meta.GetTtl() - 1, Origin: meta, } var req2 v2object.HeadRequest @@ -964,13 +967,16 @@ func convertHashPrm(signer ecdsa.PrivateKey, ss sessions, req *protoobject.GetRa var onceResign sync.Once meta := req.GetMetaHeader() + if meta == nil { + return getsvc.RangeHashPrm{}, errors.New("missing meta header") + } p.SetRangeHashRequestForwarder(func(ctx context.Context, node client.NodeInfo, c client.MultiAddressClient) ([][]byte, error) { var err error onceResign.Do(func() { req.MetaHeader = &protosession.RequestMetaHeader{ // TODO: #1165 think how to set the other fields Version: newCurrentProtoVersionMessage(), - Ttl: meta.GetTtl() - 1, // FIXME: meta can be nil + Ttl: meta.GetTtl() - 1, Origin: meta, } var req2 v2object.GetRangeHashRequest @@ -1174,12 +1180,15 @@ func convertGetPrm(signer ecdsa.PrivateKey, req *protoobject.GetRequest, stream var onceHdr sync.Once var respondedPayload int meta := req.GetMetaHeader() + if meta == nil { + return getsvc.Prm{}, errors.New("missing meta header") + } p.SetRequestForwarder(func(ctx context.Context, node client.NodeInfo, c client.MultiAddressClient) (*object.Object, error) { var err error onceResign.Do(func() { req.MetaHeader = &protosession.RequestMetaHeader{ // TODO: #1165 think how to set the other fields - Ttl: meta.GetTtl() - 1, // FIXME: meta can be nil + Ttl: meta.GetTtl() - 1, Origin: meta, } var req2 v2object.GetRequest @@ -1449,12 +1458,15 @@ func convertRangePrm(signer ecdsa.PrivateKey, req *protoobject.GetRangeRequest, var onceResign sync.Once var respondedPayload int meta := req.GetMetaHeader() + if meta == nil { + return getsvc.RangePrm{}, errors.New("missing meta header") + } p.SetRequestForwarder(func(ctx context.Context, node client.NodeInfo, c client.MultiAddressClient) (*object.Object, error) { var err error onceResign.Do(func() { req.MetaHeader = &protosession.RequestMetaHeader{ // TODO: #1165 think how to set the other fields - Ttl: meta.GetTtl() - 1, // FIXME: meta can be nil + Ttl: meta.GetTtl() - 1, Origin: meta, } var req2 v2object.GetRangeRequest @@ -1690,12 +1702,15 @@ func convertSearchPrm(ctx context.Context, signer ecdsa.PrivateKey, req *protoob var onceResign sync.Once meta := req.GetMetaHeader() + if meta == nil { + return searchsvc.Prm{}, errors.New("missing meta header") + } p.SetRequestForwarder(func(node client.NodeInfo, c client.MultiAddressClient) ([]oid.ID, error) { var err error onceResign.Do(func() { req.MetaHeader = &protosession.RequestMetaHeader{ // TODO: #1165 think how to set the other fields - Ttl: meta.GetTtl() - 1, // FIXME: meta can be nil + Ttl: meta.GetTtl() - 1, Origin: meta, } var req2 v2object.SearchRequest From 7841b8194a6ac05ca150573abf5e75442f471acc Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 29 Jan 2025 13:46:27 +0300 Subject: [PATCH 7/8] object: pass proper ctx into remote put Signed-off-by: Roman Khimov --- pkg/services/object/server.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pkg/services/object/server.go b/pkg/services/object/server.go index 04709eef2d..d079fb9af1 100644 --- a/pkg/services/object/server.go +++ b/pkg/services/object/server.go @@ -215,6 +215,7 @@ func (s *server) sendStatusPutResponse(stream protoobject.ObjectService_PutServe } type putStream struct { + ctx context.Context signer ecdsa.PrivateKey base *putsvc.Streamer @@ -225,8 +226,9 @@ type putStream struct { expBytes, recvBytes uint64 // payload } -func newIntermediatePutStream(signer ecdsa.PrivateKey, base *putsvc.Streamer) *putStream { +func newIntermediatePutStream(signer ecdsa.PrivateKey, base *putsvc.Streamer, ctx context.Context) *putStream { return &putStream{ + ctx: ctx, signer: signer, base: base, } @@ -237,7 +239,7 @@ func (x *putStream) sendToRemoteNode(node client.NodeInfo, c client.MultiAddress nodePub := node.PublicKey() addrs := node.AddressGroup() for i := range addrs { - err := putToRemoteNode(c, addrs[i], nodePub, x.initReq, x.chunkReqs) + err := putToRemoteNode(x.ctx, c, addrs[i], nodePub, x.initReq, x.chunkReqs) if err == nil { return nil } @@ -249,12 +251,12 @@ func (x *putStream) sendToRemoteNode(node client.NodeInfo, c client.MultiAddress return firstErr } -func putToRemoteNode(c client.MultiAddressClient, addr network.Address, nodePub []byte, +func putToRemoteNode(ctx context.Context, c client.MultiAddressClient, addr network.Address, nodePub []byte, initReq *protoobject.PutRequest, chunkReqs []*protoobject.PutRequest) error { var stream protoobject.ObjectService_PutClient err := c.RawForAddress(addr, func(conn *grpc.ClientConn) error { var err error - stream, err = protoobject.NewObjectServiceClient(conn).Put(context.TODO()) // FIXME: use proper context + stream, err = protoobject.NewObjectServiceClient(conn).Put(ctx) return err }) if err != nil { @@ -411,7 +413,7 @@ func (s *server) Put(gStream protoobject.ObjectService_PutServer) error { var req *protoobject.PutRequest var resp *protoobject.PutResponse - ps := newIntermediatePutStream(s.signer, stream) + ps := newIntermediatePutStream(s.signer, stream, gStream.Context()) for { if req, err = gStream.Recv(); err != nil { if errors.Is(err, io.EOF) { From b39930eab381beb4a5381c10eda070414eae6db1 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 29 Jan 2025 15:46:20 +0300 Subject: [PATCH 8/8] object: drop context-related FIXME I have no idea what it means. We're operating in the original request context and it should be sufficient for now. Additional time-limited subcontexts can be created, but that's a different problem. If the upper layer exits this context ends up anyway. Signed-off-by: Roman Khimov --- pkg/services/object/server.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/pkg/services/object/server.go b/pkg/services/object/server.go index d079fb9af1..b1c5e638b8 100644 --- a/pkg/services/object/server.go +++ b/pkg/services/object/server.go @@ -1228,7 +1228,6 @@ func continueGetFromRemoteNode(ctx context.Context, c client.MultiAddressClient, var getStream protoobject.ObjectService_GetClient err := c.RawForAddress(addr, func(conn *grpc.ClientConn) error { var err error - // FIXME: context should be cancelled on return from upper func getStream, err = protoobject.NewObjectServiceClient(conn).Get(ctx, req) return err }) @@ -1506,7 +1505,6 @@ func continueRangeFromRemoteNode(ctx context.Context, c client.MultiAddressClien var rangeStream protoobject.ObjectService_GetRangeClient err := c.RawForAddress(addr, func(conn *grpc.ClientConn) error { var err error - // FIXME: context should be cancelled on return from upper func rangeStream, err = protoobject.NewObjectServiceClient(conn).GetRange(ctx, req) return err }) @@ -1749,7 +1747,6 @@ func searchOnRemoteNode(ctx context.Context, c client.MultiAddressClient, addr n var searchStream protoobject.ObjectService_SearchClient if err := c.RawForAddress(addr, func(conn *grpc.ClientConn) error { var err error - // FIXME: context should be cancelled on return from upper func searchStream, err = protoobject.NewObjectServiceClient(conn).Search(ctx, req) return err }); err != nil {