diff options
author | Péter Szilágyi <peterke@gmail.com> | 2016-04-12 22:51:09 +0800 |
---|---|---|
committer | Péter Szilágyi <peterke@gmail.com> | 2016-04-12 22:51:09 +0800 |
commit | 1e9b504ee7c7ebfd4b2658c66fad53fe6d440811 (patch) | |
tree | 66d229bcfb3381fe0964faf573fc6cf6b0073c75 /cmd | |
parent | 33e4f51749cdfcb9125159aae8481a8130e50062 (diff) | |
parent | 6498df7b0290139df57629568d824dfa242900cc (diff) | |
download | go-tangerine-1e9b504ee7c7ebfd4b2658c66fad53fe6d440811.tar go-tangerine-1e9b504ee7c7ebfd4b2658c66fad53fe6d440811.tar.gz go-tangerine-1e9b504ee7c7ebfd4b2658c66fad53fe6d440811.tar.bz2 go-tangerine-1e9b504ee7c7ebfd4b2658c66fad53fe6d440811.tar.lz go-tangerine-1e9b504ee7c7ebfd4b2658c66fad53fe6d440811.tar.xz go-tangerine-1e9b504ee7c7ebfd4b2658c66fad53fe6d440811.tar.zst go-tangerine-1e9b504ee7c7ebfd4b2658c66fad53fe6d440811.zip |
Merge pull request #2284 from fjl/accounts-addr-cache
accounts: cache key addresses
Diffstat (limited to 'cmd')
-rw-r--r-- | cmd/geth/accountcmd.go | 323 | ||||
-rw-r--r-- | cmd/geth/accountcmd_test.go | 292 | ||||
-rw-r--r-- | cmd/geth/chaincmd.go | 2 | ||||
-rw-r--r-- | cmd/geth/js.go | 87 | ||||
-rw-r--r-- | cmd/geth/js_test.go | 39 | ||||
-rw-r--r-- | cmd/geth/main.go | 262 | ||||
-rw-r--r-- | cmd/geth/run_test.go | 290 | ||||
-rw-r--r-- | cmd/geth/testdata/empty.js | 1 | ||||
-rw-r--r-- | cmd/geth/testdata/guswallet.json | 6 | ||||
-rw-r--r-- | cmd/geth/testdata/passwords.txt | 3 | ||||
-rw-r--r-- | cmd/geth/testdata/wrong-passwords.txt | 3 | ||||
-rw-r--r-- | cmd/gethrpctest/main.go | 11 | ||||
-rw-r--r-- | cmd/utils/cmd.go | 50 | ||||
-rw-r--r-- | cmd/utils/flags.go | 60 | ||||
-rw-r--r-- | cmd/utils/input.go | 98 | ||||
-rw-r--r-- | cmd/utils/jeth.go | 9 |
16 files changed, 1092 insertions, 444 deletions
diff --git a/cmd/geth/accountcmd.go b/cmd/geth/accountcmd.go new file mode 100644 index 000000000..bf754c72f --- /dev/null +++ b/cmd/geth/accountcmd.go @@ -0,0 +1,323 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. + +package main + +import ( + "fmt" + "io/ioutil" + + "github.com/codegangsta/cli" + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/cmd/utils" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" +) + +var ( + walletCommand = cli.Command{ + Name: "wallet", + Usage: "ethereum presale wallet", + Subcommands: []cli.Command{ + { + Action: importWallet, + Name: "import", + Usage: "import ethereum presale wallet", + }, + }, + Description: ` + + get wallet import /path/to/my/presale.wallet + +will prompt for your password and imports your ether presale account. +It can be used non-interactively with the --password option taking a +passwordfile as argument containing the wallet password in plaintext. + +`} + accountCommand = cli.Command{ + Action: accountList, + Name: "account", + Usage: "manage accounts", + Description: ` + +Manage accounts lets you create new accounts, list all existing accounts, +import a private key into a new account. + +' help' shows a list of subcommands or help for one subcommand. + +It supports interactive mode, when you are prompted for password as well as +non-interactive mode where passwords are supplied via a given password file. +Non-interactive mode is only meant for scripted use on test networks or known +safe environments. + +Make sure you remember the password you gave when creating a new account (with +either new or import). Without it you are not able to unlock your account. + +Note that exporting your key in unencrypted format is NOT supported. + +Keys are stored under <DATADIR>/keys. +It is safe to transfer the entire directory or the individual keys therein +between ethereum nodes by simply copying. +Make sure you backup your keys regularly. + +In order to use your account to send transactions, you need to unlock them using +the '--unlock' option. The argument is a space separated list of addresses or +indexes. If used non-interactively with a passwordfile, the file should contain +the respective passwords one per line. If you unlock n accounts and the password +file contains less than n entries, then the last password is meant to apply to +all remaining accounts. + +And finally. DO NOT FORGET YOUR PASSWORD. +`, + Subcommands: []cli.Command{ + { + Action: accountList, + Name: "list", + Usage: "print account addresses", + }, + { + Action: accountCreate, + Name: "new", + Usage: "create a new account", + Description: ` + + ethereum account new + +Creates a new account. Prints the address. + +The account is saved in encrypted format, you are prompted for a passphrase. + +You must remember this passphrase to unlock your account in the future. + +For non-interactive use the passphrase can be specified with the --password flag: + + ethereum --password <passwordfile> account new + +Note, this is meant to be used for testing only, it is a bad idea to save your +password to file or expose in any other way. + `, + }, + { + Action: accountUpdate, + Name: "update", + Usage: "update an existing account", + Description: ` + + ethereum account update <address> + +Update an existing account. + +The account is saved in the newest version in encrypted format, you are prompted +for a passphrase to unlock the account and another to save the updated file. + +This same command can therefore be used to migrate an account of a deprecated +format to the newest format or change the password for an account. + +For non-interactive use the passphrase can be specified with the --password flag: + + ethereum --password <passwordfile> account update <address> + +Since only one password can be given, only format update can be performed, +changing your password is only possible interactively. + `, + }, + { + Action: accountImport, + Name: "import", + Usage: "import a private key into a new account", + Description: ` + + ethereum account import <keyfile> + +Imports an unencrypted private key from <keyfile> and creates a new account. +Prints the address. + +The keyfile is assumed to contain an unencrypted private key in hexadecimal format. + +The account is saved in encrypted format, you are prompted for a passphrase. + +You must remember this passphrase to unlock your account in the future. + +For non-interactive use the passphrase can be specified with the -password flag: + + ethereum --password <passwordfile> account import <keyfile> + +Note: +As you can directly copy your encrypted accounts to another ethereum instance, +this import mechanism is not needed when you transfer an account between +nodes. + `, + }, + }, + } +) + +func accountList(ctx *cli.Context) { + accman := utils.MakeAccountManager(ctx) + for i, acct := range accman.Accounts() { + fmt.Printf("Account #%d: {%x} %s\n", i, acct.Address, acct.File) + } +} + +// tries unlocking the specified account a few times. +func unlockAccount(ctx *cli.Context, accman *accounts.Manager, address string, i int, passwords []string) (accounts.Account, string) { + account, err := utils.MakeAddress(accman, address) + if err != nil { + utils.Fatalf("Could not list accounts: %v", err) + } + for trials := 0; trials < 3; trials++ { + prompt := fmt.Sprintf("Unlocking account %s | Attempt %d/%d", address, trials+1, 3) + password := getPassPhrase(prompt, false, i, passwords) + err = accman.Unlock(account, password) + if err == nil { + glog.V(logger.Info).Infof("Unlocked account %x", account.Address) + return account, password + } + if err, ok := err.(*accounts.AmbiguousAddrError); ok { + glog.V(logger.Info).Infof("Unlocked account %x", account.Address) + return ambiguousAddrRecovery(accman, err, password), password + } + if err != accounts.ErrDecrypt { + // No need to prompt again if the error is not decryption-related. + break + } + } + // All trials expended to unlock account, bail out + utils.Fatalf("Failed to unlock account %s (%v)", address, err) + return accounts.Account{}, "" +} + +// getPassPhrase retrieves the passwor associated with an account, either fetched +// from a list of preloaded passphrases, or requested interactively from the user. +func getPassPhrase(prompt string, confirmation bool, i int, passwords []string) string { + // If a list of passwords was supplied, retrieve from them + if len(passwords) > 0 { + if i < len(passwords) { + return passwords[i] + } + return passwords[len(passwords)-1] + } + // Otherwise prompt the user for the password + if prompt != "" { + fmt.Println(prompt) + } + password, err := utils.Stdin.PasswordPrompt("Passphrase: ") + if err != nil { + utils.Fatalf("Failed to read passphrase: %v", err) + } + if confirmation { + confirm, err := utils.Stdin.PasswordPrompt("Repeat passphrase: ") + if err != nil { + utils.Fatalf("Failed to read passphrase confirmation: %v", err) + } + if password != confirm { + utils.Fatalf("Passphrases do not match") + } + } + return password +} + +func ambiguousAddrRecovery(am *accounts.Manager, err *accounts.AmbiguousAddrError, auth string) accounts.Account { + fmt.Printf("Multiple key files exist for address %x:\n", err.Addr) + for _, a := range err.Matches { + fmt.Println(" ", a.File) + } + fmt.Println("Testing your passphrase against all of them...") + var match *accounts.Account + for _, a := range err.Matches { + if err := am.Unlock(a, auth); err == nil { + match = &a + break + } + } + if match == nil { + utils.Fatalf("None of the listed files could be unlocked.") + } + fmt.Printf("Your passphrase unlocked %s\n", match.File) + fmt.Println("In order to avoid this warning, you need to remove the following duplicate key files:") + for _, a := range err.Matches { + if a != *match { + fmt.Println(" ", a.File) + } + } + return *match +} + +// accountCreate creates a new account into the keystore defined by the CLI flags. +func accountCreate(ctx *cli.Context) { + accman := utils.MakeAccountManager(ctx) + password := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx)) + + account, err := accman.NewAccount(password) + if err != nil { + utils.Fatalf("Failed to create account: %v", err) + } + fmt.Printf("Address: {%x}\n", account.Address) +} + +// accountUpdate transitions an account from a previous format to the current +// one, also providing the possibility to change the pass-phrase. +func accountUpdate(ctx *cli.Context) { + if len(ctx.Args()) == 0 { + utils.Fatalf("No accounts specified to update") + } + accman := utils.MakeAccountManager(ctx) + + account, oldPassword := unlockAccount(ctx, accman, ctx.Args().First(), 0, nil) + newPassword := getPassPhrase("Please give a new password. Do not forget this password.", true, 0, nil) + if err := accman.Update(account, oldPassword, newPassword); err != nil { + utils.Fatalf("Could not update the account: %v", err) + } +} + +func importWallet(ctx *cli.Context) { + keyfile := ctx.Args().First() + if len(keyfile) == 0 { + utils.Fatalf("keyfile must be given as argument") + } + keyJson, err := ioutil.ReadFile(keyfile) + if err != nil { + utils.Fatalf("Could not read wallet file: %v", err) + } + + accman := utils.MakeAccountManager(ctx) + passphrase := getPassPhrase("", false, 0, utils.MakePasswordList(ctx)) + + acct, err := accman.ImportPreSaleKey(keyJson, passphrase) + if err != nil { + utils.Fatalf("%v", err) + } + fmt.Printf("Address: {%x}\n", acct.Address) +} + +func accountImport(ctx *cli.Context) { + keyfile := ctx.Args().First() + if len(keyfile) == 0 { + utils.Fatalf("keyfile must be given as argument") + } + key, err := crypto.LoadECDSA(keyfile) + if err != nil { + utils.Fatalf("keyfile must be given as argument") + } + accman := utils.MakeAccountManager(ctx) + passphrase := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx)) + acct, err := accman.ImportECDSA(key, passphrase) + if err != nil { + utils.Fatalf("Could not create the account: %v", err) + } + fmt.Printf("Address: {%x}\n", acct.Address) +} diff --git a/cmd/geth/accountcmd_test.go b/cmd/geth/accountcmd_test.go new file mode 100644 index 000000000..b6abde6d8 --- /dev/null +++ b/cmd/geth/accountcmd_test.go @@ -0,0 +1,292 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. + +package main + +import ( + "io/ioutil" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/cespare/cp" +) + +// These tests are 'smoke tests' for the account related +// subcommands and flags. +// +// For most tests, the test files from package accounts +// are copied into a temporary keystore directory. + +func tmpDatadirWithKeystore(t *testing.T) string { + datadir := tmpdir(t) + keystore := filepath.Join(datadir, "keystore") + source := filepath.Join("..", "..", "accounts", "testdata", "keystore") + if err := cp.CopyAll(keystore, source); err != nil { + t.Fatal(err) + } + return datadir +} + +func TestAccountListEmpty(t *testing.T) { + geth := runGeth(t, "account") + geth.expectExit() +} + +func TestAccountList(t *testing.T) { + datadir := tmpDatadirWithKeystore(t) + geth := runGeth(t, "--datadir", datadir, "account") + defer geth.expectExit() + if runtime.GOOS == "windows" { + geth.expect(` +Account #0: {7ef5a6135f1fd6a02593eedc869c6d41d934aef8} {{.Datadir}}\keystore\UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 +Account #1: {f466859ead1932d743d622cb74fc058882e8648a} {{.Datadir}}\keystore\aaa +Account #2: {289d485d9771714cce91d3393d764e1311907acc} {{.Datadir}}\keystore\zzz +`) + } else { + geth.expect(` +Account #0: {7ef5a6135f1fd6a02593eedc869c6d41d934aef8} {{.Datadir}}/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 +Account #1: {f466859ead1932d743d622cb74fc058882e8648a} {{.Datadir}}/keystore/aaa +Account #2: {289d485d9771714cce91d3393d764e1311907acc} {{.Datadir}}/keystore/zzz +`) + } +} + +func TestAccountNew(t *testing.T) { + geth := runGeth(t, "--lightkdf", "account", "new") + defer geth.expectExit() + geth.expect(` +Your new account is locked with a password. Please give a password. Do not forget this password. +!! Unsupported terminal, password will be echoed. +Passphrase: {{.InputLine "foobar"}} +Repeat passphrase: {{.InputLine "foobar"}} +`) + geth.expectRegexp(`Address: \{[0-9a-f]{40}\}\n`) +} + +func TestAccountNewBadRepeat(t *testing.T) { + geth := runGeth(t, "--lightkdf", "account", "new") + defer geth.expectExit() + geth.expect(` +Your new account is locked with a password. Please give a password. Do not forget this password. +!! Unsupported terminal, password will be echoed. +Passphrase: {{.InputLine "something"}} +Repeat passphrase: {{.InputLine "something else"}} +Fatal: Passphrases do not match +`) +} + +func TestAccountUpdate(t *testing.T) { + datadir := tmpDatadirWithKeystore(t) + geth := runGeth(t, + "--datadir", datadir, "--lightkdf", + "account", "update", "f466859ead1932d743d622cb74fc058882e8648a") + defer geth.expectExit() + geth.expect(` +Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3 +!! Unsupported terminal, password will be echoed. +Passphrase: {{.InputLine "foobar"}} +Please give a new password. Do not forget this password. +Passphrase: {{.InputLine "foobar2"}} +Repeat passphrase: {{.InputLine "foobar2"}} +`) +} + +func TestWalletImport(t *testing.T) { + geth := runGeth(t, "--lightkdf", "wallet", "import", "testdata/guswallet.json") + defer geth.expectExit() + geth.expect(` +!! Unsupported terminal, password will be echoed. +Passphrase: {{.InputLine "foo"}} +Address: {d4584b5f6229b7be90727b0fc8c6b91bb427821f} +`) + + files, err := ioutil.ReadDir(filepath.Join(geth.Datadir, "keystore")) + if len(files) != 1 { + t.Errorf("expected one key file in keystore directory, found %d files (error: %v)", len(files), err) + } +} + +func TestWalletImportBadPassword(t *testing.T) { + geth := runGeth(t, "--lightkdf", "wallet", "import", "testdata/guswallet.json") + defer geth.expectExit() + geth.expect(` +!! Unsupported terminal, password will be echoed. +Passphrase: {{.InputLine "wrong"}} +Fatal: could not decrypt key with given passphrase +`) +} + +func TestUnlockFlag(t *testing.T) { + datadir := tmpDatadirWithKeystore(t) + geth := runGeth(t, + "--datadir", datadir, "--nat", "none", "--nodiscover", "--dev", + "--unlock", "f466859ead1932d743d622cb74fc058882e8648a", + "js", "testdata/empty.js") + geth.expect(` +Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3 +!! Unsupported terminal, password will be echoed. +Passphrase: {{.InputLine "foobar"}} +`) + geth.expectExit() + + wantMessages := []string{ + "Unlocked account f466859ead1932d743d622cb74fc058882e8648a", + } + for _, m := range wantMessages { + if strings.Index(geth.stderrText(), m) == -1 { + t.Errorf("stderr text does not contain %q", m) + } + } +} + +func TestUnlockFlagWrongPassword(t *testing.T) { + datadir := tmpDatadirWithKeystore(t) + geth := runGeth(t, + "--datadir", datadir, "--nat", "none", "--nodiscover", "--dev", + "--unlock", "f466859ead1932d743d622cb74fc058882e8648a") + defer geth.expectExit() + geth.expect(` +Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3 +!! Unsupported terminal, password will be echoed. +Passphrase: {{.InputLine "wrong1"}} +Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 2/3 +Passphrase: {{.InputLine "wrong2"}} +Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 3/3 +Passphrase: {{.InputLine "wrong3"}} +Fatal: Failed to unlock account f466859ead1932d743d622cb74fc058882e8648a (could not decrypt key with given passphrase) +`) +} + +// https://github.com/ethereum/go-ethereum/issues/1785 +func TestUnlockFlagMultiIndex(t *testing.T) { + datadir := tmpDatadirWithKeystore(t) + geth := runGeth(t, + "--datadir", datadir, "--nat", "none", "--nodiscover", "--dev", + "--unlock", "0,2", + "js", "testdata/empty.js") + geth.expect(` +Unlocking account 0 | Attempt 1/3 +!! Unsupported terminal, password will be echoed. +Passphrase: {{.InputLine "foobar"}} +Unlocking account 2 | Attempt 1/3 +Passphrase: {{.InputLine "foobar"}} +`) + geth.expectExit() + + wantMessages := []string{ + "Unlocked account 7ef5a6135f1fd6a02593eedc869c6d41d934aef8", + "Unlocked account 289d485d9771714cce91d3393d764e1311907acc", + } + for _, m := range wantMessages { + if strings.Index(geth.stderrText(), m) == -1 { + t.Errorf("stderr text does not contain %q", m) + } + } +} + +func TestUnlockFlagPasswordFile(t *testing.T) { + datadir := tmpDatadirWithKeystore(t) + geth := runGeth(t, + "--datadir", datadir, "--nat", "none", "--nodiscover", "--dev", + "--password", "testdata/passwords.txt", "--unlock", "0,2", + "js", "testdata/empty.js") + geth.expectExit() + + wantMessages := []string{ + "Unlocked account 7ef5a6135f1fd6a02593eedc869c6d41d934aef8", + "Unlocked account 289d485d9771714cce91d3393d764e1311907acc", + } + for _, m := range wantMessages { + if strings.Index(geth.stderrText(), m) == -1 { + t.Errorf("stderr text does not contain %q", m) + } + } +} + +func TestUnlockFlagPasswordFileWrongPassword(t *testing.T) { + datadir := tmpDatadirWithKeystore(t) + geth := runGeth(t, + "--datadir", datadir, "--nat", "none", "--nodiscover", "--dev", + "--password", "testdata/wrong-passwords.txt", "--unlock", "0,2") + defer geth.expectExit() + geth.expect(` +Fatal: Failed to unlock account 0 (could not decrypt key with given passphrase) +`) +} + +func TestUnlockFlagAmbiguous(t *testing.T) { + store := filepath.Join("..", "..", "accounts", "testdata", "dupes") + geth := runGeth(t, + "--keystore", store, "--nat", "none", "--nodiscover", "--dev", + "--unlock", "f466859ead1932d743d622cb74fc058882e8648a", + "js", "testdata/empty.js") + defer geth.expectExit() + + // Helper for the expect template, returns absolute keystore path. + geth.setTemplateFunc("keypath", func(file string) string { + abs, _ := filepath.Abs(filepath.Join(store, file)) + return abs + }) + geth.expect(` +Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3 +!! Unsupported terminal, password will be echoed. +Passphrase: {{.InputLine "foobar"}} +Multiple key files exist for address f466859ead1932d743d622cb74fc058882e8648a: + {{keypath "1"}} + {{keypath "2"}} +Testing your passphrase against all of them... +Your passphrase unlocked {{keypath "1"}} +In order to avoid this warning, you need to remove the following duplicate key files: + {{keypath "2"}} +`) + geth.expectExit() + + wantMessages := []string{ + "Unlocked account f466859ead1932d743d622cb74fc058882e8648a", + } + for _, m := range wantMessages { + if strings.Index(geth.stderrText(), m) == -1 { + t.Errorf("stderr text does not contain %q", m) + } + } +} + +func TestUnlockFlagAmbiguousWrongPassword(t *testing.T) { + store := filepath.Join("..", "..", "accounts", "testdata", "dupes") + geth := runGeth(t, + "--keystore", store, "--nat", "none", "--nodiscover", "--dev", + "--unlock", "f466859ead1932d743d622cb74fc058882e8648a") + defer geth.expectExit() + + // Helper for the expect template, returns absolute keystore path. + geth.setTemplateFunc("keypath", func(file string) string { + abs, _ := filepath.Abs(filepath.Join(store, file)) + return abs + }) + geth.expect(` +Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3 +!! Unsupported terminal, password will be echoed. +Passphrase: {{.InputLine "wrong"}} +Multiple key files exist for address f466859ead1932d743d622cb74fc058882e8648a: + {{keypath "1"}} + {{keypath "2"}} +Testing your passphrase against all of them... +Fatal: None of the listed files could be unlocked. +`) + geth.expectExit() +} diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index 868ee7db1..32eacc99e 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -116,7 +116,7 @@ func exportChain(ctx *cli.Context) { } func removeDB(ctx *cli.Context) { - confirm, err := utils.PromptConfirm("Remove local database?") + confirm, err := utils.Stdin.ConfirmPrompt("Remove local database?") if err != nil { utils.Fatalf("%v", err) } diff --git a/cmd/geth/js.go b/cmd/geth/js.go index 68c906443..d5518f94b 100644 --- a/cmd/geth/js.go +++ b/cmd/geth/js.go @@ -17,7 +17,6 @@ package main import ( - "bufio" "fmt" "math/big" "os" @@ -28,6 +27,7 @@ import ( "strings" "github.com/codegangsta/cli" + "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/registrar" @@ -46,30 +46,6 @@ var ( exit = regexp.MustCompile("^\\s*exit\\s*;*\\s*$") ) -type prompter interface { - AppendHistory(string) - Prompt(p string) (string, error) - PasswordPrompt(p string) (string, error) -} - -type dumbterm struct{ r *bufio.Reader } - -func (r dumbterm) Prompt(p string) (string, error) { - fmt.Print(p) - line, err := r.r.ReadString('\n') - return strings.TrimSuffix(line, "\n"), err -} - -func (r dumbterm) PasswordPrompt(p string) (string, error) { - fmt.Println("!! Unsupported terminal, password will echo.") - fmt.Print(p) - input, err := bufio.NewReader(os.Stdin).ReadString('\n') - fmt.Println() - return input, err -} - -func (r dumbterm) AppendHistory(string) {} - type jsre struct { re *re.JSRE stack *node.Node @@ -78,7 +54,6 @@ type jsre struct { atexit func() corsDomain string client rpc.Client - prompter } func makeCompleter(re *jsre) liner.WordCompleter { @@ -106,27 +81,11 @@ func newLightweightJSRE(docRoot string, client rpc.Client, datadir string, inter js := &jsre{ps1: "> "} js.wait = make(chan *big.Int) js.client = client - js.re = re.New(docRoot) if err := js.apiBindings(); err != nil { utils.Fatalf("Unable to initialize console - %v", err) } - - if !liner.TerminalSupported() || !interactive { - js.prompter = dumbterm{bufio.NewReader(os.Stdin)} - } else { - lr := liner.NewLiner() - js.withHistory(datadir, func(hist *os.File) { lr.ReadHistory(hist) }) - lr.SetCtrlCAborts(true) - lr.SetWordCompleter(makeCompleter(js)) - lr.SetTabCompletionStyle(liner.TabPrints) - js.prompter = lr - js.atexit = func() { - js.withHistory(datadir, func(hist *os.File) { hist.Truncate(0); lr.WriteHistory(hist) }) - lr.Close() - close(js.wait) - } - } + js.setupInput(datadir) return js } @@ -136,28 +95,27 @@ func newJSRE(stack *node.Node, docRoot, corsDomain string, client rpc.Client, in js.corsDomain = corsDomain js.wait = make(chan *big.Int) js.client = client - js.re = re.New(docRoot) if err := js.apiBindings(); err != nil { utils.Fatalf("Unable to connect - %v", err) } + js.setupInput(stack.DataDir()) + return js +} - if !liner.TerminalSupported() || !interactive { - js.prompter = dumbterm{bufio.NewReader(os.Stdin)} - } else { - lr := liner.NewLiner() - js.withHistory(stack.DataDir(), func(hist *os.File) { lr.ReadHistory(hist) }) - lr.SetCtrlCAborts(true) - lr.SetWordCompleter(makeCompleter(js)) - lr.SetTabCompletionStyle(liner.TabPrints) - js.prompter = lr - js.atexit = func() { - js.withHistory(stack.DataDir(), func(hist *os.File) { hist.Truncate(0); lr.WriteHistory(hist) }) - lr.Close() - close(js.wait) - } +func (self *jsre) setupInput(datadir string) { + self.withHistory(datadir, func(hist *os.File) { utils.Stdin.ReadHistory(hist) }) + utils.Stdin.SetCtrlCAborts(true) + utils.Stdin.SetWordCompleter(makeCompleter(self)) + utils.Stdin.SetTabCompletionStyle(liner.TabPrints) + self.atexit = func() { + self.withHistory(datadir, func(hist *os.File) { + hist.Truncate(0) + utils.Stdin.WriteHistory(hist) + }) + utils.Stdin.Close() + close(self.wait) } - return js } func (self *jsre) batch(statement string) { @@ -290,7 +248,7 @@ func (js *jsre) apiBindings() error { } func (self *jsre) AskPassword() (string, bool) { - pass, err := self.PasswordPrompt("Passphrase: ") + pass, err := utils.Stdin.PasswordPrompt("Passphrase: ") if err != nil { return "", false } @@ -315,7 +273,7 @@ func (self *jsre) ConfirmTransaction(tx string) bool { func (self *jsre) UnlockAccount(addr []byte) bool { fmt.Printf("Please unlock account %x.\n", addr) - pass, err := self.PasswordPrompt("Passphrase: ") + pass, err := utils.Stdin.PasswordPrompt("Passphrase: ") if err != nil { return false } @@ -324,7 +282,8 @@ func (self *jsre) UnlockAccount(addr []byte) bool { if err := self.stack.Service(ðereum); err != nil { return false } - if err := ethereum.AccountManager().Unlock(common.BytesToAddress(addr), pass); err != nil { + a := accounts.Account{Address: common.BytesToAddress(addr)} + if err := ethereum.AccountManager().Unlock(a, pass); err != nil { return false } else { fmt.Println("Account is now unlocked for this session.") @@ -365,7 +324,7 @@ func (self *jsre) interactive() { go func() { defer close(inputln) for { - line, err := self.Prompt(<-prompt) + line, err := utils.Stdin.Prompt(<-prompt) if err != nil { if err == liner.ErrPromptAborted { // ctrl-C self.resetPrompt() @@ -404,7 +363,7 @@ func (self *jsre) interactive() { self.setIndent() if indentCount <= 0 { if mustLogInHistory(str) { - self.AppendHistory(str[:len(str)-1]) + utils.Stdin.AppendHistory(str[:len(str)-1]) } self.parseInput(str) str = "" diff --git a/cmd/geth/js_test.go b/cmd/geth/js_test.go index e0c4dacbc..baf572359 100644 --- a/cmd/geth/js_test.go +++ b/cmd/geth/js_test.go @@ -42,18 +42,17 @@ import ( const ( testSolcPath = "" solcVersion = "0.9.23" - - testKey = "e6fab74a43941f82d89cb7faa408e227cdad3153c4720e540e855c19b15e6674" - testAddress = "0x8605cdbbdb6d264aa742e77020dcbc58fcdce182" - testBalance = "10000000000000000000" + testAddress = "0x8605cdbbdb6d264aa742e77020dcbc58fcdce182" + testBalance = "10000000000000000000" // of empty string testHash = "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" ) var ( - versionRE = regexp.MustCompile(strconv.Quote(`"compilerVersion":"` + solcVersion + `"`)) - testNodeKey = crypto.ToECDSA(common.Hex2Bytes("4b50fa71f5c3eeb8fdc452224b2395af2fcc3d125e06c32c82e048c0559db03f")) - testGenesis = `{"` + testAddress[2:] + `": {"balance": "` + testBalance + `"}}` + versionRE = regexp.MustCompile(strconv.Quote(`"compilerVersion":"` + solcVersion + `"`)) + testNodeKey, _ = crypto.HexToECDSA("4b50fa71f5c3eeb8fdc452224b2395af2fcc3d125e06c32c82e048c0559db03f") + testAccount, _ = crypto.HexToECDSA("e6fab74a43941f82d89cb7faa408e227cdad3153c4720e540e855c19b15e6674") + testGenesis = `{"` + testAddress[2:] + `": {"balance": "` + testBalance + `"}}` ) type testjethre struct { @@ -62,17 +61,6 @@ type testjethre struct { client *httpclient.HTTPClient } -func (self *testjethre) UnlockAccount(acc []byte) bool { - var ethereum *eth.Ethereum - self.stack.Service(ðereum) - - err := ethereum.AccountManager().Unlock(common.BytesToAddress(acc), "") - if err != nil { - panic("unable to unlock") - } - return true -} - // Temporary disabled while natspec hasn't been migrated //func (self *testjethre) ConfirmTransaction(tx string) bool { // var ethereum *eth.Ethereum @@ -94,17 +82,14 @@ func testREPL(t *testing.T, config func(*eth.Config)) (string, *testjethre, *nod t.Fatal(err) } // Create a networkless protocol stack - stack, err := node.New(&node.Config{PrivateKey: testNodeKey, Name: "test", NoDiscovery: true}) + stack, err := node.New(&node.Config{DataDir: tmp, PrivateKey: testNodeKey, Name: "test", NoDiscovery: true}) if err != nil { t.Fatalf("failed to create node: %v", err) } // Initialize and register the Ethereum protocol - keystore := crypto.NewKeyStorePlain(filepath.Join(tmp, "keystore")) - accman := accounts.NewManager(keystore) - + accman := accounts.NewPlaintextManager(filepath.Join(tmp, "keystore")) db, _ := ethdb.NewMemDatabase() core.WriteGenesisBlockForTesting(db, core.GenesisAccount{common.HexToAddress(testAddress), common.String2Big(testBalance)}) - ethConf := ð.Config{ ChainConfig: &core.ChainConfig{HomesteadBlock: new(big.Int)}, TestGenesisState: db, @@ -122,15 +107,11 @@ func testREPL(t *testing.T, config func(*eth.Config)) (string, *testjethre, *nod t.Fatalf("failed to register ethereum protocol: %v", err) } // Initialize all the keys for testing - keyb, err := crypto.HexToECDSA(testKey) + a, err := accman.ImportECDSA(testAccount, "") if err != nil { t.Fatal(err) } - key := crypto.NewKeyFromECDSA(keyb) - if err := keystore.StoreKey(key, ""); err != nil { - t.Fatal(err) - } - if err := accman.Unlock(key.Address, ""); err != nil { + if err := accman.Unlock(a, ""); err != nil { t.Fatal(err) } // Start the node and assemble the REPL tester diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 584df7316..512a5f183 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -29,7 +29,6 @@ import ( "github.com/codegangsta/cli" "github.com/ethereum/ethash" - "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" @@ -75,6 +74,8 @@ func init() { removedbCommand, dumpCommand, monitorCommand, + accountCommand, + walletCommand, { Action: makedag, Name: "makedag", @@ -111,144 +112,6 @@ The output of this command is supposed to be machine-readable. `, }, { - Name: "wallet", - Usage: "ethereum presale wallet", - Subcommands: []cli.Command{ - { - Action: importWallet, - Name: "import", - Usage: "import ethereum presale wallet", - }, - }, - Description: ` - - get wallet import /path/to/my/presale.wallet - -will prompt for your password and imports your ether presale account. -It can be used non-interactively with the --password option taking a -passwordfile as argument containing the wallet password in plaintext. - -`}, - { - Action: accountList, - Name: "account", - Usage: "manage accounts", - Description: ` - -Manage accounts lets you create new accounts, list all existing accounts, -import a private key into a new account. - -' help' shows a list of subcommands or help for one subcommand. - -It supports interactive mode, when you are prompted for password as well as -non-interactive mode where passwords are supplied via a given password file. -Non-interactive mode is only meant for scripted use on test networks or known -safe environments. - -Make sure you remember the password you gave when creating a new account (with -either new or import). Without it you are not able to unlock your account. - -Note that exporting your key in unencrypted format is NOT supported. - -Keys are stored under <DATADIR>/keys. -It is safe to transfer the entire directory or the individual keys therein -between ethereum nodes by simply copying. -Make sure you backup your keys regularly. - -In order to use your account to send transactions, you need to unlock them using -the '--unlock' option. The argument is a space separated list of addresses or -indexes. If used non-interactively with a passwordfile, the file should contain -the respective passwords one per line. If you unlock n accounts and the password -file contains less than n entries, then the last password is meant to apply to -all remaining accounts. - -And finally. DO NOT FORGET YOUR PASSWORD. -`, - Subcommands: []cli.Command{ - { - Action: accountList, - Name: "list", - Usage: "print account addresses", - }, - { - Action: accountCreate, - Name: "new", - Usage: "create a new account", - Description: ` - - ethereum account new - -Creates a new account. Prints the address. - -The account is saved in encrypted format, you are prompted for a passphrase. - -You must remember this passphrase to unlock your account in the future. - -For non-interactive use the passphrase can be specified with the --password flag: - - ethereum --password <passwordfile> account new - -Note, this is meant to be used for testing only, it is a bad idea to save your -password to file or expose in any other way. - `, - }, - { - Action: accountUpdate, - Name: "update", - Usage: "update an existing account", - Description: ` - - ethereum account update <address> - -Update an existing account. - -The account is saved in the newest version in encrypted format, you are prompted -for a passphrase to unlock the account and another to save the updated file. - -This same command can therefore be used to migrate an account of a deprecated -format to the newest format or change the password for an account. - -For non-interactive use the passphrase can be specified with the --password flag: - - ethereum --password <passwordfile> account update <address> - -Since only one password can be given, only format update can be performed, -changing your password is only possible interactively. - -Note that account update has the a side effect that the order of your accounts -changes. - `, - }, - { - Action: accountImport, - Name: "import", - Usage: "import a private key into a new account", - Description: ` - - ethereum account import <keyfile> - -Imports an unencrypted private key from <keyfile> and creates a new account. -Prints the address. - -The keyfile is assumed to contain an unencrypted private key in hexadecimal format. - -The account is saved in encrypted format, you are prompted for a passphrase. - -You must remember this passphrase to unlock your account in the future. - -For non-interactive use the passphrase can be specified with the -password flag: - - ethereum --password <passwordfile> account import <keyfile> - -Note: -As you can directly copy your encrypted accounts to another ethereum instance, -this import mechanism is not needed when you transfer an account between -nodes. - `, - }, - }, - }, - { Action: initGenesis, Name: "init", Usage: "bootstraps and initialises a new genesis block (JSON)", @@ -289,6 +152,7 @@ JavaScript API. See https://github.com/ethereum/go-ethereum/wiki/Javascipt-Conso `, }, } + app.Flags = []cli.Flag{ utils.IdentityFlag, utils.UnlockedAccountFlag, @@ -373,6 +237,7 @@ JavaScript API. See https://github.com/ethereum/go-ethereum/wiki/Javascipt-Conso app.After = func(ctx *cli.Context) error { logger.Flush() debug.Exit() + utils.Stdin.Close() // Resets terminal mode. return nil } } @@ -524,25 +389,6 @@ func execScripts(ctx *cli.Context) { node.Stop() } -// tries unlocking the specified account a few times. -func unlockAccount(ctx *cli.Context, accman *accounts.Manager, address string, i int, passwords []string) (common.Address, string) { - account, err := utils.MakeAddress(accman, address) - if err != nil { - utils.Fatalf("Unlock error: %v", err) - } - - for trials := 0; trials < 3; trials++ { - prompt := fmt.Sprintf("Unlocking account %s | Attempt %d/%d", address, trials+1, 3) - password := getPassPhrase(prompt, false, i, passwords) - if err := accman.Unlock(account, password); err == nil { - return account, password - } - } - // All trials expended to unlock account, bail out - utils.Fatalf("Failed to unlock account: %s", address) - return common.Address{}, "" -} - // startNode boots up the system node and all registered protocols, after which // it unlocks any requested accounts, and starts the RPC/IPC interfaces and the // miner. @@ -572,106 +418,6 @@ func startNode(ctx *cli.Context, stack *node.Node) { } } -func accountList(ctx *cli.Context) { - accman := utils.MakeAccountManager(ctx) - accts, err := accman.Accounts() - if err != nil { - utils.Fatalf("Could not list accounts: %v", err) - } - for i, acct := range accts { - fmt.Printf("Account #%d: %x\n", i, acct) - } -} - -// getPassPhrase retrieves the passwor associated with an account, either fetched -// from a list of preloaded passphrases, or requested interactively from the user. -func getPassPhrase(prompt string, confirmation bool, i int, passwords []string) string { - // If a list of passwords was supplied, retrieve from them - if len(passwords) > 0 { - if i < len(passwords) { - return passwords[i] - } - return passwords[len(passwords)-1] - } - // Otherwise prompt the user for the password - fmt.Println(prompt) - password, err := utils.PromptPassword("Passphrase: ", true) - if err != nil { - utils.Fatalf("Failed to read passphrase: %v", err) - } - if confirmation { - confirm, err := utils.PromptPassword("Repeat passphrase: ", false) - if err != nil { - utils.Fatalf("Failed to read passphrase confirmation: %v", err) - } - if password != confirm { - utils.Fatalf("Passphrases do not match") - } - } - return password -} - -// accountCreate creates a new account into the keystore defined by the CLI flags. -func accountCreate(ctx *cli.Context) { - accman := utils.MakeAccountManager(ctx) - password := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx)) - - account, err := accman.NewAccount(password) - if err != nil { - utils.Fatalf("Failed to create account: %v", err) - } - fmt.Printf("Address: %x\n", account) -} - -// accountUpdate transitions an account from a previous format to the current -// one, also providing the possibility to change the pass-phrase. -func accountUpdate(ctx *cli.Context) { - if len(ctx.Args()) == 0 { - utils.Fatalf("No accounts specified to update") - } - accman := utils.MakeAccountManager(ctx) - - account, oldPassword := unlockAccount(ctx, accman, ctx.Args().First(), 0, nil) - newPassword := getPassPhrase("Please give a new password. Do not forget this password.", true, 0, nil) - if err := accman.Update(account, oldPassword, newPassword); err != nil { - utils.Fatalf("Could not update the account: %v", err) - } -} - -func importWallet(ctx *cli.Context) { - keyfile := ctx.Args().First() - if len(keyfile) == 0 { - utils.Fatalf("keyfile must be given as argument") - } - keyJson, err := ioutil.ReadFile(keyfile) - if err != nil { - utils.Fatalf("Could not read wallet file: %v", err) - } - - accman := utils.MakeAccountManager(ctx) - passphrase := getPassPhrase("", false, 0, utils.MakePasswordList(ctx)) - - acct, err := accman.ImportPreSaleKey(keyJson, passphrase) - if err != nil { - utils.Fatalf("Could not create the account: %v", err) - } - fmt.Printf("Address: %x\n", acct) -} - -func accountImport(ctx *cli.Context) { - keyfile := ctx.Args().First() - if len(keyfile) == 0 { - utils.Fatalf("keyfile must be given as argument") - } - accman := utils.MakeAccountManager(ctx) - passphrase := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx)) - acct, err := accman.Import(keyfile, passphrase) - if err != nil { - utils.Fatalf("Could not create the account: %v", err) - } - fmt.Printf("Address: %x\n", acct) -} - func makedag(ctx *cli.Context) { args := ctx.Args() wrongArgs := func() { diff --git a/cmd/geth/run_test.go b/cmd/geth/run_test.go new file mode 100644 index 000000000..a82eb9d68 --- /dev/null +++ b/cmd/geth/run_test.go @@ -0,0 +1,290 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. + +package main + +import ( + "bufio" + "bytes" + "fmt" + "html/template" + "io" + "io/ioutil" + "os" + "os/exec" + "regexp" + "sync" + "testing" + "time" +) + +func tmpdir(t *testing.T) string { + dir, err := ioutil.TempDir("", "geth-test") + if err != nil { + t.Fatal(err) + } + return dir +} + +type testgeth struct { + // For total convenience, all testing methods are available. + *testing.T + // template variables for expect + Datadir string + Executable string + Func template.FuncMap + + removeDatadir bool + cmd *exec.Cmd + stdout *bufio.Reader + stdin io.WriteCloser + stderr *testlogger +} + +func init() { + // Run the app if we're the child process for runGeth. + if os.Getenv("GETH_TEST_CHILD") != "" { + app.RunAndExitOnError() + os.Exit(0) + } +} + +// spawns geth with the given command line args. If the args don't set --datadir, the +// child g gets a temporary data directory. +func runGeth(t *testing.T, args ...string) *testgeth { + tt := &testgeth{T: t, Executable: os.Args[0]} + for i, arg := range args { + if arg == "-datadir" || arg == "--datadir" { + if i < len(args)-1 { + tt.Datadir = args[i+1] + } + break + } + } + if tt.Datadir == "" { + tt.Datadir = tmpdir(t) + tt.removeDatadir = true + args = append([]string{"-datadir", tt.Datadir}, args...) + // Remove the temporary datadir if something fails below. + defer func() { + if t.Failed() { + os.RemoveAll(tt.Datadir) + } + }() + } + + // Boot "geth". This actually runs the test binary but the init function + // will prevent any tests from running. + tt.stderr = &testlogger{t: t} + tt.cmd = exec.Command(os.Args[0], args...) + tt.cmd.Env = append(os.Environ(), "GETH_TEST_CHILD=1") + tt.cmd.Stderr = tt.stderr + stdout, err := tt.cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + tt.stdout = bufio.NewReader(stdout) + if tt.stdin, err = tt.cmd.StdinPipe(); err != nil { + t.Fatal(err) + } + if err := tt.cmd.Start(); err != nil { + t.Fatal(err) + } + return tt +} + +// InputLine writes the given text to the childs stdin. +// This method can also be called from an expect template, e.g.: +// +// geth.expect(`Passphrase: {{.InputLine "password"}}`) +func (tt *testgeth) InputLine(s string) string { + io.WriteString(tt.stdin, s+"\n") + return "" +} + +func (tt *testgeth) setTemplateFunc(name string, fn interface{}) { + if tt.Func == nil { + tt.Func = make(map[string]interface{}) + } + tt.Func[name] = fn +} + +// expect runs its argument as a template, then expects the +// child process to output the result of the template within 5s. +// +// If the template starts with a newline, the newline is removed +// before matching. +func (tt *testgeth) expect(tplsource string) { + // Generate the expected output by running the template. + tpl := template.Must(template.New("").Funcs(tt.Func).Parse(tplsource)) + wantbuf := new(bytes.Buffer) + if err := tpl.Execute(wantbuf, tt); err != nil { + panic(err) + } + // Trim exactly one newline at the beginning. This makes tests look + // much nicer because all expect strings are at column 0. + want := bytes.TrimPrefix(wantbuf.Bytes(), []byte("\n")) + if err := tt.matchExactOutput(want); err != nil { + tt.Fatal(err) + } + tt.Logf("Matched stdout text:\n%s", want) +} + +func (tt *testgeth) matchExactOutput(want []byte) error { + buf := make([]byte, len(want)) + n := 0 + tt.withKillTimeout(func() { n, _ = io.ReadFull(tt.stdout, buf) }) + buf = buf[:n] + if n < len(want) || !bytes.Equal(buf, want) { + // Grab any additional buffered output in case of mismatch + // because it might help with debugging. + buf = append(buf, make([]byte, tt.stdout.Buffered())...) + tt.stdout.Read(buf[n:]) + // Find the mismatch position. + for i := 0; i < n; i++ { + if want[i] != buf[i] { + return fmt.Errorf("Output mismatch at ◊:\n---------------- (stdout text)\n%s◊%s\n---------------- (expected text)\n%s", + buf[:i], buf[i:n], want) + } + } + if n < len(want) { + return fmt.Errorf("Not enough output, got until ◊:\n---------------- (stdout text)\n%s\n---------------- (expected text)\n%s◊%s", + buf, want[:n], want[n:]) + } + } + return nil +} + +// expectRegexp expects the child process to output text matching the +// given regular expression within 5s. +// +// Note that an arbitrary amount of output may be consumed by the +// regular expression. This usually means that expect cannot be used +// after expectRegexp. +func (tt *testgeth) expectRegexp(resource string) (*regexp.Regexp, []string) { + var ( + re = regexp.MustCompile(resource) + rtee = &runeTee{in: tt.stdout} + matches []int + ) + tt.withKillTimeout(func() { matches = re.FindReaderSubmatchIndex(rtee) }) + output := rtee.buf.Bytes() + if matches == nil { + tt.Fatalf("Output did not match:\n---------------- (stdout text)\n%s\n---------------- (regular expression)\n%s", + output, resource) + return re, nil + } + tt.Logf("Matched stdout text:\n%s", output) + var submatch []string + for i := 0; i < len(matches); i += 2 { + submatch = append(submatch, string(output[i:i+1])) + } + return re, submatch +} + +// expectExit expects the child process to exit within 5s without +// printing any additional text on stdout. +func (tt *testgeth) expectExit() { + var output []byte + tt.withKillTimeout(func() { + output, _ = ioutil.ReadAll(tt.stdout) + }) + tt.cmd.Wait() + if tt.removeDatadir { + os.RemoveAll(tt.Datadir) + } + if len(output) > 0 { + tt.Errorf("Unmatched stdout text:\n%s", output) + } +} + +func (tt *testgeth) interrupt() { + tt.cmd.Process.Signal(os.Interrupt) +} + +// stderrText returns any stderr output written so far. +// The returned text holds all log lines after expectExit has +// returned. +func (tt *testgeth) stderrText() string { + tt.stderr.mu.Lock() + defer tt.stderr.mu.Unlock() + return tt.stderr.buf.String() +} + +func (tt *testgeth) withKillTimeout(fn func()) { + timeout := time.AfterFunc(5*time.Second, func() { + tt.Log("killing the child process (timeout)") + tt.cmd.Process.Kill() + if tt.removeDatadir { + os.RemoveAll(tt.Datadir) + } + }) + defer timeout.Stop() + fn() +} + +// testlogger logs all written lines via t.Log and also +// collects them for later inspection. +type testlogger struct { + t *testing.T + mu sync.Mutex + buf bytes.Buffer +} + +func (tl *testlogger) Write(b []byte) (n int, err error) { + lines := bytes.Split(b, []byte("\n")) + for _, line := range lines { + if len(line) > 0 { + tl.t.Logf("(stderr) %s", line) + } + } + tl.mu.Lock() + tl.buf.Write(b) + tl.mu.Unlock() + return len(b), err +} + +// runeTee collects text read through it into buf. +type runeTee struct { + in interface { + io.Reader + io.ByteReader + io.RuneReader + } + buf bytes.Buffer +} + +func (rtee *runeTee) Read(b []byte) (n int, err error) { + n, err = rtee.in.Read(b) + rtee.buf.Write(b[:n]) + return n, err +} + +func (rtee *runeTee) ReadRune() (r rune, size int, err error) { + r, size, err = rtee.in.ReadRune() + if err == nil { + rtee.buf.WriteRune(r) + } + return r, size, err +} + +func (rtee *runeTee) ReadByte() (b byte, err error) { + b, err = rtee.in.ReadByte() + if err == nil { + rtee.buf.WriteByte(b) + } + return b, err +} diff --git a/cmd/geth/testdata/empty.js b/cmd/geth/testdata/empty.js new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/cmd/geth/testdata/empty.js @@ -0,0 +1 @@ + diff --git a/cmd/geth/testdata/guswallet.json b/cmd/geth/testdata/guswallet.json new file mode 100644 index 000000000..e8ea4f332 --- /dev/null +++ b/cmd/geth/testdata/guswallet.json @@ -0,0 +1,6 @@ +{ + "encseed": "26d87f5f2bf9835f9a47eefae571bc09f9107bb13d54ff12a4ec095d01f83897494cf34f7bed2ed34126ecba9db7b62de56c9d7cd136520a0427bfb11b8954ba7ac39b90d4650d3448e31185affcd74226a68f1e94b1108e6e0a4a91cdd83eba", + "ethaddr": "d4584b5f6229b7be90727b0fc8c6b91bb427821f", + "email": "gustav.simonsson@gmail.com", + "btcaddr": "1EVknXyFC68kKNLkh6YnKzW41svSRoaAcx" +} diff --git a/cmd/geth/testdata/passwords.txt b/cmd/geth/testdata/passwords.txt new file mode 100644 index 000000000..96f98c7f4 --- /dev/null +++ b/cmd/geth/testdata/passwords.txt @@ -0,0 +1,3 @@ +foobar +foobar +foobar diff --git a/cmd/geth/testdata/wrong-passwords.txt b/cmd/geth/testdata/wrong-passwords.txt new file mode 100644 index 000000000..7d1e338bb --- /dev/null +++ b/cmd/geth/testdata/wrong-passwords.txt @@ -0,0 +1,3 @@ +wrong +wrong +wrong diff --git a/cmd/gethrpctest/main.go b/cmd/gethrpctest/main.go index 4843f8658..2e07e9426 100644 --- a/cmd/gethrpctest/main.go +++ b/cmd/gethrpctest/main.go @@ -108,24 +108,23 @@ func MakeSystemNode(keydir string, privkey string, test *tests.BlockTest) (*node return nil, err } // Create the keystore and inject an unlocked account if requested - keystore := crypto.NewKeyStorePassphrase(keydir, crypto.StandardScryptN, crypto.StandardScryptP) - accman := accounts.NewManager(keystore) - + accman := accounts.NewPlaintextManager(keydir) if len(privkey) > 0 { key, err := crypto.HexToECDSA(privkey) if err != nil { return nil, err } - if err := keystore.StoreKey(crypto.NewKeyFromECDSA(key), ""); err != nil { + a, err := accman.ImportECDSA(key, "") + if err != nil { return nil, err } - if err := accman.Unlock(crypto.NewKeyFromECDSA(key).Address, ""); err != nil { + if err := accman.Unlock(a, ""); err != nil { return nil, err } } // Initialize and register the Ethereum protocol db, _ := ethdb.NewMemDatabase() - if _, err := test.InsertPreState(db, accman); err != nil { + if _, err := test.InsertPreState(db); err != nil { return nil, err } ethConf := ð.Config{ diff --git a/cmd/utils/cmd.go b/cmd/utils/cmd.go index eb7907b0c..d331f762f 100644 --- a/cmd/utils/cmd.go +++ b/cmd/utils/cmd.go @@ -18,13 +18,11 @@ package utils import ( - "bufio" "fmt" "io" "os" "os/signal" "regexp" - "strings" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" @@ -34,17 +32,12 @@ import ( "github.com/ethereum/go-ethereum/logger/glog" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/rlp" - "github.com/peterh/liner" ) const ( importBatchSize = 2500 ) -var ( - interruptCallbacks = []func(os.Signal){} -) - func openLogFile(Datadir string, filename string) *os.File { path := common.AbsolutePath(Datadir, filename) file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) @@ -54,49 +47,6 @@ func openLogFile(Datadir string, filename string) *os.File { return file } -func PromptConfirm(prompt string) (bool, error) { - var ( - input string - err error - ) - prompt = prompt + " [y/N] " - - // if liner.TerminalSupported() { - // fmt.Println("term") - // lr := liner.NewLiner() - // defer lr.Close() - // input, err = lr.Prompt(prompt) - // } else { - fmt.Print(prompt) - input, err = bufio.NewReader(os.Stdin).ReadString('\n') - fmt.Println() - // } - - if len(input) > 0 && strings.ToUpper(input[:1]) == "Y" { - return true, nil - } else { - return false, nil - } - - return false, err -} - -func PromptPassword(prompt string, warnTerm bool) (string, error) { - if liner.TerminalSupported() { - lr := liner.NewLiner() - defer lr.Close() - return lr.PasswordPrompt(prompt) - } - if warnTerm { - fmt.Println("!! Unsupported terminal, password will be echoed.") - } - fmt.Print(prompt) - input, err := bufio.NewReader(os.Stdin).ReadString('\n') - input = strings.TrimRight(input, "\r\n") - fmt.Println() - return input, err -} - // Fatalf formats a message to standard error and exits the program. // The message is also printed to standard output if standard error // is redirected to a different file. diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 1d70245ab..ceed04cd3 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -551,45 +551,36 @@ func MakeDatabaseHandles() int { // MakeAccountManager creates an account manager from set command line flags. func MakeAccountManager(ctx *cli.Context) *accounts.Manager { // Create the keystore crypto primitive, light if requested - scryptN := crypto.StandardScryptN - scryptP := crypto.StandardScryptP - + scryptN := accounts.StandardScryptN + scryptP := accounts.StandardScryptP if ctx.GlobalBool(LightKDFFlag.Name) { - scryptN = crypto.LightScryptN - scryptP = crypto.LightScryptP + scryptN = accounts.LightScryptN + scryptP = accounts.LightScryptP } - // Assemble an account manager using the configured datadir - var ( - datadir = MustMakeDataDir(ctx) - keystoredir = MakeKeyStoreDir(datadir, ctx) - keystore = crypto.NewKeyStorePassphrase(keystoredir, scryptN, scryptP) - ) - return accounts.NewManager(keystore) + datadir := MustMakeDataDir(ctx) + keydir := MakeKeyStoreDir(datadir, ctx) + return accounts.NewManager(keydir, scryptN, scryptP) } // MakeAddress converts an account specified directly as a hex encoded string or // a key index in the key store to an internal account representation. -func MakeAddress(accman *accounts.Manager, account string) (a common.Address, err error) { +func MakeAddress(accman *accounts.Manager, account string) (accounts.Account, error) { // If the specified account is a valid address, return it if common.IsHexAddress(account) { - return common.HexToAddress(account), nil + return accounts.Account{Address: common.HexToAddress(account)}, nil } // Otherwise try to interpret the account as a keystore index index, err := strconv.Atoi(account) if err != nil { - return a, fmt.Errorf("invalid account address or index %q", account) - } - hex, err := accman.AddressByIndex(index) - if err != nil { - return a, fmt.Errorf("can't get account #%d (%v)", index, err) + return accounts.Account{}, fmt.Errorf("invalid account address or index %q", account) } - return common.HexToAddress(hex), nil + return accman.AccountByIndex(index) } // MakeEtherbase retrieves the etherbase either from the directly specified // command line flags or from the keystore if CLI indexed. func MakeEtherbase(accman *accounts.Manager, ctx *cli.Context) common.Address { - accounts, _ := accman.Accounts() + accounts := accman.Accounts() if !ctx.GlobalIsSet(EtherbaseFlag.Name) && len(accounts) == 0 { glog.V(logger.Error).Infoln("WARNING: No etherbase set and no accounts found as default") return common.Address{} @@ -599,11 +590,11 @@ func MakeEtherbase(accman *accounts.Manager, ctx *cli.Context) common.Address { return common.Address{} } // If the specified etherbase is a valid address, return it - addr, err := MakeAddress(accman, etherbase) + account, err := MakeAddress(accman, etherbase) if err != nil { Fatalf("Option %q: %v", EtherbaseFlag.Name, err) } - return addr + return account.Address } // MakeMinerExtra resolves extradata for the miner from the set command line flags @@ -615,17 +606,22 @@ func MakeMinerExtra(extra []byte, ctx *cli.Context) []byte { return extra } -// MakePasswordList loads up a list of password from a file specified by the -// command line flags. +// MakePasswordList reads password lines from the file specified by --password. func MakePasswordList(ctx *cli.Context) []string { - if path := ctx.GlobalString(PasswordFileFlag.Name); path != "" { - blob, err := ioutil.ReadFile(path) - if err != nil { - Fatalf("Failed to read password file: %v", err) - } - return strings.Split(string(blob), "\n") + path := ctx.GlobalString(PasswordFileFlag.Name) + if path == "" { + return nil + } + text, err := ioutil.ReadFile(path) + if err != nil { + Fatalf("Failed to read password file: %v", err) + } + lines := strings.Split(string(text), "\n") + // Sanitise DOS line endings. + for i := range lines { + lines[i] = strings.TrimRight(lines[i], "\r") } - return nil + return lines } // MakeSystemNode sets up a local node, configures the services to launch and diff --git a/cmd/utils/input.go b/cmd/utils/input.go new file mode 100644 index 000000000..523d5a587 --- /dev/null +++ b/cmd/utils/input.go @@ -0,0 +1,98 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. + +package utils + +import ( + "fmt" + "strings" + + "github.com/peterh/liner" +) + +// Holds the stdin line reader. +// Only this reader may be used for input because it keeps +// an internal buffer. +var Stdin = newUserInputReader() + +type userInputReader struct { + *liner.State + warned bool + supported bool + normalMode liner.ModeApplier + rawMode liner.ModeApplier +} + +func newUserInputReader() *userInputReader { + r := new(userInputReader) + // Get the original mode before calling NewLiner. + // This is usually regular "cooked" mode where characters echo. + normalMode, _ := liner.TerminalMode() + // Turn on liner. It switches to raw mode. + r.State = liner.NewLiner() + rawMode, err := liner.TerminalMode() + if err != nil || !liner.TerminalSupported() { + r.supported = false + } else { + r.supported = true + r.normalMode = normalMode + r.rawMode = rawMode + // Switch back to normal mode while we're not prompting. + normalMode.ApplyMode() + } + return r +} + +func (r *userInputReader) Prompt(prompt string) (string, error) { + if r.supported { + r.rawMode.ApplyMode() + defer r.normalMode.ApplyMode() + } else { + // liner tries to be smart about printing the prompt + // and doesn't print anything if input is redirected. + // Un-smart it by printing the prompt always. + fmt.Print(prompt) + prompt = "" + defer fmt.Println() + } + return r.State.Prompt(prompt) +} + +func (r *userInputReader) PasswordPrompt(prompt string) (passwd string, err error) { + if r.supported { + r.rawMode.ApplyMode() + defer r.normalMode.ApplyMode() + return r.State.PasswordPrompt(prompt) + } + if !r.warned { + fmt.Println("!! Unsupported terminal, password will be echoed.") + r.warned = true + } + // Just as in Prompt, handle printing the prompt here instead of relying on liner. + fmt.Print(prompt) + passwd, err = r.State.Prompt("") + fmt.Println() + return passwd, err +} + +func (r *userInputReader) ConfirmPrompt(prompt string) (bool, error) { + prompt = prompt + " [y/N] " + input, err := r.Prompt(prompt) + if len(input) > 0 && strings.ToUpper(input[:1]) == "Y" { + return true, nil + } + return false, err +} diff --git a/cmd/utils/jeth.go b/cmd/utils/jeth.go index 35fcd4bed..708d457c6 100644 --- a/cmd/utils/jeth.go +++ b/cmd/utils/jeth.go @@ -75,8 +75,9 @@ func (self *Jeth) UnlockAccount(call otto.FunctionCall) (response otto.Value) { // if password is not given or as null value -> ask user for password if call.Argument(1).IsUndefined() || call.Argument(1).IsNull() { fmt.Printf("Unlock account %s\n", account) - if password, err := PromptPassword("Passphrase: ", true); err == nil { - passwd, _ = otto.ToValue(password) + if input, err := Stdin.PasswordPrompt("Passphrase: "); err != nil { + return otto.FalseValue() + passwd, _ = otto.ToValue(input) } else { throwJSExeception(err.Error()) } @@ -111,11 +112,11 @@ func (self *Jeth) NewAccount(call otto.FunctionCall) (response otto.Value) { var passwd string if len(call.ArgumentList) == 0 { var err error - passwd, err = PromptPassword("Passphrase: ", true) + passwd, err = Stdin.PasswordPrompt("Passphrase: ") if err != nil { return otto.FalseValue() } - passwd2, err := PromptPassword("Repeat passphrase: ", true) + passwd2, err := Stdin.PasswordPrompt("Repeat passphrase: ") if err != nil { return otto.FalseValue() } |