import logging import re from datetime import datetime from typing import Any import pytz import tornado from tornado import httputil from tornado.web import Application from core.config import ALLOW_SPOTTING from core.constants import UNKNOWN_BAND from core.lookup_helper import infer_band_from_freq from core.prometheus_metrics_handler import api_requests_counter from core.sig_utils import get_ref_regex_for_sig from core.utils import safe_json_dumps from data.spot import Spot class APISpotHandler(tornado.web.RequestHandler): """API request handler for /api/v1/spot (POST)""" def __init__(self, application: "Application", request: httputil.HTTPServerRequest, **kwargs: Any): self._spots = None self._web_server_metrics = None super().__init__(application, request, **kwargs) def initialize(self, spots, web_server_metrics): self._spots = spots self._web_server_metrics = web_server_metrics def post(self): try: # Metrics self._web_server_metrics["last_api_access_time"] = datetime.now(pytz.UTC) self._web_server_metrics["api_access_counter"] += 1 self._web_server_metrics["status"] = "OK" api_requests_counter.inc() # Reject if not allowed if not ALLOW_SPOTTING: self.set_status(401) self.write(safe_json_dumps("Error - this server does not allow new spots to be added via the API.")) self.set_header("Cache-Control", "no-store") self.set_header("Content-Type", "application/json") return # Reject if format not json if not self.request.headers.get('Content-Type', '').startswith("application/json"): self.set_status(415) self.write(safe_json_dumps("Error - request Content-Type must be application/json")) self.set_header("Cache-Control", "no-store") self.set_header("Content-Type", "application/json") return # Reject if request body is empty post_data = self.request.body if not post_data: self.set_status(422) self.write(safe_json_dumps("Error - request body is empty")) self.set_header("Cache-Control", "no-store") self.set_header("Content-Type", "application/json") return # Read in the request body as JSON then convert to a Spot object json_spot = tornado.escape.json_decode(post_data) spot = Spot(**json_spot) # Reject if no timestamp, frequency, dx_call or de_call if not spot.time or not spot.dx_call or not spot.freq or not spot.de_call: self.set_status(422) self.write(safe_json_dumps("Error - 'time', 'dx_call', 'freq' and 'de_call' must be provided as a minimum.")) self.set_header("Cache-Control", "no-store") self.set_header("Content-Type", "application/json") return # Reject invalid-looking callsigns if not re.match(r"^[A-Za-z0-9/\-]*$", spot.dx_call): self.set_status(422) self.write(safe_json_dumps("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): self.set_status(422) self.write(safe_json_dumps("Error - '" + spot.de_call + "' does not look like a valid callsign.")) self.set_header("Cache-Control", "no-store") self.set_header("Content-Type", "application/json") return # Reject if frequency not in a known band if infer_band_from_freq(spot.freq) == UNKNOWN_BAND: self.set_status(422) self.write(safe_json_dumps("Error - Frequency of " + str(spot.freq / 1000.0) + "kHz is not in a known band.")) self.set_header("Cache-Control", "no-store") self.set_header("Content-Type", "application/json") return # Reject if grid formatting incorrect if spot.dx_grid and not re.match( r"^([A-R]{2}[0-9]{2}[A-X]{2}[0-9]{2}[A-X]{2}|[A-R]{2}[0-9]{2}[A-X]{2}[0-9]{2}|[A-R]{2}[0-9]{2}[A-X]{2}|[A-R]{2}[0-9]{2})$", spot.dx_grid.upper()): self.set_status(422) self.write(safe_json_dumps("Error - '" + spot.dx_grid + "' does not look like a valid Maidenhead grid.")) self.set_header("Cache-Control", "no-store") self.set_header("Content-Type", "application/json") return # Reject if sig_ref format incorrect for sig if spot.sig and spot.sig_refs and len(spot.sig_refs) > 0 and spot.sig_refs[0].id and get_ref_regex_for_sig( spot.sig) and not re.match(get_ref_regex_for_sig(spot.sig), spot.sig_refs[0].id): self.set_status(422) self.write(safe_json_dumps( "Error - '" + spot.sig_refs[0].id + "' does not look like a valid reference for " + spot.sig + ".")) self.set_header("Cache-Control", "no-store") self.set_header("Content-Type", "application/json") return # infer missing data, and add it to our database. spot.source = "API" spot.infer_missing() self._spots.set(spot.id, spot) self.write(safe_json_dumps("OK")) self.set_status(201) self.set_header("Cache-Control", "no-store") self.set_header("Content-Type", "application/json") except Exception as e: logging.error("Exception when handling client request to add spot API: %s", e, exc_info=True) self.write(safe_json_dumps("Error - an internal server error occurred.")) self.set_status(500) self.set_header("Cache-Control", "no-store") self.set_header("Content-Type", "application/json")