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 }