Merge pull request #1727 from nspcc-dev/nonascii

Handle terminal escape codes correctly in CLI
This commit is contained in:
Roman Khimov 2021-02-11 16:47:46 +03:00 committed by GitHub
commit 11c89257b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 94 additions and 47 deletions

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)

View file

@ -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()
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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")

2
go.mod
View file

@ -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

6
go.sum
View file

@ -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=