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