diff --git a/electrum/gui/qml/components/NetworkOverview.qml b/electrum/gui/qml/components/NetworkOverview.qml index 7e9b21f2b..87f67d21a 100644 --- a/electrum/gui/qml/components/NetworkOverview.qml +++ b/electrum/gui/qml/components/NetworkOverview.qml @@ -296,6 +296,7 @@ Pane { dialog.open() } } + } } diff --git a/electrum/gui/qml/components/controls/ServerConfig.qml b/electrum/gui/qml/components/controls/ServerConfig.qml index 47bd4e680..6442f6834 100644 --- a/electrum/gui/qml/components/controls/ServerConfig.qml +++ b/electrum/gui/qml/components/controls/ServerConfig.qml @@ -50,9 +50,73 @@ Item { } } - Label { - text: qsTr("Server") - enabled: address_tf.enabled + RowLayout { + Layout.fillWidth: true + + Label { + text: qsTr("Server") + enabled: address_tf.enabled + Layout.fillWidth: true + } + + ToolButton { + icon.source: '../../../icons/delete.png' + ToolTip.text: qsTr('Reset network data') + ToolTip.visible: hovered + onClicked: resetMenu.open() + + Menu { + id: resetMenu + + MenuItem { + text: qsTr('SSL certificates') + onTriggered: { + var dialog = app.messageDialog.createObject(app, { + title: qsTr('Are you sure?'), + text: qsTr('This will remove cached SSL certificates for servers and reconnect to fetch them again.'), + yesno: true + }) + dialog.accepted.connect(function() { + var removed = Network.clearPinnedServerCertificates() + var msg = removed < 0 + ? qsTr('Failed to reset SSL certificates.') + : removed > 0 + ? qsTr('%1 certificate files were removed.').arg(removed) + : qsTr('No cached certificate files were found.') + app.messageDialog.createObject(app, { + title: qsTr('Reset SSL certificates'), + text: msg + }).open() + }) + dialog.open() + } + } + + MenuItem { + text: qsTr('Known servers') + onTriggered: { + var dialog = app.messageDialog.createObject(app, { + title: qsTr('Are you sure?'), + text: qsTr('This will remove the list of known servers.'), + yesno: true + }) + dialog.accepted.connect(function() { + var removed = Network.clearRecentServers() + var msg = removed < 0 + ? qsTr('Failed to reset known servers.') + : removed > 0 + ? qsTr('%1 server(s) were removed.').arg(removed) + : qsTr('No known servers were found.') + app.messageDialog.createObject(app, { + title: qsTr('Reset known servers'), + text: msg + }).open() + }) + dialog.open() + } + } + } + } } TextHighlightPane { diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py index 099e21a80..a6a500dec 100644 --- a/electrum/gui/qml/qenetwork.py +++ b/electrum/gui/qml/qenetwork.py @@ -306,3 +306,19 @@ class QENetwork(QObject, QtEventListener): @pyqtSlot() def probeTor(self): ProxySettings.probe_tor(self.torProbeFinished.emit) # via signal + + @pyqtSlot(result=int) + def clearPinnedServerCertificates(self): + try: + return self.network.run_from_another_thread(self.network.clear_pinned_server_certs()) + except Exception: + self._logger.exception("failed to clear pinned server certificates") + return -1 + + @pyqtSlot(result=int) + def clearRecentServers(self): + try: + return self.network.clear_recent_servers() + except Exception: + self._logger.exception("failed to clear recent servers") + return -1 diff --git a/electrum/gui/qt/network_dialog.py b/electrum/gui/qt/network_dialog.py index 4d7c68008..a0d6b5d85 100644 --- a/electrum/gui/qt/network_dialog.py +++ b/electrum/gui/qt/network_dialog.py @@ -29,7 +29,7 @@ from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot from PyQt6.QtWidgets import ( QTreeWidget, QTreeWidgetItem, QMenu, QGridLayout, QComboBox, QLineEdit, QDialog, QVBoxLayout, QHeaderView, QCheckBox, QTabWidget, QWidget, QLabel, QPushButton, QHBoxLayout, - QListWidget, QListWidgetItem, + QListWidget, QListWidgetItem, QMessageBox, ) from PyQt6.QtGui import QIntValidator @@ -444,6 +444,16 @@ class ServerWidget(QWidget, QtEventListener): self.layout().addWidget(self.nodes_list_widget) self.nodes_list_widget.update() + self.clear_certs_button = QPushButton(_('Reset SSL certificates')) + self.clear_certs_button.clicked.connect(self.clear_pinned_server_certs) + self.clear_servers_button = QPushButton(_('Reset known servers')) + self.clear_servers_button.clicked.connect(self.clear_recent_servers) + buttons = QHBoxLayout() + buttons.addStretch(1) + buttons.addWidget(self.clear_certs_button) + buttons.addWidget(self.clear_servers_button) + self.layout().addLayout(buttons) + self.register_callbacks() self.destroyed.connect(lambda: self.unregister_callbacks()) @@ -584,6 +594,54 @@ class ServerWidget(QWidget, QtEventListener): _logger.debug(f"set_server: {new_net_params=}") self.network.run_from_another_thread(self.network.set_parameters(new_net_params)) + def clear_pinned_server_certs(self): + result = QMessageBox.question( + self, + _('Reset SSL certificates'), + _('This will remove cached SSL certificates for servers and reconnect to fetch them again.') + + '\n\n' + + _('Do you want to continue?'), + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No, + ) + if result != QMessageBox.StandardButton.Yes: + return + try: + removed = self.network.run_from_another_thread(self.network.clear_pinned_server_certs()) + except Exception as e: + _logger.exception("failed to clear pinned server certificates") + QMessageBox.critical(self, _('Reset SSL certificates'), str(e)) + return + if removed > 0: + msg = _('{} certificate files were removed.').format(removed) + else: + msg = _('No cached certificate files were found.') + QMessageBox.information(self, _('Reset SSL certificates'), msg) + + def clear_recent_servers(self): + result = QMessageBox.question( + self, + _('Reset known servers'), + _('This will remove the list of known servers.') + + '\n\n' + + _('Do you want to continue?'), + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No, + ) + if result != QMessageBox.StandardButton.Yes: + return + try: + removed = self.network.clear_recent_servers() + except Exception as e: + _logger.exception("failed to clear recent servers") + QMessageBox.critical(self, _('Reset known servers'), str(e)) + return + if removed > 0: + msg = _('{} server(s) were removed.').format(removed) + else: + msg = _('No known servers were found.') + QMessageBox.information(self, _('Reset known servers'), msg) + class NostrWidget(QWidget, QtEventListener): diff --git a/electrum/network.py b/electrum/network.py index 966a31e6f..9b474f7e3 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -408,6 +408,36 @@ class Network(Logger, NetworkRetryManager[ServerAddr]): """Our guess whether the device has Internet-connectivity.""" return self._has_ever_managed_to_connect_to_server + def clear_recent_servers(self) -> int: + """Clear the list of recently used servers.""" + with self.recent_servers_lock: + count = len(self._recent_servers) + self._recent_servers = [] + self._save_recent_servers() + self.logger.info(f"cleared {count} recent server(s)") + return count + + async def clear_pinned_server_certs(self) -> int: + """Delete cached server SSL certs and reconnect interfaces.""" + certs_dir = os.path.join(self.config.path, 'certs') + util.make_dir(certs_dir) + removed = 0 + for entry in os.scandir(certs_dir): + if not entry.is_file(): + continue + try: + os.unlink(entry.path) + removed += 1 + except OSError as e: + self.logger.warning(f"failed to delete cert file {entry.path!r}: {e!r}") + self.logger.info(f"removed {removed} cached server cert(s)") + if removed: + with self.interfaces_lock: + interfaces = list(self.interfaces.values()) + for iface in interfaces: + await self._close_interface(iface) + return removed + def has_channel_db(self): return self.channel_db is not None