mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-06-24 05:35:10 +00:00
Move user credentials into HTTP request headers to prevent them being logged in the server logs
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -75,10 +75,7 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let spotProvidersEnabledByDefault = { % raw
|
||||
json_encode(web_ui_options["spot-providers-enabled-by-default"]) %
|
||||
}
|
||||
;
|
||||
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
||||
</script>
|
||||
<script src="/js/spotsbandsandmap.js?v=1781901371"></script>
|
||||
<script src="/js/bands.js?v=1781901371"></script>
|
||||
|
||||
@@ -93,10 +93,7 @@
|
||||
<script src="/vendor/js/leaflet-workedallbritainireland.js"></script>
|
||||
|
||||
<script>
|
||||
let spotProvidersEnabledByDefault = { % raw
|
||||
json_encode(web_ui_options["spot-providers-enabled-by-default"]) %
|
||||
}
|
||||
;
|
||||
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
||||
</script>
|
||||
<script src="/js/spotsbandsandmap.js?v=1781901371"></script>
|
||||
<script src="/js/map.js?v=1781901371"></script>
|
||||
|
||||
@@ -114,10 +114,7 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let spotProvidersEnabledByDefault = { % raw
|
||||
json_encode(web_ui_options["spot-providers-enabled-by-default"]) %
|
||||
}
|
||||
;
|
||||
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
||||
</script>
|
||||
<script src="/js/spotsbandsandmap.js?v=1781901371"></script>
|
||||
<script src="/js/spots.js?v=1781901371"></script>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user