diff --git a/cli/executor_test.go b/cli/executor_test.go index 6f39abb81..7274e1301 100644 --- a/cli/executor_test.go +++ b/cli/executor_test.go @@ -1,7 +1,6 @@ package main import ( - "bufio" "bytes" "errors" "io" @@ -27,7 +26,7 @@ import ( "github.com/urfave/cli" "go.uber.org/zap" "go.uber.org/zap/zaptest" - "golang.org/x/crypto/ssh/terminal" + "golang.org/x/term" ) const ( @@ -100,9 +99,6 @@ func newExecutorWithConfig(t *testing.T, needChain bool, f func(*config.Config)) } e.CLI.Writer = e.Out e.CLI.ErrWriter = e.Err - rw := bufio.NewReadWriter(bufio.NewReader(e.In), bufio.NewWriter(ioutil.Discard)) - require.Nil(t, input.Terminal) // check that tests clean up properly - input.Terminal = terminal.NewTerminal(rw, "") if needChain { e.Chain, e.RPC, e.NetSrv = newTestChain(t, f) } @@ -191,7 +187,12 @@ func (e *executor) Run(t *testing.T, args ...string) { func (e *executor) run(args ...string) error { e.Out.Reset() e.Err.Reset() + input.Terminal = term.NewTerminal(input.ReadWriter{ + Reader: e.In, + Writer: ioutil.Discard, + }, "") err := e.CLI.Run(args) + input.Terminal = nil e.In.Reset() return err } diff --git a/cli/input/input.go b/cli/input/input.go index 1a15f541e..fa61dee10 100644 --- a/cli/input/input.go +++ b/cli/input/input.go @@ -1,44 +1,57 @@ package input import ( - "bufio" - "fmt" "io" "os" - "strings" "syscall" - "golang.org/x/crypto/ssh/terminal" + "golang.org/x/term" ) // Terminal is a terminal used for input. If `nil`, stdin is used. -var Terminal *terminal.Terminal +var Terminal *term.Terminal -// ReadLine reads line from the input without trailing '\n' -func ReadLine(w io.Writer, prompt string) (string, error) { - if Terminal != nil { - _, err := Terminal.Write([]byte(prompt)) - if err != nil { - return "", err - } - raw, err := Terminal.ReadLine() - return strings.TrimRight(raw, "\n"), err - } - fmt.Fprint(w, prompt) - buf := bufio.NewReader(os.Stdin) - return buf.ReadString('\n') +// ReadWriter combiner reader and writer. +type ReadWriter struct { + io.Reader + io.Writer } -// ReadPassword reads user password with prompt. -func ReadPassword(w io.Writer, prompt string) (string, error) { - if Terminal != nil { - return Terminal.ReadPassword(prompt) +// ReadLine reads line from the input without trailing '\n' +func ReadLine(prompt string) (string, error) { + trm := Terminal + if trm == nil { + s, err := term.MakeRaw(syscall.Stdin) + if err != nil { + panic(err) + } + defer term.Restore(syscall.Stdin, s) + trm = term.NewTerminal(ReadWriter{ + Reader: os.Stdin, + Writer: os.Stdout, + }, "") } - fmt.Fprint(w, prompt) - rawPass, err := terminal.ReadPassword(syscall.Stdin) + return readLine(trm, prompt) +} + +func readLine(trm *term.Terminal, prompt string) (string, error) { + _, err := trm.Write([]byte(prompt)) if err != nil { return "", err } - fmt.Fprintln(w) - return strings.TrimRight(string(rawPass), "\n"), nil + return trm.ReadLine() +} + +// ReadPassword reads user password with prompt. +func ReadPassword(prompt string) (string, error) { + trm := Terminal + if trm == nil { + s, err := term.MakeRaw(syscall.Stdin) + if err != nil { + panic(err) + } + defer term.Restore(syscall.Stdin, s) + trm = term.NewTerminal(ReadWriter{os.Stdin, os.Stdout}, prompt) + } + return trm.ReadPassword(prompt) } diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 790fb9ec1..f7ea2fed2 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -770,7 +770,7 @@ func getAccFromContext(ctx *cli.Context) (*wallet.Account, error) { return nil, cli.NewExitError(fmt.Errorf("wallet contains no account for '%s'", address.Uint160ToString(addr)), 1) } - rawPass, err := input.ReadPassword(ctx.App.Writer, + rawPass, err := input.ReadPassword( fmt.Sprintf("Enter account %s password > ", address.Uint160ToString(addr))) if err != nil { return nil, cli.NewExitError(err, 1) diff --git a/cli/vm/vm.go b/cli/vm/vm.go index 5ea7c68a3..3bb725828 100644 --- a/cli/vm/vm.go +++ b/cli/vm/vm.go @@ -1,6 +1,9 @@ package vm import ( + "os" + + "github.com/abiosoft/readline" vmcli "github.com/nspcc-dev/neo-go/pkg/vm/cli" "github.com/urfave/cli" ) @@ -18,6 +21,9 @@ func NewCommands() []cli.Command { } func startVMPrompt(ctx *cli.Context) error { - p := vmcli.New() + p := vmcli.NewWithConfig(true, os.Exit, &readline.Config{ + Stdout: ctx.App.Writer, + Stderr: ctx.App.ErrWriter, + }) return p.Run() } diff --git a/cli/wallet/validator.go b/cli/wallet/validator.go index 50b09d38f..40e4e351f 100644 --- a/cli/wallet/validator.go +++ b/cli/wallet/validator.go @@ -197,7 +197,7 @@ func getDecryptedAccount(ctx *cli.Context, wall *wallet.Wallet, addr util.Uint16 return nil, fmt.Errorf("can't find account for the address: %s", address.Uint160ToString(addr)) } - if pass, err := input.ReadPassword(ctx.App.Writer, "Password > "); err != nil { + if pass, err := input.ReadPassword("Password > "); err != nil { fmt.Println("ERROR", pass, err) return nil, err } else if err := acc.Decrypt(pass); err != nil { diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index 915321559..204b050da 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -261,7 +261,7 @@ func convertWallet(ctx *cli.Context) error { defer newWallet.Close() for _, acc := range wall.Accounts { - pass, err := input.ReadPassword(ctx.App.Writer, fmt.Sprintf("Enter passphrase for account %s (label '%s') > ", acc.Address, acc.Label)) + pass, err := input.ReadPassword(fmt.Sprintf("Enter passphrase for account %s (label '%s') > ", acc.Address, acc.Label)) if err != nil { return cli.NewExitError(err, 1) } @@ -331,7 +331,7 @@ loop: for _, wif := range wifs { if decrypt { - pass, err := input.ReadPassword(ctx.App.Writer, "Enter password > ") + pass, err := input.ReadPassword("Enter password > ") if err != nil { return cli.NewExitError(err, 1) } @@ -505,7 +505,7 @@ func removeAccount(ctx *cli.Context) error { } func askForConsent(w io.Writer) bool { - response, err := input.ReadLine(w, "Are you sure? [y/N]: ") + response, err := input.ReadLine("Are you sure? [y/N]: ") if err == nil { response = strings.ToLower(strings.TrimSpace(response)) if response == "y" || response == "yes" { @@ -522,7 +522,7 @@ func dumpWallet(ctx *cli.Context) error { return cli.NewExitError(err, 1) } if ctx.Bool("decrypt") { - pass, err := input.ReadPassword(ctx.App.Writer, "Enter wallet password > ") + pass, err := input.ReadPassword("Enter wallet password > ") if err != nil { return cli.NewExitError(err, 1) } @@ -563,12 +563,12 @@ func createWallet(ctx *cli.Context) error { } func readAccountInfo(w io.Writer) (string, string, error) { - rawName, _ := input.ReadLine(w, "Enter the name of the account > ") - phrase, err := input.ReadPassword(w, "Enter passphrase > ") + rawName, _ := input.ReadLine("Enter the name of the account > ") + phrase, err := input.ReadPassword("Enter passphrase > ") if err != nil { return "", "", err } - phraseCheck, err := input.ReadPassword(w, "Confirm passphrase > ") + phraseCheck, err := input.ReadPassword("Confirm passphrase > ") if err != nil { return "", "", err } @@ -600,7 +600,7 @@ func newAccountFromWIF(w io.Writer, wif string) (*wallet.Account, error) { // note: NEP2 strings always have length of 58 even though // base58 strings can have different lengths even if slice lengths are equal if len(wif) == 58 { - pass, err := input.ReadPassword(w, "Enter password > ") + pass, err := input.ReadPassword("Enter password > ") if err != nil { return nil, err } diff --git a/cli/wallet_test.go b/cli/wallet_test.go index 6fbb2a9cc..923c5950e 100644 --- a/cli/wallet_test.go +++ b/cli/wallet_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "github.com/abiosoft/readline" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address" @@ -19,13 +20,37 @@ import ( ) func TestWalletInit(t *testing.T) { - tmpDir := os.TempDir() + tmpDir := path.Join(os.TempDir(), "neogo.test.walletinit") + require.NoError(t, os.Mkdir(tmpDir, os.ModePerm)) + defer os.RemoveAll(tmpDir) + e := newExecutor(t, false) defer e.Close(t) walletPath := path.Join(tmpDir, "wallet.json") e.Run(t, "neo-go", "wallet", "init", "--wallet", walletPath) - defer os.Remove(walletPath) + + t.Run("terminal escape codes", func(t *testing.T) { + walletPath := path.Join(tmpDir, "walletrussian.json") + bksp := string([]byte{ + byte(readline.CharBackward), + byte(readline.CharDelete), + }) + e.In.WriteString("буквыы" + + bksp + bksp + bksp + + "andmore\r") + e.In.WriteString("пароу" + bksp + "ль\r") + e.In.WriteString("пароль\r") + e.Run(t, "neo-go", "wallet", "init", "--account", + "--wallet", walletPath) + + w, err := wallet.NewWalletFromFile(walletPath) + require.NoError(t, err) + require.Len(t, w.Accounts, 1) + require.Equal(t, "букandmore", w.Accounts[0].Label) + require.NoError(t, w.Accounts[0].Decrypt("пароль")) + w.Close() + }) t.Run("CreateAccount", func(t *testing.T) { e.In.WriteString("testname\r") diff --git a/go.mod b/go.mod index e67da8419..83bc560ee 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( go.uber.org/atomic v1.4.0 go.uber.org/zap v1.10.0 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 - golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd // indirect + golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf golang.org/x/text v0.3.0 golang.org/x/tools v0.0.0-20180318012157-96caea41033d gopkg.in/abiosoft/ishell.v2 v2.0.0 diff --git a/go.sum b/go.sum index 6194145fe..eefc4bb3a 100644 --- a/go.sum +++ b/go.sum @@ -297,8 +297,10 @@ golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/nt golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M= +golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20180318012157-96caea41033d h1:Xmo0nLTRYewf0eXDvo12nMSuOgNQ4283hdbOHIUf7h8=