github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/pkg/services/oracle/neofs/neofs.go (about)

     1  package neofs
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net/url"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
    14  	"github.com/nspcc-dev/neo-go/pkg/util"
    15  	"github.com/nspcc-dev/neofs-sdk-go/client"
    16  	cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
    17  	"github.com/nspcc-dev/neofs-sdk-go/object"
    18  	oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
    19  	"github.com/nspcc-dev/neofs-sdk-go/user"
    20  )
    21  
    22  const (
    23  	// URIScheme is the name of neofs URI scheme.
    24  	URIScheme = "neofs"
    25  
    26  	// rangeSep is a separator between offset and length.
    27  	rangeSep = '|'
    28  
    29  	rangeCmd  = "range"
    30  	headerCmd = "header"
    31  	hashCmd   = "hash"
    32  )
    33  
    34  // Various validation errors.
    35  var (
    36  	ErrInvalidScheme    = errors.New("invalid URI scheme")
    37  	ErrMissingObject    = errors.New("object ID is missing from URI")
    38  	ErrInvalidContainer = errors.New("container ID is invalid")
    39  	ErrInvalidObject    = errors.New("object ID is invalid")
    40  	ErrInvalidRange     = errors.New("object range is invalid (expected 'Offset|Length')")
    41  	ErrInvalidCommand   = errors.New("invalid command")
    42  )
    43  
    44  // Get returns a neofs object from the provided url.
    45  // URI scheme is "neofs:<Container-ID>/<Object-ID/<Command>/<Params>".
    46  // If Command is not provided, full object is requested.
    47  func Get(ctx context.Context, priv *keys.PrivateKey, u *url.URL, addr string) (io.ReadCloser, error) {
    48  	objectAddr, ps, err := parseNeoFSURL(u)
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  
    53  	c, err := client.New(client.PrmInit{})
    54  	if err != nil {
    55  		return nil, fmt.Errorf("failed to create client: %w", err)
    56  	}
    57  
    58  	var (
    59  		res  = clientCloseWrapper{c: c}
    60  		prmd client.PrmDial
    61  	)
    62  	prmd.SetServerURI(addr)
    63  	prmd.SetContext(ctx)
    64  	err = c.Dial(prmd) //nolint:contextcheck // contextcheck: Function `Dial->Balance->SendUnary->Init->setNeoFSAPIServer` should pass the context parameter
    65  	if err != nil {
    66  		return res, err
    67  	}
    68  
    69  	var s = user.NewAutoIDSignerRFC6979(priv.PrivateKey)
    70  	switch {
    71  	case len(ps) == 0 || ps[0] == "": // Get request
    72  		res.ReadCloser, err = getPayload(ctx, s, c, objectAddr)
    73  	case ps[0] == rangeCmd:
    74  		res.ReadCloser, err = getRange(ctx, s, c, objectAddr, ps[1:]...)
    75  	case ps[0] == headerCmd:
    76  		res.ReadCloser, err = getHeader(ctx, s, c, objectAddr)
    77  	case ps[0] == hashCmd:
    78  		res.ReadCloser, err = getHash(ctx, s, c, objectAddr, ps[1:]...)
    79  	default:
    80  		err = ErrInvalidCommand
    81  	}
    82  	return res, err
    83  }
    84  
    85  type clientCloseWrapper struct {
    86  	io.ReadCloser
    87  	c *client.Client
    88  }
    89  
    90  func (w clientCloseWrapper) Close() error {
    91  	var res error
    92  	if w.ReadCloser != nil {
    93  		res = w.ReadCloser.Close()
    94  	}
    95  	w.c.Close()
    96  	return res
    97  }
    98  
    99  // parseNeoFSURL returns parsed neofs address.
   100  func parseNeoFSURL(u *url.URL) (*oid.Address, []string, error) {
   101  	if u.Scheme != URIScheme {
   102  		return nil, nil, ErrInvalidScheme
   103  	}
   104  
   105  	ps := strings.Split(u.Opaque, "/")
   106  	if len(ps) < 2 {
   107  		return nil, nil, ErrMissingObject
   108  	}
   109  
   110  	var containerID cid.ID
   111  	if err := containerID.DecodeString(ps[0]); err != nil {
   112  		return nil, nil, fmt.Errorf("%w: %w", ErrInvalidContainer, err)
   113  	}
   114  
   115  	var objectID oid.ID
   116  	if err := objectID.DecodeString(ps[1]); err != nil {
   117  		return nil, nil, fmt.Errorf("%w: %w", ErrInvalidObject, err)
   118  	}
   119  	var objAddr = new(oid.Address)
   120  	objAddr.SetContainer(containerID)
   121  	objAddr.SetObject(objectID)
   122  	return objAddr, ps[2:], nil
   123  }
   124  
   125  func getPayload(ctx context.Context, s user.Signer, c *client.Client, addr *oid.Address) (io.ReadCloser, error) {
   126  	var iorc io.ReadCloser
   127  	_, rc, err := c.ObjectGetInit(ctx, addr.Container(), addr.Object(), s, client.PrmObjectGet{})
   128  	if rc != nil {
   129  		iorc = rc
   130  	}
   131  	return iorc, err
   132  }
   133  
   134  func getRange(ctx context.Context, s user.Signer, c *client.Client, addr *oid.Address, ps ...string) (io.ReadCloser, error) {
   135  	var iorc io.ReadCloser
   136  	if len(ps) == 0 {
   137  		return nil, ErrInvalidRange
   138  	}
   139  	r, err := parseRange(ps[0])
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  
   144  	rc, err := c.ObjectRangeInit(ctx, addr.Container(), addr.Object(), r.GetOffset(), r.GetLength(), s, client.PrmObjectRange{})
   145  	if rc != nil {
   146  		iorc = rc
   147  	}
   148  	return iorc, err
   149  }
   150  
   151  func getObjHeader(ctx context.Context, s user.Signer, c *client.Client, addr *oid.Address) (*object.Object, error) {
   152  	return c.ObjectHead(ctx, addr.Container(), addr.Object(), s, client.PrmObjectHead{})
   153  }
   154  
   155  func getHeader(ctx context.Context, s user.Signer, c *client.Client, addr *oid.Address) (io.ReadCloser, error) {
   156  	obj, err := getObjHeader(ctx, s, c, addr)
   157  	if err != nil {
   158  		return nil, err
   159  	}
   160  	res, err := obj.MarshalHeaderJSON()
   161  	if err != nil {
   162  		return nil, err
   163  	}
   164  	return io.NopCloser(bytes.NewReader(res)), nil
   165  }
   166  
   167  func getHash(ctx context.Context, s user.Signer, c *client.Client, addr *oid.Address, ps ...string) (io.ReadCloser, error) {
   168  	if len(ps) == 0 || ps[0] == "" { // hash of the full payload
   169  		obj, err := getObjHeader(ctx, s, c, addr)
   170  		if err != nil {
   171  			return nil, err
   172  		}
   173  		sum, flag := obj.PayloadChecksum()
   174  		if !flag {
   175  			return nil, errors.New("missing checksum in the reply")
   176  		}
   177  		return io.NopCloser(bytes.NewReader(sum.Value())), nil
   178  	}
   179  	r, err := parseRange(ps[0])
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  	var hashPrm client.PrmObjectHash
   184  	hashPrm.SetRangeList(r.GetOffset(), r.GetLength())
   185  
   186  	hashes, err := c.ObjectHash(ctx, addr.Container(), addr.Object(), s, hashPrm)
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  	if len(hashes) == 0 {
   191  		return nil, fmt.Errorf("%w: empty response", ErrInvalidRange)
   192  	}
   193  	u256, err := util.Uint256DecodeBytesBE(hashes[0])
   194  	if err != nil {
   195  		return nil, fmt.Errorf("decode Uint256: %w", err)
   196  	}
   197  	res, err := u256.MarshalJSON()
   198  	if err != nil {
   199  		return nil, err
   200  	}
   201  	return io.NopCloser(bytes.NewReader(res)), nil
   202  }
   203  
   204  func parseRange(s string) (*object.Range, error) {
   205  	sepIndex := strings.IndexByte(s, rangeSep)
   206  	if sepIndex < 0 {
   207  		return nil, ErrInvalidRange
   208  	}
   209  	offset, err := strconv.ParseUint(s[:sepIndex], 10, 64)
   210  	if err != nil {
   211  		return nil, fmt.Errorf("%w: invalid offset", ErrInvalidRange)
   212  	}
   213  	length, err := strconv.ParseUint(s[sepIndex+1:], 10, 64)
   214  	if err != nil {
   215  		return nil, fmt.Errorf("%w: invalid length", ErrInvalidRange)
   216  	}
   217  
   218  	r := object.NewRange()
   219  	r.SetOffset(offset)
   220  	r.SetLength(length)
   221  	return r, nil
   222  }