WIP: Cache head results in the request context #974
10 changed files with 164 additions and 137 deletions
|
@ -100,7 +100,7 @@ func (c *Checker) StickyBitCheck(info v2.RequestInfo, owner user.ID) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckEACL is a main check function for extended ACL.
|
// CheckEACL is a main check function for extended ACL.
|
||||||
func (c *Checker) CheckEACL(msg any, reqInfo v2.RequestInfo) error {
|
func (c *Checker) CheckEACL(ctx context.Context, msg any, reqInfo v2.RequestInfo) error {
|
||||||
basicACL := reqInfo.BasicACL()
|
basicACL := reqInfo.BasicACL()
|
||||||
if !basicACL.Extendable() {
|
if !basicACL.Extendable() {
|
||||||
return nil
|
return nil
|
||||||
|
@ -136,7 +136,7 @@ func (c *Checker) CheckEACL(msg any, reqInfo v2.RequestInfo) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
hdrSrc, err := c.getHeaderSource(cnr, msg, reqInfo)
|
hdrSrc, err := c.getHeaderSource(ctx, cnr, msg, reqInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -173,7 +173,7 @@ func getRole(reqInfo v2.RequestInfo) eaclSDK.Role {
|
||||||
return eaclRole
|
return eaclRole
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Checker) getHeaderSource(cnr cid.ID, msg any, reqInfo v2.RequestInfo) (eaclSDK.TypedHeaderSource, error) {
|
func (c *Checker) getHeaderSource(ctx context.Context, cnr cid.ID, msg any, reqInfo v2.RequestInfo) (eaclSDK.TypedHeaderSource, error) {
|
||||||
var xHeaderSource eaclV2.XHeaderSource
|
var xHeaderSource eaclV2.XHeaderSource
|
||||||
if req, ok := msg.(eaclV2.Request); ok {
|
if req, ok := msg.(eaclV2.Request); ok {
|
||||||
xHeaderSource = eaclV2.NewRequestXHeaderSource(req)
|
xHeaderSource = eaclV2.NewRequestXHeaderSource(req)
|
||||||
|
@ -181,7 +181,7 @@ func (c *Checker) getHeaderSource(cnr cid.ID, msg any, reqInfo v2.RequestInfo) (
|
||||||
xHeaderSource = eaclV2.NewResponseXHeaderSource(msg.(eaclV2.Response), reqInfo.Request().(eaclV2.Request))
|
xHeaderSource = eaclV2.NewResponseXHeaderSource(msg.(eaclV2.Response), reqInfo.Request().(eaclV2.Request))
|
||||||
}
|
}
|
||||||
|
|
||||||
hdrSrc, err := eaclV2.NewMessageHeaderSource(&localStorage{ls: c.localStorage}, xHeaderSource, cnr, eaclV2.WithOID(reqInfo.ObjectID()))
|
hdrSrc, err := eaclV2.NewMessageHeaderSource(ctx, &localStorage{ls: c.localStorage}, xHeaderSource, cnr, reqInfo.ObjectID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("can't parse headers: %w", err)
|
return nil, fmt.Errorf("can't parse headers: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,10 +103,11 @@ func TestHeadRequest(t *testing.T) {
|
||||||
|
|
||||||
newSource := func(t *testing.T) eaclSDK.TypedHeaderSource {
|
newSource := func(t *testing.T) eaclSDK.TypedHeaderSource {
|
||||||
hdrSrc, err := NewMessageHeaderSource(
|
hdrSrc, err := NewMessageHeaderSource(
|
||||||
|
context.TODO(),
|
||||||
lStorage,
|
lStorage,
|
||||||
NewRequestXHeaderSource(req),
|
NewRequestXHeaderSource(req),
|
||||||
addr.Container(),
|
addr.Container(),
|
||||||
WithOID(&id))
|
&id)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return hdrSrc
|
return hdrSrc
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
objectV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
objectV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
||||||
refsV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
refsV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object"
|
||||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||||
eaclSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
|
eaclSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
|
||||||
objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||||
|
@ -16,8 +17,6 @@ import (
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Option func(*cfg)
|
|
||||||
|
|
||||||
type cfg struct {
|
type cfg struct {
|
||||||
storage ObjectStorage
|
storage ObjectStorage
|
||||||
|
|
||||||
|
@ -46,24 +45,21 @@ type headerSource struct {
|
||||||
incompleteObjectHeaders bool
|
incompleteObjectHeaders bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMessageHeaderSource(os ObjectStorage, xhs XHeaderSource, cnrID cid.ID, opts ...Option) (eaclSDK.TypedHeaderSource, error) {
|
func NewMessageHeaderSource(ctx context.Context, os ObjectStorage, xhs XHeaderSource, cnrID cid.ID, objID *oid.ID) (eaclSDK.TypedHeaderSource, error) {
|
||||||
cfg := &cfg{
|
cfg := &cfg{
|
||||||
storage: os,
|
storage: os,
|
||||||
cnr: cnrID,
|
cnr: cnrID,
|
||||||
|
obj: objID,
|
||||||
msg: xhs,
|
msg: xhs,
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range opts {
|
|
||||||
opts[i](cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.msg == nil {
|
if cfg.msg == nil {
|
||||||
return nil, errors.New("message is not provided")
|
return nil, errors.New("message is not provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
var res headerSource
|
var res headerSource
|
||||||
|
|
||||||
err := cfg.readObjectHeaders(&res)
|
err := cfg.readObjectHeaders(ctx, &res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -96,18 +92,18 @@ func (x xHeader) Value() string {
|
||||||
|
|
||||||
var errMissingOID = errors.New("object ID is missing")
|
var errMissingOID = errors.New("object ID is missing")
|
||||||
|
|
||||||
func (h *cfg) readObjectHeaders(dst *headerSource) error {
|
func (h *cfg) readObjectHeaders(ctx context.Context, dst *headerSource) error {
|
||||||
switch m := h.msg.(type) {
|
switch m := h.msg.(type) {
|
||||||
default:
|
default:
|
||||||
panic(fmt.Sprintf("unexpected message type %T", h.msg))
|
panic(fmt.Sprintf("unexpected message type %T", h.msg))
|
||||||
case requestXHeaderSource:
|
case requestXHeaderSource:
|
||||||
return h.readObjectHeadersFromRequestXHeaderSource(m, dst)
|
return h.readObjectHeadersFromRequestXHeaderSource(ctx, m, dst)
|
||||||
case responseXHeaderSource:
|
case responseXHeaderSource:
|
||||||
return h.readObjectHeadersResponseXHeaderSource(m, dst)
|
return h.readObjectHeadersResponseXHeaderSource(ctx, m, dst)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *cfg) readObjectHeadersFromRequestXHeaderSource(m requestXHeaderSource, dst *headerSource) error {
|
func (h *cfg) readObjectHeadersFromRequestXHeaderSource(ctx context.Context, m requestXHeaderSource, dst *headerSource) error {
|
||||||
switch req := m.req.(type) {
|
switch req := m.req.(type) {
|
||||||
case
|
case
|
||||||
*objectV2.GetRequest,
|
*objectV2.GetRequest,
|
||||||
|
@ -116,7 +112,7 @@ func (h *cfg) readObjectHeadersFromRequestXHeaderSource(m requestXHeaderSource,
|
||||||
return errMissingOID
|
return errMissingOID
|
||||||
}
|
}
|
||||||
|
|
||||||
objHeaders, completed := h.localObjectHeaders(h.cnr, h.obj)
|
objHeaders, completed := h.localObjectHeaders(ctx, h.cnr, h.obj)
|
||||||
|
|
||||||
dst.objectHeaders = objHeaders
|
dst.objectHeaders = objHeaders
|
||||||
dst.incompleteObjectHeaders = !completed
|
dst.incompleteObjectHeaders = !completed
|
||||||
|
@ -154,10 +150,10 @@ func (h *cfg) readObjectHeadersFromRequestXHeaderSource(m requestXHeaderSource,
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *cfg) readObjectHeadersResponseXHeaderSource(m responseXHeaderSource, dst *headerSource) error {
|
func (h *cfg) readObjectHeadersResponseXHeaderSource(ctx context.Context, m responseXHeaderSource, dst *headerSource) error {
|
||||||
switch resp := m.resp.(type) {
|
switch resp := m.resp.(type) {
|
||||||
default:
|
default:
|
||||||
objectHeaders, completed := h.localObjectHeaders(h.cnr, h.obj)
|
objectHeaders, completed := h.localObjectHeaders(ctx, h.cnr, h.obj)
|
||||||
|
|
||||||
dst.objectHeaders = objectHeaders
|
dst.objectHeaders = objectHeaders
|
||||||
dst.incompleteObjectHeaders = !completed
|
dst.incompleteObjectHeaders = !completed
|
||||||
|
@ -198,14 +194,25 @@ func (h *cfg) readObjectHeadersResponseXHeaderSource(m responseXHeaderSource, ds
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *cfg) localObjectHeaders(cnr cid.ID, idObj *oid.ID) ([]eaclSDK.Header, bool) {
|
func (h *cfg) localObjectHeaders(ctx context.Context, cnr cid.ID, idObj *oid.ID) ([]eaclSDK.Header, bool) {
|
||||||
if idObj != nil {
|
if idObj != nil {
|
||||||
var addr oid.Address
|
var addr oid.Address
|
||||||
addr.SetContainer(cnr)
|
addr.SetContainer(cnr)
|
||||||
addr.SetObject(*idObj)
|
addr.SetObject(*idObj)
|
||||||
|
|
||||||
obj, err := h.storage.Head(context.TODO(), addr)
|
reqCtx, _ := object.FromRequestContext(ctx)
|
||||||
|
if reqCtx != nil {
|
||||||
|
hdr := reqCtx.GetHeader(addr)
|
||||||
|
if hdr != nil {
|
||||||
|
return headersFromObject(hdr, cnr, idObj), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
obj, err := h.storage.Head(ctx, addr)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
if reqCtx != nil {
|
||||||
|
reqCtx.Header.Store(obj)
|
||||||
|
}
|
||||||
return headersFromObject(obj, cnr, idObj), true
|
return headersFromObject(obj, cnr, idObj), true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
package v2
|
|
||||||
|
|
||||||
import (
|
|
||||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
|
||||||
)
|
|
||||||
|
|
||||||
func WithOID(v *oid.ID) Option {
|
|
||||||
return func(c *cfg) {
|
|
||||||
c.obj = v
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
sessionV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
sessionV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
|
||||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||||
|
@ -157,3 +158,13 @@ func unmarshalPublicKeyWithOwner(rawKey []byte) (*user.ID, *keys.PublicKey, erro
|
||||||
|
|
||||||
return &idSender, key, nil
|
return &idSender, key, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r RequestInfo) toRequestContext() *object.RequestContext {
|
||||||
|
return &object.RequestContext{
|
||||||
|
Namespace: r.ContainerNamespace(),
|
||||||
|
ContainerOwner: r.ContainerOwner(),
|
||||||
|
SenderKey: r.SenderKey(),
|
||||||
|
Role: r.RequestRole(),
|
||||||
|
SoftAPECheck: r.IsSoftAPECheck(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -108,24 +108,11 @@ func New(next object.ServiceServer,
|
||||||
type wrappedGetObjectStream struct {
|
type wrappedGetObjectStream struct {
|
||||||
object.GetObjectStream
|
object.GetObjectStream
|
||||||
|
|
||||||
requestInfo RequestInfo
|
requestContext *object.RequestContext
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *wrappedGetObjectStream) Context() context.Context {
|
func (w *wrappedGetObjectStream) Context() context.Context {
|
||||||
return context.WithValue(w.GetObjectStream.Context(), object.RequestContextKey, &object.RequestContext{
|
return object.NewRequestContext(w.GetObjectStream.Context(), w.requestContext)
|
||||||
Namespace: w.requestInfo.ContainerNamespace(),
|
|
||||||
ContainerOwner: w.requestInfo.ContainerOwner(),
|
|
||||||
SenderKey: w.requestInfo.SenderKey(),
|
|
||||||
Role: w.requestInfo.RequestRole(),
|
|
||||||
SoftAPECheck: w.requestInfo.IsSoftAPECheck(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func newWrappedGetObjectStreamStream(getObjectStream object.GetObjectStream, reqInfo RequestInfo) object.GetObjectStream {
|
|
||||||
return &wrappedGetObjectStream{
|
|
||||||
GetObjectStream: getObjectStream,
|
|
||||||
requestInfo: reqInfo,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// wrappedRangeStream propagates RequestContext into GetObjectRangeStream's context.
|
// wrappedRangeStream propagates RequestContext into GetObjectRangeStream's context.
|
||||||
|
@ -133,24 +120,11 @@ func newWrappedGetObjectStreamStream(getObjectStream object.GetObjectStream, req
|
||||||
type wrappedRangeStream struct {
|
type wrappedRangeStream struct {
|
||||||
object.GetObjectRangeStream
|
object.GetObjectRangeStream
|
||||||
|
|
||||||
requestInfo RequestInfo
|
requestContext *object.RequestContext
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *wrappedRangeStream) Context() context.Context {
|
func (w *wrappedRangeStream) Context() context.Context {
|
||||||
return context.WithValue(w.GetObjectRangeStream.Context(), object.RequestContextKey, &object.RequestContext{
|
return object.NewRequestContext(w.GetObjectRangeStream.Context(), w.requestContext)
|
||||||
Namespace: w.requestInfo.ContainerNamespace(),
|
|
||||||
ContainerOwner: w.requestInfo.ContainerOwner(),
|
|
||||||
SenderKey: w.requestInfo.SenderKey(),
|
|
||||||
Role: w.requestInfo.RequestRole(),
|
|
||||||
SoftAPECheck: w.requestInfo.IsSoftAPECheck(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func newWrappedRangeStream(rangeStream object.GetObjectRangeStream, reqInfo RequestInfo) object.GetObjectRangeStream {
|
|
||||||
return &wrappedRangeStream{
|
|
||||||
GetObjectRangeStream: rangeStream,
|
|
||||||
requestInfo: reqInfo,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// wrappedSearchStream propagates RequestContext into SearchStream's context.
|
// wrappedSearchStream propagates RequestContext into SearchStream's context.
|
||||||
|
@ -158,24 +132,11 @@ func newWrappedRangeStream(rangeStream object.GetObjectRangeStream, reqInfo Requ
|
||||||
type wrappedSearchStream struct {
|
type wrappedSearchStream struct {
|
||||||
object.SearchStream
|
object.SearchStream
|
||||||
|
|
||||||
requestInfo RequestInfo
|
requestContext *object.RequestContext
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *wrappedSearchStream) Context() context.Context {
|
func (w *wrappedSearchStream) Context() context.Context {
|
||||||
return context.WithValue(w.SearchStream.Context(), object.RequestContextKey, &object.RequestContext{
|
return object.NewRequestContext(w.SearchStream.Context(), w.requestContext)
|
||||||
Namespace: w.requestInfo.ContainerNamespace(),
|
|
||||||
ContainerOwner: w.requestInfo.ContainerOwner(),
|
|
||||||
SenderKey: w.requestInfo.SenderKey(),
|
|
||||||
Role: w.requestInfo.RequestRole(),
|
|
||||||
SoftAPECheck: w.requestInfo.IsSoftAPECheck(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func newWrappedSearchStream(searchStream object.SearchStream, reqInfo RequestInfo) object.SearchStream {
|
|
||||||
return &wrappedSearchStream{
|
|
||||||
SearchStream: searchStream,
|
|
||||||
requestInfo: reqInfo,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get implements ServiceServer interface, makes ACL checks and calls
|
// Get implements ServiceServer interface, makes ACL checks and calls
|
||||||
|
@ -222,16 +183,23 @@ func (b Service) Get(request *objectV2.GetRequest, stream object.GetObjectStream
|
||||||
|
|
||||||
reqInfo.obj = obj
|
reqInfo.obj = obj
|
||||||
|
|
||||||
|
reqCtx := reqInfo.toRequestContext()
|
||||||
if reqInfo.IsSoftAPECheck() {
|
if reqInfo.IsSoftAPECheck() {
|
||||||
if !b.checker.CheckBasicACL(reqInfo) {
|
if !b.checker.CheckBasicACL(reqInfo) {
|
||||||
return basicACLErr(reqInfo)
|
return basicACLErr(reqInfo)
|
||||||
} else if err := b.checker.CheckEACL(request, reqInfo); err != nil {
|
}
|
||||||
|
|
||||||
|
ctx := object.NewRequestContext(stream.Context(), reqCtx)
|
||||||
|
if err := b.checker.CheckEACL(ctx, request, reqInfo); err != nil {
|
||||||
return eACLErr(reqInfo, err)
|
return eACLErr(reqInfo, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.next.Get(request, &getStreamBasicChecker{
|
return b.next.Get(request, &getStreamBasicChecker{
|
||||||
GetObjectStream: newWrappedGetObjectStreamStream(stream, reqInfo),
|
GetObjectStream: &wrappedGetObjectStream{
|
||||||
|
GetObjectStream: stream,
|
||||||
|
requestContext: reqCtx,
|
||||||
|
},
|
||||||
info: reqInfo,
|
info: reqInfo,
|
||||||
checker: b.checker,
|
checker: b.checker,
|
||||||
})
|
})
|
||||||
|
@ -291,17 +259,20 @@ func (b Service) Head(
|
||||||
|
|
||||||
reqInfo.obj = obj
|
reqInfo.obj = obj
|
||||||
|
|
||||||
|
ctx = requestContext(ctx, reqInfo)
|
||||||
if reqInfo.IsSoftAPECheck() {
|
if reqInfo.IsSoftAPECheck() {
|
||||||
if !b.checker.CheckBasicACL(reqInfo) {
|
if !b.checker.CheckBasicACL(reqInfo) {
|
||||||
return nil, basicACLErr(reqInfo)
|
return nil, basicACLErr(reqInfo)
|
||||||
} else if err := b.checker.CheckEACL(request, reqInfo); err != nil {
|
}
|
||||||
|
|
||||||
|
if err := b.checker.CheckEACL(ctx, request, reqInfo); err != nil {
|
||||||
return nil, eACLErr(reqInfo, err)
|
return nil, eACLErr(reqInfo, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := b.next.Head(requestContext(ctx, reqInfo), request)
|
resp, err := b.next.Head(ctx, request)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if err = b.checker.CheckEACL(resp, reqInfo); err != nil {
|
if err = b.checker.CheckEACL(ctx, resp, reqInfo); err != nil {
|
||||||
err = eACLErr(reqInfo, err)
|
err = eACLErr(reqInfo, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -344,17 +315,24 @@ func (b Service) Search(request *objectV2.SearchRequest, stream object.SearchStr
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reqCtx := reqInfo.toRequestContext()
|
||||||
if reqInfo.IsSoftAPECheck() {
|
if reqInfo.IsSoftAPECheck() {
|
||||||
if !b.checker.CheckBasicACL(reqInfo) {
|
if !b.checker.CheckBasicACL(reqInfo) {
|
||||||
return basicACLErr(reqInfo)
|
return basicACLErr(reqInfo)
|
||||||
} else if err := b.checker.CheckEACL(request, reqInfo); err != nil {
|
}
|
||||||
|
|
||||||
|
ctx := object.NewRequestContext(stream.Context(), reqCtx)
|
||||||
|
if err := b.checker.CheckEACL(ctx, request, reqInfo); err != nil {
|
||||||
return eACLErr(reqInfo, err)
|
return eACLErr(reqInfo, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.next.Search(request, &searchStreamBasicChecker{
|
return b.next.Search(request, &searchStreamBasicChecker{
|
||||||
checker: b.checker,
|
checker: b.checker,
|
||||||
SearchStream: newWrappedSearchStream(stream, reqInfo),
|
SearchStream: &wrappedSearchStream{
|
||||||
|
SearchStream: stream,
|
||||||
|
requestContext: reqCtx,
|
||||||
|
},
|
||||||
info: reqInfo,
|
info: reqInfo,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -404,15 +382,15 @@ func (b Service) Delete(
|
||||||
|
|
||||||
reqInfo.obj = obj
|
reqInfo.obj = obj
|
||||||
|
|
||||||
|
ctx = requestContext(ctx, reqInfo)
|
||||||
if reqInfo.IsSoftAPECheck() {
|
if reqInfo.IsSoftAPECheck() {
|
||||||
if !b.checker.CheckBasicACL(reqInfo) {
|
if !b.checker.CheckBasicACL(reqInfo) {
|
||||||
return nil, basicACLErr(reqInfo)
|
return nil, basicACLErr(reqInfo)
|
||||||
} else if err := b.checker.CheckEACL(request, reqInfo); err != nil {
|
} else if err := b.checker.CheckEACL(ctx, request, reqInfo); err != nil {
|
||||||
return nil, eACLErr(reqInfo, err)
|
return nil, eACLErr(reqInfo, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return b.next.Delete(ctx, request)
|
||||||
return b.next.Delete(requestContext(ctx, reqInfo), request)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b Service) GetRange(request *objectV2.GetRangeRequest, stream object.GetObjectRangeStream) error {
|
func (b Service) GetRange(request *objectV2.GetRangeRequest, stream object.GetObjectRangeStream) error {
|
||||||
|
@ -457,29 +435,30 @@ func (b Service) GetRange(request *objectV2.GetRangeRequest, stream object.GetOb
|
||||||
|
|
||||||
reqInfo.obj = obj
|
reqInfo.obj = obj
|
||||||
|
|
||||||
|
reqCtx := reqInfo.toRequestContext()
|
||||||
if reqInfo.IsSoftAPECheck() {
|
if reqInfo.IsSoftAPECheck() {
|
||||||
if !b.checker.CheckBasicACL(reqInfo) {
|
if !b.checker.CheckBasicACL(reqInfo) {
|
||||||
return basicACLErr(reqInfo)
|
return basicACLErr(reqInfo)
|
||||||
} else if err := b.checker.CheckEACL(request, reqInfo); err != nil {
|
}
|
||||||
|
|
||||||
|
ctx := object.NewRequestContext(stream.Context(), reqCtx)
|
||||||
|
if err := b.checker.CheckEACL(ctx, request, reqInfo); err != nil {
|
||||||
return eACLErr(reqInfo, err)
|
return eACLErr(reqInfo, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.next.GetRange(request, &rangeStreamBasicChecker{
|
return b.next.GetRange(request, &rangeStreamBasicChecker{
|
||||||
checker: b.checker,
|
checker: b.checker,
|
||||||
GetObjectRangeStream: newWrappedRangeStream(stream, reqInfo),
|
GetObjectRangeStream: &wrappedRangeStream{
|
||||||
|
GetObjectRangeStream: stream,
|
||||||
|
requestContext: reqCtx,
|
||||||
|
},
|
||||||
info: reqInfo,
|
info: reqInfo,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestContext(ctx context.Context, reqInfo RequestInfo) context.Context {
|
func requestContext(ctx context.Context, reqInfo RequestInfo) context.Context {
|
||||||
return context.WithValue(ctx, object.RequestContextKey, &object.RequestContext{
|
return object.NewRequestContext(ctx, reqInfo.toRequestContext())
|
||||||
Namespace: reqInfo.ContainerNamespace(),
|
|
||||||
ContainerOwner: reqInfo.ContainerOwner(),
|
|
||||||
SenderKey: reqInfo.SenderKey(),
|
|
||||||
Role: reqInfo.RequestRole(),
|
|
||||||
SoftAPECheck: reqInfo.IsSoftAPECheck(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b Service) GetRangeHash(
|
func (b Service) GetRangeHash(
|
||||||
|
@ -527,15 +506,18 @@ func (b Service) GetRangeHash(
|
||||||
|
|
||||||
reqInfo.obj = obj
|
reqInfo.obj = obj
|
||||||
|
|
||||||
|
ctx = requestContext(ctx, reqInfo)
|
||||||
if reqInfo.IsSoftAPECheck() {
|
if reqInfo.IsSoftAPECheck() {
|
||||||
if !b.checker.CheckBasicACL(reqInfo) {
|
if !b.checker.CheckBasicACL(reqInfo) {
|
||||||
return nil, basicACLErr(reqInfo)
|
return nil, basicACLErr(reqInfo)
|
||||||
} else if err := b.checker.CheckEACL(request, reqInfo); err != nil {
|
}
|
||||||
|
|
||||||
|
if err := b.checker.CheckEACL(ctx, request, reqInfo); err != nil {
|
||||||
return nil, eACLErr(reqInfo, err)
|
return nil, eACLErr(reqInfo, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.next.GetRangeHash(requestContext(ctx, reqInfo), request)
|
return b.next.GetRangeHash(ctx, request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b Service) PutSingle(ctx context.Context, request *objectV2.PutSingleRequest) (*objectV2.PutSingleResponse, error) {
|
func (b Service) PutSingle(ctx context.Context, request *objectV2.PutSingleRequest) (*objectV2.PutSingleResponse, error) {
|
||||||
|
@ -586,16 +568,18 @@ func (b Service) PutSingle(ctx context.Context, request *objectV2.PutSingleReque
|
||||||
|
|
||||||
reqInfo.obj = obj
|
reqInfo.obj = obj
|
||||||
|
|
||||||
|
ctx = requestContext(ctx, reqInfo)
|
||||||
if reqInfo.IsSoftAPECheck() {
|
if reqInfo.IsSoftAPECheck() {
|
||||||
if !b.checker.CheckBasicACL(reqInfo) || !b.checker.StickyBitCheck(reqInfo, idOwner) {
|
if !b.checker.CheckBasicACL(reqInfo) || !b.checker.StickyBitCheck(reqInfo, idOwner) {
|
||||||
return nil, basicACLErr(reqInfo)
|
return nil, basicACLErr(reqInfo)
|
||||||
}
|
}
|
||||||
if err := b.checker.CheckEACL(request, reqInfo); err != nil {
|
|
||||||
|
if err := b.checker.CheckEACL(ctx, request, reqInfo); err != nil {
|
||||||
return nil, eACLErr(reqInfo, err)
|
return nil, eACLErr(reqInfo, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.next.PutSingle(requestContext(ctx, reqInfo), request)
|
return b.next.PutSingle(ctx, request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p putStreamBasicChecker) Send(ctx context.Context, request *objectV2.PutRequest) error {
|
func (p putStreamBasicChecker) Send(ctx context.Context, request *objectV2.PutRequest) error {
|
||||||
|
@ -706,7 +690,7 @@ func (p putStreamBasicChecker) CloseAndRecv(ctx context.Context) (*objectV2.PutR
|
||||||
|
|
||||||
func (g *getStreamBasicChecker) Send(resp *objectV2.GetResponse) error {
|
func (g *getStreamBasicChecker) Send(resp *objectV2.GetResponse) error {
|
||||||
if _, ok := resp.GetBody().GetObjectPart().(*objectV2.GetObjectPartInit); ok {
|
if _, ok := resp.GetBody().GetObjectPart().(*objectV2.GetObjectPartInit); ok {
|
||||||
if err := g.checker.CheckEACL(resp, g.info); err != nil {
|
if err := g.checker.CheckEACL(g.GetObjectStream.Context(), resp, g.info); err != nil {
|
||||||
return eACLErr(g.info, err)
|
return eACLErr(g.info, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -715,7 +699,7 @@ func (g *getStreamBasicChecker) Send(resp *objectV2.GetResponse) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *rangeStreamBasicChecker) Send(resp *objectV2.GetRangeResponse) error {
|
func (g *rangeStreamBasicChecker) Send(resp *objectV2.GetRangeResponse) error {
|
||||||
if err := g.checker.CheckEACL(resp, g.info); err != nil {
|
if err := g.checker.CheckEACL(g.GetObjectRangeStream.Context(), resp, g.info); err != nil {
|
||||||
return eACLErr(g.info, err)
|
return eACLErr(g.info, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -723,7 +707,7 @@ func (g *rangeStreamBasicChecker) Send(resp *objectV2.GetRangeResponse) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *searchStreamBasicChecker) Send(resp *objectV2.SearchResponse) error {
|
func (g *searchStreamBasicChecker) Send(resp *objectV2.SearchResponse) error {
|
||||||
if err := g.checker.CheckEACL(resp, g.info); err != nil {
|
if err := g.checker.CheckEACL(g.SearchStream.Context(), resp, g.info); err != nil {
|
||||||
return eACLErr(g.info, err)
|
return eACLErr(g.info, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package v2
|
package v2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -12,7 +14,7 @@ type ACLChecker interface {
|
||||||
CheckBasicACL(RequestInfo) bool
|
CheckBasicACL(RequestInfo) bool
|
||||||
// CheckEACL must return non-nil error if request
|
// CheckEACL must return non-nil error if request
|
||||||
// doesn't pass extended ACL validation.
|
// doesn't pass extended ACL validation.
|
||||||
CheckEACL(any, RequestInfo) error
|
CheckEACL(context.Context, any, RequestInfo) error
|
||||||
// StickyBitCheck must return true only if sticky bit
|
// StickyBitCheck must return true only if sticky bit
|
||||||
// is disabled or enabled but request contains correct
|
// is disabled or enabled but request contains correct
|
||||||
// owner field.
|
// owner field.
|
||||||
|
|
|
@ -3,8 +3,6 @@ package ape
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
objectV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
objectV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
||||||
|
@ -18,8 +16,6 @@ import (
|
||||||
nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
|
nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errFailedToCastToRequestContext = errors.New("failed cast to RequestContext")
|
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
log *logger.Logger
|
log *logger.Logger
|
||||||
|
|
||||||
|
@ -101,25 +97,13 @@ func (g *getStreamBasicChecker) Send(resp *objectV2.GetResponse) error {
|
||||||
return g.GetObjectStream.Send(resp)
|
return g.GetObjectStream.Send(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestContext(ctx context.Context) (*objectSvc.RequestContext, error) {
|
|
||||||
untyped := ctx.Value(objectSvc.RequestContextKey)
|
|
||||||
if untyped == nil {
|
|
||||||
return nil, fmt.Errorf("no key %s in context", objectSvc.RequestContextKey)
|
|
||||||
}
|
|
||||||
rc, ok := untyped.(*objectSvc.RequestContext)
|
|
||||||
if !ok {
|
|
||||||
return nil, errFailedToCastToRequestContext
|
|
||||||
}
|
|
||||||
return rc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Service) Get(request *objectV2.GetRequest, stream objectSvc.GetObjectStream) error {
|
func (c *Service) Get(request *objectV2.GetRequest, stream objectSvc.GetObjectStream) error {
|
||||||
cnrID, objID, err := getAddressParamsSDK(request.GetBody().GetAddress().GetContainerID(), request.GetBody().GetAddress().GetObjectID())
|
cnrID, objID, err := getAddressParamsSDK(request.GetBody().GetAddress().GetContainerID(), request.GetBody().GetAddress().GetObjectID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return toStatusErr(err)
|
return toStatusErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
reqCtx, err := requestContext(stream.Context())
|
reqCtx, err := objectSvc.FromRequestContext(stream.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return toStatusErr(err)
|
return toStatusErr(err)
|
||||||
}
|
}
|
||||||
|
@ -156,7 +140,7 @@ type putStreamBasicChecker struct {
|
||||||
|
|
||||||
func (p *putStreamBasicChecker) Send(ctx context.Context, request *objectV2.PutRequest) error {
|
func (p *putStreamBasicChecker) Send(ctx context.Context, request *objectV2.PutRequest) error {
|
||||||
if partInit, ok := request.GetBody().GetObjectPart().(*objectV2.PutObjectPartInit); ok {
|
if partInit, ok := request.GetBody().GetObjectPart().(*objectV2.PutObjectPartInit); ok {
|
||||||
reqCtx, err := requestContext(ctx)
|
reqCtx, err := objectSvc.FromRequestContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return toStatusErr(err)
|
return toStatusErr(err)
|
||||||
}
|
}
|
||||||
|
@ -205,7 +189,7 @@ func (c *Service) Head(ctx context.Context, request *objectV2.HeadRequest) (*obj
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
reqCtx, err := requestContext(ctx)
|
reqCtx, err := objectSvc.FromRequestContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -219,6 +203,7 @@ func (c *Service) Head(ctx context.Context, request *objectV2.HeadRequest) (*obj
|
||||||
SenderKey: hex.EncodeToString(reqCtx.SenderKey),
|
SenderKey: hex.EncodeToString(reqCtx.SenderKey),
|
||||||
ContainerOwner: reqCtx.ContainerOwner,
|
ContainerOwner: reqCtx.ContainerOwner,
|
||||||
SoftAPECheck: reqCtx.SoftAPECheck,
|
SoftAPECheck: reqCtx.SoftAPECheck,
|
||||||
|
Header: reqCtx.Header.Load().ToV2().GetHeader(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, toStatusErr(err)
|
return nil, toStatusErr(err)
|
||||||
|
@ -273,7 +258,7 @@ func (c *Service) Search(request *objectV2.SearchRequest, stream objectSvc.Searc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reqCtx, err := requestContext(stream.Context())
|
reqCtx, err := objectSvc.FromRequestContext(stream.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return toStatusErr(err)
|
return toStatusErr(err)
|
||||||
}
|
}
|
||||||
|
@ -300,7 +285,7 @@ func (c *Service) Delete(ctx context.Context, request *objectV2.DeleteRequest) (
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
reqCtx, err := requestContext(ctx)
|
reqCtx, err := objectSvc.FromRequestContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -333,7 +318,7 @@ func (c *Service) GetRange(request *objectV2.GetRangeRequest, stream objectSvc.G
|
||||||
return toStatusErr(err)
|
return toStatusErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
reqCtx, err := requestContext(stream.Context())
|
reqCtx, err := objectSvc.FromRequestContext(stream.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return toStatusErr(err)
|
return toStatusErr(err)
|
||||||
}
|
}
|
||||||
|
@ -361,7 +346,7 @@ func (c *Service) GetRangeHash(ctx context.Context, request *objectV2.GetRangeHa
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
reqCtx, err := requestContext(ctx)
|
reqCtx, err := objectSvc.FromRequestContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -398,7 +383,7 @@ func (c *Service) PutSingle(ctx context.Context, request *objectV2.PutSingleRequ
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
reqCtx, err := requestContext(ctx)
|
reqCtx, err := objectSvc.FromRequestContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/internal/logs"
|
"git.frostfs.info/TrueCloudLab/frostfs-node/internal/logs"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
|
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
|
||||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||||
objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||||
|
@ -50,6 +51,12 @@ func (r *request) executeLocal(ctx context.Context) {
|
||||||
|
|
||||||
func (r *request) get(ctx context.Context) (*objectSDK.Object, error) {
|
func (r *request) get(ctx context.Context) (*objectSDK.Object, error) {
|
||||||
if r.headOnly() {
|
if r.headOnly() {
|
||||||
|
reqCtx, _ := object.FromRequestContext(ctx)
|
||||||
|
if reqCtx != nil && !r.isRaw() {
|
||||||
|
if hdr := reqCtx.GetHeader(r.address()); hdr != nil {
|
||||||
|
return hdr, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
return r.localStorage.Head(ctx, r.address(), r.isRaw())
|
return r.localStorage.Head(ctx, r.address(), r.isRaw())
|
||||||
}
|
}
|
||||||
if rng := r.ctxRange(); rng != nil {
|
if rng := r.ctxRange(); rng != nil {
|
||||||
|
|
|
@ -1,13 +1,20 @@
|
||||||
package object
|
package object
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/object"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
|
||||||
|
objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||||
|
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RequestContextKeyT struct{}
|
type requestContextKeyT struct{}
|
||||||
|
|
||||||
var RequestContextKey = RequestContextKeyT{}
|
var requestContextKey = requestContextKeyT{}
|
||||||
|
|
||||||
// RequestContext is a context passed between middleware handlers.
|
// RequestContext is a context passed between middleware handlers.
|
||||||
type RequestContext struct {
|
type RequestContext struct {
|
||||||
|
@ -20,4 +27,38 @@ type RequestContext struct {
|
||||||
Role acl.Role
|
Role acl.Role
|
||||||
|
|
||||||
SoftAPECheck bool
|
SoftAPECheck bool
|
||||||
|
|
||||||
|
Header atomic.Pointer[objectSDK.Object]
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRequestContext returns a copy of ctx which carries value.
|
||||||
|
func NewRequestContext(ctx context.Context, value *RequestContext) context.Context {
|
||||||
|
return context.WithValue(ctx, requestContextKey, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromRequestContext returns RequestContext value stored in ctx if any.
|
||||||
|
func FromRequestContext(ctx context.Context) (*RequestContext, error) {
|
||||||
|
reqCtx, ok := ctx.Value(requestContextKey).(*RequestContext)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("no key %s in context", requestContextKey)
|
||||||
|
}
|
||||||
|
return reqCtx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHeader returns header if it is present and matches cid + oid pair.
|
||||||
|
func (r *RequestContext) GetHeader(addr oid.Address) *objectSDK.Object {
|
||||||
|
if r == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hdr := r.Header.Load()
|
||||||
|
if hdr == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
storedAddr := object.AddressOf(hdr)
|
||||||
|
if addr.Equals(storedAddr) {
|
||||||
|
return hdr
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue