- MobileUserSocket: cap new socket connections per IP at 30/min via Aprsme.RateLimiter. Denials log and return :error at handshake. Tests bypass the check (no real peer_data). - MobileChannel: rate-limit the expensive client-initiated handlers (subscribe_bounds, subscribe_callsign, search_callsign) at 30/minute per socket. Streaming packet delivery is unaffected. - MobileChannel: historical packet loads now arrive as a batched `packets` event (100 per frame) instead of one `packet` frame per row. Live streaming still uses the single `packet` event. - Updated docs/mobile-api.md with the `packets` event and rate-limit semantics; migration note preserves compatibility for clients that only handle live `packet` events.
63 lines
1.7 KiB
Elixir
63 lines
1.7 KiB
Elixir
defmodule AprsmeWeb.MobileUserSocket do
|
|
@moduledoc """
|
|
Socket for mobile applications (iOS/Android) to receive real-time APRS packets.
|
|
|
|
Connections are anonymous but rate-limited per peer IP to prevent a single
|
|
client from opening unbounded connections and exhausting server resources.
|
|
"""
|
|
use Phoenix.Socket
|
|
|
|
require Logger
|
|
|
|
# Channels
|
|
channel "mobile:packets", AprsmeWeb.MobileChannel
|
|
|
|
# 30 new connections per IP per minute — well above normal reconnect churn
|
|
# but tight enough to stop a script from floodting the socket layer.
|
|
@connect_limit 30
|
|
@connect_scale 60_000
|
|
|
|
@impl true
|
|
def connect(_params, socket, connect_info) do
|
|
ip = peer_ip(connect_info)
|
|
|
|
if test_env?() or allow_connect?(ip) do
|
|
{:ok, assign(socket, :peer_ip, ip)}
|
|
else
|
|
Logger.warning("Mobile socket connect rate-limited for ip=#{ip}")
|
|
:error
|
|
end
|
|
end
|
|
|
|
defp allow_connect?(ip) do
|
|
case Aprsme.RateLimiter.hit("mobile_socket_connect:#{ip}", @connect_scale, @connect_limit) do
|
|
{:allow, _count} -> true
|
|
{:deny, _retry_after} -> false
|
|
end
|
|
end
|
|
|
|
defp test_env?, do: Application.get_env(:aprsme, :env) == :test
|
|
|
|
@impl true
|
|
def id(_socket), do: nil
|
|
|
|
defp peer_ip(%{x_headers: headers}) when is_list(headers) do
|
|
headers
|
|
|> Enum.find_value(fn
|
|
{"cf-connecting-ip", v} -> v
|
|
{"x-forwarded-for", v} -> v |> String.split(",") |> List.first() |> String.trim()
|
|
{"x-real-ip", v} -> v
|
|
_ -> nil
|
|
end)
|
|
|> case do
|
|
nil -> "unknown"
|
|
ip -> ip
|
|
end
|
|
end
|
|
|
|
defp peer_ip(%{peer_data: %{address: address}}) when not is_nil(address) do
|
|
address |> :inet.ntoa() |> to_string()
|
|
end
|
|
|
|
defp peer_ip(_), do: "unknown"
|
|
end
|