labels.py (7615B)
1 import asyncio 2 import hashlib 3 import json 4 import sys 5 import traceback 6 from typing import Union, TYPE_CHECKING 7 8 import base64 9 10 from electrum.plugin import BasePlugin, hook 11 from electrum.crypto import aes_encrypt_with_iv, aes_decrypt_with_iv 12 from electrum.i18n import _ 13 from electrum.util import log_exceptions, ignore_exceptions, make_aiohttp_session 14 from electrum.network import Network 15 16 if TYPE_CHECKING: 17 from electrum.wallet import Abstract_Wallet 18 19 20 class ErrorConnectingServer(Exception): 21 def __init__(self, reason: Union[str, Exception] = None): 22 self.reason = reason 23 24 def __str__(self): 25 header = _("Error connecting to {} server").format('Labels') 26 reason = self.reason 27 if isinstance(reason, BaseException): 28 reason = repr(reason) 29 return f"{header}: {reason}" if reason else header 30 31 32 class LabelsPlugin(BasePlugin): 33 34 def __init__(self, parent, config, name): 35 BasePlugin.__init__(self, parent, config, name) 36 self.target_host = 'labels.electrum.org' 37 self.wallets = {} 38 39 def encode(self, wallet: 'Abstract_Wallet', msg: str) -> str: 40 password, iv, wallet_id = self.wallets[wallet] 41 encrypted = aes_encrypt_with_iv(password, iv, msg.encode('utf8')) 42 return base64.b64encode(encrypted).decode() 43 44 def decode(self, wallet: 'Abstract_Wallet', message: str) -> str: 45 password, iv, wallet_id = self.wallets[wallet] 46 decoded = base64.b64decode(message) 47 decrypted = aes_decrypt_with_iv(password, iv, decoded) 48 return decrypted.decode('utf8') 49 50 def get_nonce(self, wallet: 'Abstract_Wallet'): 51 # nonce is the nonce to be used with the next change 52 nonce = wallet.db.get('wallet_nonce') 53 if nonce is None: 54 nonce = 1 55 self.set_nonce(wallet, nonce) 56 return nonce 57 58 def set_nonce(self, wallet: 'Abstract_Wallet', nonce): 59 self.logger.info(f"set {wallet.basename()} nonce to {nonce}") 60 wallet.db.put("wallet_nonce", nonce) 61 62 @hook 63 def set_label(self, wallet: 'Abstract_Wallet', item, label): 64 if wallet not in self.wallets: 65 return 66 if not item: 67 return 68 nonce = self.get_nonce(wallet) 69 wallet_id = self.wallets[wallet][2] 70 bundle = {"walletId": wallet_id, 71 "walletNonce": nonce, 72 "externalId": self.encode(wallet, item), 73 "encryptedLabel": self.encode(wallet, label)} 74 asyncio.run_coroutine_threadsafe(self.do_post_safe("/label", bundle), wallet.network.asyncio_loop) 75 # Caller will write the wallet 76 self.set_nonce(wallet, nonce + 1) 77 78 @ignore_exceptions 79 @log_exceptions 80 async def do_post_safe(self, *args): 81 await self.do_post(*args) 82 83 async def do_get(self, url = "/labels"): 84 url = 'https://' + self.target_host + url 85 network = Network.get_instance() 86 proxy = network.proxy if network else None 87 async with make_aiohttp_session(proxy) as session: 88 async with session.get(url) as result: 89 return await result.json() 90 91 async def do_post(self, url = "/labels", data=None): 92 url = 'https://' + self.target_host + url 93 network = Network.get_instance() 94 proxy = network.proxy if network else None 95 async with make_aiohttp_session(proxy) as session: 96 async with session.post(url, json=data) as result: 97 try: 98 return await result.json() 99 except Exception as e: 100 raise Exception('Could not decode: ' + await result.text()) from e 101 102 async def push_thread(self, wallet: 'Abstract_Wallet'): 103 wallet_data = self.wallets.get(wallet, None) 104 if not wallet_data: 105 raise Exception('Wallet {} not loaded'.format(wallet)) 106 wallet_id = wallet_data[2] 107 bundle = {"labels": [], 108 "walletId": wallet_id, 109 "walletNonce": self.get_nonce(wallet)} 110 for key, value in wallet.get_all_labels().items(): 111 try: 112 encoded_key = self.encode(wallet, key) 113 encoded_value = self.encode(wallet, value) 114 except: 115 self.logger.info(f'cannot encode {repr(key)} {repr(value)}') 116 continue 117 bundle["labels"].append({'encryptedLabel': encoded_value, 118 'externalId': encoded_key}) 119 await self.do_post("/labels", bundle) 120 121 async def pull_thread(self, wallet: 'Abstract_Wallet', force: bool): 122 wallet_data = self.wallets.get(wallet, None) 123 if not wallet_data: 124 raise Exception('Wallet {} not loaded'.format(wallet)) 125 wallet_id = wallet_data[2] 126 nonce = 1 if force else self.get_nonce(wallet) - 1 127 self.logger.info(f"asking for labels since nonce {nonce}") 128 try: 129 response = await self.do_get("/labels/since/%d/for/%s" % (nonce, wallet_id)) 130 except Exception as e: 131 raise ErrorConnectingServer(e) from e 132 if response["labels"] is None: 133 self.logger.info('no new labels') 134 return 135 result = {} 136 for label in response["labels"]: 137 try: 138 key = self.decode(wallet, label["externalId"]) 139 value = self.decode(wallet, label["encryptedLabel"]) 140 except: 141 continue 142 try: 143 json.dumps(key) 144 json.dumps(value) 145 except: 146 self.logger.info(f'error: no json {key}') 147 continue 148 result[key] = value 149 150 for key, value in result.items(): 151 if force or not wallet.get_label(key): 152 wallet._set_label(key, value) 153 154 self.logger.info(f"received {len(response)} labels") 155 self.set_nonce(wallet, response["nonce"] + 1) 156 self.on_pulled(wallet) 157 158 def on_pulled(self, wallet: 'Abstract_Wallet') -> None: 159 raise NotImplementedError() 160 161 @ignore_exceptions 162 @log_exceptions 163 async def pull_safe_thread(self, wallet: 'Abstract_Wallet', force: bool): 164 try: 165 await self.pull_thread(wallet, force) 166 except ErrorConnectingServer as e: 167 self.logger.info(repr(e)) 168 169 def pull(self, wallet: 'Abstract_Wallet', force: bool): 170 if not wallet.network: raise Exception(_('You are offline.')) 171 return asyncio.run_coroutine_threadsafe(self.pull_thread(wallet, force), wallet.network.asyncio_loop).result() 172 173 def push(self, wallet: 'Abstract_Wallet'): 174 if not wallet.network: raise Exception(_('You are offline.')) 175 return asyncio.run_coroutine_threadsafe(self.push_thread(wallet), wallet.network.asyncio_loop).result() 176 177 def start_wallet(self, wallet: 'Abstract_Wallet'): 178 if not wallet.network: return # 'offline' mode 179 mpk = wallet.get_fingerprint() 180 if not mpk: 181 return 182 mpk = mpk.encode('ascii') 183 password = hashlib.sha1(mpk).hexdigest()[:32].encode('ascii') 184 iv = hashlib.sha256(password).digest()[:16] 185 wallet_id = hashlib.sha256(mpk).hexdigest() 186 self.wallets[wallet] = (password, iv, wallet_id) 187 nonce = self.get_nonce(wallet) 188 self.logger.info(f"wallet {wallet.basename()} nonce is {nonce}") 189 # If there is an auth token we can try to actually start syncing 190 asyncio.run_coroutine_threadsafe(self.pull_safe_thread(wallet, False), wallet.network.asyncio_loop) 191 192 def stop_wallet(self, wallet): 193 self.wallets.pop(wallet, None)