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