update_checker.py (6084B)
1 # Copyright (C) 2019 The Electrum developers 2 # Distributed under the MIT software license, see the accompanying 3 # file LICENCE or http://www.opensource.org/licenses/mit-license.php 4 5 import asyncio 6 import base64 7 from distutils.version import StrictVersion 8 9 from PyQt5.QtCore import Qt, QThread, pyqtSignal 10 from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QLabel, QProgressBar, 11 QHBoxLayout, QPushButton, QDialog) 12 13 from electrum import version 14 from electrum import constants 15 from electrum import ecc 16 from electrum.i18n import _ 17 from electrum.util import make_aiohttp_session 18 from electrum.logging import Logger 19 from electrum.network import Network 20 21 22 class UpdateCheck(QDialog, Logger): 23 url = "https://electrum.org/version" 24 download_url = "https://electrum.org/#download" 25 26 VERSION_ANNOUNCEMENT_SIGNING_KEYS = ( 27 "13xjmVAB1EATPP8RshTE8S8sNwwSUM9p1P", 28 ) 29 30 def __init__(self, *, latest_version=None): 31 QDialog.__init__(self) 32 self.setWindowTitle('Electrum - ' + _('Update Check')) 33 self.content = QVBoxLayout() 34 self.content.setContentsMargins(*[10]*4) 35 36 self.heading_label = QLabel() 37 self.content.addWidget(self.heading_label) 38 39 self.detail_label = QLabel() 40 self.detail_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse) 41 self.detail_label.setOpenExternalLinks(True) 42 self.content.addWidget(self.detail_label) 43 44 self.pb = QProgressBar() 45 self.pb.setMaximum(0) 46 self.pb.setMinimum(0) 47 self.content.addWidget(self.pb) 48 49 versions = QHBoxLayout() 50 versions.addWidget(QLabel(_("Current version: {}".format(version.ELECTRUM_VERSION)))) 51 self.latest_version_label = QLabel(_("Latest version: {}".format(" "))) 52 versions.addWidget(self.latest_version_label) 53 self.content.addLayout(versions) 54 55 self.update_view(latest_version) 56 57 self.update_check_thread = UpdateCheckThread() 58 self.update_check_thread.checked.connect(self.on_version_retrieved) 59 self.update_check_thread.failed.connect(self.on_retrieval_failed) 60 self.update_check_thread.start() 61 62 close_button = QPushButton(_("Close")) 63 close_button.clicked.connect(self.close) 64 self.content.addWidget(close_button) 65 self.setLayout(self.content) 66 self.show() 67 68 def on_version_retrieved(self, version): 69 self.update_view(version) 70 71 def on_retrieval_failed(self): 72 self.heading_label.setText('<h2>' + _("Update check failed") + '</h2>') 73 self.detail_label.setText(_("Sorry, but we were unable to check for updates. Please try again later.")) 74 self.pb.hide() 75 76 @staticmethod 77 def is_newer(latest_version): 78 return latest_version > StrictVersion(version.ELECTRUM_VERSION) 79 80 def update_view(self, latest_version=None): 81 if latest_version: 82 self.pb.hide() 83 self.latest_version_label.setText(_("Latest version: {}".format(latest_version))) 84 if self.is_newer(latest_version): 85 self.heading_label.setText('<h2>' + _("There is a new update available") + '</h2>') 86 url = "<a href='{u}'>{u}</a>".format(u=UpdateCheck.download_url) 87 self.detail_label.setText(_("You can download the new version from {}.").format(url)) 88 else: 89 self.heading_label.setText('<h2>' + _("Already up to date") + '</h2>') 90 self.detail_label.setText(_("You are already on the latest version of Electrum.")) 91 else: 92 self.heading_label.setText('<h2>' + _("Checking for updates...") + '</h2>') 93 self.detail_label.setText(_("Please wait while Electrum checks for available updates.")) 94 95 96 class UpdateCheckThread(QThread, Logger): 97 checked = pyqtSignal(object) 98 failed = pyqtSignal() 99 100 def __init__(self): 101 QThread.__init__(self) 102 Logger.__init__(self) 103 self.network = Network.get_instance() 104 105 async def get_update_info(self): 106 # note: Use long timeout here as it is not critical that we get a response fast, 107 # and it's bad not to get an update notification just because we did not wait enough. 108 async with make_aiohttp_session(proxy=self.network.proxy, timeout=120) as session: 109 async with session.get(UpdateCheck.url) as result: 110 signed_version_dict = await result.json(content_type=None) 111 # example signed_version_dict: 112 # { 113 # "version": "3.9.9", 114 # "signatures": { 115 # "1Lqm1HphuhxKZQEawzPse8gJtgjm9kUKT4": "IA+2QG3xPRn4HAIFdpu9eeaCYC7S5wS/sDxn54LJx6BdUTBpse3ibtfq8C43M7M1VfpGkD5tsdwl5C6IfpZD/gQ=" 116 # } 117 # } 118 version_num = signed_version_dict['version'] 119 sigs = signed_version_dict['signatures'] 120 for address, sig in sigs.items(): 121 if address not in UpdateCheck.VERSION_ANNOUNCEMENT_SIGNING_KEYS: 122 continue 123 sig = base64.b64decode(sig) 124 msg = version_num.encode('utf-8') 125 if ecc.verify_message_with_address(address=address, sig65=sig, message=msg, 126 net=constants.BitcoinMainnet): 127 self.logger.info(f"valid sig for version announcement '{version_num}' from address '{address}'") 128 break 129 else: 130 raise Exception('no valid signature for version announcement') 131 return StrictVersion(version_num.strip()) 132 133 def run(self): 134 if not self.network: 135 self.failed.emit() 136 return 137 try: 138 update_info = asyncio.run_coroutine_threadsafe(self.get_update_info(), self.network.asyncio_loop).result() 139 except Exception as e: 140 self.logger.info(f"got exception: '{repr(e)}'") 141 self.failed.emit() 142 else: 143 self.checked.emit(update_info)