tarakan/lib/tarakan_web/plugs/code_browser_rate_limit.ex
Maxfield Luke af6077b9c3
All checks were successful
CI and deploy / Test (push) Successful in 4m52s
CI and deploy / Deploy production (push) Successful in 23s
Initial commit on Forgejo
Fresh repository history for elektrine/tarakan hosted at
https://git.elektrine.com/elektrine/tarakan.
2026-07-29 04:43:40 -04:00

58 lines
1.7 KiB
Elixir

defmodule TarakanWeb.Plugs.CodeBrowserRateLimit do
@moduledoc """
Per-IP budget for browser traffic, which includes the code browser.
Anonymous code browsing spawns git subprocesses against local mirrors and
can enqueue outbound mirror fetches, so each client IP gets a generous
request budget (default 300/min). IPv6 clients bucket by their /64
prefix. Platform admins and moderators are exempt (operational tooling);
the plug runs after the session scope is fetched so the exemption can see
the current role.
"""
import Plug.Conn
alias Tarakan.RateLimiter
alias TarakanWeb.Plugs.ClientIp
@defaults [limit: 300, window_seconds: 60]
@unlimited_roles ~w(admin moderator)
@doc false
def init(opts) do
configured = Application.get_env(:tarakan, __MODULE__, [])
@defaults |> Keyword.merge(configured) |> Keyword.merge(opts)
end
@doc false
def call(conn, opts) do
if unlimited_actor?(conn) do
conn
else
limit = Keyword.fetch!(opts, :limit)
window_seconds = Keyword.fetch!(opts, :window_seconds)
key = {:code_browser_ip, ClientIp.remote_ip_bucket(conn)}
case RateLimiter.check(key, limit, window_seconds) do
:ok -> conn
{:error, _reason, retry_after} -> reject(conn, retry_after)
end
end
end
defp unlimited_actor?(conn) do
case conn.assigns[:current_scope] do
%{platform_role: role} when role in @unlimited_roles -> true
%{account: %{platform_role: role}} when role in @unlimited_roles -> true
_ -> false
end
end
defp reject(conn, retry_after) do
conn
|> put_resp_header("retry-after", Integer.to_string(retry_after))
|> put_resp_content_type("text/plain")
|> send_resp(429, "too many requests")
|> halt()
end
end