From d467559dcd4276cbd992fa1e5adc050fbb7d6e39 Mon Sep 17 00:00:00 2001 From: Daniel Krol Date: Fri, 10 Jun 2022 21:07:55 -0400 Subject: [PATCH] test client: factor out wallet sync API into class In preparation for adding the LBRY SDK api class --- server/server.go | 2 +- test_client/README.md | 52 +++++----- test_client/gen-readme.py | 16 +-- test_client/test_client.py | 196 +++++++++++++++++++++++-------------- 4 files changed, 159 insertions(+), 107 deletions(-) diff --git a/server/server.go b/server/server.go index d466233..7baf00d 100644 --- a/server/server.go +++ b/server/server.go @@ -103,7 +103,7 @@ func getPostData(w http.ResponseWriter, req *http.Request, reqStruct PostRequest } if err := json.NewDecoder(req.Body).Decode(&reqStruct); err != nil { - errorJson(w, http.StatusBadRequest, "Malformed request body JSON") + errorJson(w, http.StatusBadRequest, "Request body JSON malformed or structure mismatch") return false } diff --git a/test_client/README.md b/test_client/README.md index eed1e62..d3b4d31 100644 --- a/test_client/README.md +++ b/test_client/README.md @@ -25,9 +25,9 @@ Now that the account exists, grab an auth token with both clients. ``` >>> c1.get_auth_token() -Got auth token: a489d5cacc0a3db4811c34d203683482d90c605b03ae007fa5ae32ef17252bd9 +Got auth token: b646a357038d394ef7b70f350c666aeb29e24072c7eeaaad4fb6759c1fca281a >>> c2.get_auth_token() -Got auth token: 1fe687db8ab493ed260f499b674cfa49edefd3c03a718905c62d3f850dc50567 +Got auth token: bd24ae67cb1bde2da8a27f2c2e5ec8ff1f9bebccb11e16dd35aea31bf422133c ``` ## Syncing @@ -38,7 +38,7 @@ Note that after POSTing, it says it "got" a new wallet. This is because the POST ``` >>> c1.new_wallet_state() ->>> c1.post_wallet() +>>> c1.update_wallet() Successfully updated wallet state on server Got new walletState: WalletState(sequence=1, encrypted_wallet='-') @@ -57,7 +57,7 @@ WalletState(sequence=1, encrypted_wallet='-') Push a new version, GET it with the other client. Even though we haven't edited the encrypted wallet yet, we can still increment the sequence number. ``` ->>> c2.post_wallet() +>>> c2.update_wallet() Successfully updated wallet state on server Got new walletState: WalletState(sequence=2, encrypted_wallet='-') @@ -75,21 +75,21 @@ For demo purposes, this test client represents each change to the wallet by appe '-' >>> c1.change_encrypted_wallet() >>> c1.cur_encrypted_wallet() -'-:cfF6' +'-:a776' ``` The wallet is synced between the clients. ``` ->>> c1.post_wallet() +>>> c1.update_wallet() Successfully updated wallet state on server Got new walletState: -WalletState(sequence=3, encrypted_wallet='-:cfF6') +WalletState(sequence=3, encrypted_wallet='-:a776') >>> c2.get_wallet() Got latest walletState: -WalletState(sequence=3, encrypted_wallet='-:cfF6') +WalletState(sequence=3, encrypted_wallet='-:a776') >>> c2.cur_encrypted_wallet() -'-:cfF6' +'-:a776' ``` ## Merging Changes @@ -100,44 +100,44 @@ Both clients create changes. They now have diverging wallets. >>> c1.change_encrypted_wallet() >>> c2.change_encrypted_wallet() >>> c1.cur_encrypted_wallet() -'-:cfF6:565b' +'-:a776:8fc8' >>> c2.cur_encrypted_wallet() -'-:cfF6:6De1' +'-:a776:2433' ``` One client POSTs its change first. ``` ->>> c1.post_wallet() +>>> c1.update_wallet() Successfully updated wallet state on server Got new walletState: -WalletState(sequence=4, encrypted_wallet='-:cfF6:565b') +WalletState(sequence=4, encrypted_wallet='-:a776:8fc8') ``` The other client pulls that change, and _merges_ those changes on top of the changes it had saved locally. -The _merge base_ that a given client uses is the last version that it successfully got from or POSTed to the server. You can see the merge base here: `"-:cfF6"`, the first part of the wallet which both clients had in common before the merge. +The _merge base_ that a given client uses is the last version that it successfully got from or POSTed to the server. You can see the merge base here: `"-:a776"`, the first part of the wallet which both clients had in common before the merge. ``` >>> c2.get_wallet() Got latest walletState: -WalletState(sequence=4, encrypted_wallet='-:cfF6:565b') +WalletState(sequence=4, encrypted_wallet='-:a776:8fc8') >>> c2.cur_encrypted_wallet() -'-:cfF6:565b:6De1' +'-:a776:8fc8:2433' ``` Finally, the client with the merged wallet pushes it to the server, and the other client GETs the update. ``` ->>> c2.post_wallet() +>>> c2.update_wallet() Successfully updated wallet state on server Got new walletState: -WalletState(sequence=5, encrypted_wallet='-:cfF6:565b:6De1') +WalletState(sequence=5, encrypted_wallet='-:a776:8fc8:2433') >>> c1.get_wallet() Got latest walletState: -WalletState(sequence=5, encrypted_wallet='-:cfF6:565b:6De1') +WalletState(sequence=5, encrypted_wallet='-:a776:8fc8:2433') >>> c1.cur_encrypted_wallet() -'-:cfF6:565b:6De1' +'-:a776:8fc8:2433' ``` ## Conflicts @@ -148,22 +148,22 @@ A client cannot POST if it is not up to date. It needs to merge in any new chang ``` >>> c2.change_encrypted_wallet() ->>> c2.post_wallet() +>>> c2.update_wallet() Successfully updated wallet state on server Got new walletState: -WalletState(sequence=6, encrypted_wallet='-:cfF6:565b:6De1:053a') +WalletState(sequence=6, encrypted_wallet='-:a776:8fc8:2433:0FdD') >>> c1.change_encrypted_wallet() ->>> c1.post_wallet() +>>> c1.update_wallet() Wallet state out of date. Getting updated wallet state. Try posting again after this. Got new walletState: -WalletState(sequence=6, encrypted_wallet='-:cfF6:565b:6De1:053a') +WalletState(sequence=6, encrypted_wallet='-:a776:8fc8:2433:0FdD') ``` Now the merge is complete, and the client can make a second POST request containing the merged wallet. ``` ->>> c1.post_wallet() +>>> c1.update_wallet() Successfully updated wallet state on server Got new walletState: -WalletState(sequence=7, encrypted_wallet='-:cfF6:565b:6De1:053a:6774') +WalletState(sequence=7, encrypted_wallet='-:a776:8fc8:2433:0FdD:BA43') ``` diff --git a/test_client/gen-readme.py b/test_client/gen-readme.py index 9bceddd..2f11156 100644 --- a/test_client/gen-readme.py +++ b/test_client/gen-readme.py @@ -59,7 +59,7 @@ Note that after POSTing, it says it "got" a new wallet. This is because the POST code_block(""" c1.new_wallet_state() -c1.post_wallet() +c1.update_wallet() """) print(""" @@ -77,7 +77,7 @@ Push a new version, GET it with the other client. Even though we haven't edited """) code_block(""" -c2.post_wallet() +c2.update_wallet() c1.get_wallet() """) @@ -98,7 +98,7 @@ The wallet is synced between the clients. """) code_block(""" -c1.post_wallet() +c1.update_wallet() c2.get_wallet() c2.cur_encrypted_wallet() """) @@ -123,7 +123,7 @@ One client POSTs its change first. """) code_block(""" -c1.post_wallet() +c1.update_wallet() """) print(""" @@ -142,7 +142,7 @@ Finally, the client with the merged wallet pushes it to the server, and the othe """) code_block(""" -c2.post_wallet() +c2.update_wallet() c1.get_wallet() c1.cur_encrypted_wallet() """) @@ -157,9 +157,9 @@ A client cannot POST if it is not up to date. It needs to merge in any new chang code_block(""" c2.change_encrypted_wallet() -c2.post_wallet() +c2.update_wallet() c1.change_encrypted_wallet() -c1.post_wallet() +c1.update_wallet() """) print(""" @@ -167,5 +167,5 @@ Now the merge is complete, and the client can make a second POST request contain """) code_block(""" -c1.post_wallet() +c1.update_wallet() """) diff --git a/test_client/test_client.py b/test_client/test_client.py index d0174cc..8e532f6 100755 --- a/test_client/test_client.py +++ b/test_client/test_client.py @@ -5,18 +5,101 @@ from pprint import pprint CURRENT_VERSION = 1 -BASE_URL = 'http://localhost:8090' -AUTH_URL = BASE_URL + '/auth/full' -REGISTER_URL = BASE_URL + '/signup' -WALLET_URL = BASE_URL + '/wallet' +WalletState = namedtuple('WalletState', ['sequence', 'encrypted_wallet']) + +class WalletSync(): + BASE_URL = 'http://localhost:8090' + AUTH_URL = BASE_URL + '/auth/full' + REGISTER_URL = BASE_URL + '/signup' + WALLET_URL = BASE_URL + '/wallet' + + @classmethod + def register(cls, email, password): + body = json.dumps({ + 'email': email, + 'password': password, + }) + response = requests.post(cls.REGISTER_URL, body) + if response.status_code != 201: + print ('Error', response.status_code) + print (response.content) + return False + return True + + @classmethod + def get_auth_token(cls, email, password, device_id): + body = json.dumps({ + 'email': email, + 'password': password, + 'deviceId': device_id, + }) + response = requests.post(cls.AUTH_URL, body) + if response.status_code != 200: + print ('Error', response.status_code) + print (response.content) + return None + + return response.json()['token'] + + @classmethod + def get_wallet(cls, token): + params = { + 'token': token, + } + response = requests.get(cls.WALLET_URL, params=params) + + # TODO check response version on client side now + + if response.status_code != 200: + print ('Error', response.status_code) + print (response.content) + return None, None + + wallet_state = WalletState( + encrypted_wallet=response.json()['encryptedWallet'], + sequence=response.json()['sequence'], + ) + hmac = response.json()['hmac'] + return wallet_state, hmac + + @classmethod + def update_wallet(cls, wallet_state, hmac, token): + body = json.dumps({ + 'version': CURRENT_VERSION, + 'token': token, + "encryptedWallet": wallet_state.encrypted_wallet, + "sequence": wallet_state.sequence, + "hmac": hmac, + }) + + response = requests.post(cls.WALLET_URL, body) + + # TODO check that response.json().version == CURRENT_VERSION + + if response.status_code == 200: + conflict = False + print ('Successfully updated wallet state on server') + elif response.status_code == 409: + conflict = True + print ('Wallet state out of date. Getting updated wallet state. Try posting again after this.') + # Not an error! We still want to merge in the returned wallet. + else: + print ('Error', response.status_code) + print (response.content) + return None, None, None + + wallet_state = WalletState( + encrypted_wallet=response.json()['encryptedWallet'], + sequence=response.json()['sequence'], + ) + hmac = response.json()['hmac'] + return wallet_state, hmac, conflict # TODO - We should have: # * self.last_synced_wallet_state - as described # * self.current_wallet_state - WalletState(cur_encrypted_wallet(), sequence + 1) - and current_wallet_state # We don't need it yet but we'd be avoiding the entire point of the syncing system. At least keep it around in this demo. -WalletState = namedtuple('WalletState', ['sequence', 'encrypted_wallet']) - # TODO - do this correctly. This is a hack example. def derive_login_password(root_password): return hashlib.sha256(root_password.encode('utf-8')).hexdigest()[:10] @@ -88,29 +171,22 @@ class Client(): self.root_password = root_password def register(self): - body = json.dumps({ - 'email': self.email, - 'password': derive_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") + success = WalletSync.register( + self.email, + derive_login_password(self.root_password), + ) + if success: + print ("Registered") def get_auth_token(self): - body = json.dumps({ - 'email': self.email, - 'password': derive_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) + token = WalletSync.get_auth_token( + self.email, + derive_login_password(self.root_password), + self.device_id, + ) + if not token: return - self.auth_token = json.loads(response.content)['token'] + self.auth_token = token print ("Got auth token: ", self.auth_token) # TODO - What about cases where we are managing multiple different wallets? @@ -119,31 +195,22 @@ class Client(): # 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(self): - params = { - 'token': self.auth_token, - } - response = requests.get(WALLET_URL, params=params) - if response.status_code != 200: - # TODO check response version on client side now - print ('Error', response.status_code) - print (response.content) + new_wallet_state, hmac = WalletSync.get_wallet(self.auth_token) + + # If there was a failure + if not new_wallet_state: return hmac_key = derive_hmac_key(self.root_password) - - new_wallet_state = WalletState( - encrypted_wallet=response.json()['encryptedWallet'], - sequence=response.json()['sequence'], - ) - hmac = response.json()['hmac'] if not check_hmac(new_wallet_state, hmac_key, hmac): print ('Error - bad hmac on new wallet') - print (response.content) + print (new_wallet_state, hmac) return 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) + print ('current:', self.wallet_state) + print ('got:', new_wallet_state) return if self.wallet_state is None: @@ -156,7 +223,7 @@ class Client(): print ("Got latest walletState:") pprint(self.wallet_state) - def post_wallet(self): + def update_wallet(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 @@ -171,45 +238,30 @@ class Client(): encrypted_wallet=self.cur_encrypted_wallet(), sequence=self.wallet_state.sequence + 1 ) - wallet_request = { - 'version': CURRENT_VERSION, - 'token': self.auth_token, - "encryptedWallet": submitted_wallet_state.encrypted_wallet, - "sequence": submitted_wallet_state.sequence, - "hmac": create_hmac(submitted_wallet_state, hmac_key), - } + hmac = create_hmac(submitted_wallet_state, hmac_key) - response = requests.post(WALLET_URL, json.dumps(wallet_request)) + # Submit our wallet, get the latest wallet back as a response + new_wallet_state, new_hmac, conflict = WalletSync.update_wallet(submitted_wallet_state, hmac, self.auth_token) - if response.status_code == 200: - # TODO check response version on client side now - # 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 posting again after this.') - # 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) + # If there was a failure (not just a conflict) + if not new_wallet_state: return - # Now we get a new wallet back as a response - # TODO - factor this code into the same thing as the get_wallet function + # If there's not a conflict, we submitted successfully and should reset our previously local changes + if not conflict: + self._encrypted_wallet_local_changes = '' - new_wallet_state = WalletState( - encrypted_wallet=response.json()['encryptedWallet'], - sequence=response.json()['sequence'], - ) - hmac = response.json()['hmac'] - if not check_hmac(new_wallet_state, hmac_key, hmac): + # TODO - there's some code in common here with the get_wallet function. factor it out. + + if not check_hmac(new_wallet_state, hmac_key, new_hmac): print ('Error - bad hmac on new wallet') - print (response.content) + print (new_wallet_state, hmac) return 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) + print ('current:', self.wallet_state) + print ('got:', new_wallet_state) return self.wallet_state = new_wallet_state