1
0

network: create ProxySettings class replacing dict and encapsulating proxy related funcs,

allow enable/disable proxy without nuking proxy mode, host and port (explicit enable_proxy config setting),
move tor probe from frontend to backend code, add probe buttons for Qt and QML
This commit is contained in:
Sander van Grieken
2025-03-03 13:34:05 +01:00
parent f2b1d09a88
commit fea598cfbe
15 changed files with 297 additions and 184 deletions

View File

@@ -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()

View File

@@ -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 || '')
}
}

View File

@@ -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'])
}
}

View File

@@ -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

View File

@@ -22,14 +22,12 @@ Item {
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 +49,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 +98,30 @@ Item {
enabled: proxy_enabled_cb.checked
}
}
Pane {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: constants.paddingLarge
padding: 0
background: Rectangle {
color: Material.dialogColor
}
FlatButton {
enabled: proxy_enabled_cb.checked
text: qsTr('Detect TOR proxy')
onClicked: Network.probeTor()
}
}
}
Connections {
target: Network
function onTorProbeFinished(host, port) {
if (host && port) {
proxytype.currentIndex = 0
proxy_port = ""+port
proxy_address = host
}
}
}
}

View File

@@ -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.simple_config 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()
@@ -253,14 +255,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()
@@ -289,3 +291,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

View File

@@ -1004,12 +1004,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")

View File

@@ -26,15 +26,16 @@
from enum import IntEnum
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtWidgets import (QTreeWidget, QTreeWidgetItem, QMenu, QGridLayout, QComboBox,
QLineEdit, QDialog, QVBoxLayout, QHeaderView, QCheckBox,
QTabWidget, QWidget, QLabel)
from PyQt6.QtWidgets import (
QTreeWidget, QTreeWidgetItem, QMenu, QGridLayout, QComboBox, QLineEdit, QDialog, QVBoxLayout, QHeaderView,
QCheckBox, QTabWidget, QWidget, QLabel, QPushButton
)
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, deserialize_proxy
from electrum.network import Network, ProxySettings
from electrum.logging import get_logger
from .util import (Buttons, CloseButton, HelpButton, read_QIcon, char_width_in_lineedit,
@@ -220,6 +221,8 @@ class ProxyWidget(QWidget):
'socks5': 'SOCKS5/TOR'
}
torProbeFinished = pyqtSignal([str, int], arguments=['host', 'port'])
def __init__(self, network: Network, parent=None):
super().__init__(parent)
self.network = network
@@ -229,25 +232,19 @@ class ProxyWidget(QWidget):
# proxy setting.
self.proxy_cb = QCheckBox(_('Use proxy'))
self.proxy_cb.stateChanged.connect(self.on_proxy_settings_changed)
self.proxy_mode = QComboBox()
for k, v in self.PROXY_MODES.items():
self.proxy_mode.addItem(v, k)
self.proxy_mode.setCurrentIndex(1)
self.proxy_mode.currentIndexChanged.connect(self.on_proxy_settings_changed)
self.proxy_host = QLineEdit()
self.proxy_host.editingFinished.connect(self.on_proxy_settings_changed)
self.proxy_port = QLineEdit()
self.proxy_port.editingFinished.connect(self.on_proxy_settings_changed)
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.editingFinished.connect(self.on_proxy_settings_changed)
self.proxy_user.setPlaceholderText(_("Proxy username"))
self.proxy_password = PasswordLineEdit()
self.proxy_password.editingFinished.connect(self.on_proxy_settings_changed)
self.proxy_password.setPlaceholderText(_("Proxy password"))
grid = QGridLayout(self)
@@ -264,64 +261,90 @@ class ProxyWidget(QWidget):
grid.addWidget(self.proxy_password, 2, 3, 1, 2)
spacer = QVBoxLayout()
spacer.addStretch()
grid.addLayout(spacer, 3, 0, 1, 4)
spacer.addStretch(1)
grid.addLayout(spacer, 3, 0, 1, 5)
self.detect_button = QPushButton(_('Detect TOR proxy'))
grid.addWidget(self.detect_button, 4, 0, 1, 5, alignment=Qt.AlignmentFlag.AlignHCenter)
spacer = QVBoxLayout()
spacer.addStretch(1)
grid.addLayout(spacer, 5, 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]:
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():
return
net_params = self.network.get_parameters()
if self.proxy_cb.isChecked():
if not self.proxy_port.hasAcceptableInput():
return
proxy = {'mode': str(self.proxy_mode.currentData()),
'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
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_config = deserialize_proxy(self.config.NETWORK_PROXY)
if not proxy_config:
proxy_config = {"mode": "none", "host": "localhost", "port": "9050"}
use_proxy = proxy_config.get('mode') != 'none'
self.proxy_cb.setChecked(use_proxy)
self.proxy_mode.setCurrentText(self.PROXY_MODES.get(proxy_config.get("mode")))
self.proxy_host.setText(proxy_config.get('host'))
self.proxy_port.setText(proxy_config.get('port'))
self.proxy_user.setText(self.config.NETWORK_PROXY_USER)
self.proxy_password.setText(self.config.NETWORK_PROXY_PASSWORD)
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.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):
return {
'enabled': self.proxy_cb.isChecked(),
'mode': self.proxy_mode.currentData(),
'host': self.proxy_host.text(),
'port': self.proxy_port.text(),
'user': self.proxy_user.text(),
'password': self.proxy_password.text()
}
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):
ProxySettings.probe_tor(self.torProbeFinished.emit) # via signal
def on_tor_probe_finished(self, host: str, port: int):
if host is not None:
self.proxy_mode.setCurrentIndex(1)
self.proxy_host.setText(host)
self.proxy_port.setText(str(port))
self.update()
class ServerWidget(QWidget, QtEventListener):

View File

@@ -93,11 +93,10 @@ class WCProxyConfig(WizardComponent):
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):

View File

@@ -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,

View File

@@ -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
@@ -156,65 +159,129 @@ 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
class ProxySettings:
MODES = ['socks4', 'socks5']
probe_thread = None
proxy_modes = ['socks4', 'socks5']
def __init__(self):
self.enabled = False
self.mode = 'socks5'
self.host = ''
self.port = ''
self.user = None
self.password = None
def set_defaults(self):
self.__init__() # call __init__ for default values
def serialize_proxy(p):
if not isinstance(p, dict):
return None
return ':'.join([p.get('mode'), p.get('host'), p.get('port')])
def serialize_proxy_cfgstr(self):
return ':'.join([self.mode, self.host, self.port])
def deserialize_proxy_cfgstr(self, s: Optional[str], user: str = None, password: str = None) -> Optional[dict]:
if s is None or not isinstance(s, str) or s.lower() == 'none':
self.set_defaults()
self.user = user
self.password = password
return
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"}
args = s.split(':')
if args[0] in ProxySettings.MODES:
self.mode = args[0]
args = args[1:]
args = s.split(':')
if args[0] in proxy_modes:
proxy['mode'] = args[0]
args = args[1:]
def is_valid_port(ps: str):
try:
return 0 < int(ps) < 65535
except ValueError:
return False
def is_valid_port(ps: str):
try:
return 0 < int(ps) < 65535
except ValueError:
return False
def is_valid_host(ph: str):
try:
NetAddress(ph, '1')
except ValueError:
return False
return True
def is_valid_host(ph: str):
try:
NetAddress(ph, '1')
except ValueError:
return False
return True
# 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
# 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 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_ENABLE_PROXY
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]):
def detect_task(finished: Callable[[str | None, int | None], None]):
net_addr = detect_tor_socks_proxy()
if net_addr is None:
finished(None, None)
else:
host = net_addr[0]
port = net_addr[1]
finished(host, port)
cls.probe_thread = None
proxy['user'] = user
proxy['password'] = password
if cls.probe_thread: # don't spam threads
return
cls.probe_thread = threading.Thread(target=detect_task, args=[on_finished], daemon=True)
cls.probe_thread.start()
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
@@ -323,7 +390,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()
@@ -364,7 +431,6 @@ class Network(Logger, NetworkRetryManager[ServerAddr]):
self._has_ever_managed_to_connect_to_server = False
self._was_started = False
def has_internet_connection(self) -> bool:
"""Our guess whether the device has Internet-connectivity."""
return self._has_ever_managed_to_connect_to_server
@@ -513,8 +579,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):
@@ -637,7 +702,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
@@ -653,7 +718,7 @@ class Network(Logger, NetworkRetryManager[ServerAddr]):
def _detect_if_proxy_is_tor(self) -> None:
def tor_probe_task(p):
assert p is not None
is_tor = util.is_tor_socks_port(p['host'], int(p['port']))
is_tor = 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')
@@ -661,32 +726,38 @@ class Network(Logger, NetworkRetryManager[ServerAddr]):
util.trigger_callback('tor_probed', is_tor)
proxy = self.proxy
if proxy and proxy['mode'] == 'socks5':
if proxy and proxy.enabled and proxy.mode == 'socks5':
t = threading.Thread(target=tor_probe_task, args=(proxy,), daemon=True)
t.start()
@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_ENABLE_PROXY = 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_ENABLE_PROXY != 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 \
@@ -863,7 +934,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

View File

@@ -950,6 +950,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_ENABLE_PROXY = 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)

View File

@@ -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,
)
@@ -1551,6 +1551,7 @@ 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:
@@ -1969,10 +1970,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 +1982,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

View File

@@ -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):