wallet-sync-server/test_client/test_client.py
2022-06-07 18:24:01 -04:00

230 lines
8.6 KiB
Python
Executable file

#!/bin/python3
import random, string, json, uuid, requests, hashlib
from pprint import pprint
BASE_URL = 'http://localhost:8090'
AUTH_URL = BASE_URL + '/auth/full'
REGISTER_URL = BASE_URL + '/signup'
WALLET_STATE_URL = BASE_URL + '/wallet-state'
def wallet_state_sequence(wallet_state):
if 'deviceId' not in wallet_state:
return 0
return wallet_state['lastSynced'][wallet_state['deviceId']]
# TODO - do this correctly
def create_login_password(root_password):
return hashlib.sha256(root_password.encode('utf-8')).hexdigest()[:32]
# TODO - do this correctly
def create_encryption_key(root_password):
return hashlib.sha256(root_password.encode('utf-8')).hexdigest()[32:]
# TODO - do this correctly
def check_hmac(wallet_state, encryption_key, hmac):
return hmac == 'Good HMAC'
# TODO - do this correctly
def create_hmac(wallet_state, encryption_key):
return 'Good HMAC'
class Client():
def _validate_new_wallet_state(self, new_wallet_state):
if self.wallet_state is None:
# All of the validations here are in reference to what the device already
# has. If this device is getting a wallet state for the first time, there
# is no basis for comparison.
return True
# Make sure that the new sequence is overall later.
if wallet_state_sequence(new_wallet_state) <= wallet_state_sequence(self.wallet_state):
return False
for dev_id in self.wallet_state['lastSynced']:
if dev_id == self.device_id:
# Check if the new wallet has the latest changes from this device
if new_wallet_state['lastSynced'][dev_id] != self.wallet_state['lastSynced'][dev_id]:
return False
else:
# Check if the new wallet somehow regressed on any of the other devices
# This most likely means a bug in another client
if new_wallet_state['lastSynced'][dev_id] < self.wallet_state['lastSynced'][dev_id]:
return False
return True
def __init__(self):
# Represents normal client behavior (though a real client will of course save device id)
self.device_id = str(uuid.uuid4())
self.auth_token = 'bad token'
self.wallet_state = None
# TODO - save change to disk in between, associated with account and/or
# wallet
self._encrypted_wallet_local_changes = ''
# TODO - make this act more sdk-like. in fact maybe even install the sdk?
# TODO - This does not deal with the question of tying accounts to wallets.
# Does a new wallet state mean a we're creating a new account? What happens
# if we create a new wallet state tied to an existing account? Do we merge it
# with what's on the server anyway? Do we refuse to merge, or warn the user?
# Etc. This sort of depends on how the LBRY Desktop/SDK usually behave. For
# now, it'll end up just merging any un-saved local changes with whatever is
# on the server.
def new_wallet_state(self):
# camel-cased to ease json interop
self.wallet_state = {'lastSynced': {}, 'encryptedWallet': ''}
# TODO - actual encryption with encryption_key
self._encrypted_wallet_local_changes = ''
def set_account(self, email, root_password):
self.email = email
self.root_password = root_password
def register(self):
body = json.dumps({
'email': self.email,
'password': create_login_password(self.root_password),
})
response = requests.post(REGISTER_URL, body)
if response.status_code != 201:
print ('Error', response.status_code)
print (response.content)
return
print ("Registered")
def get_auth_token(self):
body = json.dumps({
'email': self.email,
'password': create_login_password(self.root_password),
'deviceId': self.device_id,
})
response = requests.post(AUTH_URL, body)
if response.status_code != 200:
print ('Error', response.status_code)
print (response.content)
return
self.auth_token = json.loads(response.content)['token']
print ("Got auth token: ", self.auth_token)
# TODO - What about cases where we are managing multiple different wallets?
# Some will have lower sequences. If you accidentally mix it up client-side,
# you might end up overwriting one with a lower sequence entirely. Maybe we
# want to annotate them with which account we're talking about. Again, we
# should see how LBRY Desktop/SDK deal with it.
def get_wallet_state(self):
params = {
'token': self.auth_token,
}
response = requests.get(WALLET_STATE_URL, params=params)
if response.status_code != 200:
print ('Error', response.status_code)
print (response.content)
return
new_wallet_state_str = json.loads(response.content)['walletStateJson']
new_wallet_state = json.loads(new_wallet_state_str)
encryption_key = create_encryption_key(self.root_password)
hmac = json.loads(response.content)['hmac']
if not check_hmac(new_wallet_state_str, encryption_key, hmac):
print ('Error - bad hmac on new wallet')
print (response.content)
return
# In reality, we'd examine, merge, verify, validate etc this new wallet state.
if self.wallet_state != new_wallet_state and not self._validate_new_wallet_state(new_wallet_state):
print ('Error - new wallet does not validate')
print (response.content)
return
if self.wallet_state is None:
# This is if we're getting a wallet_state for the first time. Initialize
# the local changes.
self._encrypted_wallet_local_changes = ''
self.wallet_state = new_wallet_state
print ("Got latest walletState:")
pprint(self.wallet_state)
def post_wallet_state(self):
# Create a *new* wallet state, indicating that it was last updated by this
# device, with the updated sequence, and include our local encrypted wallet changes.
# Don't set self.wallet_state to this until we know that it's accepted by
# the server.
if not self.wallet_state:
print ("No wallet state to post.")
return
submitted_wallet_state = {
"deviceId": self.device_id,
"lastSynced": dict(self.wallet_state['lastSynced']),
"encryptedWallet": self.cur_encrypted_wallet(),
}
submitted_wallet_state['lastSynced'][self.device_id] = wallet_state_sequence(self.wallet_state) + 1
encryption_key = create_encryption_key(self.root_password)
submitted_wallet_state_str = json.dumps(submitted_wallet_state)
submitted_wallet_state_hmac = create_hmac(submitted_wallet_state_str, encryption_key)
body = json.dumps({
'token': self.auth_token,
'walletStateJson': submitted_wallet_state_str,
'hmac': submitted_wallet_state_hmac
})
response = requests.post(WALLET_STATE_URL, body)
if response.status_code == 200:
# Our local changes are no longer local, so we reset them
self._encrypted_wallet_local_changes = ''
print ('Successfully updated wallet state on server')
elif response.status_code == 409:
print ('Wallet state out of date. Getting updated wallet state. Try again.')
# Don't return yet! We got the updated state here, so we still process it below.
else:
print ('Error', response.status_code)
print (response.content)
return
# Now we get a new wallet state back as a response
# TODO - factor this into the same thing as the get_wallet_state function
new_wallet_state_str = json.loads(response.content)['walletStateJson']
new_wallet_state_hmac = json.loads(response.content)['hmac']
new_wallet_state = json.loads(new_wallet_state_str)
if not check_hmac(new_wallet_state_str, encryption_key, new_wallet_state_hmac):
print ('Error - bad hmac on new wallet')
print (response.content)
return
# In reality, we'd examine, merge, verify, validate etc this new wallet state.
if submitted_wallet_state != new_wallet_state and not self._validate_new_wallet_state(new_wallet_state):
print ('Error - new wallet does not validate')
print (response.content)
return
self.wallet_state = new_wallet_state
print ("Got new walletState:")
pprint(self.wallet_state)
def change_encrypted_wallet(self):
if not self.wallet_state:
print ("No wallet state, so we can't add to it yet.")
return
self._encrypted_wallet_local_changes += ':' + ''.join(random.choice(string.hexdigits) for x in range(4))
def cur_encrypted_wallet(self):
if not self.wallet_state:
print ("No wallet state, so no encrypted wallet.")
return
# The local changes on top of whatever came from the server
# If we pull new changes from server, we "rebase" these on top of it
# If we push changes, the full "rebased" version gets committed to the server
return self.wallet_state['encryptedWallet'] + self._encrypted_wallet_local_changes