Skip to content

Commit d855199

Browse files
authored
Merge pull request #9773 from f321x/improve_relay_string_input
qt: validate and deduplicate relay config input in qt gui
2 parents 4b23ca1 + ee7d2ee commit d855199

File tree

2 files changed

+34
-5
lines changed

2 files changed

+34
-5
lines changed

electrum/gui/qt/settings_dialog.py

+12-3
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@
2424
# SOFTWARE.
2525

2626
import ast
27-
from typing import TYPE_CHECKING
27+
from typing import TYPE_CHECKING, Dict
2828

2929
from PyQt6.QtCore import Qt
3030
from PyQt6.QtWidgets import (QComboBox, QTabWidget, QDialog, QSpinBox, QCheckBox, QLabel,
3131
QVBoxLayout, QGridLayout, QLineEdit, QWidget, QHBoxLayout, QSlider)
3232

3333
from electrum.i18n import _, languages
3434
from electrum import util
35-
from electrum.util import base_units_list, event_listener
35+
from electrum.util import base_units_list, event_listener, is_valid_websocket_url
3636

3737
from electrum.gui import messages
3838

@@ -181,7 +181,16 @@ def lnfee_slider_released():
181181
self.nostr_relays_e = QLineEdit(nostr_relays)
182182

183183
def on_nostr_edit():
184-
self.config.NOSTR_RELAYS = str(self.nostr_relays_e.text())
184+
relays: Dict[str, None] = dict() # dicts keep insertion order
185+
for url in self.nostr_relays_e.text().split(','):
186+
url = url.strip()
187+
if url and is_valid_websocket_url(url):
188+
relays[url] = None
189+
if relays.keys():
190+
self.config.NOSTR_RELAYS = ",".join(relays.keys())
191+
else: # if no valid relays are given, assign default relays from config
192+
self.config.NOSTR_RELAYS = None
193+
self.nostr_relays_e.setText(self.config.NOSTR_RELAYS)
185194
self.nostr_relays_e.editingFinished.connect(on_nostr_edit)
186195

187196
msat_cb = checkbox_from_configvar(self.config.cv.BTC_AMOUNTS_PREC_POST_SAT)

electrum/util.py

+22-2
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,13 @@
3131
from datetime import datetime, timezone, timedelta
3232
import decimal
3333
from decimal import Decimal
34-
import urllib
34+
from urllib.parse import urlparse
3535
import threading
3636
import hmac
3737
import hashlib
3838
import stat
3939
import locale
4040
import asyncio
41-
import urllib.request, urllib.parse, urllib.error
4241
import builtins
4342
import json
4443
import time
@@ -699,6 +698,27 @@ def is_valid_email(s):
699698
regexp = r"[^@]+@[^@]+\.[^@]+"
700699
return re.match(regexp, s) is not None
701700

701+
def is_valid_websocket_url(url: str) -> bool:
702+
"""
703+
uses this django url validation regex:
704+
https://github.com/django/django/blob/2c6906a0c4673a7685817156576724aba13ad893/django/core/validators.py#L45C1-L52C43
705+
Note: this is not perfect, urls and their parsing can get very complex (see recent django code).
706+
however its sufficient for catching weird user input in the gui dialog
707+
"""
708+
# stores the compiled regex in the function object itself to avoid recompiling it every call
709+
if not hasattr(is_valid_websocket_url, "regex"):
710+
is_valid_websocket_url.regex = re.compile(
711+
r'^(?:ws|wss)://' # ws:// or wss://
712+
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain...
713+
r'localhost|' # localhost...
714+
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4
715+
r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' # ...or ipv6
716+
r'(?::\d+)?' # optional port
717+
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
718+
try:
719+
return re.match(is_valid_websocket_url.regex, url) is not None
720+
except Exception:
721+
return False
702722

703723
def is_hash256_str(text: Any) -> bool:
704724
if not isinstance(text, str): return False

0 commit comments

Comments
 (0)