Merge branch 'rate-limiter-runtime-settings' into 'develop'

RateLimiter improvements: runtime configurability, no default limits in tests

See merge request pleroma/pleroma!2250
This commit is contained in:
rinpatch 2020-02-29 21:52:33 +00:00
commit 19e559fe51
6 changed files with 218 additions and 184 deletions

View file

@ -7,12 +7,14 @@ defmodule Pleroma.Plugs.RateLimiter do
## Configuration
A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. The basic configuration is a tuple where:
A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration.
The basic configuration is a tuple where:
* The first element: `scale` (Integer). The time scale in milliseconds.
* The second element: `limit` (Integer). How many requests to limit in the time scale provided.
It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated.
It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a
list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated.
To disable a limiter set its value to `nil`.
@ -64,91 +66,102 @@ defmodule Pleroma.Plugs.RateLimiter do
import Pleroma.Web.TranslationHelpers
import Plug.Conn
alias Pleroma.Config
alias Pleroma.Plugs.RateLimiter.LimiterSupervisor
alias Pleroma.User
require Logger
def init(opts) do
limiter_name = Keyword.get(opts, :name)
@doc false
def init(plug_opts) do
plug_opts
end
case Pleroma.Config.get([:rate_limit, limiter_name]) do
nil ->
nil
config ->
name_root = Keyword.get(opts, :bucket_name, limiter_name)
%{
name: name_root,
limits: config,
opts: opts
}
def call(conn, plug_opts) do
if disabled?() do
handle_disabled(conn)
else
action_settings = action_settings(plug_opts)
handle(conn, action_settings)
end
end
# Do not limit if there is no limiter configuration
def call(conn, nil), do: conn
defp handle_disabled(conn) do
if Config.get(:env) == :prod do
Logger.warn("Rate limiter is disabled for localhost/socket")
end
def call(conn, settings) do
case disabled?() do
true ->
if Pleroma.Config.get(:env) == :prod,
do: Logger.warn("Rate limiter is disabled for localhost/socket")
conn
end
defp handle(conn, nil), do: conn
defp handle(conn, action_settings) do
action_settings
|> incorporate_conn_info(conn)
|> check_rate()
|> case do
{:ok, _count} ->
conn
false ->
settings
|> incorporate_conn_info(conn)
|> check_rate()
|> case do
{:ok, _count} ->
conn
{:error, _count} ->
render_throttled_error(conn)
end
{:error, _count} ->
render_throttled_error(conn)
end
end
def disabled? do
localhost_or_socket =
Pleroma.Config.get([Pleroma.Web.Endpoint, :http, :ip])
Config.get([Pleroma.Web.Endpoint, :http, :ip])
|> Tuple.to_list()
|> Enum.join(".")
|> String.match?(~r/^local|^127.0.0.1/)
remote_ip_disabled = not Pleroma.Config.get([Pleroma.Plugs.RemoteIp, :enabled])
remote_ip_disabled = not Config.get([Pleroma.Plugs.RemoteIp, :enabled])
localhost_or_socket and remote_ip_disabled
end
def inspect_bucket(conn, name_root, settings) do
settings =
settings
|> incorporate_conn_info(conn)
@inspect_bucket_not_found {:error, :not_found}
bucket_name = make_bucket_name(%{settings | name: name_root})
key_name = make_key_name(settings)
limit = get_limits(settings)
def inspect_bucket(conn, bucket_name_root, plug_opts) do
with %{name: _} = action_settings <- action_settings(plug_opts) do
action_settings = incorporate_conn_info(action_settings, conn)
bucket_name = make_bucket_name(%{action_settings | name: bucket_name_root})
key_name = make_key_name(action_settings)
limit = get_limits(action_settings)
case Cachex.get(bucket_name, key_name) do
{:error, :no_cache} ->
{:err, :not_found}
case Cachex.get(bucket_name, key_name) do
{:error, :no_cache} ->
@inspect_bucket_not_found
{:ok, nil} ->
{0, limit}
{:ok, nil} ->
{0, limit}
{:ok, value} ->
{value, limit - value}
{:ok, value} ->
{value, limit - value}
end
else
_ -> @inspect_bucket_not_found
end
end
defp check_rate(settings) do
bucket_name = make_bucket_name(settings)
key_name = make_key_name(settings)
limit = get_limits(settings)
def action_settings(plug_opts) do
with limiter_name when is_atom(limiter_name) <- plug_opts[:name],
limits when not is_nil(limits) <- Config.get([:rate_limit, limiter_name]) do
bucket_name_root = Keyword.get(plug_opts, :bucket_name, limiter_name)
%{
name: bucket_name_root,
limits: limits,
opts: plug_opts
}
end
end
defp check_rate(action_settings) do
bucket_name = make_bucket_name(action_settings)
key_name = make_key_name(action_settings)
limit = get_limits(action_settings)
case Cachex.get_and_update(bucket_name, key_name, &increment_value(&1, limit)) do
{:commit, value} ->
@ -158,8 +171,8 @@ defmodule Pleroma.Plugs.RateLimiter do
{:error, value}
{:error, :no_cache} ->
initialize_buckets(settings)
check_rate(settings)
initialize_buckets(action_settings)
check_rate(action_settings)
end
end
@ -169,16 +182,19 @@ defmodule Pleroma.Plugs.RateLimiter do
defp increment_value(val, _limit), do: {:commit, val + 1}
defp incorporate_conn_info(settings, %{assigns: %{user: %User{id: user_id}}, params: params}) do
Map.merge(settings, %{
defp incorporate_conn_info(action_settings, %{
assigns: %{user: %User{id: user_id}},
params: params
}) do
Map.merge(action_settings, %{
mode: :user,
conn_params: params,
conn_info: "#{user_id}"
})
end
defp incorporate_conn_info(settings, %{params: params} = conn) do
Map.merge(settings, %{
defp incorporate_conn_info(action_settings, %{params: params} = conn) do
Map.merge(action_settings, %{
mode: :anon,
conn_params: params,
conn_info: "#{ip(conn)}"
@ -197,10 +213,10 @@ defmodule Pleroma.Plugs.RateLimiter do
|> halt()
end
defp make_key_name(settings) do
defp make_key_name(action_settings) do
""
|> attach_params(settings)
|> attach_identity(settings)
|> attach_selected_params(action_settings)
|> attach_identity(action_settings)
end
defp get_scale(_, {scale, _}), do: scale
@ -215,21 +231,23 @@ defmodule Pleroma.Plugs.RateLimiter do
defp get_limits(%{limits: [{_, limit}, _]}), do: limit
defp make_bucket_name(%{mode: :user, name: name_root}),
do: user_bucket_name(name_root)
defp make_bucket_name(%{mode: :user, name: bucket_name_root}),
do: user_bucket_name(bucket_name_root)
defp make_bucket_name(%{mode: :anon, name: name_root}),
do: anon_bucket_name(name_root)
defp make_bucket_name(%{mode: :anon, name: bucket_name_root}),
do: anon_bucket_name(bucket_name_root)
defp attach_params(input, %{conn_params: conn_params, opts: opts}) do
param_string =
opts
defp attach_selected_params(input, %{conn_params: conn_params, opts: plug_opts}) do
params_string =
plug_opts
|> Keyword.get(:params, [])
|> Enum.sort()
|> Enum.map(&Map.get(conn_params, &1, ""))
|> Enum.join(":")
"#{input}#{param_string}"
[input, params_string]
|> Enum.join(":")
|> String.replace_leading(":", "")
end
defp initialize_buckets(%{name: _name, limits: nil}), do: :ok
@ -245,6 +263,6 @@ defmodule Pleroma.Plugs.RateLimiter do
defp attach_identity(base, %{mode: :anon, conn_info: conn_info}),
do: "ip:#{base}:#{conn_info}"
defp user_bucket_name(name_root), do: "user:#{name_root}" |> String.to_atom()
defp anon_bucket_name(name_root), do: "anon:#{name_root}" |> String.to_atom()
defp user_bucket_name(bucket_name_root), do: "user:#{bucket_name_root}" |> String.to_atom()
defp anon_bucket_name(bucket_name_root), do: "anon:#{bucket_name_root}" |> String.to_atom()
end