- Beacon detail endpoints (LiveView + REST API) now hide unapproved beacons from anonymous and unauthorized viewers; only the submitter and admins can see pending records before approval. Adds Beacons.get_visible_beacon/2 with scope-aware checks. - API contact pagination now honors per_page end-to-end. Radio.list_contacts/1 accepts :per_page and clamps to 200. - API rate limiter: ETS table is now owned by a long-lived Sweeper GenServer (won't die with a request task); Sweeper periodically prunes expired-window rows to bound memory; init_table/0 race is rescued. - /scores/cells and /weather/cells: add per-IP rate limiting and a shared GridBounds clamp/413 guard so global / oversized viewports no longer drive unbounded binary responses. - NEXRAD PNG unfilter (sub/up/average/paeth): replace acc++[byte] + Enum.at(acc, idx-bpp) with O(n) binary recursion. Decode time for the 12200x5400 n0q frame goes from quadratic to linear. - LiveTableFooter.parse_page and ScoresFile.fetch_bound: switch String.to_integer/String.to_float to Integer.parse/Float.parse, fall back to defaults instead of raising. - PathLive and MapLive band-event handlers: replace String.to_integer(params["band"]) with parse_int / parse_band_param so a non-numeric band parameter no longer crashes the LiveView.
73 lines
2.7 KiB
Elixir
73 lines
2.7 KiB
Elixir
defmodule MicrowavepropWeb.GridBounds do
|
||
@moduledoc """
|
||
Bounds clamp + sanity checks for grid-cell HTTP endpoints
|
||
(`/scores/cells`, `/weather/cells`).
|
||
|
||
The propagation grid covers HRRR (CONUS) and HRDPS (Canada to
|
||
60°N): roughly lat 25-60, lon -141 to -52 — about 35° × 90° =
|
||
3150 sq degrees. Any caller-provided viewport is clamped to a
|
||
generous superset of that bbox, and viewports whose area exceeds
|
||
the supported region are rejected as `:viewport_too_large` so a
|
||
malicious caller cannot trigger global / multi-million-cell
|
||
binary responses.
|
||
|
||
`nil` bounds are passed through (caller intends the full grid).
|
||
"""
|
||
|
||
# Generous superset of HRRR + HRDPS — a few extra degrees on each
|
||
# edge so legitimate clients zoomed slightly past the data extent
|
||
# still get a 200 with the actual covered cells.
|
||
@lat_min 20.0
|
||
@lat_max 65.0
|
||
@lon_min -150.0
|
||
@lon_max -50.0
|
||
|
||
# Comfortably above the HRDPS+HRRR union (~3150 sq deg), but tight
|
||
# enough that the fully-clamped extent (~4500 sq deg) still trips
|
||
# the cap — a "give me everything" request is exactly the DoS
|
||
# shape we want to reject.
|
||
@max_viewport_area_sq_deg 4000.0
|
||
|
||
@type bounds :: %{optional(String.t()) => float()} | nil
|
||
|
||
@doc """
|
||
Clamp `bounds` to the supported grid extent. Returns:
|
||
|
||
* `{:ok, nil}` when bounds are nil (caller wants the full grid)
|
||
* `{:ok, clamped_bounds}` when the clamped viewport area is OK
|
||
* `{:error, :viewport_too_large}` when the clamped area still
|
||
exceeds `@max_viewport_area_sq_deg` (catches degenerate
|
||
requests like global bounds that survive clamping)
|
||
* `{:error, :invalid_bounds}` when north < south or east < west
|
||
"""
|
||
@spec clamp(bounds()) ::
|
||
{:ok, bounds()} | {:error, :viewport_too_large | :invalid_bounds}
|
||
def clamp(nil), do: {:ok, nil}
|
||
|
||
def clamp(%{"south" => s, "north" => n, "west" => w, "east" => e})
|
||
when is_number(s) and is_number(n) and is_number(w) and is_number(e) do
|
||
south = clamp_value(s, @lat_min, @lat_max)
|
||
north = clamp_value(n, @lat_min, @lat_max)
|
||
west = clamp_value(w, @lon_min, @lon_max)
|
||
east = clamp_value(e, @lon_min, @lon_max)
|
||
|
||
cond do
|
||
north < south or east < west ->
|
||
{:error, :invalid_bounds}
|
||
|
||
(north - south) * (east - west) > @max_viewport_area_sq_deg ->
|
||
{:error, :viewport_too_large}
|
||
|
||
true ->
|
||
{:ok, %{"south" => south, "north" => north, "west" => west, "east" => east}}
|
||
end
|
||
end
|
||
|
||
def clamp(_), do: {:error, :invalid_bounds}
|
||
|
||
@doc "Maximum viewport area in square degrees."
|
||
@spec max_viewport_area_sq_deg() :: float()
|
||
def max_viewport_area_sq_deg, do: @max_viewport_area_sq_deg
|
||
|
||
defp clamp_value(v, lo, hi), do: v |> max(lo) |> min(hi)
|
||
end
|