78 lines
2 KiB
Elixir
78 lines
2 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
|
|
ua = connect_info[:user_agent] || "unknown"
|
|
|
|
socket =
|
|
socket
|
|
|> assign(:peer_ip, ip)
|
|
|> assign(:user_agent, ua)
|
|
|
|
# Track connection for analytics (fire-and-forget)
|
|
Aprsme.PlausibleAnalytics.track(
|
|
%{user_agent: ua, remote_ip: ip, request_path: "/mobile/websocket"},
|
|
"ws_connect"
|
|
)
|
|
|
|
Aprsme.ApiMetrics.track(%{}, "ws_connect")
|
|
|
|
{:ok, socket}
|
|
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
|