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

View File

@@ -2,7 +2,7 @@ import os
from electrum import SimpleConfig
from electrum.interface import ServerAddr
from electrum.network import NetworkParameters
from electrum.network import NetworkParameters, ProxySettings
from electrum.plugin import Plugins
from electrum.wizard import ServerConnectWizard, NewWalletWizard
from tests import ElectrumTestCase
@@ -88,7 +88,7 @@ class ServerConnectWizardTestCase(WizardTestCase):
self.assertTrue(w.is_last_view(v.view, d))
self.assertTrue(w._daemon.network.run_called)
self.assertEqual(NetworkParameters(server=None, proxy=d_proxy, auto_connect=True, oneserver=None), w._daemon.network.parameters)
self.assertEqual(NetworkParameters(server=None, proxy=ProxySettings.from_dict(d_proxy), auto_connect=True, oneserver=None), w._daemon.network.parameters)
async def test_proxy_and_server(self):
w = ServerConnectWizard(DaemonMock(self.config))