mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-09-29 02:32:05 +00:00
Create the concept of API keys to allow third party clients to skip the CAPTCHA check on spot submission
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import hmac
|
||||
import logging
|
||||
import re
|
||||
import threading
|
||||
@@ -11,7 +12,13 @@ from tornado.ioloop import IOLoop
|
||||
from tornado.web import Application
|
||||
|
||||
from core.activity_utils import get_ref_regex_for_activity
|
||||
from core.config import ALLOW_SPOTTING, ALLOW_UPSTREAM_SPOTTING, RECAPTCHA_SECRET_KEY
|
||||
from core.config import (
|
||||
ALLOW_SPOTTING,
|
||||
ALLOW_UPSTREAM_SPOTTING,
|
||||
API_KEYS,
|
||||
PROTECT_SPOT_SUBMISSION,
|
||||
RECAPTCHA_SECRET_KEY,
|
||||
)
|
||||
from core.constants import UNKNOWN_BAND
|
||||
from core.utils import infer_band_from_freq, safe_json_dumps
|
||||
from data.spot import Spot
|
||||
@@ -84,17 +91,34 @@ class APISpotHandler(tornado.web.RequestHandler):
|
||||
submit_upstream = len(upstream_provider_names) > 0
|
||||
captcha_token = handling.get("captcha_token", None)
|
||||
|
||||
# Verify CAPTCHA if required
|
||||
if RECAPTCHA_SECRET_KEY:
|
||||
if not captcha_token:
|
||||
self.set_status(422)
|
||||
self.write(safe_json_dumps("Error - CAPTCHA token is required for spot submission."))
|
||||
self.set_header("Cache-Control", "no-store")
|
||||
self.set_header("Content-Type", "application/json")
|
||||
return
|
||||
if not await IOLoop.current().run_in_executor(None, self._verify_recaptcha, captcha_token):
|
||||
self.set_status(422)
|
||||
self.write(safe_json_dumps("Error - CAPTCHA verification failed."))
|
||||
# If spot submission is protected, the client must either provide a valid API key in the request header, or
|
||||
# a valid CAPTCHA token in the request body. API keys are how we allow trusted third-party clients to submit
|
||||
# spots, as they can't solve a CAPTCHA.
|
||||
if PROTECT_SPOT_SUBMISSION:
|
||||
api_key = self.request.headers.get("X-API-Key", "")
|
||||
if api_key:
|
||||
if not self._is_valid_api_key(api_key):
|
||||
self.set_status(401)
|
||||
self.write(safe_json_dumps("Error - API key not recognised."))
|
||||
self.set_header("Cache-Control", "no-store")
|
||||
self.set_header("Content-Type", "application/json")
|
||||
return
|
||||
elif captcha_token and RECAPTCHA_SECRET_KEY:
|
||||
if not await IOLoop.current().run_in_executor(None, self._verify_recaptcha, captcha_token):
|
||||
self.set_status(422)
|
||||
self.write(safe_json_dumps("Error - CAPTCHA verification failed."))
|
||||
self.set_header("Cache-Control", "no-store")
|
||||
self.set_header("Content-Type", "application/json")
|
||||
return
|
||||
else:
|
||||
self.set_status(401)
|
||||
if RECAPTCHA_SECRET_KEY:
|
||||
message = (
|
||||
"Error - this server requires either an API key or a CAPTCHA token for spot submission."
|
||||
)
|
||||
else:
|
||||
message = "Error - this server requires an API key for spot submission."
|
||||
self.write(safe_json_dumps(message))
|
||||
self.set_header("Cache-Control", "no-store")
|
||||
self.set_header("Content-Type", "application/json")
|
||||
return
|
||||
@@ -246,9 +270,7 @@ class APISpotHandler(tornado.web.RequestHandler):
|
||||
# Submit spot to the upstream provider. Run in a separate thread otherwise this blocks the whole web server
|
||||
# for everyone!
|
||||
await IOLoop.current().run_in_executor(None, provider.submit_spot, spot, credentials)
|
||||
logger.info(
|
||||
f"Spot of {spot.dx_call} by {spot.de_call} submitted upstream to {upstream_provider_name}."
|
||||
)
|
||||
logger.info(f"Spot of {spot.dx_call} by {spot.de_call} submitted upstream to {upstream_provider_name}.")
|
||||
# Trigger a re-poll after 3 second so the spot appears quickly. (Submitting to a cluster node is slower than
|
||||
# this, but we get data as a live stream from cluster anyway, so force_poll does nothing in that case. This
|
||||
# is really just for the HTTP providers when we submit a spot to them)
|
||||
@@ -272,6 +294,17 @@ class APISpotHandler(tornado.web.RequestHandler):
|
||||
return p
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _is_valid_api_key(api_key):
|
||||
"""Check whether the supplied API key is one of the ones allowed in config."""
|
||||
|
||||
# HMAC Compare Digest is a recommended thing for security reasons. If you just compare strings
|
||||
# then the comparison returns at the first non-matching character, which means in theory you can
|
||||
# use the time it takes to compare strings to figure out how much of the string you've got right.
|
||||
# With the digest approach it's not the real strings being compared but generated digests, so
|
||||
# it will take a constant amount of time regardless of how well the actual strings match.
|
||||
return any(hmac.compare_digest(api_key.encode(), k.encode()) for k in API_KEYS)
|
||||
|
||||
@staticmethod
|
||||
def _verify_recaptcha(token):
|
||||
"""Verify a Google reCAPTCHA v2 token. Returns True if valid."""
|
||||
|
||||
Reference in New Issue
Block a user