From ee7d2ee17daeca9ee4af08208d39ba720a344672 Mon Sep 17 00:00:00 2001 From: f321x Date: Wed, 30 Apr 2025 14:15:01 +0200 Subject: [PATCH] validate and deduplicate relay config input in qt gui Adds validation and deduplication of the relay urls entered in the QT settings dialog. This is supposed to prevent malformed or duplicated relay entries. Also resets the relays to the default value if no (valid) url is entered. This prevents the user from getting stuck without relays (otherwise the user would have to research for relay urls manually if they don't know any). --- electrum/gui/qt/settings_dialog.py | 15 ++++++++++++--- electrum/util.py | 24 ++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py index aadd04b71..99a3c6eda 100644 --- a/electrum/gui/qt/settings_dialog.py +++ b/electrum/gui/qt/settings_dialog.py @@ -24,7 +24,7 @@ # SOFTWARE. import ast -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict from PyQt6.QtCore import Qt from PyQt6.QtWidgets import (QComboBox, QTabWidget, QDialog, QSpinBox, QCheckBox, QLabel, @@ -32,7 +32,7 @@ from PyQt6.QtWidgets import (QComboBox, QTabWidget, QDialog, QSpinBox, QCheckB from electrum.i18n import _, languages from electrum import util -from electrum.util import base_units_list, event_listener +from electrum.util import base_units_list, event_listener, is_valid_websocket_url from electrum.gui import messages @@ -181,7 +181,16 @@ class SettingsDialog(QDialog, QtEventListener): self.nostr_relays_e = QLineEdit(nostr_relays) def on_nostr_edit(): - self.config.NOSTR_RELAYS = str(self.nostr_relays_e.text()) + relays: Dict[str, None] = dict() # dicts keep insertion order + for url in self.nostr_relays_e.text().split(','): + url = url.strip() + if url and is_valid_websocket_url(url): + relays[url] = None + if relays.keys(): + self.config.NOSTR_RELAYS = ",".join(relays.keys()) + else: # if no valid relays are given, assign default relays from config + self.config.NOSTR_RELAYS = None + self.nostr_relays_e.setText(self.config.NOSTR_RELAYS) self.nostr_relays_e.editingFinished.connect(on_nostr_edit) msat_cb = checkbox_from_configvar(self.config.cv.BTC_AMOUNTS_PREC_POST_SAT) diff --git a/electrum/util.py b/electrum/util.py index 4f210d3ef..0af4a70ae 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -31,14 +31,13 @@ from typing import (NamedTuple, Union, TYPE_CHECKING, Tuple, Optional, Callable, from datetime import datetime, timezone, timedelta import decimal from decimal import Decimal -import urllib +from urllib.parse import urlparse import threading import hmac import hashlib import stat import locale import asyncio -import urllib.request, urllib.parse, urllib.error import builtins import json import time @@ -699,6 +698,27 @@ def is_valid_email(s): regexp = r"[^@]+@[^@]+\.[^@]+" return re.match(regexp, s) is not None +def is_valid_websocket_url(url: str) -> bool: + """ + uses this django url validation regex: + https://github.com/django/django/blob/2c6906a0c4673a7685817156576724aba13ad893/django/core/validators.py#L45C1-L52C43 + Note: this is not perfect, urls and their parsing can get very complex (see recent django code). + however its sufficient for catching weird user input in the gui dialog + """ + # stores the compiled regex in the function object itself to avoid recompiling it every call + if not hasattr(is_valid_websocket_url, "regex"): + is_valid_websocket_url.regex = re.compile( + r'^(?:ws|wss)://' # ws:// or wss:// + r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... + r'localhost|' # localhost... + r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4 + r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' # ...or ipv6 + r'(?::\d+)?' # optional port + r'(?:/?|[/?]\S+)$', re.IGNORECASE) + try: + return re.match(is_valid_websocket_url.regex, url) is not None + except Exception: + return False def is_hash256_str(text: Any) -> bool: if not isinstance(text, str): return False