1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Plugs.RateLimiter do
10 A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration.
11 The basic configuration is a tuple where:
13 * The first element: `scale` (Integer). The time scale in milliseconds.
14 * The second element: `limit` (Integer). How many requests to limit in the time scale provided.
16 It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a
17 list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated.
19 To disable a limiter set its value to `nil`.
23 config :pleroma, :rate_limit,
25 two: [{10_000, 10}, {10_000, 50}],
28 Here we have three limiters:
30 * `one` which is not over 10req/1s
31 * `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users
32 * `foobar` which is disabled
38 plug(Pleroma.Plugs.RateLimiter, name: :limiter_name)
39 plug(Pleroma.Plugs.RateLimiter, options) # :name is a required option
43 * `name` required, always used to fetch the limit values from the config
44 * `bucket_name` overrides name for counting purposes (e.g. to have a separate limit for a set of actions)
45 * `params` appends values of specified request params (e.g. ["id"]) to bucket name
49 plug(Pleroma.Plugs.RateLimiter, [name: :one] when action == :one)
50 plug(Pleroma.Plugs.RateLimiter, [name: :two] when action in [:two, :three])
53 Pleroma.Plugs.RateLimiter,
54 [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]]
55 when action in ~w(fav_status unfav_status)a
58 or inside a router pipeline:
62 plug(Pleroma.Plugs.RateLimiter, name: :one)
66 import Pleroma.Web.TranslationHelpers
70 alias Pleroma.Plugs.RateLimiter.LimiterSupervisor
76 def init(plug_opts) do
80 def call(conn, plug_opts) do
84 action_settings = action_settings(plug_opts)
85 handle(conn, action_settings)
89 defp handle_disabled(conn) do
91 "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."
97 defp handle(conn, nil), do: conn
99 defp handle(conn, action_settings) do
101 |> incorporate_conn_info(conn)
108 render_throttled_error(conn)
112 def disabled?(conn) do
113 localhost_or_socket =
114 case Config.get([Pleroma.Web.Endpoint, :http, :ip]) do
115 {127, 0, 0, 1} -> true
116 {0, 0, 0, 0, 0, 0, 0, 1} -> true
121 remote_ip_not_found =
122 if Map.has_key?(conn.assigns, :remote_ip_found),
123 do: !conn.assigns.remote_ip_found,
126 localhost_or_socket and remote_ip_not_found
129 @inspect_bucket_not_found {:error, :not_found}
131 def inspect_bucket(conn, bucket_name_root, plug_opts) do
132 with %{name: _} = action_settings <- action_settings(plug_opts) do
133 action_settings = incorporate_conn_info(action_settings, conn)
134 bucket_name = make_bucket_name(%{action_settings | name: bucket_name_root})
135 key_name = make_key_name(action_settings)
136 limit = get_limits(action_settings)
138 case Cachex.get(bucket_name, key_name) do
139 {:error, :no_cache} ->
140 @inspect_bucket_not_found
146 {value, limit - value}
149 _ -> @inspect_bucket_not_found
153 def action_settings(plug_opts) do
154 with limiter_name when is_atom(limiter_name) <- plug_opts[:name],
155 limits when not is_nil(limits) <- Config.get([:rate_limit, limiter_name]) do
156 bucket_name_root = Keyword.get(plug_opts, :bucket_name, limiter_name)
159 name: bucket_name_root,
166 defp check_rate(action_settings) do
167 bucket_name = make_bucket_name(action_settings)
168 key_name = make_key_name(action_settings)
169 limit = get_limits(action_settings)
171 case Cachex.get_and_update(bucket_name, key_name, &increment_value(&1, limit)) do
178 {:error, :no_cache} ->
179 initialize_buckets!(action_settings)
180 check_rate(action_settings)
184 defp increment_value(nil, _limit), do: {:commit, 1}
186 defp increment_value(val, limit) when val >= limit, do: {:ignore, val}
188 defp increment_value(val, _limit), do: {:commit, val + 1}
190 defp incorporate_conn_info(action_settings, %{
191 assigns: %{user: %User{id: user_id}},
194 Map.merge(action_settings, %{
197 conn_info: "#{user_id}"
201 defp incorporate_conn_info(action_settings, %{params: params} = conn) do
202 Map.merge(action_settings, %{
205 conn_info: "#{ip(conn)}"
209 defp ip(%{remote_ip: remote_ip}) do
215 defp render_throttled_error(conn) do
217 |> render_error(:too_many_requests, "Throttled")
221 defp make_key_name(action_settings) do
223 |> attach_selected_params(action_settings)
224 |> attach_identity(action_settings)
227 defp get_scale(_, {scale, _}), do: scale
229 defp get_scale(:anon, [{scale, _}, {_, _}]), do: scale
231 defp get_scale(:user, [{_, _}, {scale, _}]), do: scale
233 defp get_limits(%{limits: {_scale, limit}}), do: limit
235 defp get_limits(%{mode: :user, limits: [_, {_, limit}]}), do: limit
237 defp get_limits(%{limits: [{_, limit}, _]}), do: limit
239 defp make_bucket_name(%{mode: :user, name: bucket_name_root}),
240 do: user_bucket_name(bucket_name_root)
242 defp make_bucket_name(%{mode: :anon, name: bucket_name_root}),
243 do: anon_bucket_name(bucket_name_root)
245 defp attach_selected_params(input, %{conn_params: conn_params, opts: plug_opts}) do
248 |> Keyword.get(:params, [])
250 |> Enum.map(&Map.get(conn_params, &1, ""))
253 [input, params_string]
255 |> String.replace_leading(":", "")
258 defp initialize_buckets!(%{name: _name, limits: nil}), do: :ok
260 defp initialize_buckets!(%{name: name, limits: limits}) do
262 LimiterSupervisor.add_or_return_limiter(anon_bucket_name(name), get_scale(:anon, limits))
265 LimiterSupervisor.add_or_return_limiter(user_bucket_name(name), get_scale(:user, limits))
270 defp attach_identity(base, %{mode: :user, conn_info: conn_info}),
271 do: "user:#{base}:#{conn_info}"
273 defp attach_identity(base, %{mode: :anon, conn_info: conn_info}),
274 do: "ip:#{base}:#{conn_info}"
276 defp user_bucket_name(bucket_name_root), do: "user:#{bucket_name_root}" |> String.to_atom()
277 defp anon_bucket_name(bucket_name_root), do: "anon:#{bucket_name_root}" |> String.to_atom()