electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

network_dialog.py (19698B)


      1 #!/usr/bin/env python
      2 #
      3 # Electrum - lightweight Bitcoin client
      4 # Copyright (C) 2012 thomasv@gitorious
      5 #
      6 # Permission is hereby granted, free of charge, to any person
      7 # obtaining a copy of this software and associated documentation files
      8 # (the "Software"), to deal in the Software without restriction,
      9 # including without limitation the rights to use, copy, modify, merge,
     10 # publish, distribute, sublicense, and/or sell copies of the Software,
     11 # and to permit persons to whom the Software is furnished to do so,
     12 # subject to the following conditions:
     13 #
     14 # The above copyright notice and this permission notice shall be
     15 # included in all copies or substantial portions of the Software.
     16 #
     17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
     18 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
     19 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
     20 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
     21 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
     22 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
     23 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     24 # SOFTWARE.
     25 
     26 import socket
     27 import time
     28 from enum import IntEnum
     29 from typing import Tuple, TYPE_CHECKING
     30 
     31 from PyQt5.QtCore import Qt, pyqtSignal, QThread
     32 from PyQt5.QtWidgets import (QTreeWidget, QTreeWidgetItem, QMenu, QGridLayout, QComboBox,
     33                              QLineEdit, QDialog, QVBoxLayout, QHeaderView, QCheckBox,
     34                              QTabWidget, QWidget, QLabel)
     35 from PyQt5.QtGui import QFontMetrics
     36 
     37 from electrum.i18n import _
     38 from electrum import constants, blockchain, util
     39 from electrum.interface import ServerAddr, PREFERRED_NETWORK_PROTOCOL
     40 from electrum.network import Network
     41 from electrum.logging import get_logger
     42 
     43 from .util import (Buttons, CloseButton, HelpButton, read_QIcon, char_width_in_lineedit,
     44                    PasswordLineEdit)
     45 
     46 if TYPE_CHECKING:
     47     from electrum.simple_config import SimpleConfig
     48 
     49 
     50 _logger = get_logger(__name__)
     51 
     52 protocol_names = ['TCP', 'SSL']
     53 protocol_letters = 'ts'
     54 
     55 class NetworkDialog(QDialog):
     56     def __init__(self, network, config, network_updated_signal_obj):
     57         QDialog.__init__(self)
     58         self.setWindowTitle(_('Network'))
     59         self.setMinimumSize(500, 500)
     60         self.nlayout = NetworkChoiceLayout(network, config)
     61         self.network_updated_signal_obj = network_updated_signal_obj
     62         vbox = QVBoxLayout(self)
     63         vbox.addLayout(self.nlayout.layout())
     64         vbox.addLayout(Buttons(CloseButton(self)))
     65         self.network_updated_signal_obj.network_updated_signal.connect(
     66             self.on_update)
     67         util.register_callback(self.on_network, ['network_updated'])
     68 
     69     def on_network(self, event, *args):
     70         self.network_updated_signal_obj.network_updated_signal.emit(event, args)
     71 
     72     def on_update(self):
     73         self.nlayout.update()
     74 
     75 
     76 
     77 class NodesListWidget(QTreeWidget):
     78     """List of connected servers."""
     79 
     80     SERVER_ADDR_ROLE = Qt.UserRole + 100
     81     CHAIN_ID_ROLE = Qt.UserRole + 101
     82     ITEMTYPE_ROLE = Qt.UserRole + 102
     83 
     84     class ItemType(IntEnum):
     85         CHAIN = 0
     86         CONNECTED_SERVER = 1
     87         DISCONNECTED_SERVER = 2
     88         TOPLEVEL = 3
     89 
     90     def __init__(self, parent):
     91         QTreeWidget.__init__(self)
     92         self.parent = parent  # type: NetworkChoiceLayout
     93         self.setHeaderLabels([_('Server'), _('Height')])
     94         self.setContextMenuPolicy(Qt.CustomContextMenu)
     95         self.customContextMenuRequested.connect(self.create_menu)
     96 
     97     def create_menu(self, position):
     98         item = self.currentItem()
     99         if not item:
    100             return
    101         item_type = item.data(0, self.ITEMTYPE_ROLE)
    102         menu = QMenu()
    103         if item_type == self.ItemType.CONNECTED_SERVER:
    104             server = item.data(0, self.SERVER_ADDR_ROLE)  # type: ServerAddr
    105             menu.addAction(_("Use as server"), lambda: self.parent.follow_server(server))
    106         elif item_type == self.ItemType.DISCONNECTED_SERVER:
    107             server = item.data(0, self.SERVER_ADDR_ROLE)  # type: ServerAddr
    108             def func():
    109                 self.parent.server_e.setText(server.net_addr_str())
    110                 self.parent.set_server()
    111             menu.addAction(_("Use as server"), func)
    112         elif item_type == self.ItemType.CHAIN:
    113             chain_id = item.data(0, self.CHAIN_ID_ROLE)
    114             menu.addAction(_("Follow this branch"), lambda: self.parent.follow_branch(chain_id))
    115         else:
    116             return
    117         menu.exec_(self.viewport().mapToGlobal(position))
    118 
    119     def keyPressEvent(self, event):
    120         if event.key() in [ Qt.Key_F2, Qt.Key_Return, Qt.Key_Enter ]:
    121             self.on_activated(self.currentItem(), self.currentColumn())
    122         else:
    123             QTreeWidget.keyPressEvent(self, event)
    124 
    125     def on_activated(self, item, column):
    126         # on 'enter' we show the menu
    127         pt = self.visualItemRect(item).bottomLeft()
    128         pt.setX(50)
    129         self.customContextMenuRequested.emit(pt)
    130 
    131     def update(self, *, network: Network, servers: dict, use_tor: bool):
    132         self.clear()
    133 
    134         # connected servers
    135         connected_servers_item = QTreeWidgetItem([_("Connected nodes"), ''])
    136         connected_servers_item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.TOPLEVEL)
    137         chains = network.get_blockchains()
    138         n_chains = len(chains)
    139         for chain_id, interfaces in chains.items():
    140             b = blockchain.blockchains.get(chain_id)
    141             if b is None: continue
    142             name = b.get_name()
    143             if n_chains > 1:
    144                 x = QTreeWidgetItem([name + '@%d'%b.get_max_forkpoint(), '%d'%b.height()])
    145                 x.setData(0, self.ITEMTYPE_ROLE, self.ItemType.CHAIN)
    146                 x.setData(0, self.CHAIN_ID_ROLE, b.get_id())
    147             else:
    148                 x = connected_servers_item
    149             for i in interfaces:
    150                 star = ' *' if i == network.interface else ''
    151                 item = QTreeWidgetItem([f"{i.server.to_friendly_name()}" + star, '%d'%i.tip])
    152                 item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.CONNECTED_SERVER)
    153                 item.setData(0, self.SERVER_ADDR_ROLE, i.server)
    154                 item.setToolTip(0, str(i.server))
    155                 x.addChild(item)
    156             if n_chains > 1:
    157                 connected_servers_item.addChild(x)
    158 
    159         # disconnected servers
    160         disconnected_servers_item = QTreeWidgetItem([_("Other known servers"), ""])
    161         disconnected_servers_item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.TOPLEVEL)
    162         connected_hosts = set([iface.host for ifaces in chains.values() for iface in ifaces])
    163         protocol = PREFERRED_NETWORK_PROTOCOL
    164         for _host, d in sorted(servers.items()):
    165             if _host in connected_hosts:
    166                 continue
    167             if _host.endswith('.onion') and not use_tor:
    168                 continue
    169             port = d.get(protocol)
    170             if port:
    171                 server = ServerAddr(_host, port, protocol=protocol)
    172                 item = QTreeWidgetItem([server.net_addr_str(), ""])
    173                 item.setData(0, self.ITEMTYPE_ROLE, self.ItemType.DISCONNECTED_SERVER)
    174                 item.setData(0, self.SERVER_ADDR_ROLE, server)
    175                 disconnected_servers_item.addChild(item)
    176 
    177         self.addTopLevelItem(connected_servers_item)
    178         self.addTopLevelItem(disconnected_servers_item)
    179 
    180         connected_servers_item.setExpanded(True)
    181         for i in range(connected_servers_item.childCount()):
    182             connected_servers_item.child(i).setExpanded(True)
    183         disconnected_servers_item.setExpanded(True)
    184 
    185         # headers
    186         h = self.header()
    187         h.setStretchLastSection(False)
    188         h.setSectionResizeMode(0, QHeaderView.Stretch)
    189         h.setSectionResizeMode(1, QHeaderView.ResizeToContents)
    190 
    191         super().update()
    192 
    193 
    194 class NetworkChoiceLayout(object):
    195 
    196     def __init__(self, network: Network, config: 'SimpleConfig', wizard=False):
    197         self.network = network
    198         self.config = config
    199         self.tor_proxy = None
    200 
    201         self.tabs = tabs = QTabWidget()
    202         proxy_tab = QWidget()
    203         blockchain_tab = QWidget()
    204         tabs.addTab(blockchain_tab, _('Overview'))
    205         tabs.addTab(proxy_tab, _('Proxy'))
    206 
    207         fixed_width_hostname = 24 * char_width_in_lineedit()
    208         fixed_width_port = 6 * char_width_in_lineedit()
    209 
    210         # Proxy tab
    211         grid = QGridLayout(proxy_tab)
    212         grid.setSpacing(8)
    213 
    214         # proxy setting
    215         self.proxy_cb = QCheckBox(_('Use proxy'))
    216         self.proxy_cb.clicked.connect(self.check_disable_proxy)
    217         self.proxy_cb.clicked.connect(self.set_proxy)
    218 
    219         self.proxy_mode = QComboBox()
    220         self.proxy_mode.addItems(['SOCKS4', 'SOCKS5'])
    221         self.proxy_host = QLineEdit()
    222         self.proxy_host.setFixedWidth(fixed_width_hostname)
    223         self.proxy_port = QLineEdit()
    224         self.proxy_port.setFixedWidth(fixed_width_port)
    225         self.proxy_user = QLineEdit()
    226         self.proxy_user.setPlaceholderText(_("Proxy user"))
    227         self.proxy_password = PasswordLineEdit()
    228         self.proxy_password.setPlaceholderText(_("Password"))
    229         self.proxy_password.setFixedWidth(fixed_width_port)
    230 
    231         self.proxy_mode.currentIndexChanged.connect(self.set_proxy)
    232         self.proxy_host.editingFinished.connect(self.set_proxy)
    233         self.proxy_port.editingFinished.connect(self.set_proxy)
    234         self.proxy_user.editingFinished.connect(self.set_proxy)
    235         self.proxy_password.editingFinished.connect(self.set_proxy)
    236 
    237         self.proxy_mode.currentIndexChanged.connect(self.proxy_settings_changed)
    238         self.proxy_host.textEdited.connect(self.proxy_settings_changed)
    239         self.proxy_port.textEdited.connect(self.proxy_settings_changed)
    240         self.proxy_user.textEdited.connect(self.proxy_settings_changed)
    241         self.proxy_password.textEdited.connect(self.proxy_settings_changed)
    242 
    243         self.tor_cb = QCheckBox(_("Use Tor Proxy"))
    244         self.tor_cb.setIcon(read_QIcon("tor_logo.png"))
    245         self.tor_cb.hide()
    246         self.tor_cb.clicked.connect(self.use_tor_proxy)
    247 
    248         grid.addWidget(self.tor_cb, 1, 0, 1, 3)
    249         grid.addWidget(self.proxy_cb, 2, 0, 1, 3)
    250         grid.addWidget(HelpButton(_('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.')), 2, 4)
    251         grid.addWidget(self.proxy_mode, 4, 1)
    252         grid.addWidget(self.proxy_host, 4, 2)
    253         grid.addWidget(self.proxy_port, 4, 3)
    254         grid.addWidget(self.proxy_user, 5, 2)
    255         grid.addWidget(self.proxy_password, 5, 3)
    256         grid.setRowStretch(7, 1)
    257 
    258         # Blockchain Tab
    259         grid = QGridLayout(blockchain_tab)
    260         msg =  ' '.join([
    261             _("Electrum connects to several nodes in order to download block headers and find out the longest blockchain."),
    262             _("This blockchain is used to verify the transactions sent by your transaction server.")
    263         ])
    264         self.status_label = QLabel('')
    265         grid.addWidget(QLabel(_('Status') + ':'), 0, 0)
    266         grid.addWidget(self.status_label, 0, 1, 1, 3)
    267         grid.addWidget(HelpButton(msg), 0, 4)
    268 
    269         self.autoconnect_cb = QCheckBox(_('Select server automatically'))
    270         self.autoconnect_cb.setEnabled(self.config.is_modifiable('auto_connect'))
    271         self.autoconnect_cb.clicked.connect(self.set_server)
    272         self.autoconnect_cb.clicked.connect(self.update)
    273         msg = ' '.join([
    274             _("If auto-connect is enabled, Electrum will always use a server that is on the longest blockchain."),
    275             _("If it is disabled, you have to choose a server you want to use. Electrum will warn you if your server is lagging.")
    276         ])
    277         grid.addWidget(self.autoconnect_cb, 1, 0, 1, 3)
    278         grid.addWidget(HelpButton(msg), 1, 4)
    279 
    280         self.server_e = QLineEdit()
    281         self.server_e.setFixedWidth(fixed_width_hostname + fixed_width_port)
    282         self.server_e.editingFinished.connect(self.set_server)
    283         msg = _("Electrum sends your wallet addresses to a single server, in order to receive your transaction history.")
    284         grid.addWidget(QLabel(_('Server') + ':'), 2, 0)
    285         grid.addWidget(self.server_e, 2, 1, 1, 3)
    286         grid.addWidget(HelpButton(msg), 2, 4)
    287 
    288         self.height_label = QLabel('')
    289         msg = _('This is the height of your local copy of the blockchain.')
    290         grid.addWidget(QLabel(_('Blockchain') + ':'), 3, 0)
    291         grid.addWidget(self.height_label, 3, 1)
    292         grid.addWidget(HelpButton(msg), 3, 4)
    293 
    294         self.split_label = QLabel('')
    295         grid.addWidget(self.split_label, 4, 0, 1, 3)
    296 
    297         self.nodes_list_widget = NodesListWidget(self)
    298         grid.addWidget(self.nodes_list_widget, 6, 0, 1, 5)
    299 
    300         vbox = QVBoxLayout()
    301         vbox.addWidget(tabs)
    302         self.layout_ = vbox
    303         # tor detector
    304         self.td = td = TorDetector()
    305         td.found_proxy.connect(self.suggest_proxy)
    306         td.start()
    307 
    308         self.fill_in_proxy_settings()
    309         self.update()
    310 
    311     def check_disable_proxy(self, b):
    312         if not self.config.is_modifiable('proxy'):
    313             b = False
    314         for w in [self.proxy_mode, self.proxy_host, self.proxy_port, self.proxy_user, self.proxy_password]:
    315             w.setEnabled(b)
    316 
    317     def enable_set_server(self):
    318         if self.config.is_modifiable('server'):
    319             enabled = not self.autoconnect_cb.isChecked()
    320             self.server_e.setEnabled(enabled)
    321         else:
    322             for w in [self.autoconnect_cb, self.server_e, self.nodes_list_widget]:
    323                 w.setEnabled(False)
    324 
    325     def update(self):
    326         net_params = self.network.get_parameters()
    327         server = net_params.server
    328         auto_connect = net_params.auto_connect
    329         if not self.server_e.hasFocus():
    330             self.server_e.setText(server.to_friendly_name())
    331         self.autoconnect_cb.setChecked(auto_connect)
    332 
    333         height_str = "%d "%(self.network.get_local_height()) + _('blocks')
    334         self.height_label.setText(height_str)
    335         n = len(self.network.get_interfaces())
    336         status = _("Connected to {0} nodes.").format(n) if n > 1 else _("Connected to {0} node.").format(n) if n == 1 else _("Not connected")
    337         self.status_label.setText(status)
    338         chains = self.network.get_blockchains()
    339         if len(chains) > 1:
    340             chain = self.network.blockchain()
    341             forkpoint = chain.get_max_forkpoint()
    342             name = chain.get_name()
    343             msg = _('Chain split detected at block {0}').format(forkpoint) + '\n'
    344             msg += (_('You are following branch') if auto_connect else _('Your server is on branch'))+ ' ' + name
    345             msg += ' (%d %s)' % (chain.get_branch_size(), _('blocks'))
    346         else:
    347             msg = ''
    348         self.split_label.setText(msg)
    349         self.nodes_list_widget.update(network=self.network,
    350                                       servers=self.network.get_servers(),
    351                                       use_tor=self.tor_cb.isChecked())
    352         self.enable_set_server()
    353 
    354     def fill_in_proxy_settings(self):
    355         proxy_config = self.network.get_parameters().proxy
    356         if not proxy_config:
    357             proxy_config = {"mode": "none", "host": "localhost", "port": "9050"}
    358 
    359         b = proxy_config.get('mode') != "none"
    360         self.check_disable_proxy(b)
    361         if b:
    362             self.proxy_cb.setChecked(True)
    363             self.proxy_mode.setCurrentIndex(
    364                 self.proxy_mode.findText(str(proxy_config.get("mode").upper())))
    365 
    366         self.proxy_host.setText(proxy_config.get("host"))
    367         self.proxy_port.setText(proxy_config.get("port"))
    368         self.proxy_user.setText(proxy_config.get("user", ""))
    369         self.proxy_password.setText(proxy_config.get("password", ""))
    370 
    371     def layout(self):
    372         return self.layout_
    373 
    374     def follow_branch(self, chain_id):
    375         self.network.run_from_another_thread(self.network.follow_chain_given_id(chain_id))
    376         self.update()
    377 
    378     def follow_server(self, server: ServerAddr):
    379         self.network.run_from_another_thread(self.network.follow_chain_given_server(server))
    380         self.update()
    381 
    382     def accept(self):
    383         pass
    384 
    385     def set_server(self):
    386         net_params = self.network.get_parameters()
    387         try:
    388             server = ServerAddr.from_str_with_inference(str(self.server_e.text()))
    389             if not server: raise Exception("failed to parse")
    390         except Exception:
    391             return
    392         net_params = net_params._replace(server=server,
    393                                          auto_connect=self.autoconnect_cb.isChecked())
    394         self.network.run_from_another_thread(self.network.set_parameters(net_params))
    395 
    396     def set_proxy(self):
    397         net_params = self.network.get_parameters()
    398         if self.proxy_cb.isChecked():
    399             proxy = { 'mode':str(self.proxy_mode.currentText()).lower(),
    400                       'host':str(self.proxy_host.text()),
    401                       'port':str(self.proxy_port.text()),
    402                       'user':str(self.proxy_user.text()),
    403                       'password':str(self.proxy_password.text())}
    404         else:
    405             proxy = None
    406             self.tor_cb.setChecked(False)
    407         net_params = net_params._replace(proxy=proxy)
    408         self.network.run_from_another_thread(self.network.set_parameters(net_params))
    409 
    410     def suggest_proxy(self, found_proxy):
    411         if found_proxy is None:
    412             self.tor_cb.hide()
    413             return
    414         self.tor_proxy = found_proxy
    415         self.tor_cb.setText("Use Tor proxy at port " + str(found_proxy[1]))
    416         if (self.proxy_cb.isChecked()
    417                 and self.proxy_mode.currentIndex() == self.proxy_mode.findText('SOCKS5')
    418                 and self.proxy_host.text() == "127.0.0.1"
    419                 and self.proxy_port.text() == str(found_proxy[1])):
    420             self.tor_cb.setChecked(True)
    421         self.tor_cb.show()
    422 
    423     def use_tor_proxy(self, use_it):
    424         if not use_it:
    425             self.proxy_cb.setChecked(False)
    426         else:
    427             socks5_mode_index = self.proxy_mode.findText('SOCKS5')
    428             if socks5_mode_index == -1:
    429                 _logger.info("can't find proxy_mode 'SOCKS5'")
    430                 return
    431             self.proxy_mode.setCurrentIndex(socks5_mode_index)
    432             self.proxy_host.setText("127.0.0.1")
    433             self.proxy_port.setText(str(self.tor_proxy[1]))
    434             self.proxy_user.setText("")
    435             self.proxy_password.setText("")
    436             self.tor_cb.setChecked(True)
    437             self.proxy_cb.setChecked(True)
    438         self.check_disable_proxy(use_it)
    439         self.set_proxy()
    440 
    441     def proxy_settings_changed(self):
    442         self.tor_cb.setChecked(False)
    443 
    444 
    445 class TorDetector(QThread):
    446     found_proxy = pyqtSignal(object)
    447 
    448     def __init__(self):
    449         QThread.__init__(self)
    450 
    451     def run(self):
    452         # Probable ports for Tor to listen at
    453         ports = [9050, 9150]
    454         while True:
    455             for p in ports:
    456                 net_addr = ("127.0.0.1", p)
    457                 if TorDetector.is_tor_port(net_addr):
    458                     self.found_proxy.emit(net_addr)
    459                     break
    460             else:
    461                 self.found_proxy.emit(None)
    462             time.sleep(10)
    463 
    464     @staticmethod
    465     def is_tor_port(net_addr: Tuple[str, int]) -> bool:
    466         try:
    467             with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    468                 s.settimeout(0.1)
    469                 s.connect(net_addr)
    470                 # Tor responds uniquely to HTTP-like requests
    471                 s.send(b"GET\n")
    472                 if b"Tor is not an HTTP Proxy" in s.recv(1024):
    473                     return True
    474         except socket.error:
    475             pass
    476         return False