diff --git a/config-example.yml b/config-example.yml index 2842f3d..bc080b6 100644 --- a/config-example.yml +++ b/config-example.yml @@ -354,13 +354,21 @@ allow_spotting: true # this, and will log in using the spotter's callsign to send the spot. Upstream spotting to POTA, SOTA etc. is a work # in progress. Requires allow_spotting to also be true. Set to false to only accept spots into the local Spothole # database, without forwarding them to any external service. -allow_upstream_spotting: false +allow_upstream_spotting: true -# Google reCAPTCHA v2 keys for CAPTCHA protection on upstream spot submission. Both keys must be set to enable CAPTCHA. -# Leave both empty to disable CAPTCHA (e.g. for a private/trusted server) or if allow_spotting is false, in which case -# they will do nothing. Note that with CAPTCHA enabled, this will prevent third-party clients submitting spots through -# Spothole unless the clients are web-based, use the same site key, have their domains enabled in your reCAPTCHA config, -# and of course their user solves the CAPTCHA. +# Require a CAPTCHA or API key to submit spots? If false, anyone can submit spots via the web interface or the API. If +# true, users of the web interface must solve a CAPTCHA (see reCAPTCHA config below), and third-party clients must +# provide one of the API keys listed below. Recommended for public servers where spotting is allowed. +protect_spot_submission: true + +# API keys used by third-party client developers. Clients provide a key in the "X-API-Key" request header when calling +# the add spot API, and this is compared against the server's list below. Generate a long random string for each +# client you want to allow (e.g. openssl rand -hex 32), and send it to them privately. To revoke a client's access, +# remove their key from this list and restart Spothole. +api_keys: [] + +# Google reCAPTCHA v2 keys for CAPTCHA protection on spot submission from the web UI. Both keys must be set to enable +# CAPTCHA. They are only used if protect_spot_submission is true. # You can sign up for reCAPTCHA at https://www.google.com/recaptcha/ recaptcha_site_key: "" recaptcha_secret_key: "" diff --git a/core/config.py b/core/config.py index 9218951..f8fc384 100644 --- a/core/config.py +++ b/core/config.py @@ -31,8 +31,12 @@ ALLOW_SPOTTING = config.get("allow_spotting", True) ALLOW_UPSTREAM_SPOTTING = config.get("allow_upstream_spotting", True) WEB_UI_OPTIONS = config.get("web_ui_options", {}) API_ONLY_MODE = config.get("api_only_mode", False) +API_KEYS = [k for k in (config.get("api_keys") or []) if k] RECAPTCHA_SECRET_KEY = config.get("recaptcha_secret_key", "") RECAPTCHA_SITE_KEY = config.get("recaptcha_site_key", "") +# If not explicitly set, protect spot submission if CAPTCHA is configured, as this was the behaviour before the option +# existed. This avoids silently opening up spot submission on servers that upgrade without updating their config. +PROTECT_SPOT_SUBMISSION = config.get("protect_spot_submission", bool(RECAPTCHA_SECRET_KEY)) LOG_LEVEL = config.get("log_level", "INFO") LOG_WEB_REQUESTS = config.get("log_web_requests", False) @@ -40,10 +44,17 @@ WEB_UI_OPTIONS["qrz_enabled"] = any(p["class"] == "QRZ" and p["enabled"] for p i WEB_UI_OPTIONS["hamqth_enabled"] = any( p["class"] == "HamQTH" and p["enabled"] for p in config["callsign_data_providers"] ) -WEB_UI_OPTIONS["recaptcha_site_key"] = RECAPTCHA_SITE_KEY +# The web UI only needs to show a CAPTCHA if spot submission is protected +WEB_UI_OPTIONS["recaptcha_site_key"] = RECAPTCHA_SITE_KEY if PROTECT_SPOT_SUBMISSION else "" WEB_UI_OPTIONS["allow_upstream_spotting"] = ALLOW_SPOTTING and ALLOW_UPSTREAM_SPOTTING +if ALLOW_SPOTTING and PROTECT_SPOT_SUBMISSION and not (RECAPTCHA_SITE_KEY and RECAPTCHA_SECRET_KEY): + logger.warning( + "Spot submission is protected but reCAPTCHA keys are not set, so only clients with an API key will be able to submit spots. Users of the web interface will not be able to add spots." + ) + + def create_provider_from_config(package, config_providers_entry): """Utility method to get a provider based on the class specified in its config entry. You must also provide the package to look for it in, as there are several types of provider. e.g. package "providers.spot", where the config diff --git a/static/apidocs/openapi.yml b/static/apidocs/openapi.yml index 6aef7bc..f716afc 100644 --- a/static/apidocs/openapi.yml +++ b/static/apidocs/openapi.yml @@ -9,6 +9,8 @@ info: While I provide this API for free, there are some conditions of use that you must adhere to. These are not onerous, but ensure the API can remain available to everyone. These apply even if you are getting an AI to write your client software for you. See https://spothole.app/help/usage/clients#terms for details. + If you want to submit spots from your client, an API key is required. Please contact the server owner for a key if you want to use this functionality. + ## Changelog ### 3.0 @@ -24,11 +26,16 @@ info: * **Breaking change:** In the `/options` response, `sigs` has been renamed to `activities`, and within each activity, `sig_type` has been renamed to `activity_type`. * **Breaking change:** In the `/status` response, `sig_ref_data_providers` has been renamed to `activity_ref_data_providers`, and within each provider, `sig_name` has been renamed to `activity_name`. * **Breaking change:** In the POST `/spot` `handling` object, `submit_upstream` and `upstream_provider` have been replaced by `upstream_providers`, a list of provider names, so a spot can be sent to multiple upstream providers at once. `upstream_credentials` is now a map of provider name to that provider's credentials. + * POST `/spot` now accepts an `X-API-Key` request header. On servers that protect spot submission, a valid API key allows a third party client to submit spots without needing to solve a CAPTCHA. #### Upgrading a client from v2 to v3 API endpoints In v3.0 of Spothole, the `v2` (and `v1`) API endpoints will be maintained for backwards compatibility, so if you have written a client against the `v2` API, it will continue to receive `sig`, `sig_refs` etc. as before. Where a spot or alert has more than one activity, the `v2` and `v1` APIs will return only the first one as `sig`. + If your client submits spots via POST `/spot` and uses upstream submission, replace `submit_upstream` and `upstream_provider` in the `handling` object with an `upstream_providers` list, and turn `upstream_credentials` into a map where the key is the provider name, and the value is another map of credential name to value. + + Some Spothole servers, including `spothole.app`, require a CAPTCHA to submit spots via the web interface, which third-party clients can't solve. If you want your client to submit spots to such a server, ask the server operator for an API key, and send it in the `X-API-Key` request header with each add spot request. + You are encouraged to move to the `v3` API endpoints as soon as possible. To upgrade, replace `v2` with `v3` in the URLs your code calls, then rename any use of the fields, query parameters and values listed above, and handle `activities` being a list rather than a single `sig` value. If you use the activity reference lookup, call `/lookup/activityref?activity=...&id=...` instead of `/lookup/sigref?sig=...&id=...`. ### 2.2 @@ -476,9 +483,11 @@ paths: description: > Supply a JSON object containing a `spot` sub-object (the spot data) and an optional `handling` sub-object containing server-side instructions such as upstream submission). Check `spot_submit_providers` in the - `/options` response to see which activities and providers support upstream submission. cURL example: - `curl --request POST --header \"Content-Type: application/json\" --data '{\"spot\":{\"dx_call\":\"M0TRT\",\"time\":1760019539,\"freq\":14200000,\"comment\":\"Test spot please ignore\",\"de_call\":\"M0TRT\"}}' https://spothole.app/api/v3/spot`" + `/options` response to see which activities and providers support upstream submission. If the server requires + an API key for spot submission, you must supply a valid API key in the `X-API-Key` header. operationId: spot + parameters: + - $ref: '#/components/parameters/ApiKey' requestBody: description: Object containing a "spot" sub-object with the spot data, and an optional "handling" sub-object with server-side instructions of what to do with it. required: true @@ -494,6 +503,22 @@ paths: schema: type: string example: "OK" + '401': + description: > + Spot submission is not allowed on this server, or the server requires an API key or CAPTCHA token and + neither was provided, or the API key was not recognised + content: + application/json: + schema: + type: string + example: "Error - API key not recognised." + '403': + description: Upstream submission was requested but this server does not allow it + content: + application/json: + schema: + type: string + example: "Error - this server does not allow upstream spot submission." '415': description: Incorrect Content-Type content: @@ -518,6 +543,14 @@ paths: components: parameters: + ApiKey: + name: X-API-Key + in: header + description: > + An API key issued by the server operator. On servers that protect spot submission, a valid API key allows + the spot to be submitted without a CAPTCHA token. Not required on servers that don't protect spot submission. + schema: + type: string QrzUsername: name: X-QRZ-Username in: header @@ -1410,9 +1443,10 @@ components: captcha_token: type: string description: > - A Google reCAPTCHA v2 response token. Required when submitting upstream if the - server has reCAPTCHA configured. Obtain the token by completing the reCAPTCHA - widget rendered on the Add Spot page. + A Google reCAPTCHA v2 response token. Required if the server protects spot submission, + unless a valid API key is supplied in the `X-API-Key` header instead. Only useful for the + Spothole server itself, not for third-party clients, as without access to Spothole's CAPTCHA + secret they have no way of solving the CAPTCHA anyway. They must use `X-API-Key` instead. example: "03AFY_a8Xq..." SpotStream: diff --git a/templates/add-spot.html b/templates/add-spot.html index 2252683..87864f5 100644 --- a/templates/add-spot.html +++ b/templates/add-spot.html @@ -130,7 +130,7 @@ const ALLOW_UPSTREAM_SPOTTING = {% raw safe_json_dumps(web_ui_options["allow_upstream_spotting"]) %}; - + diff --git a/templates/alerts.html b/templates/alerts.html index 5deca73..cbe4a5f 100644 --- a/templates/alerts.html +++ b/templates/alerts.html @@ -87,7 +87,7 @@ - + diff --git a/templates/bands.html b/templates/bands.html index 46e05df..f674fbf 100644 --- a/templates/bands.html +++ b/templates/bands.html @@ -82,8 +82,8 @@ const BANDS = {% raw safe_json_dumps(options["bands"]) %}; - - + + diff --git a/templates/base.html b/templates/base.html index 41749fd..a974900 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,6 +1,6 @@ {% extends "skeleton.html" %} {% block head_extra %} - + @@ -16,10 +16,10 @@ window.fetchEventSource = fetchEventSource; - - - - + + + + {% end %} {% block body %}
As well as reading data, clients can submit new spots to Spothole using the "add spot" API endpoint, e.g.
+ https://spothole.app/api/v3/spot. Spots can be added to Spothole itself, and optionally sent "upstream"
+ to other services such as the DX cluster. Check the spot_allowed and
+ spot_submit_providers fields in the "options" API response to see what the server allows.
To stop bots and spammers abusing the spotting function, a Spothole server can be configured to protect against this,
+ which means you will need an API key. API keys are issued by the operator of each Spothole server,
+ so get in touch with the server owner and let them know what your software is and how it will use the API. Once you
+ have a key, send it in the X-API-Key header of each add spot request. For example:
curl --request POST \
+ --header "Content-Type: application/json" \
+ --header "X-API-Key: your-api-key-here" \
+ --data '{"spot":{"dx_call":"M0TRT","time":1760019539,"freq":14200000,"de_call":"M0TRT"}}' \
+ https://spothole.app/api/v3/spot
+Your API key identifies your software, so please keep it private. Don't commit it to a public code repository, and + don't embed it in JavaScript or anywhere else your users could extract it. If a key is misused, the server operator + can revoke it, and spot submission from your client will stop working. Servers that don't protect spot submission + don't need an API key, and will accept spots with or without one.
+There are some simple, hopefully not onerous terms and conditions that you should agree to before writing a client for the Spothole API. These probably aren't legally binding, and I'm just a random guy on the internet, I'm not diff --git a/templates/help/usage/running.html b/templates/help/usage/running.html index d8a2441..07aa147 100644 --- a/templates/help/usage/running.html +++ b/templates/help/usage/running.html @@ -30,6 +30,14 @@ cp config-example.yml config.yml helpdesk ticket and explaining what you'll use it for. The admin team are happy with the rate of requests made by my Spothole server, so unless you change the source code of yours to radically increase the rate of querying Clublog, I'm sure they will be fine with your server too.
+If your server is public and allows spots to be submitted, you may want to protect it from bots and spammers by
+ setting protect_spot_submission to true and setting the reCAPTCHA keys in
+ config.yml. Users of the web interface will then need to solve a CAPTCHA to submit a spot. Third-party
+ client software can't do that, so if you want to allow a particular client to submit spots, generate an API key for
+ it, add it to the api_keys list in config.yml, and send it privately to the client's
+ developer. They send it in the X-API-Key header of each request, and Spothole will accept their spots.
+ To revoke a key, remove it from the list and restart Spothole. If protect_spot_submission is
+ false, anyone can submit spots and API keys aren't needed.
Once you're happy with the content of config.yml, you can proceed to running the software.
To run the software this time and any future times you want to run it directly from the command line:
source .venv/bin/activate
diff --git a/templates/map.html b/templates/map.html
index fbf5d4d..e57ff7a 100644
--- a/templates/map.html
+++ b/templates/map.html
@@ -115,8 +115,8 @@
const CARTODB_API_KEY = "{{ web_ui_options.get('cartodb_api_key', '') }}";
-
-
+
+
diff --git a/templates/spots.html b/templates/spots.html
index 789377f..964b4a4 100644
--- a/templates/spots.html
+++ b/templates/spots.html
@@ -127,8 +127,8 @@
-
-
+
+
diff --git a/templates/status.html b/templates/status.html
index df8e90e..fc2e030 100644
--- a/templates/status.html
+++ b/templates/status.html
@@ -96,7 +96,7 @@
-
+