github.com/iotexproject/iotex-core@v1.14.1-rc1/ioctl/client.go (about)

     1  // Copyright (c) 2022 IoTeX Foundation
     2  // This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability
     3  // or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed.
     4  // This source code is governed by Apache License 2.0 that can be found in the LICENSE file.
     5  
     6  package ioctl
     7  
     8  import (
     9  	"bufio"
    10  	"bytes"
    11  	"context"
    12  	"crypto/ecdsa"
    13  	"crypto/tls"
    14  	"encoding/json"
    15  	"fmt"
    16  	"net/http"
    17  	"os"
    18  	"os/exec"
    19  	"path/filepath"
    20  	"regexp"
    21  	"strings"
    22  
    23  	"github.com/ethereum/go-ethereum/accounts/keystore"
    24  	"github.com/iotexproject/iotex-proto/golang/iotexapi"
    25  	"github.com/pkg/errors"
    26  	"google.golang.org/grpc"
    27  	"google.golang.org/grpc/credentials"
    28  	"gopkg.in/yaml.v2"
    29  
    30  	"github.com/iotexproject/iotex-core/ioctl/config"
    31  	"github.com/iotexproject/iotex-core/ioctl/util"
    32  	"github.com/iotexproject/iotex-core/ioctl/validator"
    33  	"github.com/iotexproject/iotex-core/pkg/util/fileutil"
    34  )
    35  
    36  const (
    37  	_urlPattern = `[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)`
    38  )
    39  
    40  var (
    41  	//ErrInvalidEndpointOrInsecure represents that endpoint or insecure is invalid
    42  	ErrInvalidEndpointOrInsecure = errors.New("check endpoint or secureConnect in ~/.config/ioctl/default/config.default or cmd flag value if has")
    43  )
    44  
    45  type (
    46  	// Client defines the interface of an ioctl client
    47  	Client interface {
    48  		// Start starts the client
    49  		Start(context.Context) error
    50  		// Stop stops the client
    51  		Stop(context.Context) error
    52  		// Config returns the config of the client
    53  		Config() config.Config
    54  		// ConfigFilePath returns the file path of the config
    55  		ConfigFilePath() string
    56  		// SetEndpointWithFlag receives input flag value
    57  		SetEndpointWithFlag(func(*string, string, string, string))
    58  		// SetInsecureWithFlag receives input flag value
    59  		SetInsecureWithFlag(func(*bool, string, bool, string))
    60  		// APIServiceClient returns an API service client
    61  		APIServiceClient() (iotexapi.APIServiceClient, error)
    62  		// SelectTranslation select a translation based on UILanguage
    63  		SelectTranslation(map[config.Language]string) (string, config.Language)
    64  		// ReadCustomLink scans a custom link from terminal and validates it.
    65  		ReadCustomLink() (string, error)
    66  		// AskToConfirm asks user to confirm from terminal, true to continue
    67  		AskToConfirm(string) (bool, error)
    68  		// ReadSecret reads password from terminal
    69  		ReadSecret() (string, error)
    70  		// Execute a bash command
    71  		Execute(string) error
    72  		// AddressWithDefaultIfNotExist returns default address if input empty
    73  		AddressWithDefaultIfNotExist(in string) (string, error)
    74  		// Address returns address if input address|alias
    75  		Address(in string) (string, error)
    76  		// NewKeyStore creates a keystore by default walletdir
    77  		NewKeyStore() *keystore.KeyStore
    78  		// DecryptPrivateKey returns privateKey from a json blob
    79  		DecryptPrivateKey(string, string) (*ecdsa.PrivateKey, error)
    80  		// AliasMap returns the alias map: accountAddr-aliasName
    81  		AliasMap() map[string]string
    82  		// Alias returns the alias corresponding to address
    83  		Alias(string) (string, error)
    84  		// SetAlias updates aliasname and account address and not write them into the default config file
    85  		SetAlias(string, string)
    86  		// SetAliasAndSave updates aliasname and account address and write them into the default config file
    87  		SetAliasAndSave(string, string) error
    88  		// DeleteAlias delete alias from the default config file
    89  		DeleteAlias(string) error
    90  		// WriteConfig write config datas to the default config file
    91  		WriteConfig() error
    92  		// IsCryptoSm2 return true if use sm2 cryptographic algorithm, false if not use
    93  		IsCryptoSm2() bool
    94  		// QueryAnalyser sends request to Analyser endpoint
    95  		QueryAnalyser(interface{}) (*http.Response, error)
    96  		// ReadInput reads the input from stdin
    97  		ReadInput() (string, error)
    98  		// HdwalletMnemonic returns the mnemonic of hdwallet
    99  		HdwalletMnemonic(string) (string, error)
   100  		// WriteHdWalletConfigFile writes encrypting mnemonic into config file
   101  		WriteHdWalletConfigFile(string, string) error
   102  		// RemoveHdWalletConfigFile removes hdwalletConfigFile
   103  		RemoveHdWalletConfigFile() error
   104  		// IsHdWalletConfigFileExist return true if config file is existed, false if not existed
   105  		IsHdWalletConfigFileExist() bool
   106  		// Insecure returns the insecure connect option of grpc dial, default is false
   107  		Insecure() bool
   108  	}
   109  
   110  	client struct {
   111  		cfg                config.Config
   112  		conn               *grpc.ClientConn
   113  		cryptoSm2          bool
   114  		configFilePath     string
   115  		endpoint           string
   116  		insecure           bool
   117  		hdWalletConfigFile string
   118  	}
   119  
   120  	// Option sets client construction parameter
   121  	Option func(*client)
   122  
   123  	// ConfirmationMessage is the struct of an Confirmation output
   124  	ConfirmationMessage struct {
   125  		Info    string   `json:"info"`
   126  		Options []string `json:"options"`
   127  	}
   128  )
   129  
   130  // EnableCryptoSm2 enables to use sm2 cryptographic algorithm
   131  func EnableCryptoSm2() Option {
   132  	return func(c *client) {
   133  		c.cryptoSm2 = true
   134  	}
   135  }
   136  
   137  // NewClient creates a new ioctl client
   138  func NewClient(cfg config.Config, configFilePath string, opts ...Option) Client {
   139  	c := &client{
   140  		cfg:                cfg,
   141  		configFilePath:     configFilePath,
   142  		hdWalletConfigFile: cfg.Wallet + "/hdwallet",
   143  	}
   144  	for _, opt := range opts {
   145  		opt(c)
   146  	}
   147  	return c
   148  }
   149  
   150  func (c *client) Start(context.Context) error {
   151  	return nil
   152  }
   153  
   154  func (c *client) Stop(context.Context) error {
   155  	if c.conn != nil {
   156  		if err := c.conn.Close(); err != nil {
   157  			return err
   158  		}
   159  		c.conn = nil
   160  	}
   161  	return nil
   162  }
   163  
   164  func (c *client) Config() config.Config {
   165  	return c.cfg
   166  }
   167  
   168  // ConfigFilePath returns the file path for the config.
   169  func (c *client) ConfigFilePath() string {
   170  	return c.configFilePath
   171  }
   172  
   173  func (c *client) SetEndpointWithFlag(cb func(*string, string, string, string)) {
   174  	usage, _ := c.SelectTranslation(map[config.Language]string{
   175  		config.English: "set endpoint for once",
   176  		config.Chinese: "一次设置端点",
   177  	})
   178  	cb(&c.endpoint, "endpoint", c.cfg.Endpoint, usage)
   179  }
   180  
   181  func (c *client) SetInsecureWithFlag(cb func(*bool, string, bool, string)) {
   182  	usage, _ := c.SelectTranslation(map[config.Language]string{
   183  		config.English: "insecure connection for once",
   184  		config.Chinese: "一次不安全连接",
   185  	})
   186  	cb(&c.insecure, "insecure", !c.cfg.SecureConnect, usage)
   187  }
   188  
   189  func (c *client) AskToConfirm(info string) (bool, error) {
   190  	message := ConfirmationMessage{Info: info, Options: []string{"yes"}}
   191  	fmt.Println(message.String())
   192  	var confirm string
   193  	if _, err := fmt.Scanf("%s", &confirm); err != nil {
   194  		return false, err
   195  	}
   196  	return strings.EqualFold(confirm, "yes"), nil
   197  }
   198  
   199  func (c *client) ReadCustomLink() (string, error) { // notest
   200  	var link string
   201  	if _, err := fmt.Scanln(&link); err != nil {
   202  		return "", err
   203  	}
   204  
   205  	match, err := regexp.MatchString(_urlPattern, link)
   206  	if err != nil {
   207  		return "", errors.Wrapf(err, "failed to validate link %s", link)
   208  	}
   209  	if match {
   210  		return link, nil
   211  	}
   212  	return "", errors.Errorf("link is not a valid url %s", link)
   213  }
   214  
   215  func (c *client) SelectTranslation(trls map[config.Language]string) (string, config.Language) {
   216  	trl, ok := trls[c.cfg.Lang()]
   217  	if ok {
   218  		return trl, c.cfg.Lang()
   219  	}
   220  
   221  	trl, ok = trls[config.English]
   222  	if !ok {
   223  		panic("failed to pick a translation")
   224  	}
   225  	return trl, config.English
   226  }
   227  
   228  func (c *client) ReadSecret() (string, error) {
   229  	// TODO: delete util.ReadSecretFromStdin, and move code to here
   230  	return util.ReadSecretFromStdin()
   231  }
   232  
   233  func (c *client) APIServiceClient() (iotexapi.APIServiceClient, error) {
   234  	if c.conn != nil {
   235  		if err := c.conn.Close(); err != nil {
   236  			return nil, err
   237  		}
   238  	}
   239  
   240  	if c.endpoint == "" {
   241  		return nil, errors.New(`use "ioctl config set endpoint" to config endpoint first`)
   242  	}
   243  
   244  	var err error
   245  	if c.insecure {
   246  		c.conn, err = grpc.Dial(c.endpoint, grpc.WithInsecure())
   247  	} else {
   248  		c.conn, err = grpc.Dial(c.endpoint, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS12})))
   249  	}
   250  	if err != nil {
   251  		return nil, err
   252  	}
   253  	return iotexapi.NewAPIServiceClient(c.conn), nil
   254  }
   255  
   256  func (c *client) Execute(cmd string) error {
   257  	return exec.Command("bash", "-c", cmd).Run()
   258  }
   259  
   260  func (c *client) AddressWithDefaultIfNotExist(in string) (string, error) {
   261  	var address string
   262  	if !strings.EqualFold(in, "") {
   263  		address = in
   264  	} else {
   265  		if strings.EqualFold(c.cfg.DefaultAccount.AddressOrAlias, "") {
   266  			return "", errors.New(`use "ioctl config set defaultacc ADDRESS|ALIAS" to config default account first`)
   267  		}
   268  		address = c.cfg.DefaultAccount.AddressOrAlias
   269  	}
   270  	return c.Address(address)
   271  }
   272  
   273  func (c *client) Address(in string) (string, error) {
   274  	if len(in) >= validator.IoAddrLen {
   275  		if err := validator.ValidateAddress(in); err != nil {
   276  			return "", err
   277  		}
   278  		return in, nil
   279  	}
   280  	addr, ok := c.cfg.Aliases[in]
   281  	if ok {
   282  		return addr, nil
   283  	}
   284  	return "", errors.New("cannot find address from " + in)
   285  }
   286  
   287  func (c *client) NewKeyStore() *keystore.KeyStore {
   288  	return keystore.NewKeyStore(c.cfg.Wallet, keystore.StandardScryptN, keystore.StandardScryptP)
   289  }
   290  
   291  func (c *client) DecryptPrivateKey(passwordOfKeyStore, keyStorePath string) (*ecdsa.PrivateKey, error) {
   292  	keyJSON, err := os.ReadFile(filepath.Clean(keyStorePath))
   293  	if err != nil {
   294  		return nil, fmt.Errorf("keystore file \"%s\" read error", keyStorePath)
   295  	}
   296  
   297  	key, err := keystore.DecryptKey(keyJSON, passwordOfKeyStore)
   298  	if err != nil {
   299  		return nil, errors.Wrap(err, "failed to decrypt key")
   300  	}
   301  	if key != nil && key.PrivateKey != nil {
   302  		// clear private key in memory prevent from attack
   303  		defer func(k *ecdsa.PrivateKey) {
   304  			b := k.D.Bits()
   305  			for i := range b {
   306  				b[i] = 0
   307  			}
   308  		}(key.PrivateKey)
   309  	}
   310  	return key.PrivateKey, nil
   311  }
   312  
   313  func (c *client) AliasMap() map[string]string {
   314  	aliases := make(map[string]string)
   315  	for name, addr := range c.cfg.Aliases {
   316  		aliases[addr] = name
   317  	}
   318  	return aliases
   319  }
   320  
   321  func (c *client) Alias(address string) (string, error) {
   322  	if err := validator.ValidateAddress(address); err != nil {
   323  		return "", err
   324  	}
   325  	for aliasName, addr := range c.cfg.Aliases {
   326  		if addr == address {
   327  			return aliasName, nil
   328  		}
   329  	}
   330  	return "", errors.New("no alias is found")
   331  }
   332  
   333  func (c *client) SetAlias(aliasName string, addr string) {
   334  	for k, v := range c.cfg.Aliases {
   335  		if v == addr {
   336  			delete(c.cfg.Aliases, k)
   337  		}
   338  	}
   339  	c.cfg.Aliases[aliasName] = addr
   340  }
   341  
   342  func (c *client) SetAliasAndSave(aliasName string, addr string) error {
   343  	c.SetAlias(aliasName, addr)
   344  	return c.WriteConfig()
   345  }
   346  
   347  func (c *client) DeleteAlias(aliasName string) error {
   348  	delete(c.cfg.Aliases, aliasName)
   349  	return c.WriteConfig()
   350  }
   351  
   352  func (c *client) WriteConfig() error {
   353  	out, err := yaml.Marshal(&c.cfg)
   354  	if err != nil {
   355  		return errors.Wrapf(err, "failed to marshal config to config file %s", c.configFilePath)
   356  	}
   357  	if err = os.WriteFile(c.configFilePath, out, 0600); err != nil {
   358  		return errors.Wrapf(err, "failed to write to config file %s", c.configFilePath)
   359  	}
   360  	return nil
   361  }
   362  
   363  func (c *client) IsCryptoSm2() bool {
   364  	return c.cryptoSm2
   365  }
   366  
   367  func (c *client) QueryAnalyser(reqData interface{}) (*http.Response, error) {
   368  	jsonData, err := json.Marshal(reqData)
   369  	if err != nil {
   370  		return nil, errors.Wrap(err, "failed to pack in json")
   371  	}
   372  	resp, err := http.Post(c.cfg.AnalyserEndpoint+"/api.ActionsService.GetActionsByAddress", "application/json",
   373  		bytes.NewBuffer(jsonData))
   374  	if err != nil {
   375  		return nil, errors.Wrap(err, "failed to send request")
   376  	}
   377  	return resp, nil
   378  }
   379  
   380  func (c *client) ReadInput() (string, error) { // notest
   381  	in := bufio.NewReader(os.Stdin)
   382  	line, err := in.ReadString('\n')
   383  	if err != nil {
   384  		return "", err
   385  	}
   386  	return line, nil
   387  }
   388  
   389  func (c *client) HdwalletMnemonic(password string) (string, error) {
   390  	// derive key as "m/44'/304'/account'/change/index"
   391  	if !c.IsHdWalletConfigFileExist() {
   392  		return "", errors.New("run 'ioctl hdwallet create' to create your HDWallet first")
   393  	}
   394  	enctxt, err := os.ReadFile(c.hdWalletConfigFile)
   395  	if err != nil {
   396  		return "", errors.Wrapf(err, "failed to read config file %s", c.hdWalletConfigFile)
   397  	}
   398  
   399  	enckey := util.HashSHA256([]byte(password))
   400  	dectxt, err := util.Decrypt(enctxt, enckey)
   401  	if err != nil {
   402  		return "", errors.Wrap(err, "failed to decrypt")
   403  	}
   404  
   405  	dectxtLen := len(dectxt)
   406  	if dectxtLen <= 32 {
   407  		return "", errors.Errorf("incorrect data dectxtLen %d", dectxtLen)
   408  	}
   409  	mnemonic, hash := dectxt[:dectxtLen-32], dectxt[dectxtLen-32:]
   410  	if !bytes.Equal(hash, util.HashSHA256(mnemonic)) {
   411  		return "", errors.New("password error")
   412  	}
   413  	return string(mnemonic), nil
   414  }
   415  
   416  func (c *client) WriteHdWalletConfigFile(mnemonic string, password string) error {
   417  	enctxt := append([]byte(mnemonic), util.HashSHA256([]byte(mnemonic))...)
   418  	enckey := util.HashSHA256([]byte(password))
   419  	out, err := util.Encrypt(enctxt, enckey)
   420  	if err != nil {
   421  		return errors.Wrap(err, "failed to encrypting mnemonic")
   422  	}
   423  	if err := os.WriteFile(c.hdWalletConfigFile, out, 0600); err != nil {
   424  		return errors.Wrapf(err, "failed to write to config file %s", c.hdWalletConfigFile)
   425  	}
   426  	return nil
   427  }
   428  
   429  func (c *client) RemoveHdWalletConfigFile() error {
   430  	return os.Remove(c.hdWalletConfigFile)
   431  }
   432  
   433  func (c *client) IsHdWalletConfigFileExist() bool { // notest
   434  	return fileutil.FileExists(c.hdWalletConfigFile)
   435  }
   436  
   437  // Insecure returns the insecure connect option of grpc dial, default is false
   438  func (c *client) Insecure() bool {
   439  	return c.insecure
   440  }
   441  
   442  func (m *ConfirmationMessage) String() string {
   443  	line := fmt.Sprintf("%s\nOptions:", m.Info)
   444  	for _, option := range m.Options {
   445  		line += " " + option
   446  	}
   447  	line += "\nQuit for anything else."
   448  	return line
   449  }