Merge branch 'pr/9507': qt: refactor NetworkChoiceLayout to ProxyWidget+ServerWidget
ref https://github.com/spesmilo/electrum/pull/9507
This commit is contained in:
@@ -25,7 +25,7 @@ import locale
|
||||
import traceback
|
||||
import sys
|
||||
import queue
|
||||
from typing import NamedTuple, Optional
|
||||
from typing import TYPE_CHECKING, NamedTuple, Optional
|
||||
|
||||
from .version import ELECTRUM_VERSION
|
||||
from . import constants
|
||||
@@ -33,6 +33,9 @@ from .i18n import _
|
||||
from .util import make_aiohttp_session, error_text_str_to_safe_str
|
||||
from .logging import describe_os_version, Logger, get_git_version
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .network import ProxySettings
|
||||
|
||||
|
||||
class CrashReportResponse(NamedTuple):
|
||||
status: Optional[str]
|
||||
@@ -69,7 +72,7 @@ class BaseCrashReporter(Logger):
|
||||
Logger.__init__(self)
|
||||
self.exc_args = (exctype, value, tb)
|
||||
|
||||
def send_report(self, asyncio_loop, proxy, *, timeout=None) -> CrashReportResponse:
|
||||
def send_report(self, asyncio_loop, proxy: 'ProxySettings', *, timeout=None) -> CrashReportResponse:
|
||||
# FIXME the caller needs to catch generic "Exception", as this method does not have a well-defined API...
|
||||
if constants.net.GENESIS[-4:] not in ["4943", "e26f"] and ".electrum.org" in BaseCrashReporter.report_server:
|
||||
# Gah! Some kind of altcoin wants to send us crash reports.
|
||||
@@ -98,7 +101,7 @@ class BaseCrashReporter(Logger):
|
||||
)
|
||||
return ret
|
||||
|
||||
async def do_post(self, proxy, url, data) -> str:
|
||||
async def do_post(self, proxy: 'ProxySettings', url, data) -> str:
|
||||
async with make_aiohttp_session(proxy) as session:
|
||||
async with session.post(url, data=data, raise_for_status=True) as resp:
|
||||
return await resp.text()
|
||||
|
||||
@@ -227,21 +227,21 @@ Pane {
|
||||
color: Material.accentColor
|
||||
}
|
||||
Label {
|
||||
text: 'mode' in Network.proxy ? qsTr('enabled') : qsTr('disabled')
|
||||
text: Network.proxy.enabled ? qsTr('enabled') : qsTr('disabled')
|
||||
}
|
||||
|
||||
Label {
|
||||
visible: 'mode' in Network.proxy
|
||||
visible: Network.proxy.enabled
|
||||
text: qsTr('Proxy server:');
|
||||
color: Material.accentColor
|
||||
}
|
||||
Label {
|
||||
visible: 'mode' in Network.proxy
|
||||
text: Network.proxy['host'] ? Network.proxy['host'] + ':' + Network.proxy['port'] : ''
|
||||
visible: Network.proxy.enabled
|
||||
text: Network.proxy.host ? Network.proxy.host + ':' + Network.proxy.port : ''
|
||||
}
|
||||
|
||||
Label {
|
||||
visible: 'mode' in Network.proxy
|
||||
visible: Network.proxy.enabled
|
||||
text: qsTr('Proxy type:');
|
||||
color: Material.accentColor
|
||||
}
|
||||
@@ -253,8 +253,8 @@ Pane {
|
||||
source: '../../icons/tor_logo.png'
|
||||
}
|
||||
Label {
|
||||
visible: 'mode' in Network.proxy
|
||||
text: Network.isProxyTor ? 'TOR' : (Network.proxy['mode'] || '')
|
||||
visible: Network.proxy.enabled
|
||||
text: Network.isProxyTor ? 'TOR' : (Network.proxy.mode || '')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,12 +37,7 @@ ElDialog {
|
||||
text: qsTr('Ok')
|
||||
icon.source: '../../icons/confirmed.png'
|
||||
onClicked: {
|
||||
var proxy = proxyconfig.toProxyDict()
|
||||
if (proxy && proxy['enabled'] == true) {
|
||||
Network.proxy = proxy
|
||||
} else {
|
||||
Network.proxy = {'enabled': false}
|
||||
}
|
||||
Network.proxy = proxyconfig.toProxyDict()
|
||||
rootItem.close()
|
||||
}
|
||||
}
|
||||
@@ -52,17 +47,13 @@ ElDialog {
|
||||
Component.onCompleted: {
|
||||
var p = Network.proxy
|
||||
|
||||
if ('mode' in p) {
|
||||
proxyconfig.proxy_enabled = true
|
||||
proxyconfig.proxy_address = p['host']
|
||||
proxyconfig.proxy_port = p['port']
|
||||
proxyconfig.username = p['user']
|
||||
proxyconfig.password = p['password']
|
||||
proxyconfig.proxy_type = proxyconfig.proxy_type_map.map(function(x) {
|
||||
return x.value
|
||||
}).indexOf(p['mode'])
|
||||
} else {
|
||||
proxyconfig.proxy_enabled = false
|
||||
}
|
||||
proxyconfig.proxy_enabled = p['enabled']
|
||||
proxyconfig.proxy_address = p['host']
|
||||
proxyconfig.proxy_port = p['port']
|
||||
proxyconfig.username = p['user']
|
||||
proxyconfig.password = p['password']
|
||||
proxyconfig.proxy_type = proxyconfig.proxy_type_map.map(function(x) {
|
||||
return x.value
|
||||
}).indexOf(p['mode'])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ Image {
|
||||
property bool lagging: connected && Network.isLagging
|
||||
property bool fork: connected && Network.chaintips > 1
|
||||
property bool syncing: connected && Daemon.currentWallet && Daemon.currentWallet.synchronizing
|
||||
property bool proxy: connected && 'mode' in Network.proxy && Network.proxy.mode
|
||||
property bool proxy: connected && Network.proxy.enabled
|
||||
|
||||
// ?: in order to keep this a binding..
|
||||
source: Qt.resolvedUrl(!connected
|
||||
|
||||
@@ -19,17 +19,17 @@ Item {
|
||||
{ text: qsTr('SOCKS4'), value: 'socks4' }
|
||||
]
|
||||
|
||||
property bool _probing: false
|
||||
|
||||
function toProxyDict() {
|
||||
var p = {}
|
||||
p['enabled'] = pc.proxy_enabled
|
||||
if (pc.proxy_enabled) {
|
||||
var type = proxy_type_map[pc.proxy_type]['value']
|
||||
p['mode'] = type
|
||||
p['host'] = pc.proxy_address
|
||||
p['port'] = pc.proxy_port
|
||||
p['user'] = pc.username
|
||||
p['password'] = pc.password
|
||||
}
|
||||
var type = proxy_type_map[pc.proxy_type]['value']
|
||||
p['mode'] = type
|
||||
p['host'] = pc.proxy_address
|
||||
p['port'] = pc.proxy_port
|
||||
p['user'] = pc.username
|
||||
p['password'] = pc.password
|
||||
return p
|
||||
}
|
||||
|
||||
@@ -51,15 +51,6 @@ Item {
|
||||
textRole: 'text'
|
||||
valueRole: 'value'
|
||||
model: proxy_type_map
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
if (currentIndex == 0) {
|
||||
if (address.text == '' || port.text == '') {
|
||||
address.text = "127.0.0.1"
|
||||
port.text = "9050"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
@@ -109,5 +100,44 @@ Item {
|
||||
enabled: proxy_enabled_cb.checked
|
||||
}
|
||||
}
|
||||
|
||||
Pane {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: constants.paddingLarge
|
||||
padding: 0
|
||||
background: Rectangle {
|
||||
color: constants.darkerDialogBackground
|
||||
}
|
||||
FlatButton {
|
||||
enabled: proxy_enabled_cb.checked && !_probing
|
||||
text: qsTr('Detect Tor proxy')
|
||||
onClicked: {
|
||||
_probing = true
|
||||
Network.probeTor()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BusyIndicator {
|
||||
id: spinner
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: constants.paddingSmall
|
||||
Layout.preferredWidth: constants.iconSizeXLarge
|
||||
Layout.preferredHeight: constants.iconSizeXLarge
|
||||
running: visible
|
||||
visible: _probing
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Network
|
||||
function onTorProbeFinished(host, port) {
|
||||
_probing = false
|
||||
if (host && port) {
|
||||
proxytype.currentIndex = 0
|
||||
proxy_port = ""+port
|
||||
proxy_address = host
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from PyQt6.QtCore import pyqtProperty, pyqtSignal, QObject
|
||||
from PyQt6.QtCore import pyqtProperty, pyqtSignal, QObject, pyqtSlot
|
||||
|
||||
from electrum.logging import get_logger
|
||||
from electrum import constants
|
||||
from electrum.network import ProxySettings
|
||||
from electrum.interface import ServerAddr
|
||||
from electrum.fee_policy import FEERATE_DEFAULT_RELAY
|
||||
|
||||
@@ -24,6 +25,7 @@ class QENetwork(QObject, QtEventListener):
|
||||
serverHeightChanged = pyqtSignal([int], arguments=['height'])
|
||||
proxySet = pyqtSignal()
|
||||
proxyChanged = pyqtSignal()
|
||||
torProbeFinished = pyqtSignal([str, int], arguments=['host', 'port'])
|
||||
statusChanged = pyqtSignal()
|
||||
feeHistogramUpdated = pyqtSignal()
|
||||
chaintipsChanged = pyqtSignal()
|
||||
@@ -237,14 +239,14 @@ class QENetwork(QObject, QtEventListener):
|
||||
@pyqtProperty('QVariantMap', notify=proxyChanged)
|
||||
def proxy(self):
|
||||
net_params = self.network.get_parameters()
|
||||
return net_params.proxy if net_params.proxy else {}
|
||||
proxy = net_params.proxy
|
||||
return proxy.to_dict()
|
||||
|
||||
@proxy.setter
|
||||
def proxy(self, proxy_settings):
|
||||
def proxy(self, proxy_dict):
|
||||
net_params = self.network.get_parameters()
|
||||
if not proxy_settings['enabled']:
|
||||
proxy_settings = None
|
||||
net_params = net_params._replace(proxy=proxy_settings)
|
||||
proxy = ProxySettings.from_dict(proxy_dict)
|
||||
net_params = net_params._replace(proxy=proxy)
|
||||
self.network.run_from_another_thread(self.network.set_parameters(net_params))
|
||||
self.proxyChanged.emit()
|
||||
|
||||
@@ -273,3 +275,7 @@ class QENetwork(QObject, QtEventListener):
|
||||
if self._serverListModel is None:
|
||||
self._serverListModel = QEServerListModel(self.network)
|
||||
return self._serverListModel
|
||||
|
||||
@pyqtSlot()
|
||||
def probeTor(self):
|
||||
ProxySettings.probe_tor(self.torProbeFinished.emit) # via signal
|
||||
|
||||
@@ -256,7 +256,6 @@ class ElectrumGui(BaseElectrumGui, Logger):
|
||||
window.clean_up()
|
||||
if self.network_dialog:
|
||||
self.network_dialog.close()
|
||||
self.network_dialog.clean_up()
|
||||
self.network_dialog = None
|
||||
if self.lightning_dialog:
|
||||
self.lightning_dialog.close()
|
||||
@@ -294,16 +293,13 @@ class ElectrumGui(BaseElectrumGui, Logger):
|
||||
self.lightning_dialog = LightningDialog(self)
|
||||
self.lightning_dialog.bring_to_top()
|
||||
|
||||
def show_network_dialog(self):
|
||||
def show_network_dialog(self, proxy_tab=False):
|
||||
if self.network_dialog:
|
||||
self.network_dialog.on_event_network_updated()
|
||||
self.network_dialog.show()
|
||||
self.network_dialog.show(proxy_tab=proxy_tab)
|
||||
self.network_dialog.raise_()
|
||||
return
|
||||
self.network_dialog = NetworkDialog(
|
||||
network=self.daemon.network,
|
||||
config=self.config)
|
||||
self.network_dialog.show()
|
||||
self.network_dialog = NetworkDialog(network=self.daemon.network)
|
||||
self.network_dialog.show(proxy_tab=proxy_tab)
|
||||
|
||||
def _create_window_for_wallet(self, wallet):
|
||||
w = ElectrumWindow(self, wallet)
|
||||
|
||||
@@ -1005,12 +1005,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
|
||||
if self.fx.is_enabled():
|
||||
balance_text += self.fx.get_fiat_status_text(balance,
|
||||
self.base_unit(), self.get_decimal_point()) or ''
|
||||
if not self.network.proxy:
|
||||
if not self.network.proxy or not self.network.proxy.enabled:
|
||||
icon = read_QIcon("status_connected%s.png"%fork_str)
|
||||
else:
|
||||
icon = read_QIcon("status_connected_proxy%s.png"%fork_str)
|
||||
else:
|
||||
if self.network.proxy:
|
||||
if self.network.proxy and self.network.proxy.enabled:
|
||||
network_text = "{} ({})".format(_("Not connected"), _("proxy enabled"))
|
||||
else:
|
||||
network_text = _("Not connected")
|
||||
@@ -1763,7 +1763,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
|
||||
self.tor_button = StatusBarButton(
|
||||
read_QIcon("tor_logo.png"),
|
||||
_("Tor"),
|
||||
self.gui_object.show_network_dialog,
|
||||
partial(self.gui_object.show_network_dialog, proxy_tab=True),
|
||||
sb_height,
|
||||
)
|
||||
sb.addPermanentWidget(self.tor_button)
|
||||
|
||||
@@ -24,25 +24,24 @@
|
||||
# SOFTWARE.
|
||||
|
||||
from enum import IntEnum
|
||||
import threading
|
||||
|
||||
from PyQt6.QtCore import Qt, pyqtSignal, QThread
|
||||
from PyQt6.QtWidgets import (QTreeWidget, QTreeWidgetItem, QMenu, QGridLayout, QComboBox,
|
||||
QLineEdit, QDialog, QVBoxLayout, QHeaderView, QCheckBox,
|
||||
QTabWidget, QWidget, QLabel)
|
||||
from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot
|
||||
from PyQt6.QtWidgets import (
|
||||
QTreeWidget, QTreeWidgetItem, QMenu, QGridLayout, QComboBox, QLineEdit, QDialog, QVBoxLayout, QHeaderView,
|
||||
QCheckBox, QTabWidget, QWidget, QLabel, QPushButton, QHBoxLayout
|
||||
)
|
||||
from PyQt6.QtGui import QIntValidator
|
||||
|
||||
from electrum.i18n import _
|
||||
from electrum import blockchain
|
||||
from electrum.interface import ServerAddr, PREFERRED_NETWORK_PROTOCOL
|
||||
from electrum.network import Network
|
||||
from electrum.network import Network, ProxySettings, is_valid_host, is_valid_port
|
||||
from electrum.logging import get_logger
|
||||
from electrum.util import detect_tor_socks_proxy
|
||||
from electrum.simple_config import SimpleConfig
|
||||
|
||||
from .util import (Buttons, CloseButton, HelpButton, read_QIcon, char_width_in_lineedit,
|
||||
PasswordLineEdit)
|
||||
from .util import QtEventListener, qt_event_listener
|
||||
from .util import (
|
||||
Buttons, CloseButton, HelpButton, read_QIcon, char_width_in_lineedit, PasswordLineEdit, QtEventListener,
|
||||
qt_event_listener, Spinner
|
||||
)
|
||||
|
||||
|
||||
_logger = get_logger(__name__)
|
||||
@@ -52,32 +51,23 @@ protocol_letters = 'ts'
|
||||
|
||||
|
||||
class NetworkDialog(QDialog, QtEventListener):
|
||||
def __init__(self, *, network: Network, config: 'SimpleConfig'):
|
||||
def __init__(self, *, network: Network):
|
||||
QDialog.__init__(self)
|
||||
self.setWindowTitle(_('Network'))
|
||||
self.setMinimumSize(500, 500)
|
||||
self.nlayout = NetworkChoiceLayout(network, config)
|
||||
self.tabs = tabs = QTabWidget()
|
||||
self._blockchain_tab = blockchain_tab = ServerWidget(network)
|
||||
self._proxy_tab = proxy_tab = ProxyWidget(network)
|
||||
tabs.addTab(blockchain_tab, _('Overview'))
|
||||
tabs.addTab(proxy_tab, _('Proxy'))
|
||||
|
||||
vbox = QVBoxLayout(self)
|
||||
vbox.addLayout(self.nlayout.layout())
|
||||
vbox.addWidget(self.tabs)
|
||||
vbox.addLayout(Buttons(CloseButton(self)))
|
||||
self.register_callbacks()
|
||||
self._cleaned_up = False
|
||||
|
||||
def show(self):
|
||||
def show(self, *, proxy_tab: bool = False):
|
||||
super().show()
|
||||
if td := self.nlayout.td:
|
||||
td.trigger_rescan()
|
||||
|
||||
@qt_event_listener
|
||||
def on_event_network_updated(self):
|
||||
self.nlayout.update()
|
||||
|
||||
def clean_up(self):
|
||||
if self._cleaned_up:
|
||||
return
|
||||
self._cleaned_up = True
|
||||
self.nlayout.clean_up()
|
||||
self.unregister_callbacks()
|
||||
self.tabs.setCurrentWidget(self._proxy_tab if proxy_tab else self._blockchain_tab)
|
||||
|
||||
|
||||
class NodesListWidget(QTreeWidget):
|
||||
@@ -227,375 +217,172 @@ class NodesListWidget(QTreeWidget):
|
||||
super().update()
|
||||
|
||||
|
||||
class NetworkChoiceLayout(object):
|
||||
# TODO consolidate to ProxyWidget+ServerWidget
|
||||
# TODO TorDetector is unnecessary, Network tests socks5 peer and detects Tor
|
||||
# TODO apply on editingFinished is not ideal, separate Apply button and on Close?
|
||||
def __init__(self, network: Network, config: 'SimpleConfig', wizard=False):
|
||||
class ProxyWidget(QWidget):
|
||||
PROXY_MODES = {
|
||||
'socks4': 'SOCKS4',
|
||||
'socks5': 'SOCKS5/TOR'
|
||||
}
|
||||
|
||||
torProbeFinished = pyqtSignal([str, int], arguments=['host', 'port'])
|
||||
|
||||
def __init__(self, network: Network, parent=None):
|
||||
super().__init__(parent)
|
||||
self.network = network
|
||||
self.config = config
|
||||
self.tor_proxy = None
|
||||
self.config = network.config
|
||||
|
||||
self.tabs = tabs = QTabWidget()
|
||||
self._proxy_tab = proxy_tab = QWidget()
|
||||
blockchain_tab = QWidget()
|
||||
tabs.addTab(blockchain_tab, _('Overview'))
|
||||
tabs.addTab(proxy_tab, _('Proxy'))
|
||||
tabs.currentChanged.connect(self._on_tab_changed)
|
||||
|
||||
fixed_width_hostname = 24 * char_width_in_lineedit()
|
||||
fixed_width_port = 6 * char_width_in_lineedit()
|
||||
|
||||
# Proxy tab
|
||||
grid = QGridLayout(proxy_tab)
|
||||
grid.setSpacing(8)
|
||||
|
||||
# proxy setting
|
||||
# proxy setting.
|
||||
self.proxy_cb = QCheckBox(_('Use proxy'))
|
||||
self.proxy_cb.clicked.connect(self.check_disable_proxy)
|
||||
self.proxy_cb.clicked.connect(self.set_proxy)
|
||||
|
||||
self.proxy_mode = QComboBox()
|
||||
self.proxy_mode.addItems(['SOCKS4', 'SOCKS5'])
|
||||
for k, v in self.PROXY_MODES.items():
|
||||
self.proxy_mode.addItem(v, k)
|
||||
self.proxy_mode.setCurrentIndex(1)
|
||||
self.proxy_host = QLineEdit()
|
||||
self.proxy_host.setFixedWidth(fixed_width_hostname)
|
||||
self.proxy_port = QLineEdit()
|
||||
self.proxy_port.setFixedWidth(fixed_width_port)
|
||||
self.proxy_port_validator = QIntValidator(1, 65535)
|
||||
self.proxy_port.setValidator(self.proxy_port_validator)
|
||||
|
||||
self.proxy_user = QLineEdit()
|
||||
self.proxy_user.setPlaceholderText(_("Proxy user"))
|
||||
self.proxy_user.setPlaceholderText(_("Proxy username"))
|
||||
self.proxy_password = PasswordLineEdit()
|
||||
self.proxy_password.setPlaceholderText(_("Password"))
|
||||
self.proxy_password.setFixedWidth(fixed_width_port)
|
||||
|
||||
self.proxy_mode.currentIndexChanged.connect(self.set_proxy)
|
||||
self.proxy_host.editingFinished.connect(self.set_proxy)
|
||||
self.proxy_port.editingFinished.connect(self.set_proxy)
|
||||
self.proxy_user.editingFinished.connect(self.set_proxy)
|
||||
self.proxy_password.editingFinished.connect(self.set_proxy)
|
||||
|
||||
self.proxy_mode.currentIndexChanged.connect(self.proxy_settings_changed)
|
||||
self.proxy_host.textEdited.connect(self.proxy_settings_changed)
|
||||
self.proxy_port.textEdited.connect(self.proxy_settings_changed)
|
||||
self.proxy_user.textEdited.connect(self.proxy_settings_changed)
|
||||
self.proxy_password.textEdited.connect(self.proxy_settings_changed)
|
||||
|
||||
self.tor_cb = QCheckBox(_("Use Tor Proxy"))
|
||||
self.tor_cb.setIcon(read_QIcon("tor_logo.png"))
|
||||
self.tor_cb.hide()
|
||||
self.tor_cb.clicked.connect(self.use_tor_proxy)
|
||||
|
||||
grid.addWidget(self.tor_cb, 1, 0, 1, 3)
|
||||
grid.addWidget(self.proxy_cb, 2, 0, 1, 3)
|
||||
grid.addWidget(HelpButton(_('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.')), 2, 4)
|
||||
grid.addWidget(self.proxy_mode, 4, 1)
|
||||
grid.addWidget(self.proxy_host, 4, 2)
|
||||
grid.addWidget(self.proxy_port, 4, 3)
|
||||
grid.addWidget(self.proxy_user, 5, 2)
|
||||
grid.addWidget(self.proxy_password, 5, 3)
|
||||
grid.setRowStretch(7, 1)
|
||||
|
||||
# Blockchain Tab
|
||||
grid = QGridLayout(blockchain_tab)
|
||||
msg = ' '.join([
|
||||
_("Electrum connects to several nodes in order to download block headers and find out the longest blockchain."),
|
||||
_("This blockchain is used to verify the transactions sent by your transaction server.")
|
||||
])
|
||||
self.status_label = QLabel('')
|
||||
grid.addWidget(QLabel(_('Status') + ':'), 0, 0)
|
||||
grid.addWidget(self.status_label, 0, 1, 1, 3)
|
||||
grid.addWidget(HelpButton(msg), 0, 4)
|
||||
|
||||
self.autoconnect_cb = QCheckBox(_('Select server automatically'))
|
||||
self.autoconnect_cb.setEnabled(self.config.cv.NETWORK_AUTO_CONNECT.is_modifiable())
|
||||
self.autoconnect_cb.clicked.connect(self.set_server)
|
||||
self.autoconnect_cb.clicked.connect(self.update)
|
||||
msg = ' '.join([
|
||||
_("If auto-connect is enabled, Electrum will always use a server that is on the longest blockchain."),
|
||||
_("If it is disabled, you have to choose a server you want to use. Electrum will warn you if your server is lagging.")
|
||||
])
|
||||
grid.addWidget(self.autoconnect_cb, 1, 0, 1, 3)
|
||||
grid.addWidget(HelpButton(msg), 1, 4)
|
||||
|
||||
self.server_e = QLineEdit()
|
||||
self.server_e.setFixedWidth(fixed_width_hostname + fixed_width_port)
|
||||
self.server_e.editingFinished.connect(self.set_server)
|
||||
msg = _("Electrum sends your wallet addresses to a single server, in order to receive your transaction history.")
|
||||
grid.addWidget(QLabel(_('Server') + ':'), 2, 0)
|
||||
grid.addWidget(self.server_e, 2, 1, 1, 3)
|
||||
grid.addWidget(HelpButton(msg), 2, 4)
|
||||
|
||||
self.height_label = QLabel('')
|
||||
msg = _('This is the height of your local copy of the blockchain.')
|
||||
grid.addWidget(QLabel(_('Blockchain') + ':'), 3, 0)
|
||||
grid.addWidget(self.height_label, 3, 1)
|
||||
grid.addWidget(HelpButton(msg), 3, 4)
|
||||
|
||||
self.split_label = QLabel('')
|
||||
grid.addWidget(self.split_label, 4, 0, 1, 3)
|
||||
|
||||
self.nodes_list_widget = NodesListWidget(network=self.network)
|
||||
self.nodes_list_widget.followServer.connect(self.follow_server)
|
||||
self.nodes_list_widget.followChain.connect(self.follow_branch)
|
||||
|
||||
def do_set_server(server):
|
||||
self.server_e.setText(server)
|
||||
self.set_server()
|
||||
self.nodes_list_widget.setServer.connect(do_set_server)
|
||||
grid.addWidget(self.nodes_list_widget, 6, 0, 1, 5)
|
||||
|
||||
vbox = QVBoxLayout()
|
||||
vbox.addWidget(tabs)
|
||||
self.layout_ = vbox
|
||||
# tor detector
|
||||
self.td = td = TorDetector()
|
||||
td.found_proxy.connect(self.suggest_proxy)
|
||||
td.start()
|
||||
|
||||
self.fill_in_proxy_settings()
|
||||
self.update()
|
||||
|
||||
def clean_up(self):
|
||||
if self.td:
|
||||
self.td.found_proxy.disconnect()
|
||||
self.td.stop()
|
||||
self.td = None
|
||||
|
||||
def check_disable_proxy(self, b):
|
||||
if not self.config.cv.NETWORK_PROXY.is_modifiable():
|
||||
b = False
|
||||
for w in [self.proxy_mode, self.proxy_host, self.proxy_port, self.proxy_user, self.proxy_password]:
|
||||
w.setEnabled(b)
|
||||
|
||||
def enable_set_server(self):
|
||||
if self.config.cv.NETWORK_SERVER.is_modifiable():
|
||||
enabled = not self.autoconnect_cb.isChecked()
|
||||
self.server_e.setEnabled(enabled)
|
||||
else:
|
||||
for w in [self.autoconnect_cb, self.server_e, self.nodes_list_widget]:
|
||||
w.setEnabled(False)
|
||||
|
||||
def update(self):
|
||||
net_params = self.network.get_parameters()
|
||||
server = net_params.server
|
||||
auto_connect = net_params.auto_connect
|
||||
if not self.server_e.hasFocus():
|
||||
self.server_e.setText(server.to_friendly_name())
|
||||
self.autoconnect_cb.setChecked(auto_connect)
|
||||
|
||||
height_str = "%d "%(self.network.get_local_height()) + _('blocks')
|
||||
self.height_label.setText(height_str)
|
||||
self.status_label.setText(self.network.get_status())
|
||||
chains = self.network.get_blockchains()
|
||||
if len(chains) > 1:
|
||||
chain = self.network.blockchain()
|
||||
forkpoint = chain.get_max_forkpoint()
|
||||
name = chain.get_name()
|
||||
msg = _('Chain split detected at block {0}').format(forkpoint) + '\n'
|
||||
msg += (_('You are following branch') if auto_connect else _('Your server is on branch'))+ ' ' + name
|
||||
msg += ' (%d %s)' % (chain.get_branch_size(), _('blocks'))
|
||||
else:
|
||||
msg = ''
|
||||
self.split_label.setText(msg)
|
||||
self.nodes_list_widget.update()
|
||||
self.enable_set_server()
|
||||
|
||||
def fill_in_proxy_settings(self):
|
||||
proxy_config = self.network.get_parameters().proxy
|
||||
if not proxy_config:
|
||||
proxy_config = {"mode": "none", "host": "localhost", "port": "9050"}
|
||||
|
||||
b = proxy_config.get('mode') != "none"
|
||||
self.check_disable_proxy(b)
|
||||
if b:
|
||||
self.proxy_cb.setChecked(True)
|
||||
self.proxy_mode.setCurrentIndex(
|
||||
self.proxy_mode.findText(str(proxy_config.get("mode").upper())))
|
||||
|
||||
self.proxy_host.setText(proxy_config.get("host"))
|
||||
self.proxy_port.setText(proxy_config.get("port"))
|
||||
self.proxy_user.setText(proxy_config.get("user", ""))
|
||||
self.proxy_password.setText(proxy_config.get("password", ""))
|
||||
|
||||
def layout(self):
|
||||
return self.layout_
|
||||
|
||||
def follow_branch(self, chain_id):
|
||||
self.network.run_from_another_thread(self.network.follow_chain_given_id(chain_id))
|
||||
self.update()
|
||||
|
||||
def follow_server(self, server: ServerAddr):
|
||||
self.network.run_from_another_thread(self.network.follow_chain_given_server(server))
|
||||
self.update()
|
||||
|
||||
def accept(self):
|
||||
pass
|
||||
|
||||
def set_server(self):
|
||||
net_params = self.network.get_parameters()
|
||||
try:
|
||||
server = ServerAddr.from_str_with_inference(str(self.server_e.text()))
|
||||
if not server: raise Exception("failed to parse")
|
||||
except Exception:
|
||||
return
|
||||
net_params = net_params._replace(server=server,
|
||||
auto_connect=self.autoconnect_cb.isChecked())
|
||||
self.network.run_from_another_thread(self.network.set_parameters(net_params))
|
||||
|
||||
def set_proxy(self):
|
||||
net_params = self.network.get_parameters()
|
||||
if self.proxy_cb.isChecked():
|
||||
if not self.proxy_port.hasAcceptableInput():
|
||||
return
|
||||
proxy = {'mode':str(self.proxy_mode.currentText()).lower(),
|
||||
'host':str(self.proxy_host.text()),
|
||||
'port':str(self.proxy_port.text()),
|
||||
'user':str(self.proxy_user.text()),
|
||||
'password':str(self.proxy_password.text())}
|
||||
else:
|
||||
proxy = None
|
||||
self.tor_cb.setChecked(False)
|
||||
net_params = net_params._replace(proxy=proxy)
|
||||
self.network.run_from_another_thread(self.network.set_parameters(net_params))
|
||||
|
||||
def _on_tab_changed(self):
|
||||
if self.tabs.currentWidget() is self._proxy_tab:
|
||||
self.td.trigger_rescan()
|
||||
|
||||
def suggest_proxy(self, found_proxy):
|
||||
if found_proxy is None:
|
||||
self.tor_cb.hide()
|
||||
return
|
||||
self.tor_proxy = found_proxy
|
||||
self.tor_cb.setText(_("Use Tor proxy at port {}").format(str(found_proxy[1])))
|
||||
if (self.proxy_cb.isChecked()
|
||||
and self.proxy_mode.currentIndex() == self.proxy_mode.findText('SOCKS5')
|
||||
and self.proxy_host.text() == "127.0.0.1"
|
||||
and self.proxy_port.text() == str(found_proxy[1])):
|
||||
self.tor_cb.setChecked(True)
|
||||
self.tor_cb.show()
|
||||
|
||||
def use_tor_proxy(self, use_it):
|
||||
if not use_it:
|
||||
self.proxy_cb.setChecked(False)
|
||||
else:
|
||||
socks5_mode_index = self.proxy_mode.findText('SOCKS5')
|
||||
if socks5_mode_index == -1:
|
||||
_logger.info("can't find proxy_mode 'SOCKS5'")
|
||||
return
|
||||
self.proxy_mode.setCurrentIndex(socks5_mode_index)
|
||||
self.proxy_host.setText("127.0.0.1")
|
||||
self.proxy_port.setText(str(self.tor_proxy[1]))
|
||||
self.proxy_user.setText("")
|
||||
self.proxy_password.setText("")
|
||||
self.tor_cb.setChecked(True)
|
||||
self.proxy_cb.setChecked(True)
|
||||
self.check_disable_proxy(use_it)
|
||||
self.set_proxy()
|
||||
|
||||
def proxy_settings_changed(self):
|
||||
self.tor_cb.setChecked(False)
|
||||
|
||||
|
||||
class TorDetector(QThread):
|
||||
found_proxy = pyqtSignal(object)
|
||||
|
||||
def __init__(self):
|
||||
QThread.__init__(self)
|
||||
self._work_to_do_evt = threading.Event()
|
||||
self._stopping = False
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
# do rescan
|
||||
net_addr = detect_tor_socks_proxy()
|
||||
self.found_proxy.emit(net_addr)
|
||||
# wait until triggered
|
||||
self._work_to_do_evt.wait()
|
||||
self._work_to_do_evt.clear()
|
||||
if self._stopping:
|
||||
return
|
||||
|
||||
def trigger_rescan(self) -> None:
|
||||
self._work_to_do_evt.set()
|
||||
|
||||
def stop(self):
|
||||
self._stopping = True
|
||||
self._work_to_do_evt.set()
|
||||
self.exit()
|
||||
self.wait()
|
||||
|
||||
|
||||
class ProxyWidget(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
fixed_width_hostname = 24 * char_width_in_lineedit()
|
||||
fixed_width_port = 6 * char_width_in_lineedit()
|
||||
self.proxy_password.setPlaceholderText(_("Proxy password"))
|
||||
|
||||
grid = QGridLayout(self)
|
||||
grid.setSpacing(8)
|
||||
|
||||
# proxy setting.
|
||||
self.proxy_cb = QCheckBox(_('Use proxy'))
|
||||
self.proxy_mode = QComboBox()
|
||||
self.proxy_mode.addItems(['SOCKS4', 'SOCKS5'])
|
||||
self.proxy_mode.setCurrentIndex(1)
|
||||
self.proxy_host = QLineEdit()
|
||||
self.proxy_host.setFixedWidth(fixed_width_hostname)
|
||||
self.proxy_port = QLineEdit()
|
||||
self.proxy_port.setFixedWidth(fixed_width_port)
|
||||
self.proxy_user = QLineEdit()
|
||||
self.proxy_user.setPlaceholderText(_("Proxy user"))
|
||||
self.proxy_password = PasswordLineEdit()
|
||||
self.proxy_password.setPlaceholderText(_("Password"))
|
||||
self.proxy_password.setFixedWidth(fixed_width_port)
|
||||
grid.addWidget(self.proxy_cb, 0, 0, 1, 4)
|
||||
proxy_helpbutton = HelpButton(
|
||||
_('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.'))
|
||||
grid.addWidget(proxy_helpbutton, 0, 4, alignment=Qt.AlignmentFlag.AlignRight)
|
||||
grid.addWidget(self.proxy_mode, 1, 0, 1, 1)
|
||||
grid.addWidget(self.proxy_host, 1, 1, 1, 3)
|
||||
grid.addWidget(self.proxy_port, 1, 4, 1, 1)
|
||||
grid.addWidget(self.proxy_user, 2, 1, 1, 2)
|
||||
grid.addWidget(self.proxy_password, 2, 3, 1, 2)
|
||||
|
||||
grid.addWidget(self.proxy_cb, 0, 0, 1, 3)
|
||||
grid.addWidget(HelpButton(_('Proxy settings apply to all connections: with Electrum servers, but also with third-party services.')), 0, 4)
|
||||
grid.addWidget(self.proxy_mode, 1, 1)
|
||||
grid.addWidget(self.proxy_host, 1, 2)
|
||||
grid.addWidget(self.proxy_port, 1, 3)
|
||||
grid.addWidget(self.proxy_user, 2, 2)
|
||||
grid.addWidget(self.proxy_password, 2, 3)
|
||||
detect_l = QHBoxLayout()
|
||||
self.detect_button = QPushButton(_('Detect Tor proxy'))
|
||||
self.spinner = Spinner()
|
||||
self.spinner.setMargin(5)
|
||||
detect_l.addWidget(self.detect_button)
|
||||
detect_l.addWidget(self.spinner)
|
||||
|
||||
def get_proxy_settings(self):
|
||||
return {
|
||||
'enabled': self.proxy_cb.isChecked(),
|
||||
'mode': ['socks4', 'socks5'][self.proxy_mode.currentIndex()],
|
||||
'host': self.proxy_host.text(),
|
||||
'port': self.proxy_port.text(),
|
||||
'user': self.proxy_user.text(),
|
||||
'password': self.proxy_password.text()
|
||||
}
|
||||
grid.addLayout(detect_l, 3, 0, 1, 5, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||
|
||||
spacer = QVBoxLayout()
|
||||
spacer.addStretch(1)
|
||||
grid.addLayout(spacer, 4, 0, 1, 5)
|
||||
|
||||
self.update_from_config()
|
||||
self.update()
|
||||
|
||||
# connect signal handlers after init from config
|
||||
self.proxy_cb.stateChanged.connect(self.on_proxy_enable_toggle)
|
||||
self.proxy_mode.currentIndexChanged.connect(self.on_proxy_settings_changed)
|
||||
self.proxy_host.editingFinished.connect(self.on_proxy_settings_changed)
|
||||
self.proxy_port.editingFinished.connect(self.on_proxy_settings_changed)
|
||||
self.proxy_user.editingFinished.connect(self.on_proxy_settings_changed)
|
||||
self.proxy_password.editingFinished.connect(self.on_proxy_settings_changed)
|
||||
self.detect_button.clicked.connect(self.detect_tor)
|
||||
|
||||
self.torProbeFinished.connect(self.on_tor_probe_finished)
|
||||
|
||||
def update(self):
|
||||
enabled = self.proxy_cb.isChecked() and self.config.cv.NETWORK_PROXY.is_modifiable()
|
||||
for item in [
|
||||
self.proxy_mode, self.proxy_host, self.proxy_port, self.proxy_user, self.proxy_password,
|
||||
self.detect_button
|
||||
]:
|
||||
item.setEnabled(enabled)
|
||||
|
||||
if not self.proxy_port.hasAcceptableInput() and not is_valid_port(self.proxy_port.text()):
|
||||
return
|
||||
|
||||
if not is_valid_host(self.proxy_host.text()):
|
||||
return
|
||||
|
||||
net_params = self.network.get_parameters()
|
||||
proxy = self.get_proxy_settings()
|
||||
net_params = net_params._replace(proxy=proxy)
|
||||
self.network.run_from_another_thread(self.network.set_parameters(net_params))
|
||||
|
||||
def update_from_config(self):
|
||||
proxy = ProxySettings.from_config(self.config)
|
||||
self.proxy_cb.setChecked(proxy.enabled)
|
||||
self.proxy_mode.setCurrentText(self.PROXY_MODES.get(proxy.mode))
|
||||
self.proxy_host.setText(proxy.host)
|
||||
self.proxy_port.setText(proxy.port)
|
||||
self.proxy_user.setText(proxy.user)
|
||||
self.proxy_password.setText(proxy.password)
|
||||
|
||||
if not self.config.cv.NETWORK_PROXY.is_modifiable():
|
||||
for w in [
|
||||
self.proxy_cb, self.proxy_mode, self.proxy_host, self.proxy_port,
|
||||
self.proxy_user, self.proxy_password, self.detect_button
|
||||
]:
|
||||
w.setEnabled(False)
|
||||
|
||||
def on_proxy_enable_toggle(self):
|
||||
# probe if enabled and no pre-existing settings
|
||||
# if self.proxy_cb.isChecked() and (not self.proxy_host.text() or not self.proxy_port.text()):
|
||||
# self.detect_tor()
|
||||
self.update()
|
||||
|
||||
def on_proxy_settings_changed(self):
|
||||
self.update()
|
||||
|
||||
def get_proxy_settings(self) -> ProxySettings:
|
||||
proxy = ProxySettings()
|
||||
proxy.enabled = self.proxy_cb.isChecked()
|
||||
proxy.mode = self.proxy_mode.currentData()
|
||||
proxy.host = self.proxy_host.text()
|
||||
proxy.port = self.proxy_port.text()
|
||||
proxy.user = self.proxy_user.text()
|
||||
proxy.password = self.proxy_password.text()
|
||||
return proxy
|
||||
|
||||
def detect_tor(self):
|
||||
self.detect_button.setEnabled(False)
|
||||
self.spinner.setVisible(True)
|
||||
ProxySettings.probe_tor(self.torProbeFinished.emit) # via signal
|
||||
|
||||
@pyqtSlot(str, int)
|
||||
def on_tor_probe_finished(self, host: str, port: int):
|
||||
self.detect_button.setEnabled(True)
|
||||
self.spinner.setVisible(False)
|
||||
if host:
|
||||
self.proxy_mode.setCurrentIndex(1)
|
||||
self.proxy_host.setText(host)
|
||||
self.proxy_port.setText(str(port))
|
||||
self.update()
|
||||
|
||||
|
||||
class ServerWidget(QWidget, QtEventListener):
|
||||
def __init__(self, network, parent=None):
|
||||
def __init__(self, network: Network, parent=None):
|
||||
super().__init__(parent)
|
||||
self.network = network
|
||||
self.config = network.config
|
||||
|
||||
fixed_width_hostname = 24 * char_width_in_lineedit()
|
||||
fixed_width_port = 6 * char_width_in_lineedit()
|
||||
|
||||
self.setLayout(QVBoxLayout())
|
||||
|
||||
grid = QGridLayout(self)
|
||||
grid = QGridLayout()
|
||||
|
||||
msg = ' '.join([
|
||||
_("Electrum connects to several nodes in order to download block headers and find out the longest blockchain."),
|
||||
_("This blockchain is used to verify the transactions sent by your transaction server.")
|
||||
])
|
||||
self.status_label_header = QLabel(_('Status') + ':')
|
||||
self.status_label = QLabel('')
|
||||
grid.addWidget(QLabel(_('Status') + ':'), 0, 0)
|
||||
self.status_label_helpbutton = HelpButton(msg)
|
||||
grid.addWidget(self.status_label_header, 0, 0)
|
||||
grid.addWidget(self.status_label, 0, 1, 1, 3)
|
||||
grid.addWidget(HelpButton(msg), 0, 4)
|
||||
grid.addWidget(self.status_label_helpbutton, 0, 4)
|
||||
|
||||
self.autoconnect_cb = QCheckBox(_('Select server automatically'))
|
||||
self.autoconnect_cb.setEnabled(self.config.cv.NETWORK_AUTO_CONNECT.is_modifiable())
|
||||
self.autoconnect_cb.stateChanged.connect(self.on_server_settings_changed)
|
||||
|
||||
msg = ' '.join([
|
||||
_("If auto-connect is enabled, Electrum will always use a server that is on the longest blockchain."),
|
||||
_("If it is disabled, you have to choose a server you want to use. Electrum will warn you if your server is lagging.")
|
||||
@@ -604,17 +391,19 @@ class ServerWidget(QWidget, QtEventListener):
|
||||
grid.addWidget(HelpButton(msg), 1, 4)
|
||||
|
||||
self.server_e = QLineEdit()
|
||||
self.server_e.setFixedWidth(fixed_width_hostname + fixed_width_port)
|
||||
self.server_e.editingFinished.connect(self.on_server_settings_changed)
|
||||
msg = _("Electrum sends your wallet addresses to a single server, in order to receive your transaction history.")
|
||||
grid.addWidget(QLabel(_('Server') + ':'), 2, 0)
|
||||
grid.addWidget(self.server_e, 2, 1, 1, 3)
|
||||
grid.addWidget(HelpButton(msg), 2, 4)
|
||||
|
||||
self.height_label = QLabel('')
|
||||
msg = _('This is the height of your local copy of the blockchain.')
|
||||
grid.addWidget(QLabel(_('Blockchain') + ':'), 3, 0)
|
||||
self.height_label_header = QLabel(_('Blockchain') + ':')
|
||||
self.height_label = QLabel('')
|
||||
self.height_label_helpbutton = HelpButton(msg)
|
||||
grid.addWidget(self.height_label_header, 3, 0)
|
||||
grid.addWidget(self.height_label, 3, 1)
|
||||
grid.addWidget(HelpButton(msg), 3, 4)
|
||||
grid.addWidget(self.height_label_helpbutton, 3, 4)
|
||||
|
||||
self.split_label = QLabel('')
|
||||
grid.addWidget(self.split_label, 4, 0, 1, 3)
|
||||
@@ -636,9 +425,62 @@ class ServerWidget(QWidget, QtEventListener):
|
||||
self.register_callbacks()
|
||||
self.destroyed.connect(lambda: self.unregister_callbacks())
|
||||
|
||||
self.update_from_config()
|
||||
self.update()
|
||||
|
||||
@qt_event_listener
|
||||
def on_event_network_updated(self):
|
||||
self.nodes_list_widget.update()
|
||||
self.nodes_list_widget.update() # NOTE: move event handling to widget itself?
|
||||
self.update()
|
||||
|
||||
def on_server_settings_changed(self):
|
||||
if not self.network._was_started:
|
||||
self.update()
|
||||
return
|
||||
auto_connect = self.autoconnect_cb.isChecked()
|
||||
server = self.server_e.text().strip()
|
||||
net_params = self.network.get_parameters()
|
||||
if server != net_params.server or auto_connect != net_params.auto_connect:
|
||||
self.set_server()
|
||||
|
||||
def update(self):
|
||||
auto_connect = self.autoconnect_cb.isChecked()
|
||||
self.server_e.setEnabled(self.config.cv.NETWORK_SERVER.is_modifiable() and not auto_connect)
|
||||
|
||||
for item in [
|
||||
self.status_label_header, self.status_label, self.status_label_helpbutton,
|
||||
self.height_label_header, self.height_label, self.height_label_helpbutton]:
|
||||
item.setVisible(self.network._was_started)
|
||||
|
||||
msg = ''
|
||||
if self.network._was_started:
|
||||
# Network was started, so we don't run in initial setup wizard.
|
||||
# behavior in this case is to apply changes immediately.
|
||||
# Also, we show block height and potential chain tips
|
||||
height_str = _('{} blocks').format(self.network.get_local_height())
|
||||
self.height_label.setText(height_str)
|
||||
self.status_label.setText(self.network.get_status())
|
||||
chains = self.network.get_blockchains()
|
||||
if len(chains) > 1:
|
||||
chain = self.network.blockchain()
|
||||
forkpoint = chain.get_max_forkpoint()
|
||||
name = chain.get_name()
|
||||
msg = _('Chain split detected at block {0}').format(forkpoint) + '\n'
|
||||
if auto_connect:
|
||||
msg += _('You are following branch {}').format(name)
|
||||
else:
|
||||
msg += _('Your server is on branch {0} ({1} blocks)').format(name, chain.get_branch_size())
|
||||
self.split_label.setText(msg)
|
||||
|
||||
def update_from_config(self):
|
||||
auto_connect = self.config.NETWORK_AUTO_CONNECT
|
||||
self.autoconnect_cb.setChecked(auto_connect)
|
||||
server = self.config.NETWORK_SERVER
|
||||
self.server_e.setText(server)
|
||||
|
||||
self.autoconnect_cb.setEnabled(self.config.cv.NETWORK_AUTO_CONNECT.is_modifiable())
|
||||
self.server_e.setEnabled(self.config.cv.NETWORK_SERVER.is_modifiable() and not auto_connect)
|
||||
self.nodes_list_widget.setEnabled(self.config.cv.NETWORK_SERVER.is_modifiable())
|
||||
|
||||
def follow_branch(self, chain_id):
|
||||
self.network.run_from_another_thread(self.network.follow_chain_given_id(chain_id))
|
||||
|
||||
@@ -25,7 +25,7 @@ from electrum.submarine_swaps import SwapServerError
|
||||
from .amountedit import AmountEdit, BTCAmountEdit, SizedFreezableLineEdit
|
||||
from .paytoedit import InvalidPaymentIdentifier
|
||||
from .util import (WaitingDialog, HelpLabel, MessageBoxMixin, EnterButton, char_width_in_lineedit,
|
||||
get_iconname_camera, read_QIcon, ColorScheme, icon_path, IconLabel)
|
||||
get_iconname_camera, read_QIcon, ColorScheme, icon_path, IconLabel, Spinner)
|
||||
from .invoice_list import InvoiceList
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -142,14 +142,8 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
|
||||
self.paste_button.setToolTip(_('Paste invoice from clipboard'))
|
||||
self.paste_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
|
||||
self.spinner = QMovie(icon_path('spinner.gif'))
|
||||
self.spinner.setScaledSize(QSize(24, 24))
|
||||
self.spinner.setBackgroundColor(QColor('black'))
|
||||
self.spinner_l = QLabel()
|
||||
self.spinner_l.setMargin(5)
|
||||
self.spinner_l.setVisible(False)
|
||||
self.spinner_l.setMovie(self.spinner)
|
||||
grid.addWidget(self.spinner_l, 0, 1, 1, 4, Qt.AlignmentFlag.AlignRight)
|
||||
self.spinner = Spinner()
|
||||
grid.addWidget(self.spinner, 0, 1, 1, 4, Qt.AlignmentFlag.AlignRight)
|
||||
|
||||
self.save_button = EnterButton(_("Save"), self.do_save_invoice)
|
||||
self.save_button.setEnabled(False)
|
||||
@@ -216,13 +210,6 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
|
||||
|
||||
self.setTabOrder(self.send_button, self.invoice_list)
|
||||
|
||||
def showSpinner(self, b):
|
||||
self.spinner_l.setVisible(b)
|
||||
if b:
|
||||
self.spinner.start()
|
||||
else:
|
||||
self.spinner.stop()
|
||||
|
||||
def on_amount_changed(self, text):
|
||||
# FIXME: implement full valid amount check to enable/disable Pay button
|
||||
pi = self.payto_e.payment_identifier
|
||||
@@ -395,7 +382,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
|
||||
def prepare_for_send_tab_network_lookup(self):
|
||||
for btn in [self.save_button, self.send_button, self.clear_button]:
|
||||
btn.setEnabled(False)
|
||||
self.showSpinner(True)
|
||||
self.spinner.setVisible(True)
|
||||
|
||||
def payment_request_error(self, error):
|
||||
self.show_message(error)
|
||||
@@ -506,7 +493,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
|
||||
# TODO: resolve can happen while typing, we don't want message dialogs to pop up
|
||||
# currently we don't set error for emaillike recipients to avoid just that
|
||||
self.logger.debug('payment identifier resolve done')
|
||||
self.showSpinner(False)
|
||||
self.spinner.setVisible(False)
|
||||
if pi.error:
|
||||
self.show_error(pi.error)
|
||||
self.do_clear()
|
||||
@@ -562,7 +549,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
|
||||
return self.amount_e.get_amount() or 0
|
||||
|
||||
def on_finalize_done(self, pi: PaymentIdentifier):
|
||||
self.showSpinner(False)
|
||||
self.spinner.setVisible(False)
|
||||
self.update_fields()
|
||||
if pi.error:
|
||||
self.show_error(pi.error)
|
||||
|
||||
@@ -11,7 +11,7 @@ from typing import (NamedTuple, Callable, Optional, TYPE_CHECKING, List, Any, Se
|
||||
|
||||
from PyQt6 import QtCore
|
||||
from PyQt6.QtGui import (QFont, QColor, QCursor, QPixmap, QImage,
|
||||
QPalette, QIcon, QFontMetrics, QPainter, QContextMenuEvent)
|
||||
QPalette, QIcon, QFontMetrics, QPainter, QContextMenuEvent, QMovie)
|
||||
from PyQt6.QtCore import (Qt, pyqtSignal, QCoreApplication, QThread, QSize, QRect, QPoint, QObject)
|
||||
from PyQt6.QtWidgets import (QPushButton, QLabel, QMessageBox, QHBoxLayout, QVBoxLayout, QLineEdit,
|
||||
QStyle, QDialog, QGroupBox, QButtonGroup, QRadioButton,
|
||||
@@ -117,6 +117,22 @@ class AmountLabel(QLabel):
|
||||
self.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
|
||||
|
||||
class Spinner(QLabel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
QLabel.__init__(self, *args, **kwargs)
|
||||
self.spinner = QMovie(icon_path('spinner.gif'))
|
||||
self.spinner.setScaledSize(QSize(20, 20))
|
||||
self.spinner.frameChanged.connect(lambda: self.setPixmap(self.spinner.currentPixmap()))
|
||||
self.setVisible(False)
|
||||
|
||||
def setVisible(self, visible):
|
||||
if visible:
|
||||
self.spinner.start()
|
||||
else:
|
||||
self.spinner.stop()
|
||||
super().setVisible(visible)
|
||||
|
||||
|
||||
class HelpMixin:
|
||||
def __init__(self, help_text: str, *, help_title: str = None):
|
||||
assert isinstance(self, QWidget), "HelpMixin must be a QWidget instance!"
|
||||
|
||||
@@ -88,16 +88,15 @@ class WCWelcome(WizardComponent):
|
||||
class WCProxyConfig(WizardComponent):
|
||||
def __init__(self, parent, wizard):
|
||||
WizardComponent.__init__(self, parent, wizard, title=_('Proxy'))
|
||||
self.pw = ProxyWidget(self)
|
||||
self.pw = ProxyWidget(wizard._daemon.network, self)
|
||||
self.pw.proxy_cb.setChecked(True)
|
||||
self.pw.proxy_host.setText('localhost')
|
||||
self.pw.proxy_port.setText('9050')
|
||||
self.layout().addWidget(self.pw)
|
||||
self.layout().addStretch(1)
|
||||
self._valid = True
|
||||
|
||||
def apply(self):
|
||||
self.wizard_data['proxy'] = self.pw.get_proxy_settings()
|
||||
self.wizard_data['proxy'] = self.pw.get_proxy_settings().to_dict()
|
||||
|
||||
|
||||
class WCServerConfig(WizardComponent):
|
||||
|
||||
@@ -23,7 +23,7 @@ from electrum.transaction import PartialTxOutput
|
||||
from electrum.wallet import Wallet, Abstract_Wallet
|
||||
from electrum.wallet_db import WalletDB
|
||||
from electrum.storage import WalletStorage
|
||||
from electrum.network import NetworkParameters, TxBroadcastError, BestEffortRequestFailed
|
||||
from electrum.network import NetworkParameters, TxBroadcastError, BestEffortRequestFailed, ProxySettings
|
||||
from electrum.interface import ServerAddr
|
||||
from electrum.invoices import Invoice
|
||||
|
||||
@@ -753,10 +753,14 @@ class ElectrumGui(BaseElectrumGui, EventListener):
|
||||
self.show_message("Error:" + server_str + "\nIn doubt, type \"auto-connect\"")
|
||||
return False
|
||||
if out.get('server') or out.get('proxy') or out.get('proxy user') or out.get('proxy pass'):
|
||||
new_proxy_config = electrum.network.deserialize_proxy(out.get('proxy')) if out.get('proxy') else proxy_config
|
||||
if new_proxy_config:
|
||||
new_proxy_config['user'] = out.get('proxy user') if 'proxy user' in out else proxy_config['user']
|
||||
new_proxy_config['pass'] = out.get('proxy pass') if 'proxy pass' in out else proxy_config['pass']
|
||||
if out.get('proxy'):
|
||||
new_proxy_config = ProxySettings()
|
||||
new_proxy_config.deserialize_proxy_cfgstr(out.get('proxy'))
|
||||
new_proxy_config.user = out.get('proxy user', proxy_config.user)
|
||||
new_proxy_config.password = out.get('proxy pass', proxy_config.password)
|
||||
new_proxy_config.enabled = True
|
||||
else:
|
||||
new_proxy_config = proxy_config
|
||||
net_params = NetworkParameters(
|
||||
server=server_addr,
|
||||
proxy=new_proxy_config,
|
||||
|
||||
@@ -31,7 +31,10 @@ import threading
|
||||
import socket
|
||||
import json
|
||||
import sys
|
||||
from typing import NamedTuple, Optional, Sequence, List, Dict, Tuple, TYPE_CHECKING, Iterable, Set, Any, TypeVar
|
||||
from typing import (
|
||||
NamedTuple, Optional, Sequence, List, Dict, Tuple, TYPE_CHECKING, Iterable, Set, Any, TypeVar,
|
||||
Callable
|
||||
)
|
||||
import traceback
|
||||
import concurrent
|
||||
from concurrent import futures
|
||||
@@ -48,7 +51,7 @@ from . import util
|
||||
from .util import (log_exceptions, ignore_exceptions, OldTaskGroup,
|
||||
bfh, make_aiohttp_session, send_exception_to_crash_reporter,
|
||||
is_hash256_str, is_non_negative_integer, MyEncoder, NetworkRetryManager,
|
||||
error_text_str_to_safe_str)
|
||||
error_text_str_to_safe_str, detect_tor_socks_proxy)
|
||||
from .bitcoin import COIN, DummyAddress, DummyAddressUsedInTxException
|
||||
from . import constants
|
||||
from . import blockchain
|
||||
@@ -158,65 +161,133 @@ def pick_random_server(hostmap=None, *, allowed_protocols: Iterable[str],
|
||||
return random.choice(eligible) if eligible else None
|
||||
|
||||
|
||||
class NetworkParameters(NamedTuple):
|
||||
server: ServerAddr
|
||||
proxy: Optional[dict]
|
||||
auto_connect: bool
|
||||
oneserver: bool = False
|
||||
def is_valid_port(ps: str):
|
||||
try:
|
||||
return 0 < int(ps) < 65535
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
proxy_modes = ['socks4', 'socks5']
|
||||
def is_valid_host(ph: str):
|
||||
try:
|
||||
NetAddress(ph, '1')
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def serialize_proxy(p):
|
||||
if not isinstance(p, dict):
|
||||
return None
|
||||
return ':'.join([p.get('mode'), p.get('host'), p.get('port')])
|
||||
class ProxySettings:
|
||||
MODES = ['socks4', 'socks5']
|
||||
|
||||
probe_fut = None
|
||||
|
||||
def deserialize_proxy(s: Optional[str], user: str = None, password: str = None) -> Optional[dict]:
|
||||
if not isinstance(s, str):
|
||||
return None
|
||||
if s.lower() == 'none':
|
||||
return None
|
||||
proxy = {"mode": "socks5", "host": "localhost"}
|
||||
def __init__(self):
|
||||
self.enabled = False
|
||||
self.mode = 'socks5'
|
||||
self.host = ''
|
||||
self.port = ''
|
||||
self.user = None
|
||||
self.password = None
|
||||
|
||||
args = s.split(':')
|
||||
if args[0] in proxy_modes:
|
||||
proxy['mode'] = args[0]
|
||||
args = args[1:]
|
||||
def set_defaults(self):
|
||||
self.__init__() # call __init__ for default values
|
||||
|
||||
def is_valid_port(ps: str):
|
||||
try:
|
||||
return 0 < int(ps) < 65535
|
||||
except ValueError:
|
||||
return False
|
||||
def serialize_proxy_cfgstr(self):
|
||||
return ':'.join([self.mode, self.host, self.port])
|
||||
|
||||
def is_valid_host(ph: str):
|
||||
try:
|
||||
NetAddress(ph, '1')
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
def deserialize_proxy_cfgstr(self, s: Optional[str], user: str = None, password: str = None) -> None:
|
||||
if s is None or (isinstance(s, str) and s.lower() == 'none'):
|
||||
self.set_defaults()
|
||||
self.user = user
|
||||
self.password = password
|
||||
return
|
||||
|
||||
# detect migrate from old settings
|
||||
if len(args) == 4 and is_valid_host(args[0]) and is_valid_port(args[1]): # host:port:user:pass,
|
||||
proxy['host'] = args[0]
|
||||
proxy['port'] = args[1]
|
||||
proxy['user'] = args[2]
|
||||
proxy['password'] = args[3]
|
||||
if not isinstance(s, str):
|
||||
raise ValueError('proxy config not a string')
|
||||
|
||||
args = s.split(':')
|
||||
if args[0] in ProxySettings.MODES:
|
||||
self.mode = args[0]
|
||||
args = args[1:]
|
||||
|
||||
# detect migrate from old settings
|
||||
if len(args) == 4 and is_valid_host(args[0]) and is_valid_port(args[1]): # host:port:user:pass,
|
||||
self.host = args[0]
|
||||
self.port = args[1]
|
||||
self.user = args[2]
|
||||
self.password = args[3]
|
||||
else:
|
||||
self.host = ':'.join(args[:-1])
|
||||
self.port = args[-1]
|
||||
self.user = user
|
||||
self.password = password
|
||||
|
||||
if not is_valid_host(self.host) or not is_valid_port(self.port):
|
||||
self.enabled = False
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'enabled': self.enabled,
|
||||
'mode': self.mode,
|
||||
'host': self.host,
|
||||
'port': self.port,
|
||||
'user': self.user,
|
||||
'password': self.password
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config: 'SimpleConfig') -> 'ProxySettings':
|
||||
proxy = ProxySettings()
|
||||
proxy.deserialize_proxy_cfgstr(
|
||||
config.NETWORK_PROXY, config.NETWORK_PROXY_USER, config.NETWORK_PROXY_PASSWORD
|
||||
)
|
||||
proxy.enabled = config.NETWORK_PROXY_ENABLED
|
||||
return proxy
|
||||
|
||||
proxy['host'] = ':'.join(args[:-1])
|
||||
proxy['port'] = args[-1]
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict) -> 'ProxySettings':
|
||||
proxy = ProxySettings()
|
||||
proxy.enabled = d.get('enabled', proxy.enabled)
|
||||
proxy.mode = d.get('mode', proxy.mode)
|
||||
proxy.host = d.get('host', proxy.host)
|
||||
proxy.port = d.get('port', proxy.port)
|
||||
proxy.user = d.get('user', proxy.user)
|
||||
proxy.password = d.get('password', proxy.password)
|
||||
return proxy
|
||||
|
||||
if not is_valid_host(proxy['host']) or not is_valid_port(proxy['port']):
|
||||
return None
|
||||
@classmethod
|
||||
def probe_tor(cls, on_finished: Callable[[str | None, int | None], None]):
|
||||
async def detect_task(finished: Callable[[str | None, int | None], None]):
|
||||
net_addr = await detect_tor_socks_proxy()
|
||||
if net_addr is None:
|
||||
finished('', -1)
|
||||
else:
|
||||
host = net_addr[0]
|
||||
port = net_addr[1]
|
||||
finished(host, port)
|
||||
cls.probe_fut = None
|
||||
|
||||
proxy['user'] = user
|
||||
proxy['password'] = password
|
||||
if cls.probe_fut: # one probe at a time
|
||||
return
|
||||
cls.probe_fut = asyncio.run_coroutine_threadsafe(detect_task(on_finished), util.get_asyncio_loop())
|
||||
|
||||
return proxy
|
||||
def __eq__(self, other):
|
||||
return self.enabled == other.enabled \
|
||||
and self.mode == other.mode \
|
||||
and self.host == other.host \
|
||||
and self.port == other.port \
|
||||
and self.user == other.user \
|
||||
and self.password == other.password
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.enabled=} {self.mode=} {self.host=} {self.port=} {self.user=}'
|
||||
|
||||
|
||||
class NetworkParameters(NamedTuple):
|
||||
server: ServerAddr
|
||||
proxy: ProxySettings
|
||||
auto_connect: bool
|
||||
oneserver: bool = False
|
||||
|
||||
|
||||
class BestEffortRequestFailed(NetworkException): pass
|
||||
@@ -325,7 +396,7 @@ class Network(Logger, NetworkRetryManager[ServerAddr]):
|
||||
|
||||
self._allowed_protocols = {PREFERRED_NETWORK_PROTOCOL}
|
||||
|
||||
self.proxy = None # type: Optional[dict]
|
||||
self.proxy = ProxySettings()
|
||||
self.is_proxy_tor = None
|
||||
self._init_parameters_from_config()
|
||||
|
||||
@@ -535,8 +606,7 @@ class Network(Logger, NetworkRetryManager[ServerAddr]):
|
||||
dns_hacks.configure_dns_resolver()
|
||||
self.auto_connect = self.config.NETWORK_AUTO_CONNECT
|
||||
self._set_default_server()
|
||||
self._set_proxy(deserialize_proxy(self.config.NETWORK_PROXY, self.config.NETWORK_PROXY_USER,
|
||||
self.config.NETWORK_PROXY_PASSWORD))
|
||||
self._set_proxy(ProxySettings.from_config(self.config))
|
||||
self._maybe_set_oneserver()
|
||||
|
||||
def get_donation_address(self):
|
||||
@@ -659,7 +729,7 @@ class Network(Logger, NetworkRetryManager[ServerAddr]):
|
||||
self.default_server = pick_random_server(allowed_protocols=self._allowed_protocols)
|
||||
assert isinstance(self.default_server, ServerAddr), f"invalid type for default_server: {self.default_server!r}"
|
||||
|
||||
def _set_proxy(self, proxy: Optional[dict]):
|
||||
def _set_proxy(self, proxy: ProxySettings):
|
||||
if self.proxy == proxy:
|
||||
return
|
||||
|
||||
@@ -673,9 +743,9 @@ class Network(Logger, NetworkRetryManager[ServerAddr]):
|
||||
util.trigger_callback('proxy_set', self.proxy)
|
||||
|
||||
def _detect_if_proxy_is_tor(self) -> None:
|
||||
def tor_probe_task(p):
|
||||
async def tor_probe_task(p):
|
||||
assert p is not None
|
||||
is_tor = util.is_tor_socks_port(p['host'], int(p['port']))
|
||||
is_tor = await util.is_tor_socks_port(p.host, int(p.port))
|
||||
if self.proxy == p: # is this the proxy we probed?
|
||||
if self.is_proxy_tor != is_tor:
|
||||
self.logger.info(f'Proxy is {"" if is_tor else "not "}TOR')
|
||||
@@ -683,32 +753,38 @@ class Network(Logger, NetworkRetryManager[ServerAddr]):
|
||||
util.trigger_callback('tor_probed', is_tor)
|
||||
|
||||
proxy = self.proxy
|
||||
if proxy and proxy['mode'] == 'socks5':
|
||||
t = threading.Thread(target=tor_probe_task, args=(proxy,), daemon=True)
|
||||
t.start()
|
||||
if proxy and proxy.enabled and proxy.mode == 'socks5':
|
||||
# FIXME GC issues? do we need to store the Future?
|
||||
asyncio.run_coroutine_threadsafe(tor_probe_task(proxy), self.asyncio_loop)
|
||||
|
||||
@log_exceptions
|
||||
async def set_parameters(self, net_params: NetworkParameters):
|
||||
proxy = net_params.proxy
|
||||
proxy_str = serialize_proxy(proxy)
|
||||
proxy_user = proxy['user'] if proxy else None
|
||||
proxy_pass = proxy['password'] if proxy else None
|
||||
proxy_str = proxy.serialize_proxy_cfgstr()
|
||||
proxy_enabled = proxy.enabled
|
||||
proxy_user = proxy.user
|
||||
proxy_pass = proxy.password
|
||||
server = net_params.server
|
||||
# sanitize parameters
|
||||
try:
|
||||
if proxy:
|
||||
proxy_modes.index(proxy['mode']) + 1
|
||||
int(proxy['port'])
|
||||
# proxy_modes.index(proxy['mode']) + 1
|
||||
ProxySettings.MODES.index(proxy.mode) + 1
|
||||
# int(proxy['port'])
|
||||
int(proxy.port)
|
||||
except Exception:
|
||||
return
|
||||
proxy.enabled = False
|
||||
# return
|
||||
self.config.NETWORK_AUTO_CONNECT = net_params.auto_connect
|
||||
self.config.NETWORK_ONESERVER = net_params.oneserver
|
||||
self.config.NETWORK_PROXY_ENABLED = proxy_enabled
|
||||
self.config.NETWORK_PROXY = proxy_str
|
||||
self.config.NETWORK_PROXY_USER = proxy_user
|
||||
self.config.NETWORK_PROXY_PASSWORD = proxy_pass
|
||||
self.config.NETWORK_SERVER = str(server)
|
||||
# abort if changes were not allowed by config
|
||||
if self.config.NETWORK_SERVER != str(server) \
|
||||
or self.config.NETWORK_PROXY_ENABLED != proxy_enabled \
|
||||
or self.config.NETWORK_PROXY != proxy_str \
|
||||
or self.config.NETWORK_PROXY_USER != proxy_user \
|
||||
or self.config.NETWORK_PROXY_PASSWORD != proxy_pass \
|
||||
@@ -885,7 +961,7 @@ class Network(Logger, NetworkRetryManager[ServerAddr]):
|
||||
return self.config.NETWORK_TIMEOUT
|
||||
if self.oneserver and not self.auto_connect:
|
||||
return request_type.MOST_RELAXED
|
||||
if self.proxy:
|
||||
if self.proxy and self.proxy.enabled:
|
||||
return request_type.RELAXED
|
||||
return request_type.NORMAL
|
||||
|
||||
|
||||
@@ -566,6 +566,7 @@ class SimpleConfig(Logger):
|
||||
NETWORK_PROXY = ConfigVar('proxy', default=None, type_=str, convert_getter=lambda v: "none" if v is None else v)
|
||||
NETWORK_PROXY_USER = ConfigVar('proxy_user', default=None, type_=str)
|
||||
NETWORK_PROXY_PASSWORD = ConfigVar('proxy_password', default=None, type_=str)
|
||||
NETWORK_PROXY_ENABLED = ConfigVar('enable_proxy', default=lambda config: config.NETWORK_PROXY not in [None, "none"], type_=bool)
|
||||
NETWORK_SERVER = ConfigVar('server', default=None, type_=str)
|
||||
NETWORK_NOONION = ConfigVar('noonion', default=False, type_=bool)
|
||||
NETWORK_OFFLINE = ConfigVar('offline', default=False, type_=bool)
|
||||
|
||||
@@ -67,7 +67,7 @@ from .i18n import _
|
||||
from .logging import get_logger, Logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .network import Network
|
||||
from .network import Network, ProxySettings
|
||||
from .interface import Interface
|
||||
from .simple_config import SimpleConfig
|
||||
from .paymentrequest import PaymentRequest
|
||||
@@ -1313,7 +1313,7 @@ def format_short_id(short_channel_id: Optional[bytes]):
|
||||
+ 'x' + str(int.from_bytes(short_channel_id[6:], 'big'))
|
||||
|
||||
|
||||
def make_aiohttp_session(proxy: Optional[dict], headers=None, timeout=None):
|
||||
def make_aiohttp_session(proxy: Optional['ProxySettings'], headers=None, timeout=None):
|
||||
if headers is None:
|
||||
headers = {'User-Agent': 'Electrum'}
|
||||
if timeout is None:
|
||||
@@ -1324,13 +1324,13 @@ def make_aiohttp_session(proxy: Optional[dict], headers=None, timeout=None):
|
||||
timeout = aiohttp.ClientTimeout(total=timeout)
|
||||
ssl_context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=ca_path)
|
||||
|
||||
if proxy:
|
||||
if proxy and proxy.enabled:
|
||||
connector = ProxyConnector(
|
||||
proxy_type=ProxyType.SOCKS5 if proxy['mode'] == 'socks5' else ProxyType.SOCKS4,
|
||||
host=proxy['host'],
|
||||
port=int(proxy['port']),
|
||||
username=proxy.get('user', None),
|
||||
password=proxy.get('password', None),
|
||||
proxy_type=ProxyType.SOCKS5 if proxy.mode == 'socks5' else ProxyType.SOCKS4,
|
||||
host=proxy.host,
|
||||
port=int(proxy.port),
|
||||
username=proxy.user,
|
||||
password=proxy.password,
|
||||
rdns=True, # needed to prevent DNS leaks over proxy
|
||||
ssl=ssl_context,
|
||||
)
|
||||
@@ -1547,32 +1547,51 @@ class NetworkJobOnDefaultServer(Logger, ABC):
|
||||
return s
|
||||
|
||||
|
||||
def detect_tor_socks_proxy() -> Optional[Tuple[str, int]]:
|
||||
async def detect_tor_socks_proxy() -> Optional[Tuple[str, int]]:
|
||||
# Probable ports for Tor to listen at
|
||||
candidates = [
|
||||
("127.0.0.1", 9050),
|
||||
("127.0.0.1", 9051),
|
||||
("127.0.0.1", 9150),
|
||||
]
|
||||
for net_addr in candidates:
|
||||
if is_tor_socks_port(*net_addr):
|
||||
return net_addr
|
||||
return None
|
||||
|
||||
proxy_addr = None
|
||||
async def test_net_addr(net_addr):
|
||||
is_tor = await is_tor_socks_port(*net_addr)
|
||||
# set result, and cancel remaining probes
|
||||
if is_tor:
|
||||
nonlocal proxy_addr
|
||||
proxy_addr = net_addr
|
||||
await group.cancel_remaining()
|
||||
|
||||
async with OldTaskGroup() as group:
|
||||
for net_addr in candidates:
|
||||
await group.spawn(test_net_addr(net_addr))
|
||||
return proxy_addr
|
||||
|
||||
|
||||
def is_tor_socks_port(host: str, port: int) -> bool:
|
||||
@log_exceptions
|
||||
async def is_tor_socks_port(host: str, port: int) -> bool:
|
||||
# mimic "tor-resolve 0.0.0.0".
|
||||
# see https://github.com/spesmilo/electrum/issues/7317#issuecomment-1369281075
|
||||
# > this is a socks5 handshake, followed by a socks RESOLVE request as defined in
|
||||
# > [tor's socks extension spec](https://github.com/torproject/torspec/blob/7116c9cdaba248aae07a3f1d0e15d9dd102f62c5/socks-extensions.txt#L63),
|
||||
# > resolving 0.0.0.0, which being an IP, tor resolves itself without needing to ask a relay.
|
||||
writer = None
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=10) as s:
|
||||
# mimic "tor-resolve 0.0.0.0".
|
||||
# see https://github.com/spesmilo/electrum/issues/7317#issuecomment-1369281075
|
||||
# > this is a socks5 handshake, followed by a socks RESOLVE request as defined in
|
||||
# > [tor's socks extension spec](https://github.com/torproject/torspec/blob/7116c9cdaba248aae07a3f1d0e15d9dd102f62c5/socks-extensions.txt#L63),
|
||||
# > resolving 0.0.0.0, which being an IP, tor resolves itself without needing to ask a relay.
|
||||
s.send(b'\x05\x01\x00\x05\xf0\x00\x03\x070.0.0.0\x00\x00')
|
||||
if s.recv(1024) == b'\x05\x00\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00':
|
||||
async with async_timeout(10):
|
||||
reader, writer = await asyncio.open_connection(host, port)
|
||||
writer.write(b'\x05\x01\x00\x05\xf0\x00\x03\x070.0.0.0\x00\x00')
|
||||
await writer.drain()
|
||||
data = await reader.read(1024)
|
||||
if data == b'\x05\x00\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00':
|
||||
return True
|
||||
except socket.error:
|
||||
pass
|
||||
return False
|
||||
return False
|
||||
except (OSError, asyncio.TimeoutError):
|
||||
return False
|
||||
finally:
|
||||
if writer:
|
||||
writer.close()
|
||||
|
||||
|
||||
AS_LIB_USER_I_WANT_TO_MANAGE_MY_OWN_ASYNCIO_LOOP = False # used by unit tests
|
||||
@@ -1969,10 +1988,10 @@ class ESocksProxy(aiorpcx.SOCKSProxy):
|
||||
|
||||
@classmethod
|
||||
def from_network_settings(cls, network: Optional['Network']) -> Optional['ESocksProxy']:
|
||||
if not network or not network.proxy:
|
||||
if not network or not network.proxy or not network.proxy.enabled:
|
||||
return None
|
||||
proxy = network.proxy
|
||||
username, pw = proxy.get('user'), proxy.get('password')
|
||||
username, pw = proxy.user, proxy.password
|
||||
if not username or not pw:
|
||||
# is_proxy_tor is tri-state; None indicates it is still probing the proxy to test for TOR
|
||||
if network.is_proxy_tor:
|
||||
@@ -1981,10 +2000,10 @@ class ESocksProxy(aiorpcx.SOCKSProxy):
|
||||
auth = None
|
||||
else:
|
||||
auth = aiorpcx.socks.SOCKSUserAuth(username, pw)
|
||||
addr = aiorpcx.NetAddress(proxy['host'], proxy['port'])
|
||||
if proxy['mode'] == "socks4":
|
||||
addr = aiorpcx.NetAddress(proxy.host, proxy.port)
|
||||
if proxy.mode == "socks4":
|
||||
ret = cls(addr, aiorpcx.socks.SOCKS4a, auth)
|
||||
elif proxy['mode'] == "socks5":
|
||||
elif proxy.mode == "socks5":
|
||||
ret = cls(addr, aiorpcx.socks.SOCKS5, auth)
|
||||
else:
|
||||
raise NotImplementedError # http proxy not available with aiorpcx
|
||||
|
||||
@@ -7,6 +7,7 @@ from electrum.i18n import _
|
||||
from electrum.interface import ServerAddr
|
||||
from electrum.keystore import hardware_keystore
|
||||
from electrum.logging import get_logger
|
||||
from electrum.network import ProxySettings
|
||||
from electrum.plugin import run_hook
|
||||
from electrum.slip39 import EncryptedSeed
|
||||
from electrum.storage import WalletStorage, StorageEncryptionVersion
|
||||
@@ -731,9 +732,8 @@ class ServerConnectWizard(AbstractWizard):
|
||||
return
|
||||
self._logger.debug(f'configuring proxy: {proxy_settings!r}')
|
||||
net_params = self._daemon.network.get_parameters()
|
||||
if not proxy_settings['enabled']:
|
||||
proxy_settings = None
|
||||
net_params = net_params._replace(proxy=proxy_settings, auto_connect=bool(wizard_data['autoconnect']))
|
||||
proxy = ProxySettings.from_dict(proxy_settings)
|
||||
net_params = net_params._replace(proxy=proxy, auto_connect=bool(wizard_data['autoconnect']))
|
||||
self._daemon.network.run_from_another_thread(self._daemon.network.set_parameters(net_params))
|
||||
|
||||
def do_configure_server(self, wizard_data: dict):
|
||||
|
||||
Reference in New Issue
Block a user