Fresh repository history for elektrine/tarakan hosted at https://git.elektrine.com/elektrine/tarakan.
58 lines
1.7 KiB
Elixir
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
|