Browse Source

Merge #1160: Fix shutdown of already open wallet on new unlock

ed7b4e1 Fix bugs in unlock-lock logic (Adam Gibson)
master
Adam Gibson 4 years ago
parent
commit
2ca1fbc8c4
No known key found for this signature in database
GPG Key ID: 141001A1AF77F20B
  1. 145
      jmclient/jmclient/wallet_rpc.py
  2. 12
      jmclient/test/test_wallet_rpc.py

145
jmclient/jmclient/wallet_rpc.py

@ -143,8 +143,6 @@ class JMWalletDaemon(Service):
# allow the client to start/stop. # allow the client to start/stop.
self.services["wallet"] = None self.services["wallet"] = None
self.wallet_name = "None" self.wallet_name = "None"
# label for convenience:
self.wallet_service = self.services["wallet"]
# Client may start other services, but only # Client may start other services, but only
# one instance. # one instance.
self.services["snicker"] = None self.services["snicker"] = None
@ -324,6 +322,23 @@ class JMWalletDaemon(Service):
request_cookie) + ", request rejected.") request_cookie) + ", request rejected.")
raise NotAuthorized() raise NotAuthorized()
def set_token(self, wallet_name):
""" This function creates a new JWT token and sets it as our
'cookie' for API and WS. Note this always creates a new fresh token,
there is no option to manually set it, intentionally.
"""
# any random secret is OK, as long as it is not deducible/predictable:
secret_key = bintohex(os.urandom(16))
encoded_token = jwt.encode({"wallet": wallet_name,
"exp" :datetime.datetime.utcnow(
)+datetime.timedelta(minutes=30)},
secret_key)
self.cookie = encoded_token.strip()
# We want to make sure that any websocket clients use the correct
# token. The wss_factory should have been created on JMWalletDaemon
# startup, so any failure to exist here is a logic error:
self.wss_factory.valid_token = self.cookie
def get_POST_body(self, request, keys): def get_POST_body(self, request, keys):
""" given a request object, retrieve values corresponding """ given a request object, retrieve values corresponding
to keys keys in a dict, assuming they were encoded using JSON. to keys keys in a dict, assuming they were encoded using JSON.
@ -349,24 +364,15 @@ class JMWalletDaemon(Service):
Here we must also register transaction update callbacks, to fire Here we must also register transaction update callbacks, to fire
events in the websocket connection. events in the websocket connection.
""" """
if self.wallet_service: if self.services["wallet"]:
# we allow a new successful authorization (with password) # we allow a new successful authorization (with password)
# to shut down the currently running service(s), if there # to shut down the currently running service(s), if there
# are any. # are any.
# This will stop all supporting services and wipe # This will stop all supporting services and wipe
# state (so wallet, maker service and cookie/token): # state (so wallet, maker service and cookie/token):
self.stopService() self.stopService()
# any random secret is OK, as long as it is not deducible/predictable:
secret_key = bintohex(os.urandom(16)) self.services["wallet"] = WalletService(wallet)
encoded_token = jwt.encode({"wallet": wallet_name,
"exp" :datetime.datetime.utcnow(
)+datetime.timedelta(minutes=30)},
secret_key)
encoded_token = encoded_token.strip()
self.cookie = encoded_token
if self.cookie is None:
raise NotAuthorized("No cookie")
self.wallet_service = WalletService(wallet)
# restart callback needed, otherwise wallet creation will # restart callback needed, otherwise wallet creation will
# automatically lead to shutdown. # automatically lead to shutdown.
# TODO: this means that it's possible, in non-standard usage # TODO: this means that it's possible, in non-standard usage
@ -375,28 +381,26 @@ class JMWalletDaemon(Service):
# or requesting rescans, none are implemented yet. # or requesting rescans, none are implemented yet.
def dummy_restart_callback(msg): def dummy_restart_callback(msg):
jlog.warn("Ignoring rescan request from backend wallet service: " + msg) jlog.warn("Ignoring rescan request from backend wallet service: " + msg)
self.wallet_service.add_restart_callback(dummy_restart_callback) self.services["wallet"].add_restart_callback(dummy_restart_callback)
self.wallet_name = wallet_name self.wallet_name = wallet_name
self.wallet_service.register_callbacks( self.services["wallet"].register_callbacks(
[self.wss_factory.sendTxNotification], None) [self.wss_factory.sendTxNotification], None)
self.wallet_service.startService() self.services["wallet"].startService()
# now that the base WalletService is started, we want to
# make sure that any websocket clients use the correct
# token. The wss_factory should have been created on JMWalletDaemon
# startup, so any failure to exist here is a logic error:
self.wss_factory.valid_token = encoded_token
# now that the WalletService instance is active and ready to # now that the WalletService instance is active and ready to
# respond to requests, we return the status to the client: # respond to requests, we return the status to the client:
# First, prepare authentication for the calling client:
self.set_token(wallet_name)
if('seedphrase' in kwargs): if('seedphrase' in kwargs):
return make_jmwalletd_response(request, return make_jmwalletd_response(request,
status=201, status=201,
walletname=self.wallet_name, walletname=self.wallet_name,
token=encoded_token, token=self.cookie,
seedphrase=kwargs.get('seedphrase')) seedphrase=kwargs.get('seedphrase'))
else: else:
return make_jmwalletd_response(request, return make_jmwalletd_response(request,
walletname=self.wallet_name, walletname=self.wallet_name,
token=encoded_token) token=self.cookie)
def taker_finished(self, res, fromtx=False, waittime=0.0, txdetails=None): def taker_finished(self, res, fromtx=False, waittime=0.0, txdetails=None):
# This is a slimmed down version compared with what is seen in # This is a slimmed down version compared with what is seen in
@ -468,14 +472,14 @@ class JMWalletDaemon(Service):
def displaywallet(self, request, walletname): def displaywallet(self, request, walletname):
print_req(request) print_req(request)
self.check_cookie(request) self.check_cookie(request)
if not self.wallet_service: if not self.services["wallet"]:
jlog.warn("displaywallet called, but no wallet loaded") jlog.warn("displaywallet called, but no wallet loaded")
raise NoWalletFound() raise NoWalletFound()
if not self.wallet_name == walletname: if not self.wallet_name == walletname:
jlog.warn("called displaywallet with wrong wallet") jlog.warn("called displaywallet with wrong wallet")
raise InvalidRequestFormat() raise InvalidRequestFormat()
else: else:
walletinfo = wallet_display(self.wallet_service, False, jsonified=True) walletinfo = wallet_display(self.services["wallet"], False, jsonified=True)
return make_jmwalletd_response(request, walletname=walletname, walletinfo=walletinfo) return make_jmwalletd_response(request, walletname=walletname, walletinfo=walletinfo)
@app.route('/session', methods=['GET']) @app.route('/session', methods=['GET'])
@ -489,8 +493,8 @@ class JMWalletDaemon(Service):
session = not self.cookie==None session = not self.cookie==None
maker_running = self.coinjoin_state == CJ_MAKER_RUNNING maker_running = self.coinjoin_state == CJ_MAKER_RUNNING
coinjoin_in_process = self.coinjoin_state == CJ_TAKER_RUNNING coinjoin_in_process = self.coinjoin_state == CJ_TAKER_RUNNING
if self.wallet_service: if self.services["wallet"]:
if self.wallet_service.isRunning(): if self.services["wallet"].isRunning():
wallet_name = self.wallet_name wallet_name = self.wallet_name
else: else:
wallet_name = "not yet loaded" wallet_name = "not yet loaded"
@ -512,12 +516,12 @@ class JMWalletDaemon(Service):
"destination"]) "destination"])
if not payment_info_json: if not payment_info_json:
raise InvalidRequestFormat() raise InvalidRequestFormat()
if not self.wallet_service: if not self.services["wallet"]:
raise NoWalletFound() raise NoWalletFound()
if not self.wallet_name == walletname: if not self.wallet_name == walletname:
raise InvalidRequestFormat() raise InvalidRequestFormat()
try: try:
tx = direct_send(self.wallet_service, tx = direct_send(self.services["wallet"],
int(payment_info_json["amount_sats"]), int(payment_info_json["amount_sats"]),
int(payment_info_json["mixdepth"]), int(payment_info_json["mixdepth"]),
destination=payment_info_json["destination"], destination=payment_info_json["destination"],
@ -542,7 +546,7 @@ class JMWalletDaemon(Service):
"ordertype", "minsize"]) "ordertype", "minsize"])
if not config_json: if not config_json:
raise InvalidRequestFormat() raise InvalidRequestFormat()
if not self.wallet_service: if not self.services["wallet"]:
raise NoWalletFound() raise NoWalletFound()
if not self.wallet_name == walletname: if not self.wallet_name == walletname:
raise InvalidRequestFormat() raise InvalidRequestFormat()
@ -563,7 +567,7 @@ class JMWalletDaemon(Service):
config_json["cjfee_factor"] = None config_json["cjfee_factor"] = None
config_json["size_factor"] = None config_json["size_factor"] = None
self.services["maker"] = YieldGeneratorService(self.wallet_service, self.services["maker"] = YieldGeneratorService(self.services["wallet"],
dhost, dport, dhost, dport,
[config_json[x] for x in ["txfee", "cjfee_a", [config_json[x] for x in ["txfee", "cjfee_a",
"cjfee_r", "ordertype", "minsize", "cjfee_r", "ordertype", "minsize",
@ -585,7 +589,7 @@ class JMWalletDaemon(Service):
# sync has already happened (this is different from CLI yg). # sync has already happened (this is different from CLI yg).
# note: an edge case of dusty amounts is lost here; it will get # note: an edge case of dusty amounts is lost here; it will get
# picked up by Maker.try_to_create_my_orders(). # picked up by Maker.try_to_create_my_orders().
if not len(self.wallet_service.get_balance_by_mixdepth( if not len(self.services["wallet"].get_balance_by_mixdepth(
verbose=False, minconfs=1)) > 0: verbose=False, minconfs=1)) > 0:
raise NotEnoughCoinsForMaker() raise NotEnoughCoinsForMaker()
@ -600,7 +604,7 @@ class JMWalletDaemon(Service):
@app.route('/wallet/<string:walletname>/maker/stop', methods=['GET']) @app.route('/wallet/<string:walletname>/maker/stop', methods=['GET'])
def stop_maker(self, request, walletname): def stop_maker(self, request, walletname):
self.check_cookie(request) self.check_cookie(request)
if not self.wallet_service: if not self.services["wallet"]:
raise NoWalletFound() raise NoWalletFound()
if not self.wallet_name == walletname: if not self.wallet_name == walletname:
raise InvalidRequestFormat() raise InvalidRequestFormat()
@ -643,19 +647,20 @@ class JMWalletDaemon(Service):
def lockwallet(self, request, walletname): def lockwallet(self, request, walletname):
print_req(request) print_req(request)
self.check_cookie(request) self.check_cookie(request)
if self.wallet_service and not self.wallet_name == walletname: if self.services["wallet"] and not self.wallet_name == walletname:
raise InvalidRequestFormat() raise InvalidRequestFormat()
if not self.wallet_service: if not self.services["wallet"]:
jlog.warn("Called lock, but no wallet loaded") jlog.warn("Called lock, but no wallet loaded")
# we could raise NoWalletFound here, but is # we could raise NoWalletFound here, but is
# easier for clients if they can gracefully call # easier for clients if they can gracefully call
# lock multiple times: # lock multiple times:
already_locked = True already_locked = True
else: else:
self.wallet_service.stopService() self.services["wallet"].stopService()
self.cookie = None self.cookie = None
self.wss_factory.valid_token = None self.wss_factory.valid_token = None
self.wallet_service = None self.services["wallet"] = None
self.wallet_name = None
already_locked = False already_locked = False
return make_jmwalletd_response(request, walletname=walletname, return make_jmwalletd_response(request, walletname=walletname,
already_locked=already_locked) already_locked=already_locked)
@ -666,7 +671,7 @@ class JMWalletDaemon(Service):
# we only handle one wallet at a time; # we only handle one wallet at a time;
# if there is a currently unlocked wallet, # if there is a currently unlocked wallet,
# refuse to process the request: # refuse to process the request:
if self.wallet_service: if self.services["wallet"]:
raise WalletAlreadyUnlocked() raise WalletAlreadyUnlocked()
request_data = self.get_POST_body(request, request_data = self.get_POST_body(request,
["walletname", "password", "wallettype"]) ["walletname", "password", "wallettype"])
@ -716,8 +721,44 @@ class JMWalletDaemon(Service):
if not auth_json: if not auth_json:
raise InvalidRequestFormat() raise InvalidRequestFormat()
password = auth_json["password"] password = auth_json["password"]
wallet_path = get_wallet_path(walletname, None) wallet_path = get_wallet_path(walletname, None)
# for someone trying to re-access the wallet, using their
# password as authentication, and get a fresh token, we still
# need to authenticate against the password, but we cannot directly
# re-open the wallet file (it is currently locked), but we don't
# yet want to bother to shut down services - because if their
# authentication is successful, we can happily leave everything
# running (wallet service and e.g. a yieldgenerator).
# Hence here, if it is the same name, we do a read-only open
# and proceed to issue a new token if the open is successful,
# otherwise error.
if walletname == self.wallet_name:
try:
# returned wallet object is ditched:
open_test_wallet_maybe(
wallet_path, walletname, 4,
password=password.encode("utf-8"),
ask_for_password=False,
read_only=True)
except StoragePasswordError:
# actually effects authentication
raise NotAuthorized()
except StorageError:
# wallet is not openable, this should not happen
raise NoWalletFound()
except Exception:
# wallet file doesn't exist or is wrong format,
# this also shouldn't happen so raise:
raise NoWalletFound()
# no exceptions raised means we just return token:
self.set_token(self.wallet_name)
return make_jmwalletd_response(request,
walletname=self.wallet_name,
token=self.cookie)
# This is a different wallet than the one currently open;
# try to open it, then initialize the service(s):
try: try:
wallet = open_test_wallet_maybe( wallet = open_test_wallet_maybe(
wallet_path, walletname, 4, wallet_path, walletname, 4,
@ -756,7 +797,7 @@ class JMWalletDaemon(Service):
@app.route('/wallet/<string:walletname>/address/new/<string:mixdepth>', methods=['GET']) @app.route('/wallet/<string:walletname>/address/new/<string:mixdepth>', methods=['GET'])
def getaddress(self, request, walletname, mixdepth): def getaddress(self, request, walletname, mixdepth):
self.check_cookie(request) self.check_cookie(request)
if not self.wallet_service: if not self.services["wallet"]:
raise NoWalletFound() raise NoWalletFound()
if not self.wallet_name == walletname: if not self.wallet_name == walletname:
raise InvalidRequestFormat() raise InvalidRequestFormat()
@ -764,19 +805,19 @@ class JMWalletDaemon(Service):
mixdepth = int(mixdepth) mixdepth = int(mixdepth)
except ValueError: except ValueError:
raise InvalidRequestFormat() raise InvalidRequestFormat()
address = self.wallet_service.get_external_addr(mixdepth) address = self.services["wallet"].get_external_addr(mixdepth)
return make_jmwalletd_response(request, address=address) return make_jmwalletd_response(request, address=address)
@app.route('/wallet/<string:walletname>/address/timelock/new/<string:lockdate>', methods=['GET']) @app.route('/wallet/<string:walletname>/address/timelock/new/<string:lockdate>', methods=['GET'])
def gettimelockaddress(self, request, walletname, lockdate): def gettimelockaddress(self, request, walletname, lockdate):
self.check_cookie(request) self.check_cookie(request)
if not self.wallet_service: if not self.services["wallet"]:
raise NoWalletFound() raise NoWalletFound()
if not self.wallet_name == walletname: if not self.wallet_name == walletname:
raise InvalidRequestFormat() raise InvalidRequestFormat()
try: try:
timelockaddress = wallet_gettimelockaddress( timelockaddress = wallet_gettimelockaddress(
self.wallet_service.wallet, lockdate) self.services["wallet"].wallet, lockdate)
except Exception: except Exception:
raise InvalidRequestFormat() raise InvalidRequestFormat()
if timelockaddress == "": if timelockaddress == "":
@ -847,7 +888,7 @@ class JMWalletDaemon(Service):
# note: this does not raise or fail if the applied # note: this does not raise or fail if the applied
# disable state (true/false) is the same as the current # disable state (true/false) is the same as the current
# one; that is accepted and not an error. # one; that is accepted and not an error.
self.wallet_service.disable_utxo(txid, index, to_disable) self.services["wallet"].disable_utxo(txid, index, to_disable)
except AssertionError: except AssertionError:
# should be impossible because format checked by # should be impossible because format checked by
# utxostr_to_utxo: # utxostr_to_utxo:
@ -865,13 +906,13 @@ class JMWalletDaemon(Service):
@app.route('/wallet/<string:walletname>/utxos',methods=['GET']) @app.route('/wallet/<string:walletname>/utxos',methods=['GET'])
def listutxos(self, request, walletname): def listutxos(self, request, walletname):
self.check_cookie(request) self.check_cookie(request)
if not self.wallet_service: if not self.services["wallet"]:
raise NoWalletFound() raise NoWalletFound()
if not self.wallet_name == walletname: if not self.wallet_name == walletname:
raise InvalidRequestFormat() raise InvalidRequestFormat()
# note: the output of `showutxos` is already a string for CLI; # note: the output of `showutxos` is already a string for CLI;
# but we return json: # but we return json:
utxos = json.loads(wallet_showutxos(self.wallet_service, False)) utxos = json.loads(wallet_showutxos(self.services["wallet"], False))
utxos_response = self.get_listutxos_response(utxos) utxos_response = self.get_listutxos_response(utxos)
return make_jmwalletd_response(request, utxos=utxos_response) return make_jmwalletd_response(request, utxos=utxos_response)
@ -879,7 +920,7 @@ class JMWalletDaemon(Service):
@app.route('/wallet/<string:walletname>/taker/stop', methods=['GET']) @app.route('/wallet/<string:walletname>/taker/stop', methods=['GET'])
def stopcoinjoin(self, request, walletname): def stopcoinjoin(self, request, walletname):
self.check_cookie(request) self.check_cookie(request)
if not self.wallet_service: if not self.services["wallet"]:
raise NoWalletFound() raise NoWalletFound()
if not self.wallet_name == walletname: if not self.wallet_name == walletname:
raise InvalidRequestFormat() raise InvalidRequestFormat()
@ -892,7 +933,7 @@ class JMWalletDaemon(Service):
@app.route('/wallet/<string:walletname>/taker/coinjoin', methods=['POST']) @app.route('/wallet/<string:walletname>/taker/coinjoin', methods=['POST'])
def docoinjoin(self, request, walletname): def docoinjoin(self, request, walletname):
self.check_cookie(request) self.check_cookie(request)
if not self.wallet_service: if not self.services["wallet"]:
raise NoWalletFound() raise NoWalletFound()
if not self.wallet_name == walletname: if not self.wallet_name == walletname:
raise InvalidRequestFormat() raise InvalidRequestFormat()
@ -928,7 +969,7 @@ class JMWalletDaemon(Service):
raise ConfigNotPresent() raise ConfigNotPresent()
max_cj_fee= get_max_cj_fee_values(jm_single().config, max_cj_fee= get_max_cj_fee_values(jm_single().config,
None, user_callback=dummy_user_callback) None, user_callback=dummy_user_callback)
self.taker = Taker(self.wallet_service, schedule, self.taker = Taker(self.services["wallet"], schedule,
max_cj_fee = max_cj_fee, max_cj_fee = max_cj_fee,
callbacks=(self.filter_orders_callback, callbacks=(self.filter_orders_callback,
None, self.taker_finished)) None, self.taker_finished))
@ -948,9 +989,9 @@ class JMWalletDaemon(Service):
@app.route('/wallet/<walletname>/getseed', methods=['GET']) @app.route('/wallet/<walletname>/getseed', methods=['GET'])
def getseed(self, request, walletname): def getseed(self, request, walletname):
self.check_cookie(request) self.check_cookie(request)
if not self.wallet_service: if not self.services["wallet"]:
raise NoWalletFound() raise NoWalletFound()
if not self.wallet_name == walletname: if not self.wallet_name == walletname:
raise InvalidRequestFormat() raise InvalidRequestFormat()
seedphrase, _ = self.wallet_service.get_mnemonic_words() seedphrase, _ = self.services["wallet"].get_mnemonic_words()
return make_jmwalletd_response(request, seedphrase=seedphrase) return make_jmwalletd_response(request, seedphrase=seedphrase)

12
jmclient/test/test_wallet_rpc.py

@ -71,11 +71,11 @@ class WalletRPCTestBase(object):
# test down from 9 seconds to 1 minute 40s, which is too slow # test down from 9 seconds to 1 minute 40s, which is too slow
# to be acceptable. TODO: add a test with FB by speeding up # to be acceptable. TODO: add a test with FB by speeding up
# the sync for test, by some means or other. # the sync for test, by some means or other.
self.daemon.wallet_service = make_wallets_to_list(make_wallets( self.daemon.services["wallet"] = make_wallets_to_list(make_wallets(
1, wallet_structures=[wallet_structures[0]], 1, wallet_structures=[wallet_structures[0]],
mean_amt=self.mean_amt, wallet_cls=SegwitWalletFidelityBonds))[0] mean_amt=self.mean_amt, wallet_cls=SegwitWalletFidelityBonds))[0]
jm_single().bc_interface.tickchain() jm_single().bc_interface.tickchain()
sync_wallets([self.daemon.wallet_service]) sync_wallets([self.daemon.services["wallet"]])
# dummy tx example to force a notification event: # dummy tx example to force a notification event:
self.test_tx = CTransaction.deserialize(hextobin(test_tx_hex_1)) self.test_tx = CTransaction.deserialize(hextobin(test_tx_hex_1))
@ -176,7 +176,7 @@ class TrialTestWRPC_DisplayWallet(WalletRPCTestBase, unittest.TestCase):
""" """
# before starting, we have to shut down the existing # before starting, we have to shut down the existing
# wallet service (usually this would be `lock`): # wallet service (usually this would be `lock`):
self.daemon.wallet_service = None self.daemon.services["wallet"] = None
self.daemon.stopService() self.daemon.stopService()
self.daemon.auth_disabled = False self.daemon.auth_disabled = False
@ -268,12 +268,12 @@ class TrialTestWRPC_DisplayWallet(WalletRPCTestBase, unittest.TestCase):
yield self.do_request(agent, b"POST", addr, body, yield self.do_request(agent, b"POST", addr, body,
self.process_direct_send_response) self.process_direct_send_response)
# before querying the wallet display, set a label to check: # before querying the wallet display, set a label to check:
labeladdr = self.daemon.wallet_service.get_addr(0,0,0) labeladdr = self.daemon.services["wallet"].get_addr(0,0,0)
self.daemon.wallet_service.set_address_label(labeladdr, self.daemon.services["wallet"].set_address_label(labeladdr,
"test-wallet-rpc-label") "test-wallet-rpc-label")
# force the wallet service txmonitor to wake up, to see the new # force the wallet service txmonitor to wake up, to see the new
# tx before querying /display: # tx before querying /display:
self.daemon.wallet_service.transaction_monitor() self.daemon.services["wallet"].transaction_monitor()
addr = self.get_route_root() addr = self.get_route_root()
addr += "/wallet/" addr += "/wallet/"
addr += self.daemon.wallet_name addr += self.daemon.wallet_name

Loading…
Cancel
Save