aprs.me/lib/aprsme_web/plugs/rate_limiter.ex
Graham McIntire 781fdd88a3
Remove dead modules, unused deps, and commented-out code
Delete 5 unused modules: Presence (untracked), LiveView ShutdownHandler
(unused mixin), RateLimiterWrapper (pointless delegation), Passcode
(only used by own test), MapLive.Utils (pure delegates).

Fix ShutdownHandler to not call deleted Presence. Inline ParamUtils
at call sites. Remove cachex dep and 27 stale entries from mix.lock.
Clean commented-out Exq/Redis config from application.ex.
2026-02-20 08:09:22 -06:00

89 lines
2.2 KiB
Elixir

defmodule AprsmeWeb.Plugs.RateLimiter do
@moduledoc """
Rate limiting plug to prevent DoS attacks
"""
import Phoenix.Controller
import Plug.Conn
@type key_type :: :ip | :user_agent | (Plug.Conn.t() -> String.t()) | String.t()
@type init_opts :: [
scale: integer(),
limit: integer(),
key: key_type(),
error_message: String.t()
]
@spec init(Keyword.t()) :: init_opts()
def init(opts) do
# Default options
Keyword.merge(
[
# 1 minute
scale: 60_000,
# 100 requests per minute
limit: 100,
# Rate limit by IP address
key: :ip,
error_message: "Too many requests"
],
opts
)
end
@spec call(Plug.Conn.t(), init_opts()) :: Plug.Conn.t()
def call(conn, opts) do
key = get_key(conn, opts[:key])
scale = opts[:scale]
limit = opts[:limit]
error_message = opts[:error_message]
case Aprsme.RateLimiter.hit("rate_limit:#{key}", scale, limit) do
{:allow, _count} ->
conn
{:deny, _retry_after} ->
conn
|> put_status(:too_many_requests)
|> json(%{error: error_message})
|> halt()
end
end
@spec get_key(Plug.Conn.t(), key_type()) :: String.t()
defp get_key(conn, :ip) do
# Check headers in order of preference
case {get_req_header(conn, "cf-connecting-ip"), get_req_header(conn, "x-forwarded-for"),
get_req_header(conn, "x-real-ip")} do
# Cloudflare header takes precedence
{[cf | _], _, _} ->
cf
# Then standard X-Forwarded-For header
{[], [forwarded | _], _} ->
forwarded |> String.split(",") |> List.first() |> String.trim()
# Then X-Real-IP header
{[], [], [real | _]} ->
real
# Fall back to remote_ip
{[], [], []} ->
conn.remote_ip |> :inet.ntoa() |> to_string()
end
end
defp get_key(conn, :user_agent) do
case get_req_header(conn, "user-agent") do
[ua | _] -> ua
[] -> "unknown"
end
end
defp get_key(conn, custom_key) when is_function(custom_key) do
custom_key.(conn)
end
defp get_key(_conn, key) when is_binary(key) do
key
end
end