From 38bc03a60363f47813447cded0554be6e193cc76 Mon Sep 17 00:00:00 2001 From: Ian Renton Date: Mon, 17 Aug 2026 16:30:24 +0100 Subject: [PATCH] Improve checking callsign validity by regex, and fix passing v1 type credentials into v2 APIs --- core/call_lookup_helper.py | 4 +++- core/constants.py | 7 +++++++ data/spot.py | 6 +++--- providers/alert/ng3k.py | 3 ++- providers/spot/dxcluster.py | 5 +++-- providers/spot/parksnpeaks.py | 4 ++-- providers/spot/rbn.py | 3 ++- server/handlers/api/addspot.py | 6 +++--- server/handlers/api/lookups.py | 4 ++-- server/handlers/api/v1_addspot.py | 6 +++--- server/handlers/api/v1_compatability.py | 6 +++--- templates/add_spot.html | 2 +- templates/alerts.html | 2 +- templates/bands.html | 4 ++-- templates/base.html | 10 +++++----- templates/conditions.html | 2 +- templates/map.html | 4 ++-- templates/spots.html | 4 ++-- templates/status.html | 2 +- 19 files changed, 48 insertions(+), 36 deletions(-) diff --git a/core/call_lookup_helper.py b/core/call_lookup_helper.py index 9b77fa8..19e8788 100644 --- a/core/call_lookup_helper.py +++ b/core/call_lookup_helper.py @@ -1,3 +1,4 @@ +from core.constants import CALL_ONLY_PATTERN from core.data_providers import DATA_PROVIDERS from data.callsign import Callsign @@ -9,7 +10,8 @@ def get_call_info(callsign, lookup_credentials): callsign_data = Callsign(call=callsign) - if callsign: + # First check our input looks like a real callsign + if callsign and CALL_ONLY_PATTERN.match(callsign): # Sort callsign providers by priority order, so we query the highest priority (lowest numbers) first, and only # query other providers for data we are missing as we go along. for p in sorted(DATA_PROVIDERS.callsign_data_providers, key=lambda p2: p2.priority): diff --git a/core/constants.py b/core/constants.py index 3ae503a..7c1d40e 100644 --- a/core/constants.py +++ b/core/constants.py @@ -1,3 +1,5 @@ +import re + from core.config import SERVER_OWNER_CALLSIGN from data.band import Band from data.sig import SIG @@ -9,6 +11,11 @@ SOFTWARE_VERSION = "2.0-pre" HTTP_HEADERS = {"User-Agent": f"Spothole v{SOFTWARE_VERSION} (operated by {SERVER_OWNER_CALLSIGN})"} HAMQTH_PRG = f"Spothole v{SOFTWARE_VERSION} operated by {SERVER_OWNER_CALLSIGN}".replace(" ", "_") +# Generally useful regexes +CALL_REGEX = r"[A-Za-z0-9/\-]+" +CALL_PATTERN = re.compile(CALL_REGEX) +CALL_ONLY_PATTERN = re.compile(rf"^{CALL_REGEX}$") + # Special Interest Groups SIGS = [ SIG( diff --git a/data/spot.py b/data/spot.py index 62e1bba..ba7a1e8 100644 --- a/data/spot.py +++ b/data/spot.py @@ -11,7 +11,7 @@ from pyhamtools.locator import latlong_to_locator, locator_to_latlong from core.call_lookup_helper import get_call_info from core.config import MAX_SPOT_AGE -from core.constants import MODE_ALIASES, PROPAGATION_MODES +from core.constants import CALL_REGEX, MODE_ALIASES, PROPAGATION_MODES from core.geo_utils import lat_lon_to_cq_zone, lat_lon_to_itu_zone from core.sig_lookup_helper import populate_missing_sig_ref_info from core.sig_utils import ( @@ -201,14 +201,14 @@ class Spot: # If we have a spotter of "RBNHOLE", we should have the actual spotter callsign in the comment, so extract it. # RBNHole posts come from a number of providers, so it's dealt with here in the generic spot handling code. if self.de_call == "RBNHOLE" and self.comment: - rbnhole_call_match = re.search(r"\Wat ([a-z0-9/]+)\W", self.comment, re.IGNORECASE) + rbnhole_call_match = re.search(rf"\Wat ({CALL_REGEX})\W", self.comment, re.IGNORECASE) if rbnhole_call_match: self.de_call = rbnhole_call_match.group(1).upper() # If we have a spotter of "SOTAMAT", we might have the actual spotter callsign in the comment, if so extract it. # SOTAMAT can do POTA as well as SOTA, so it's dealt with here in the generic spot handling code. if self.de_call == "SOTAMAT" and self.comment: - sotamat_call_match = re.search(r"\Wfrom ([a-z0-9/]+)]", self.comment, re.IGNORECASE) + sotamat_call_match = re.search(rf"\Wfrom ({CALL_REGEX})]", self.comment, re.IGNORECASE) if sotamat_call_match: self.de_call = sotamat_call_match.group(1).upper() diff --git a/providers/alert/ng3k.py b/providers/alert/ng3k.py index 161cd6a..cc2f6ed 100644 --- a/providers/alert/ng3k.py +++ b/providers/alert/ng3k.py @@ -6,6 +6,7 @@ import pytz from rss_parser import Parser from rss_parser.models.rss import RSS +from core.constants import CALL_REGEX from data.alert import Alert from providers.alert.http_alert_provider import HTTPAlertProvider @@ -15,7 +16,7 @@ class NG3K(HTTPAlertProvider): POLL_INTERVAL_SEC = 1800 ALERTS_URL = "https://www.ng3k.com/adxo.xml" - AS_CALL_PATTERN = re.compile("as ([a-z0-9/]+)", re.IGNORECASE) + AS_CALL_PATTERN = re.compile(rf"as ({CALL_REGEX})", re.IGNORECASE) def __init__(self, provider_config): super().__init__("NG3K", provider_config, self.ALERTS_URL, self.POLL_INTERVAL_SEC) diff --git a/providers/spot/dxcluster.py b/providers/spot/dxcluster.py index f8904f1..379f14d 100644 --- a/providers/spot/dxcluster.py +++ b/providers/spot/dxcluster.py @@ -8,6 +8,7 @@ import pytz import telnetlib3 from core.config import SERVER_OWNER_CALLSIGN +from core.constants import CALL_REGEX from data.spot import Spot from providers.spot.spot_provider import SpotProvider @@ -19,11 +20,11 @@ class DXCluster(SpotProvider): See config-example.yml for examples.""" _LINE_PATTERN_EXCLUDE_RBN = re.compile( - r"^DX de ([a-z0-9/]+):\s+([0-9.]+)\s+([a-z0-9/]+)\s+(.*)\s+(\d{4}Z)", + rf"^DX de ({CALL_REGEX}):\s+([0-9.]+)\s+({CALL_REGEX})\s+(.*)\s+(\d{4}Z)", re.IGNORECASE, ) _LINE_PATTERN_ALLOW_RBN = re.compile( - r"^DX de ([a-z0-9/]+)-?#?:\s+([0-9.]+)\s+([a-z0-9/]+)\s+(.*)\s+(\d{4}Z)", + rf"^DX de ({CALL_REGEX})-?#?:\s+([0-9.]+)\s+({CALL_REGEX})\s+(.*)\s+(\d{4}Z)", re.IGNORECASE, ) diff --git a/providers/spot/parksnpeaks.py b/providers/spot/parksnpeaks.py index 9ae4f7c..2570e7a 100644 --- a/providers/spot/parksnpeaks.py +++ b/providers/spot/parksnpeaks.py @@ -5,7 +5,7 @@ from datetime import datetime import pytz import requests -from core.constants import HTTP_HEADERS +from core.constants import HTTP_HEADERS, CALL_REGEX from data.sig_ref import SIGRef from data.spot import Spot from providers.spot.http_spot_provider import HTTPSpotProvider @@ -60,7 +60,7 @@ class ParksNPeaks(HTTPSpotProvider): ) # Extract a de_call if it's in the comment but not in the "actSpoter" field - m = re.search(r"\(de ([A-Za-z0-9]*)\)", spot.comment or "") + m = re.search(rf"\(de ({CALL_REGEX})\)", spot.comment or "") if not spot.de_call and m: spot.de_call = str(m.group(1)) diff --git a/providers/spot/rbn.py b/providers/spot/rbn.py index 684f278..2f28626 100644 --- a/providers/spot/rbn.py +++ b/providers/spot/rbn.py @@ -8,6 +8,7 @@ import pytz import telnetlib3 from core.config import SERVER_OWNER_CALLSIGN +from core.constants import CALL_REGEX from data.spot import Spot from providers.spot.spot_provider import SpotProvider @@ -19,7 +20,7 @@ class RBN(SpotProvider): (port 7001) you need to instantiate two copies of this. The port is provided as an argument to the constructor.""" _LINE_PATTERN = re.compile( - r"^DX de ([a-z0-9/]+)-.*:\s+([0-9.]+)\s+([a-z0-9/]+)\s+(.*)\s+(\d{4}Z)", + rf"^DX de ({CALL_REGEX})-.*:\s+([0-9.]+)\s+({CALL_REGEX})\s+(.*)\s+(\d{4}Z)", re.IGNORECASE, ) diff --git a/server/handlers/api/addspot.py b/server/handlers/api/addspot.py index 72504c1..e59f4c7 100644 --- a/server/handlers/api/addspot.py +++ b/server/handlers/api/addspot.py @@ -11,7 +11,7 @@ from tornado import httputil from tornado.web import Application from core.config import ALLOW_SPOTTING, ALLOW_UPSTREAM_SPOTTING, RECAPTCHA_SECRET_KEY -from core.constants import UNKNOWN_BAND +from core.constants import CALL_ONLY_PATTERN, UNKNOWN_BAND from core.prometheus_metrics_handler import api_requests_counter from core.sig_utils import get_ref_regex_for_sig from core.utils import infer_band_from_freq, safe_json_dumps @@ -121,13 +121,13 @@ class APISpotHandler(tornado.web.RequestHandler): return # Reject invalid-looking callsigns - if not re.match(r"^[A-Za-z0-9/\-]*$", spot.dx_call): + if not CALL_ONLY_PATTERN.match(spot.dx_call): self.set_status(422) self.write(safe_json_dumps(f"Error - '{spot.dx_call}' does not look like a valid callsign.")) self.set_header("Cache-Control", "no-store") self.set_header("Content-Type", "application/json") return - if not re.match(r"^[A-Za-z0-9/\-]*$", spot.de_call): + if not CALL_ONLY_PATTERN.match(spot.de_call): self.set_status(422) self.write(safe_json_dumps(f"Error - '{spot.de_call}' does not look like a valid callsign.")) self.set_header("Cache-Control", "no-store") diff --git a/server/handlers/api/lookups.py b/server/handlers/api/lookups.py index ccc06ed..c700cbe 100644 --- a/server/handlers/api/lookups.py +++ b/server/handlers/api/lookups.py @@ -9,7 +9,7 @@ from tornado import httputil from tornado.web import Application from core.call_lookup_helper import get_call_info -from core.constants import SIGS +from core.constants import CALL_ONLY_PATTERN, SIGS from core.geo_utils import ( lat_lon_for_grid_sw_corner_plus_size, lat_lon_to_cq_zone, @@ -55,7 +55,7 @@ class APILookupCallHandler(tornado.web.RequestHandler): # The "call" query param must exist and look like a callsign if "call" in query_params: call = str(query_params.get("call")).upper() - if re.match(r"^[A-Z0-9/\-]*$", call): + if CALL_ONLY_PATTERN.match(call): credentials = extract_credentials(self.request.headers) callsign_data = get_call_info(call, credentials) self.write(safe_json_dumps(callsign_data)) diff --git a/server/handlers/api/v1_addspot.py b/server/handlers/api/v1_addspot.py index 04cc455..33394ab 100644 --- a/server/handlers/api/v1_addspot.py +++ b/server/handlers/api/v1_addspot.py @@ -9,7 +9,7 @@ from tornado import httputil from tornado.web import Application from core.config import ALLOW_SPOTTING -from core.constants import UNKNOWN_BAND +from core.constants import CALL_ONLY_PATTERN, UNKNOWN_BAND from core.prometheus_metrics_handler import api_requests_counter from core.sig_utils import get_ref_regex_for_sig from core.utils import infer_band_from_freq, safe_json_dumps @@ -83,13 +83,13 @@ class V1APISpotHandler(tornado.web.RequestHandler): return # Reject invalid-looking callsigns - if not re.match(r"^[A-Za-z0-9/\-]*$", spot.dx_call): + if not CALL_ONLY_PATTERN.match(spot.dx_call): self.set_status(422) self.write(safe_json_dumps(f"Error - '{spot.dx_call}' does not look like a valid callsign.")) self.set_header("Cache-Control", "no-store") self.set_header("Content-Type", "application/json") return - if not re.match(r"^[A-Za-z0-9/\-]*$", spot.de_call): + if not CALL_ONLY_PATTERN.match(spot.de_call): self.set_status(422) self.write(safe_json_dumps(f"Error - '{spot.de_call}' does not look like a valid callsign.")) self.set_header("Cache-Control", "no-store") diff --git a/server/handlers/api/v1_compatability.py b/server/handlers/api/v1_compatability.py index fa6bcbb..1fed156 100644 --- a/server/handlers/api/v1_compatability.py +++ b/server/handlers/api/v1_compatability.py @@ -2,7 +2,6 @@ import tornado from tornado.httpclient import AsyncHTTPClient from tornado.httputil import HTTPHeaders - _LEGACY_PARAM_TO_HEADER_MAP = { "qrz_username": "X-QRZ-Username", "qrz_password": "X-QRZ-Password", @@ -12,6 +11,7 @@ _LEGACY_PARAM_TO_HEADER_MAP = { "hamqth_session_id": "X-HamQTH-Session-ID", } + class V1RedirectHandler(tornado.web.RequestHandler): """Transparently proxies requests from the old API to the new one, returning whatever the v2 endpoint returns, for endpoints with no breaking changes.""" @@ -34,7 +34,7 @@ class V1RedirectHandler(tornado.web.RequestHandler): response = await client.fetch( new_url, method=self.request.method, - headers=self.request.headers, + headers=headers, body=None if self.request.method == "GET" else (self.request.body or b""), raise_error=False, follow_redirects=False, @@ -60,4 +60,4 @@ class V1RedirectHandler(tornado.web.RequestHandler): await self._proxy(path) async def post(self, path): - await self._proxy(path) \ No newline at end of file + await self._proxy(path) diff --git a/templates/add_spot.html b/templates/add_spot.html index 2c5b871..00fd09f 100644 --- a/templates/add_spot.html +++ b/templates/add_spot.html @@ -76,7 +76,7 @@ - + diff --git a/templates/alerts.html b/templates/alerts.html index 934dbf8..730b9c3 100644 --- a/templates/alerts.html +++ b/templates/alerts.html @@ -84,7 +84,7 @@ - + diff --git a/templates/bands.html b/templates/bands.html index 23a894b..27fb195 100644 --- a/templates/bands.html +++ b/templates/bands.html @@ -76,8 +76,8 @@ - - + + diff --git a/templates/base.html b/templates/base.html index 2b4eeb9..4234539 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,6 +1,6 @@ {% extends "skeleton.html" %} {% block head_extra %} - + @@ -15,10 +15,10 @@ window.fetchEventSource = fetchEventSource; - - - - + + + + {% end %} {% block body %}
diff --git a/templates/conditions.html b/templates/conditions.html index c2120f8..aa91400 100644 --- a/templates/conditions.html +++ b/templates/conditions.html @@ -284,7 +284,7 @@
- + diff --git a/templates/map.html b/templates/map.html index a9ce8a9..0c80574 100644 --- a/templates/map.html +++ b/templates/map.html @@ -109,8 +109,8 @@ - - + + diff --git a/templates/spots.html b/templates/spots.html index eb7d9de..543ec3d 100644 --- a/templates/spots.html +++ b/templates/spots.html @@ -113,8 +113,8 @@ - - + + diff --git a/templates/status.html b/templates/status.html index 3ffe859..62c483a 100644 --- a/templates/status.html +++ b/templates/status.html @@ -86,7 +86,7 @@ - +