akkoma/lib/pleroma/web/plugs/rate_limiter.ex
FloatingGhost 98cb255d12 Support elixir1.15
OTP builds to 1.15

Changelog entry

Ensure policies are fully loaded

Fix :warn

use main branch for linkify

Fix warn in tests

Migrations for phoenix 1.17

Revert "Migrations for phoenix 1.17"

This reverts commit 6a3b2f15b74ea5e33150529385215b7a531f3999.

Oban upgrade

Add default empty whitelist

mix format

limit test to amd64

OTP 26 tests for 1.15

use OTP_VERSION tag

baka

just 1.15

Massive deps update

Update locale, deps

Mix format

shell????

multiline???

?

max cases 1

use assert_recieve

don't put_env in async tests

don't async conn/fs tests

mix format

FIx some uploader issues

Fix tests
2023-08-03 17:44:09 +01:00

276 lines
7.6 KiB
Elixir

# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.RateLimiter do
@moduledoc """
## 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:
* 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.
To disable a limiter set its value to `nil`.
### Example
config :pleroma, :rate_limit,
one: {1000, 10},
two: [{10_000, 10}, {10_000, 50}],
foobar: nil
Here we have three limiters:
* `one` which is not over 10req/1s
* `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users
* `foobar` which is disabled
## Usage
AllowedSyntax:
plug(Pleroma.Web.Plugs.RateLimiter, name: :limiter_name)
plug(Pleroma.Web.Plugs.RateLimiter, options) # :name is a required option
Allowed options:
* `name` required, always used to fetch the limit values from the config
* `bucket_name` overrides name for counting purposes (e.g. to have a separate limit for a set of actions)
* `params` appends values of specified request params (e.g. ["id"]) to bucket name
Inside a controller:
plug(Pleroma.Web.Plugs.RateLimiter, [name: :one] when action == :one)
plug(Pleroma.Web.Plugs.RateLimiter, [name: :two] when action in [:two, :three])
plug(
Pleroma.Web.Plugs.RateLimiter,
[name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]]
when action in ~w(fav_status unfav_status)a
)
or inside a router pipeline:
pipeline :api do
...
plug(Pleroma.Web.Plugs.RateLimiter, name: :one)
...
end
"""
import Pleroma.Web.TranslationHelpers
import Plug.Conn
alias Pleroma.Config
alias Pleroma.User
alias Pleroma.Web.Plugs.RateLimiter.LimiterSupervisor
require Logger
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
@doc false
def init(plug_opts) do
plug_opts
end
def call(conn, plug_opts) do
if disabled?(conn) do
handle_disabled(conn)
else
action_settings = action_settings(plug_opts)
handle(conn, action_settings)
end
end
defp handle_disabled(conn) do
Logger.warning(
"Rate limiter disabled due to forwarded IP not being found. Please ensure your reverse proxy is providing the X-Forwarded-For header or disable the RemoteIP plug/rate limiter."
)
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
{:error, _count} ->
render_throttled_error(conn)
end
end
def disabled?(conn) do
if Map.has_key?(conn.assigns, :remote_ip_found),
do: !conn.assigns.remote_ip_found,
else: false
end
@inspect_bucket_not_found {:error, :not_found}
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} ->
@inspect_bucket_not_found
{:ok, nil} ->
{0, limit}
{:ok, value} ->
{value, limit - value}
end
else
_ -> @inspect_bucket_not_found
end
end
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} ->
{:ok, value}
{:ignore, value} ->
{:error, value}
{:error, :no_cache} ->
initialize_buckets!(action_settings)
check_rate(action_settings)
end
end
defp increment_value(nil, _limit), do: {:commit, 1}
defp increment_value(val, limit) when val >= limit, do: {:ignore, val}
defp increment_value(val, _limit), do: {:commit, val + 1}
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(action_settings, %{params: params} = conn) do
Map.merge(action_settings, %{
mode: :anon,
conn_params: params,
conn_info: "#{ip(conn)}"
})
end
defp ip(%{remote_ip: remote_ip}) when is_binary(remote_ip) do
remote_ip
end
defp ip(%{remote_ip: remote_ip}) when is_tuple(remote_ip) do
remote_ip
|> Tuple.to_list()
|> Enum.join(".")
end
defp ip(_), do: nil
defp render_throttled_error(conn) do
conn
|> render_error(:too_many_requests, "Throttled")
|> halt()
end
defp make_key_name(action_settings) do
""
|> attach_selected_params(action_settings)
|> attach_identity(action_settings)
end
defp get_scale(_, {scale, _}), do: scale
defp get_scale(:anon, [{scale, _}, {_, _}]), do: scale
defp get_scale(:user, [{_, _}, {scale, _}]), do: scale
defp get_limits(%{limits: {_scale, limit}}), do: limit
defp get_limits(%{mode: :user, limits: [_, {_, limit}]}), do: limit
defp get_limits(%{limits: [{_, limit}, _]}), do: limit
defp make_bucket_name(%{mode: :user, name: bucket_name_root}),
do: user_bucket_name(bucket_name_root)
defp make_bucket_name(%{mode: :anon, name: bucket_name_root}),
do: anon_bucket_name(bucket_name_root)
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, params_string]
|> Enum.join(":")
|> String.replace_leading(":", "")
end
defp initialize_buckets!(%{name: _name, limits: nil}), do: :ok
defp initialize_buckets!(%{name: name, limits: limits}) do
{:ok, _pid} =
LimiterSupervisor.add_or_return_limiter(anon_bucket_name(name), get_scale(:anon, limits))
{:ok, _pid} =
LimiterSupervisor.add_or_return_limiter(user_bucket_name(name), get_scale(:user, limits))
:ok
end
defp attach_identity(base, %{mode: :user, conn_info: conn_info}),
do: "user:#{base}:#{conn_info}"
defp attach_identity(base, %{mode: :anon, conn_info: conn_info}),
do: "ip:#{base}:#{conn_info}"
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