Skip to content

Commit

Permalink
Merge JoinMarket-Org#1136: Allow re-unlock of wallets via /unlock
Browse files Browse the repository at this point in the history
28fdaa1 Allow re-unlock of wallets via /unlock (Adam Gibson)
  • Loading branch information
AdamISZ committed Jan 3, 2022
2 parents 7d195b7 + 28fdaa1 commit e5948dd
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 46 deletions.
2 changes: 1 addition & 1 deletion jmclient/jmclient/wallet-rpc-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ paths:
type: string
responses:
'200':
$ref: "#/components/responses/Unlock-200-OK"
$ref: "#/components/responses/Lock-200-OK"
'400':
$ref: '#/components/responses/400-BadRequest'
'401':
Expand Down
60 changes: 35 additions & 25 deletions jmclient/jmclient/wallet_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ def stopService(self):
# Currently valid authorization tokens must be removed
# from the daemon:
self.cookie = None
if self.wss_factory:
self.wss_factory.valid_token = None
# if the wallet-daemon is shut down, all services
# it encapsulates must also be shut down.
for name, service in self.services.items():
Expand Down Expand Up @@ -323,9 +325,17 @@ def initialize_wallet_service(self, request, wallet, wallet_name, **kwargs):
(currently THE wallet, daemon does not yet support multiple).
This is maintained for 30 minutes currently, or until the user
switches to a new wallet.
If an existing wallet_service was in place, it needs to be stopped.
Here we must also register transaction update callbacks, to fire
events in the websocket connection.
"""
if self.wallet_service:
# we allow a new successful authorization (with password)
# to shut down the currently running service(s), if there
# are any.
# This will stop all supporting services and wipe
# state (so wallet, maker service and cookie/token):
self.stopService()
# 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,
Expand Down Expand Up @@ -355,9 +365,10 @@ def dummy_restart_callback(msg):
self.wallet_service.register_callbacks(
[self.wss_factory.sendTxNotification], None)
self.wallet_service.startService()
# now that the service is intialized, we want to
# now that the base WalletService is started, we want to
# make sure that any websocket clients use the correct
# token:
# 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
# respond to requests, we return the status to the client:
Expand Down Expand Up @@ -639,36 +650,35 @@ def createwallet(self, request):
def unlockwallet(self, request, walletname):
""" If a user succeeds in authenticating and opening a
wallet, we start the corresponding wallet service.
Notice that in the case the user fails for any reason,
then any existing wallet service, and corresponding token,
will remain active.
"""
print_req(request)
assert isinstance(request.content, BytesIO)
auth_json = self.get_POST_body(request, ["password"])
if not auth_json:
raise InvalidRequestFormat()
password = auth_json["password"]
if self.wallet_service is None:
wallet_path = get_wallet_path(walletname, None)
try:
wallet = open_test_wallet_maybe(
wallet_path, walletname, 4,
password=password.encode("utf-8"),
ask_for_password=False)
except StoragePasswordError:
raise NotAuthorized()
except RetryableStorageError:
# .lock file exists
raise LockExists()
except StorageError:
# wallet is not openable
raise NoWalletFound()
except Exception:
# wallet file doesn't exist or is wrong format
raise NoWalletFound()
return self.initialize_wallet_service(request, wallet, walletname)
else:
jlog.warn('Tried to unlock wallet, but one is already unlocked.')
jlog.warn('Currently only one active wallet at a time is supported.')
raise WalletAlreadyUnlocked()

wallet_path = get_wallet_path(walletname, None)
try:
wallet = open_test_wallet_maybe(
wallet_path, walletname, 4,
password=password.encode("utf-8"),
ask_for_password=False)
except StoragePasswordError:
raise NotAuthorized()
except RetryableStorageError:
# .lock file exists
raise LockExists()
except StorageError:
# wallet is not openable
raise NoWalletFound()
except Exception:
# wallet file doesn't exist or is wrong format
raise NoWalletFound()
return self.initialize_wallet_service(request, wallet, walletname)

#This route should return list of current wallets created.
@app.route('/wallet/all', methods=['GET'])
Expand Down
81 changes: 61 additions & 20 deletions jmclient/test/test_wallet_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

testdir = os.path.dirname(os.path.realpath(__file__))

testfileloc = "testwrpc.jmdat"
testfilename = "testwrpc"

jlog = get_log()

Expand All @@ -42,10 +42,12 @@ class WalletRPCTestBase(object):
dport = 28183
# the port for the ws
wss_port = 28283

# how many different wallets we need
num_wallet_files = 2

def setUp(self):
load_test_config()
self.clean_out_wallet_file()
self.clean_out_wallet_files()
jm_single().bc_interface.tick_forward_chain_interval = 5
jm_single().bc_interface.simulate_blocks()
# a client connnection object which is often but not always
Expand All @@ -59,7 +61,7 @@ def setUp(self):
# because we sync and start the wallet service manually here
# (and don't use wallet files yet), we won't have set a wallet name,
# so we set it here:
self.daemon.wallet_name = testfileloc
self.daemon.wallet_name = self.get_wallet_file_name(1)
r, s = self.daemon.startService()
self.listener_rpc = r
self.listener_ws = s
Expand All @@ -82,12 +84,21 @@ def get_route_root(self):
addr += api_version_string
return addr

def clean_out_wallet_file(self):
if os.path.exists(os.path.join(".", "wallets", testfileloc)):
os.remove(os.path.join(".", "wallets", testfileloc))
def clean_out_wallet_files(self):
for i in range(1, self.num_wallet_files + 1):
wfn = self.get_wallet_file_name(i, fullpath=True)
if os.path.exists(wfn):
os.remove(wfn)

def get_wallet_file_name(self, i, fullpath=False):
tfn = testfilename + str(i) + ".jmdat"
if fullpath:
return os.path.join(".", "wallets", tfn)
else:
return tfn

def tearDown(self):
self.clean_out_wallet_file()
self.clean_out_wallet_files()
for dc in reactor.getDelayedCalls():
dc.cancel()
d1 = defer.maybeDeferred(self.listener_ws.stopListening)
Expand Down Expand Up @@ -155,59 +166,89 @@ def test_create_list_lock_unlock(self):
1. create a wallet and have it persisted
to disk in ./wallets, and get a token.
2. list wallets and check they contain the new
2. lock that wallet.
3. create a second wallet as above.
4. list wallets and check they contain the new
wallet.
3. lock the existing wallet service, using the token.
4. Unlock the wallet with /unlock, get a token.
5. lock the existing wallet service, using the token.
6. Unlock the original wallet with /unlock, get a token.
7. Unlock the second wallet with /unlock, get a token.
"""
# before starting, we have to shut down the existing
# wallet service (usually this would be `lock`):
self.daemon.wallet_service = None
self.daemon.stopService()
self.daemon.auth_disabled = False

wfn1 = self.get_wallet_file_name(1)
wfn2 = self.get_wallet_file_name(2)
self.wfnames = [wfn1, wfn2]
agent = get_nontor_agent()
root = self.get_route_root()

# 1. Create first
addr = root + "/wallet/create"
addr = addr.encode()
body = BytesProducer(json.dumps({"walletname": testfileloc,
body = BytesProducer(json.dumps({"walletname": wfn1,
"password": "hunter2", "wallettype": "sw-fb"}).encode())
yield self.do_request(agent, b"POST", addr, body,
self.process_create_wallet_response)

# 2. now *lock*
addr = root + "/wallet/" + wfn1 + "/lock"
addr = addr.encode()
jlog.info("Using address: {}".format(addr))
yield self.do_request(agent, b"GET", addr, None,
self.process_lock_response, token=self.jwt_token)

# 3. Create this secondary wallet (so we can test re-unlock)
addr = root + "/wallet/create"
addr = addr.encode()
body = BytesProducer(json.dumps({"walletname": wfn2,
"password": "hunter3", "wallettype": "sw"}).encode())
yield self.do_request(agent, b"POST", addr, body,
self.process_create_wallet_response)

# 4. List wallets
addr = root + "/wallet/all"
addr = addr.encode()
# does not require a token, though we just got one.
yield self.do_request(agent, b"GET", addr, None,
self.process_list_wallets_response)

# now *lock* the existing, which will shut down the wallet
# service associated.
addr = root + "/wallet/" + self.daemon.wallet_name + "/lock"
# 5. now *lock* the active.
addr = root + "/wallet/" + wfn2 + "/lock"
addr = addr.encode()
jlog.info("Using address: {}".format(addr))
yield self.do_request(agent, b"GET", addr, None,
self.process_lock_response, token=self.jwt_token)
# wallet service should now be stopped.
addr = root + "/wallet/" + self.daemon.wallet_name + "/unlock"
# 6. Unlock the original wallet
addr = root + "/wallet/" + wfn1 + "/unlock"
addr = addr.encode()
body = BytesProducer(json.dumps({"password": "hunter2"}).encode())
yield self.do_request(agent, b"POST", addr, body,
self.process_unlock_response)

# 7. Unlock the second wallet again
addr = root + "/wallet/" + wfn2 + "/unlock"
addr = addr.encode()
body = BytesProducer(json.dumps({"password": "hunter3"}).encode())
yield self.do_request(agent, b"POST", addr, body,
self.process_unlock_response)

def process_create_wallet_response(self, response, code):
assert code == 201
json_body = json.loads(response.decode("utf-8"))
assert json_body["walletname"] == testfileloc
assert json_body["walletname"] in self.wfnames
self.jwt_token = json_body["token"]
# we don't use this in test, but it must exist:
assert json_body["seedphrase"]

def process_list_wallets_response(self, body, code):
assert code == 200
json_body = json.loads(body.decode("utf-8"))
assert json_body["wallets"] == [testfileloc]
assert set(json_body["wallets"]) == set(self.wfnames)

@defer.inlineCallbacks
def test_direct_send_and_display_wallet(self):
Expand Down Expand Up @@ -369,13 +410,13 @@ def process_session_response(self, response, code):
def process_unlock_response(self, response, code):
assert code == 200
json_body = json.loads(response.decode("utf-8"))
assert json_body["walletname"] == testfileloc
assert json_body["walletname"] in self.wfnames
self.jwt_token = json_body["token"]

def process_lock_response(self, response, code):
assert code == 200
json_body = json.loads(response.decode("utf-8"))
assert json_body["walletname"] == testfileloc
assert json_body["walletname"] in self.wfnames

@defer.inlineCallbacks
def test_do_coinjoin(self):
Expand Down

0 comments on commit e5948dd

Please sign in to comment.