3a27d6eb7e2fbfae008a4282dcbcf885c6780e31
[akkoma] / lib / pleroma / plugs / rate_limiter / rate_limiter.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Plugs.RateLimiter do
6 @moduledoc """
7
8 ## Configuration
9
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:
12
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.
15
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.
18
19 To disable a limiter set its value to `nil`.
20
21 ### Example
22
23 config :pleroma, :rate_limit,
24 one: {1000, 10},
25 two: [{10_000, 10}, {10_000, 50}],
26 foobar: nil
27
28 Here we have three limiters:
29
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
33
34 ## Usage
35
36 AllowedSyntax:
37
38 plug(Pleroma.Plugs.RateLimiter, name: :limiter_name)
39 plug(Pleroma.Plugs.RateLimiter, options) # :name is a required option
40
41 Allowed options:
42
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
46
47 Inside a controller:
48
49 plug(Pleroma.Plugs.RateLimiter, [name: :one] when action == :one)
50 plug(Pleroma.Plugs.RateLimiter, [name: :two] when action in [:two, :three])
51
52 plug(
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
56 )
57
58 or inside a router pipeline:
59
60 pipeline :api do
61 ...
62 plug(Pleroma.Plugs.RateLimiter, name: :one)
63 ...
64 end
65 """
66 import Pleroma.Web.TranslationHelpers
67 import Plug.Conn
68
69 alias Pleroma.Config
70 alias Pleroma.Plugs.RateLimiter.LimiterSupervisor
71 alias Pleroma.User
72
73 require Logger
74
75 @doc false
76 def init(plug_opts) do
77 plug_opts
78 end
79
80 def call(conn, plug_opts) do
81 if disabled?() do
82 handle_disabled(conn)
83 else
84 action_settings = action_settings(plug_opts)
85 handle(conn, action_settings)
86 end
87 end
88
89 defp handle_disabled(conn) do
90 if Config.get(:env) == :prod do
91 Logger.warn("Rate limiter is disabled for localhost/socket")
92 end
93
94 conn
95 end
96
97 defp handle(conn, nil), do: conn
98
99 defp handle(conn, action_settings) do
100 action_settings
101 |> incorporate_conn_info(conn)
102 |> check_rate()
103 |> case do
104 {:ok, _count} ->
105 conn
106
107 {:error, _count} ->
108 render_throttled_error(conn)
109 end
110 end
111
112 def disabled? do
113 localhost_or_socket =
114 Config.get([Pleroma.Web.Endpoint, :http, :ip])
115 |> Tuple.to_list()
116 |> Enum.join(".")
117 |> String.match?(~r/^local|^127.0.0.1/)
118
119 remote_ip_disabled = not Config.get([Pleroma.Plugs.RemoteIp, :enabled])
120
121 localhost_or_socket and remote_ip_disabled
122 end
123
124 def inspect_bucket(conn, bucket_name_root, plug_opts) do
125 with %{name: _} = action_settings <- action_settings(plug_opts) do
126 action_settings = incorporate_conn_info(action_settings, conn)
127 bucket_name = make_bucket_name(%{action_settings | name: bucket_name_root})
128 key_name = make_key_name(action_settings)
129 limit = get_limits(action_settings)
130
131 case Cachex.get(bucket_name, key_name) do
132 {:error, :no_cache} ->
133 {:err, :not_found}
134
135 {:ok, nil} ->
136 {0, limit}
137
138 {:ok, value} ->
139 {value, limit - value}
140 end
141 else
142 _ -> {:err, :not_found}
143 end
144 end
145
146 def action_settings(plug_opts) do
147 with limiter_name when is_atom(limiter_name) <- plug_opts[:name],
148 limits when not is_nil(limits) <- Config.get([:rate_limit, limiter_name]) do
149 bucket_name_root = Keyword.get(plug_opts, :bucket_name, limiter_name)
150
151 %{
152 name: bucket_name_root,
153 limits: limits,
154 opts: plug_opts
155 }
156 end
157 end
158
159 defp check_rate(action_settings) do
160 bucket_name = make_bucket_name(action_settings)
161 key_name = make_key_name(action_settings)
162 limit = get_limits(action_settings)
163
164 case Cachex.get_and_update(bucket_name, key_name, &increment_value(&1, limit)) do
165 {:commit, value} ->
166 {:ok, value}
167
168 {:ignore, value} ->
169 {:error, value}
170
171 {:error, :no_cache} ->
172 initialize_buckets(action_settings)
173 check_rate(action_settings)
174 end
175 end
176
177 defp increment_value(nil, _limit), do: {:commit, 1}
178
179 defp increment_value(val, limit) when val >= limit, do: {:ignore, val}
180
181 defp increment_value(val, _limit), do: {:commit, val + 1}
182
183 defp incorporate_conn_info(action_settings, %{
184 assigns: %{user: %User{id: user_id}},
185 params: params
186 }) do
187 Map.merge(action_settings, %{
188 mode: :user,
189 conn_params: params,
190 conn_info: "#{user_id}"
191 })
192 end
193
194 defp incorporate_conn_info(action_settings, %{params: params} = conn) do
195 Map.merge(action_settings, %{
196 mode: :anon,
197 conn_params: params,
198 conn_info: "#{ip(conn)}"
199 })
200 end
201
202 defp ip(%{remote_ip: remote_ip}) do
203 remote_ip
204 |> Tuple.to_list()
205 |> Enum.join(".")
206 end
207
208 defp render_throttled_error(conn) do
209 conn
210 |> render_error(:too_many_requests, "Throttled")
211 |> halt()
212 end
213
214 defp make_key_name(action_settings) do
215 ""
216 |> attach_selected_params(action_settings)
217 |> attach_identity(action_settings)
218 end
219
220 defp get_scale(_, {scale, _}), do: scale
221
222 defp get_scale(:anon, [{scale, _}, {_, _}]), do: scale
223
224 defp get_scale(:user, [{_, _}, {scale, _}]), do: scale
225
226 defp get_limits(%{limits: {_scale, limit}}), do: limit
227
228 defp get_limits(%{mode: :user, limits: [_, {_, limit}]}), do: limit
229
230 defp get_limits(%{limits: [{_, limit}, _]}), do: limit
231
232 defp make_bucket_name(%{mode: :user, name: bucket_name_root}),
233 do: user_bucket_name(bucket_name_root)
234
235 defp make_bucket_name(%{mode: :anon, name: bucket_name_root}),
236 do: anon_bucket_name(bucket_name_root)
237
238 defp attach_selected_params(input, %{conn_params: conn_params, opts: plug_opts}) do
239 params_string =
240 plug_opts
241 |> Keyword.get(:params, [])
242 |> Enum.sort()
243 |> Enum.map(&Map.get(conn_params, &1, ""))
244 |> Enum.join(":")
245
246 [input, params_string]
247 |> Enum.join(":")
248 |> String.replace_leading(":", "")
249 end
250
251 defp initialize_buckets(%{name: _name, limits: nil}), do: :ok
252
253 defp initialize_buckets(%{name: name, limits: limits}) do
254 LimiterSupervisor.add_limiter(anon_bucket_name(name), get_scale(:anon, limits))
255 LimiterSupervisor.add_limiter(user_bucket_name(name), get_scale(:user, limits))
256 end
257
258 defp attach_identity(base, %{mode: :user, conn_info: conn_info}),
259 do: "user:#{base}:#{conn_info}"
260
261 defp attach_identity(base, %{mode: :anon, conn_info: conn_info}),
262 do: "ip:#{base}:#{conn_info}"
263
264 defp user_bucket_name(bucket_name_root), do: "user:#{bucket_name_root}" |> String.to_atom()
265 defp anon_bucket_name(bucket_name_root), do: "anon:#{bucket_name_root}" |> String.to_atom()
266 end