diff --git a/data/lookup_credentials.py b/data/lookup_credentials.py
index ae80f83..65737df 100644
--- a/data/lookup_credentials.py
+++ b/data/lookup_credentials.py
@@ -12,15 +12,15 @@ class LookupCredentials:
hamqth_session_id: str = "" # alternative to username/password
-def extract_credentials(query_params):
- """Build a LookupCredentials from HTTP query params; returns None if no usable credentials are present."""
+def extract_credentials(headers):
+ """Build a LookupCredentials from HTTP request headers; returns None if no usable credentials are present."""
creds = LookupCredentials(
- qrz_username=query_params.get("qrz_username", ""),
- qrz_password=query_params.get("qrz_password", ""),
- qrz_session_key=query_params.get("qrz_session_key", ""),
- hamqth_username=query_params.get("hamqth_username", ""),
- hamqth_password=query_params.get("hamqth_password", ""),
- hamqth_session_id=query_params.get("hamqth_session_id", ""),
+ qrz_username=headers.get("X-QRZ-Username", ""),
+ qrz_password=headers.get("X-QRZ-Password", ""),
+ qrz_session_key=headers.get("X-QRZ-Session-Key", ""),
+ hamqth_username=headers.get("X-HamQTH-Username", ""),
+ hamqth_password=headers.get("X-HamQTH-Password", ""),
+ hamqth_session_id=headers.get("X-HamQTH-Session-ID", ""),
)
has_qrz = creds.qrz_session_key or (creds.qrz_username and creds.qrz_password)
has_hamqth = creds.hamqth_session_id or (creds.hamqth_username and creds.hamqth_password)
diff --git a/server/handlers/api/alerts.py b/server/handlers/api/alerts.py
index 3649fde..10083e9 100644
--- a/server/handlers/api/alerts.py
+++ b/server/handlers/api/alerts.py
@@ -53,7 +53,7 @@ class APIAlertsHandler(tornado.web.RequestHandler):
query_params = {k: v[0].decode("utf-8") for k, v in self.request.arguments.items()}
# Fetch all alerts matching the query, then optionally enrich with online data
- credentials = extract_credentials(query_params)
+ credentials = extract_credentials(self.request.headers)
data = get_alert_list_with_filters(self._alerts, query_params)
if credentials:
data = self._enrich(data, credentials)
@@ -104,7 +104,7 @@ class APIAlertsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
# request.arguments contains lists for each param key because technically the client can supply multiple,
# reduce that to just the first entry, and convert bytes to string
self._query_params = {k: v[0].decode("utf-8") for k, v in self.request.arguments.items()}
- self._credentials = extract_credentials(self._query_params)
+ self._credentials = extract_credentials(self.request.headers)
# Create a alert queue and add it to the web server's list. The web server will fill this when alerts arrive
self._alert_queue = Queue(maxsize=SSE_HANDLER_MAX_QUEUE_SIZE)
diff --git a/server/handlers/api/lookups.py b/server/handlers/api/lookups.py
index 3672269..8e6db2c 100644
--- a/server/handlers/api/lookups.py
+++ b/server/handlers/api/lookups.py
@@ -47,7 +47,7 @@ class APILookupCallHandler(tornado.web.RequestHandler):
if re.match(r"^[A-Z0-9/\-]*$", call):
# Take the callsign, make a "fake spot" so we can run infer_missing() on it, then repack the
# resulting data in the correct way for the API response.
- credentials = extract_credentials(query_params)
+ credentials = extract_credentials(self.request.headers)
fake_spot = Spot(dx_call=call)
fake_spot.infer_missing(credentials)
data = {
diff --git a/server/handlers/api/spots.py b/server/handlers/api/spots.py
index 6695347..6882265 100644
--- a/server/handlers/api/spots.py
+++ b/server/handlers/api/spots.py
@@ -53,7 +53,7 @@ class APISpotsHandler(tornado.web.RequestHandler):
query_params = {k: v[0].decode("utf-8") for k, v in self.request.arguments.items()}
# Fetch all spots matching the query, then optionally enrich with online data
- credentials = extract_credentials(query_params)
+ credentials = extract_credentials(self.request.headers)
data = get_spot_list_with_filters(self._spots, query_params)
if credentials:
data = self._enrich(data, credentials)
@@ -106,7 +106,7 @@ class APISpotsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
# request.arguments contains lists for each param key because technically the client can supply multiple,
# reduce that to just the first entry, and convert bytes to string
self._query_params = {k: v[0].decode("utf-8") for k, v in self.request.arguments.items()}
- self._credentials = extract_credentials(self._query_params)
+ self._credentials = extract_credentials(self.request.headers)
# Create a spot queue and add it to the web server's list. The web server will fill this when spots arrive
self._spot_queue = Queue(maxsize=SSE_HANDLER_MAX_QUEUE_SIZE)
diff --git a/templates/bands.html b/templates/bands.html
index 54a70fc..bda6c4a 100644
--- a/templates/bands.html
+++ b/templates/bands.html
@@ -75,10 +75,7 @@
diff --git a/templates/map.html b/templates/map.html
index a85ab7a..b3a95ac 100644
--- a/templates/map.html
+++ b/templates/map.html
@@ -93,10 +93,7 @@
diff --git a/templates/spots.html b/templates/spots.html
index d9883f9..1b28f56 100644
--- a/templates/spots.html
+++ b/templates/spots.html
@@ -114,10 +114,7 @@
diff --git a/webassets/apidocs/openapi.yml b/webassets/apidocs/openapi.yml
index 71c305c..355d43a 100644
--- a/webassets/apidocs/openapi.yml
+++ b/webassets/apidocs/openapi.yml
@@ -17,9 +17,11 @@ info:
### 2.0
- * POST `/spot` now supports upstream submission to external providers such as POTA and SOTA. The "add spot" API has a **breaking change** to enable this: instead of just posting the spot object itself as the JSON content of the POST, this has moved into a `spot` object within the structure. A new `handling` object alongside it contains the `submit_upstream`, `upstream_provider`, `upstream_credentials`, and `captcha_token` fields which control the server handling of the spot.
+ * POST `/spot` now supports upstream submission to the spotting services associated with various SIGs.
+ * **Breaking change:** The "add spot" API has changed to enable this: instead of just posting the spot object itself as the JSON content of the POST, this has moved into a `spot` object within the structure. A new `handling` object alongside it contains the `submit_upstream`, `upstream_provider`, `upstream_credentials`, and `captcha_token` fields which control the server handling of the spot.
* POST `/spot` now supports Google reCaptcha and (if the site owner has set it up) now requires `captcha_token` in order to successfully submit. (This is used to lock down the submit function and prevent submission via Spothole by bots or third-party clients.)
* GET `/options` now returns `spot_submit_providers`, a map of SIG names to the names of providers that support upstream spot submission for that SIG. (This allows clients to present the user with options of where a new spot can be sent to.)
+ * **Breaking change:** A user's QRZ.com and HamQTH credentials are now supplied as request headers (`X-QRZ-Username`, `X-QRZ-Password`, `X-QRZ-Session-Key`, `X-HamQTH-Username`, `X-HamQTH-Password`, `X-HamQTH-Session-ID`) rather than query parameters, to keep credentials out of server logs.
### 1.3
@@ -398,39 +400,39 @@ paths:
components:
parameters:
QrzUsername:
- name: qrz_username
- in: query
- description: "QRZ.com username for online callsign lookup, which will enrich the returned spots and alerts with extra data. Requires a QRZ.com XML Subscriber (paid) account. Supply together with `qrz_password`, or supply `qrz_session_key` instead."
+ name: X-QRZ-Username
+ in: header
+ description: "QRZ.com username for online callsign lookup, which will enrich the returned spots and alerts with extra data. Requires a QRZ.com XML Subscriber (paid) account. Supply together with `X-QRZ-Password`, or supply `X-QRZ-Session-Key` instead."
schema:
type: string
QrzPassword:
- name: qrz_password
- in: query
- description: "QRZ.com password. Supply together with `qrz_username`."
+ name: X-QRZ-Password
+ in: header
+ description: "QRZ.com password. Supply together with `X-QRZ-Username`."
schema:
type: string
QrzSessionKey:
- name: qrz_session_key
- in: query
- description: "A pre-obtained QRZ.com XML session key, as an alternative to supplying `qrz_username` and `qrz_password`. See https://www.qrz.com/docs/xml/current_spec.html for details on how to obtain one for the user."
+ name: X-QRZ-Session-Key
+ in: header
+ description: "A pre-obtained QRZ.com XML session key, as an alternative to supplying `X-QRZ-Username` and `X-QRZ-Password`. See https://www.qrz.com/docs/xml/current_spec.html for details on how to obtain one for the user."
schema:
type: string
HamqthUsername:
- name: hamqth_username
- in: query
- description: "HamQTH username for online callsign lookup, which will enrich the returned spots and alerts with extra data. Supply together with `hamqth_password`, or supply `hamqth_session_id` instead."
+ name: X-HamQTH-Username
+ in: header
+ description: "HamQTH username for online callsign lookup, which will enrich the returned spots and alerts with extra data. Supply together with `X-HamQTH-Password`, or supply `X-HamQTH-Session-ID` instead."
schema:
type: string
HamqthPassword:
- name: hamqth_password
- in: query
- description: "HamQTH password. Supply together with `hamqth_username`."
+ name: X-HamQTH-Password
+ in: header
+ description: "HamQTH password. Supply together with `X-HamQTH-Username`."
schema:
type: string
HamqthSessionId:
- name: hamqth_session_id
- in: query
- description: "A pre-obtained HamQTH session ID, as an alternative to supplying `hamqth_username` and `hamqth_password`. See https://www.hamqth.com/developers.php for details on how to retrieve one for a user."
+ name: X-HamQTH-Session-ID
+ in: header
+ description: "A pre-obtained HamQTH session ID, as an alternative to supplying `X-HamQTH-Username` and `X-HamQTH-Password`. See https://www.hamqth.com/developers.php for details on how to retrieve one for a user."
schema:
type: string
SpotSource:
diff --git a/webassets/js/alerts.js b/webassets/js/alerts.js
index d9f3970..9cb9205 100644
--- a/webassets/js/alerts.js
+++ b/webassets/js/alerts.js
@@ -6,7 +6,7 @@ let alerts = [];
// Load alerts and populate the table.
function loadAlerts() {
- $.getJSON('/api/v2/alerts' + buildQueryString(false), function (jsonData) {
+ $.ajax({url: '/api/v2/alerts' + buildQueryString(), dataType: 'json', headers: getCredentialHeaders(), success: function (jsonData) {
// Store last updated time
lastUpdateTime = moment.utc();
updateRefreshDisplay();
@@ -14,11 +14,11 @@ function loadAlerts() {
alerts = jsonData;
// Update table
updateTable();
- });
+ }});
}
// Build a query string for the API, based on the filters that the user has selected.
-function buildQueryString(includeCredentials) {
+function buildQueryString() {
let str = "?";
["dx_continent", "source"].forEach(fn => {
if (!allFilterOptionsSelected(fn)) {
@@ -33,9 +33,6 @@ function buildQueryString(includeCredentials) {
if ($("#dxpeditions_skip_max_duration_check")[0].checked) {
str = str + "&dxpeditions_skip_max_duration_check=true";
}
- if (includeCredentials) {
- str = str + getCredentialQueryString();
- }
return str;
}
diff --git a/webassets/js/bands.js b/webassets/js/bands.js
index 39229dd..ef10f64 100644
--- a/webassets/js/bands.js
+++ b/webassets/js/bands.js
@@ -12,7 +12,7 @@ BAND_COLUMN_SPOT_DIV_HEIGHT_PX = BAND_COLUMN_FONT_SIZE * 1.6;
// Load spots and populate the bands display.
function loadSpots() {
- $.getJSON('/api/v2/spots' + buildQueryString(false), function (jsonData) {
+ $.ajax({url: '/api/v2/spots' + buildQueryString(), dataType: 'json', headers: getCredentialHeaders(), success: function (jsonData) {
// Store last updated time
lastUpdateTime = moment.utc();
updateRefreshDisplay();
@@ -20,11 +20,11 @@ function loadSpots() {
spots = jsonData;
// Update bands display
updateBands();
- });
+ }});
}
// Build a query string for the API, based on the filters that the user has selected.
-function buildQueryString(includeCredentials) {
+function buildQueryString() {
let str = "?";
["dx_continent", "de_continent", "mode", "source", "band", "sig"].forEach(fn => {
if (!allFilterOptionsSelected(fn)) {
@@ -34,9 +34,6 @@ function buildQueryString(includeCredentials) {
str = str + "max_age=" + $("#max-spot-age option:selected").val();
// Additional filters for the bands view: No dupes, no QRT
str = str + "&dedupe=true&allow_qrt=false";
- if (includeCredentials) {
- str = str + getCredentialQueryString();
- }
return str;
}
diff --git a/webassets/js/common.js b/webassets/js/common.js
index 3580075..0b0b43c 100644
--- a/webassets/js/common.js
+++ b/webassets/js/common.js
@@ -273,23 +273,23 @@ function closeDataPanel() {
closePanel("#data-area");
}
-// Build a query string fragment containing any QRZ.com / HamQTH credentials the user has supplied,
+// Build a headers object containing any QRZ.com / HamQTH credentials the user has supplied,
// provided the corresponding "enabled" checkbox is ticked.
-function getCredentialQueryString() {
- let str = "";
+function getCredentialHeaders() {
+ const headers = {};
if ($("#qrz-enabled")[0] && $("#qrz-enabled")[0].checked) {
const qrzUsername = $("#qrz-username").val();
const qrzPassword = $("#qrz-password").val();
- if (qrzUsername) str += "&qrz_username=" + encodeURIComponent(qrzUsername);
- if (qrzPassword) str += "&qrz_password=" + encodeURIComponent(qrzPassword);
+ if (qrzUsername) headers["X-QRZ-Username"] = qrzUsername;
+ if (qrzPassword) headers["X-QRZ-Password"] = qrzPassword;
}
if ($("#hamqth-enabled")[0] && $("#hamqth-enabled")[0].checked) {
const hamqthUsername = $("#hamqth-username").val();
const hamqthPassword = $("#hamqth-password").val();
- if (hamqthUsername) str += "&hamqth_username=" + encodeURIComponent(hamqthUsername);
- if (hamqthPassword) str += "&hamqth_password=" + encodeURIComponent(hamqthPassword);
+ if (hamqthUsername) headers["X-HamQTH-Username"] = hamqthUsername;
+ if (hamqthPassword) headers["X-HamQTH-Password"] = hamqthPassword;
}
- return str;
+ return headers;
}
diff --git a/webassets/js/map.js b/webassets/js/map.js
index 889f3ef..9e25907 100644
--- a/webassets/js/map.js
+++ b/webassets/js/map.js
@@ -28,7 +28,7 @@ let firstLoad = true;
// Load spots and populate the map.
function loadSpots() {
- $.getJSON('/api/v2/spots' + buildQueryString(true), function (jsonData) {
+ $.ajax({url: '/api/v2/spots' + buildQueryString(), dataType: 'json', headers: getCredentialHeaders(), success: function (jsonData) {
// Store data
spots = jsonData;
// Update map
@@ -36,11 +36,11 @@ function loadSpots() {
if ($("#showTerminator")[0].checked) {
terminator.setTime();
}
- });
+ }});
}
// Build a query string for the API, based on the filters that the user has selected.
-function buildQueryString(includeCredentials) {
+function buildQueryString() {
let str = "?";
["dx_continent", "de_continent", "mode", "source", "band", "sig"].forEach(fn => {
if (!allFilterOptionsSelected(fn)) {
@@ -50,9 +50,6 @@ function buildQueryString(includeCredentials) {
str = str + "max_age=" + $("#max-spot-age option:selected").val();
// Additional filters for the map view: No dupes, no QRT, only spots with good locations
str = str + "&dedupe=true&allow_qrt=false";
- if (includeCredentials) {
- str = str + getCredentialQueryString();
- }
return str;
}
diff --git a/webassets/js/spots.js b/webassets/js/spots.js
index 18cb17e..5586865 100644
--- a/webassets/js/spots.js
+++ b/webassets/js/spots.js
@@ -20,7 +20,7 @@ function loadSpots() {
}
// Make the new query
- $.getJSON('/api/v2/spots' + buildQueryString(false), function (jsonData) {
+ $.ajax({url: '/api/v2/spots' + buildQueryString(), dataType: 'json', headers: getCredentialHeaders(), success: function (jsonData) {
// Store data
spots = jsonData;
// Update table
@@ -30,7 +30,7 @@ function loadSpots() {
if (run) {
startSSEConnection();
}
- });
+ }});
}
// Start an SSE connection (closing an existing one if it exists). This will then be used to add to the table on the
@@ -39,7 +39,7 @@ function startSSEConnection() {
if (evtSource != null) {
evtSource.close();
}
- evtSource = new EventSource('/api/v2/spots/stream' + buildQueryString(true));
+ evtSource = new EventSource('/api/v2/spots/stream' + buildQueryString());
evtSource.onmessage = function (event) {
// Get the new spot
@@ -86,7 +86,7 @@ function startSSEConnection() {
}
// Build a query string for the API, based on the filters that the user has selected.
-function buildQueryString(includeCredentials) {
+function buildQueryString() {
let str = "?";
["dx_continent", "de_continent", "mode", "source", "band", "sig"].forEach(fn => {
if (!allFilterOptionsSelected(fn)) {
@@ -97,9 +97,6 @@ function buildQueryString(includeCredentials) {
if ($("#search").val() !== "") {
str = str + "&text_includes=" + encodeURIComponent($("#search").val());
}
- if (includeCredentials) {
- str = str + getCredentialQueryString();
- }
return str;
}