aprs.me/lib/aprsme_web/plugs/rate_limiter.ex
Graham McIntire 935b70b4b1
Fix rate limiter header check bug
Fixed ArgumentError in rate limiter by replacing cond statement with
proper case pattern matching. The cond was incorrectly evaluating
assignment expressions as truthy values instead of checking if headers
were empty lists.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-25 09:30:59 -05:00

78 lines
1.8 KiB
Elixir

defmodule AprsmeWeb.Plugs.RateLimiter do
@moduledoc """
Rate limiting plug to prevent DoS attacks
"""
import Phoenix.Controller
import Plug.Conn
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
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
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