From ef61625893852ec83c4e4bcc93289b938f2f9428 Mon Sep 17 00:00:00 2001 From: Holger Nahrstaedt <holger@nahrstaedt.de> Date: Fri, 27 Apr 2018 13:50:01 +0200 Subject: [PATCH] Added password_storage to config for managing keyring/environment for walletpassword cli * set password_storage keyring or set password_storage environment added. password_storage = environment is the default behavior and compatible with all old version. * when keyring is used, the wallet password can be stored with the keyring module. * nextnode when the first node and the current url not matches, will this command changes the nodes list, in way that the first node and the current url matches * nextnode skipps now not working nodes * createwallet improved and keyring added * changewalletpassphrase improved steem * set_password_storage added to set the password_storage config * move_current_node_to_front function added, which shifts the nodes until current url and first node matches storage * add default for password_storage wallet * password_storage is used unit tess * test_cli adapted * test_market adapted on changes --- beem/cli.py | 137 ++++++++++++++++++++++++++++---------- beem/steem.py | 27 ++++++++ beem/storage.py | 1 + beem/wallet.py | 17 +++-- tests/beem/test_cli.py | 10 ++- tests/beem/test_market.py | 6 +- 6 files changed, 154 insertions(+), 44 deletions(-) diff --git a/beem/cli.py b/beem/cli.py index a2a74f1b..2223bc66 100644 --- a/beem/cli.py +++ b/beem/cli.py @@ -40,7 +40,10 @@ click.disable_unicode_literals_warning = True log = logging.getLogger(__name__) try: import keyring - KEYRING_AVAILABLE = True + if not isinstance(keyring.get_keyring(), keyring.backends.fail.Keyring): + KEYRING_AVAILABLE = True + else: + KEYRING_AVAILABLE = False except ImportError: KEYRING_AVAILABLE = False @@ -49,6 +52,7 @@ availableConfigurationKeys = [ "default_account", "default_vote_weight", "nodes", + "password_storage" ] @@ -74,9 +78,10 @@ def prompt_flag_callback(ctx, param, value): def unlock_wallet(stm, password=None): - if not password and KEYRING_AVAILABLE: + password_storage = stm.config["password_storage"] + if not password and KEYRING_AVAILABLE and password_storage == "keyring": password = keyring.get_password("beem", "wallet") - if not password: + if not password and password_storage == "environment": password = os.environ.get("UNLOCK") if bool(password): stm.wallet.unlock(password) @@ -85,7 +90,12 @@ def unlock_wallet(stm, password=None): stm.wallet.unlock(password) if stm.wallet.locked(): - print("Wallet could not be unlocked!") + if password_storage == "keyring" or password_storage == "environment": + print("Wallet could not be unlocked with %s!" % password_storage) + password = click.prompt("Password to unlock wallet", confirmation_prompt=False, hide_input=True) + unlock_wallet(stm, password=password) + else: + print("Wallet could not be unlocked!") return False else: print("Wallet Unlocked!") @@ -161,21 +171,55 @@ def set(key, value): stm.set_default_nodes(value) else: stm.set_default_nodes("") + elif key == "password_storage": + stm.config["password_storage"] = value + if KEYRING_AVAILABLE and value == "keyring": + password = click.prompt("Password to unlock wallet (Will be stored in keyring)", confirmation_prompt=False, hide_input=True) + password = keyring.set_password("beem", "wallet", password) + elif KEYRING_AVAILABLE and value != "keyring": + try: + keyring.delete_password("beem", "wallet") + except keyring.errors.PasswordDeleteError: + print("") + if value == "environment": + print("The wallet password can be stored in the UNLOCK invironment variable to skip password prompt!") else: print("wrong key") @cli.command() -def nextnode(): +@click.option('--results', is_flag=True, default=False, help="Shows result of changing the node.") +def nextnode(results): """ Uses the next node in list """ stm = shared_steem_instance() + stm.move_current_node_to_front() node = stm.get_default_nodes() + offline = stm.offline if len(node) < 2: print("At least two nodes are needed!") return node = node[1:] + [node[0]] + if not offline: + stm.rpc.next() + stm.get_blockchain_version() + while not offline and node[0] != stm.rpc.url and len(node) > 1: + node = node[1:] + [node[0]] stm.set_default_nodes(node) + if not results: + return + + t = PrettyTable(["Key", "Value"]) + t.align = "l" + if not offline: + t.add_row(["Node-Url", stm.rpc.url]) + else: + t.add_row(["Node-Url", node[0]]) + if not offline: + t.add_row(["Version", stm.get_blockchain_version()]) + else: + t.add_row(["Version", "steempy is in offline mode..."]) + print(t) @cli.command() @@ -208,19 +252,31 @@ def pingnode(raw): '--url', is_flag=True, default=False, help="Returns only the raw url value") def currentnode(version, url): - """ Returns the current node + """ Sets the currently working node at the first place in the list """ stm = shared_steem_instance() - if version: + offline = stm.offline + stm.move_current_node_to_front() + node = stm.get_default_nodes() + if version and not offline: print(stm.get_blockchain_version()) return - if url: + elif version and offline: + print("Node is offline") + return + if url and not offline: print(stm.rpc.url) return t = PrettyTable(["Key", "Value"]) t.align = "l" - t.add_row(["Node-Url", stm.rpc.url]) - t.add_row(["Version", stm.get_blockchain_version()]) + if not offline: + t.add_row(["Node-Url", stm.rpc.url]) + else: + t.add_row(["Node-Url", node[0]]) + if not offline: + t.add_row(["Version", stm.get_blockchain_version()]) + else: + t.add_row(["Version", "steempy is in offline mode..."]) print(t) @@ -233,36 +289,39 @@ def config(): t.align = "l" for key in stm.config: # hide internal config data - if key in availableConfigurationKeys: + if key in availableConfigurationKeys and key != "nodes" and key != "node": t.add_row([key, stm.config[key]]) node = stm.get_default_nodes() nodes = json.dumps(node, indent=4) t.add_row(["nodes", nodes]) + if "password_storage" not in availableConfigurationKeys: + t.add_row(["password_storage", stm.config["password_storage"]]) t.add_row(["data_dir", stm.config.data_dir]) print(t) @cli.command() -@click.option( - '--wipe', is_flag=True, default=False, - prompt='Do you want to wipe your wallet? Are your sure? This is IRREVERSIBLE! If you dont have a backup you may lose access to your account!', - help="Delete old wallet. All wallet data are deleted!") -@click.option('--password', prompt=True, hide_input=True, - confirmation_prompt=True) -def createwallet(wipe, password): - """ Create new wallet with password +def createwallet(): + """ Create new wallet with a new password """ stm = shared_steem_instance() - if wipe: - stm.wallet.wipe(True) - if not password and KEYRING_AVAILABLE: - password = keyring.get_password("beem", "wallet") - if not password: - password = os.environ.get("UNLOCK") - if bool(password): - stm.wallet.unlock(password) - else: - password = click.prompt("Password to unlock wallet", confirmation_prompt=False, hide_input=True) + if stm.wallet.created(): + wipe_answer = click.prompt("'Do you want to wipe your wallet? Are your sure? This is IRREVERSIBLE! If you dont have a backup you may lose access to your account! [y/n]", + default="n") + if wipe_answer in ["y", "ye", "yes"]: + stm.wallet.wipe(True) + else: + return + password = None + password = click.prompt("New wallet password", confirmation_prompt=True, hide_input=True) + if not bool(password): + print("Password cannot be empty! Quitting...") + return + password_storage = stm.config["password_storage"] + if KEYRING_AVAILABLE and password_storage == "keyring": + password = keyring.set_password("beem", "wallet", password) + elif password_storage == "environment": + print("The new wallet password can be stored in the UNLOCK invironment variable to skip password prompt!") stm.wallet.create(password) set_shared_steem_instance(stm) @@ -279,6 +338,8 @@ def walletinfo(test_unlock): t.add_row(["locked", stm.wallet.locked()]) t.add_row(["Number of stored keys", len(stm.wallet.getPublicKeys())]) t.add_row(["sql-file", stm.wallet.keyStorage.sqlDataBaseFile]) + password_storage = stm.config["password_storage"] + t.add_row(["password_storage", password_storage]) password = os.environ.get("UNLOCK") if password is not None: t.add_row(["UNLOCK env set", "yes"]) @@ -548,16 +609,22 @@ def convert(amount, account): @cli.command() -@click.option('--oldpassword', prompt=True, hide_input=True, - confirmation_prompt=False, help='Password to unlock wallet') -@click.option('--newpassword', prompt=True, hide_input=True, - confirmation_prompt=True, help='Set new password to unlock wallet') -def changewalletpassphrase(oldpassword, newpassword): +def changewalletpassphrase(): """ Change wallet password """ stm = shared_steem_instance() - if not unlock_wallet(stm, oldpassword): + if not unlock_wallet(stm): + return + newpassword = None + newpassword = click.prompt("New wallet password", confirmation_prompt=True, hide_input=True) + if not bool(newpassword): + print("Password cannot be empty! Quitting...") return + password_storage = stm.config["password_storage"] + if KEYRING_AVAILABLE and password_storage == "keyring": + keyring.set_password("beem", "wallet", newpassword) + elif password_storage == "environment": + print("The new wallet password can be stored in the UNLOCK invironment variable to skip password prompt!") stm.wallet.changePassphrase(newpassword) diff --git a/beem/steem.py b/beem/steem.py index 2b5fa6d7..7c1bdb7a 100644 --- a/beem/steem.py +++ b/beem/steem.py @@ -575,6 +575,21 @@ class Steem(object): Account(account, steem_instance=self) config["default_account"] = account + def set_password_storage(self, password_storage): + """ Set the password storage mode. + + When set to "no", the password has to provided everytime. + When set to "environment" the password is taken from the + UNLOCK variable + When set to "keyring" the password is taken from the + python keyring module. A wallet password can be stored with + python -m keyring set beem wallet password + :param str password_storage: can be "no", + "keyring" or "environment" + + """ + config["password_storage"] = password_storage + def set_default_nodes(self, nodes): """ Set the default nodes to be used """ @@ -599,6 +614,18 @@ class Steem(object): nodes = ast.literal_eval(nodes) return nodes + def move_current_node_to_front(self): + """Returns the default node list, until the first entry + is equal to the current working node url + """ + node = self.get_default_nodes() + if len(node) < 2: + return + offline = self.offline + while not offline and node[0] != self.rpc.url and len(node) > 1: + node = node[1:] + [node[0]] + self.set_default_nodes(node) + def set_default_vote_weight(self, vote_weight): """ Set the default vote weight to be used """ diff --git a/beem/storage.py b/beem/storage.py index 1e8b5f35..3d19bac2 100644 --- a/beem/storage.py +++ b/beem/storage.py @@ -276,6 +276,7 @@ class Configuration(DataDir): nodes = get_node_list(appbase=False) + get_node_list(appbase=True) config_defaults = { "node": nodes, + "password_storage": "environment", "rpcpassword": "", "rpcuser": "", "order-expiration": 7 * 24 * 60 * 60} diff --git a/beem/wallet.py b/beem/wallet.py index 5676a7f4..f2899596 100644 --- a/beem/wallet.py +++ b/beem/wallet.py @@ -24,7 +24,10 @@ from beemapi.exceptions import NoAccessApi from .storage import configStorage as config try: import keyring - KEYRING_AVAILABLE = True + if not isinstance(keyring.get_keyring(), keyring.backends.fail.Keyring): + KEYRING_AVAILABLE = True + else: + KEYRING_AVAILABLE = False except ImportError: KEYRING_AVAILABLE = False @@ -177,17 +180,19 @@ class Wallet(object): self.masterpassword = self.masterpwd.decrypted_master def tryUnlockFromEnv(self): - """Try to fetch the unlock password first from 'UNLOCK' environment variable - and then from the keyring module keyring.get_password('beem', 'wallet') - + """Try to fetch the unlock password first from 'UNLOCK' environment variable. + This is only done, when steem.config['password_storage'] == 'environment'. + and then from the keyring module keyring.get_password('beem', 'wallet'), + when steem.config['password_storage'] == 'keyring' In order to use this, you have to store the password in the 'UNLOCK' variable or in the keyring by python -m keyring set beem wallet """ - if "UNLOCK" in os.environ: + password_storage = self.steem.config["password_storage"] + if password_storage == "environment" and "UNLOCK" in os.environ: log.debug("Trying to use environmental variable to unlock wallet") pwd = os.environ.get("UNLOCK") self.unlock(pwd) - elif KEYRING_AVAILABLE: + elif password_storage == "keyring" and KEYRING_AVAILABLE: log.debug("Trying to use keyring to unlock wallet") pwd = keyring.get_password("beem", "wallet") else: diff --git a/tests/beem/test_cli.py b/tests/beem/test_cli.py index c8b7299c..fa91b152 100644 --- a/tests/beem/test_cli.py +++ b/tests/beem/test_cli.py @@ -256,12 +256,14 @@ class Testcases(unittest.TestCase): def test_orderbook(self): runner = CliRunner() + runner.invoke(cli, ['set', 'nodes', '']) result = runner.invoke(cli, ['orderbook']) self.assertEqual(result.exit_code, 0) - result = runner.invoke(cli, ['orderbook', '--date']) + result = runner.invoke(cli, ['orderbook', '--show-date']) self.assertEqual(result.exit_code, 0) result = runner.invoke(cli, ['orderbook', '--chart']) self.assertEqual(result.exit_code, 0) + runner.invoke(cli, ['set', 'nodes', 'wss://testnet.steem.vc']) def test_buy(self): runner = CliRunner() @@ -345,12 +347,14 @@ class Testcases(unittest.TestCase): def test_currentnode(self): runner = CliRunner() + runner.invoke(cli, ['set', 'nodes', '']) result = runner.invoke(cli, ['currentnode']) self.assertEqual(result.exit_code, 0) result = runner.invoke(cli, ['currentnode', '--url']) self.assertEqual(result.exit_code, 0) result = runner.invoke(cli, ['currentnode', '--version']) self.assertEqual(result.exit_code, 0) + runner.invoke(cli, ['set', 'nodes', 'wss://testnet.steem.vc']) def test_ticker(self): runner = CliRunner() @@ -359,10 +363,14 @@ class Testcases(unittest.TestCase): def test_pricehistory(self): runner = CliRunner() + runner.invoke(cli, ['set', 'nodes', '']) result = runner.invoke(cli, ['pricehistory']) self.assertEqual(result.exit_code, 0) + runner.invoke(cli, ['set', 'nodes', 'wss://testnet.steem.vc']) def test_tradehistory(self): runner = CliRunner() + runner.invoke(cli, ['set', 'nodes', '']) result = runner.invoke(cli, ['tradehistory']) self.assertEqual(result.exit_code, 0) + runner.invoke(cli, ['set', 'nodes', 'wss://testnet.steem.vc']) diff --git a/tests/beem/test_market.py b/tests/beem/test_market.py index 35ebd79e..b8e7bd50 100644 --- a/tests/beem/test_market.py +++ b/tests/beem/test_market.py @@ -72,8 +72,8 @@ class Testcases(unittest.TestCase): m = Market(u'STEEM:SBD', steem_instance=bts) ticker = m.ticker() self.assertEqual(len(ticker), 6) - self.assertEqual(ticker['steemVolume']["symbol"], u'STEEM') - self.assertEqual(ticker['sbdVolume']["symbol"], u'SBD') + self.assertEqual(ticker['steem_volume']["symbol"], u'STEEM') + self.assertEqual(ticker['sbd_volume']["symbol"], u'SBD') @parameterized.expand([ ("non_appbase"), @@ -132,7 +132,9 @@ class Testcases(unittest.TestCase): m = Market(u'STEEM:SBD', steem_instance=bts) trades = m.trades(limit=10) trades_raw = m.trades(limit=10, raw_data=True) + trades_history = m.trade_history(limit=10) self.assertEqual(len(trades), 10) + self.assertEqual(len(trades_history), 10) self.assertEqual(len(trades_raw), 10) @parameterized.expand([ -- GitLab