Merge branch 'docs/kyclos' into 'develop'
[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. The basic configuration is a tuple where:
11
12 * The first element: `scale` (Integer). The time scale in milliseconds.
13 * The second element: `limit` (Integer). How many requests to limit in the time scale provided.
14
15 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.
16
17 To disable a limiter set its value to `nil`.
18
19 ### Example
20
21 config :pleroma, :rate_limit,
22 one: {1000, 10},
23 two: [{10_000, 10}, {10_000, 50}],
24 foobar: nil
25
26 Here we have three limiters:
27
28 * `one` which is not over 10req/1s
29 * `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users
30 * `foobar` which is disabled
31
32 ## Usage
33
34 AllowedSyntax:
35
36 plug(Pleroma.Plugs.RateLimiter, name: :limiter_name)
37 plug(Pleroma.Plugs.RateLimiter, options) # :name is a required option
38
39 Allowed options:
40
41 * `name` required, always used to fetch the limit values from the config
42 * `bucket_name` overrides name for counting purposes (e.g. to have a separate limit for a set of actions)
43 * `params` appends values of specified request params (e.g. ["id"]) to bucket name
44
45 Inside a controller:
46
47 plug(Pleroma.Plugs.RateLimiter, [name: :one] when action == :one)
48 plug(Pleroma.Plugs.RateLimiter, [name: :two] when action in [:two, :three])
49
50 plug(
51 Pleroma.Plugs.RateLimiter,
52 [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]]
53 when action in ~w(fav_status unfav_status)a
54 )
55
56 or inside a router pipeline:
57
58 pipeline :api do
59 ...
60 plug(Pleroma.Plugs.RateLimiter, name: :one)
61 ...
62 end
63 """
64 import Pleroma.Web.TranslationHelpers
65 import Plug.Conn
66
67 alias Pleroma.Plugs.RateLimiter.LimiterSupervisor
68 alias Pleroma.User
69
70 require Logger
71
72 def init(opts) do
73 limiter_name = Keyword.get(opts, :name)
74
75 case Pleroma.Config.get([:rate_limit, limiter_name]) do
76 nil ->
77 nil
78
79 config ->
80 name_root = Keyword.get(opts, :bucket_name, limiter_name)
81
82 %{
83 name: name_root,
84 limits: config,
85 opts: opts
86 }
87 end
88 end
89
90 # Do not limit if there is no limiter configuration
91 def call(conn, nil), do: conn
92
93 def call(conn, settings) do
94 case disabled?() do
95 true ->
96 if Pleroma.Config.get(:env) == :prod,
97 do: Logger.warn("Rate limiter is disabled for localhost/socket")
98
99 conn
100
101 false ->
102 settings
103 |> incorporate_conn_info(conn)
104 |> check_rate()
105 |> case do
106 {:ok, _count} ->
107 conn
108
109 {:error, _count} ->
110 render_throttled_error(conn)
111 end
112 end
113 end
114
115 def disabled? do
116 localhost_or_socket =
117 Pleroma.Config.get([Pleroma.Web.Endpoint, :http, :ip])
118 |> Tuple.to_list()
119 |> Enum.join(".")
120 |> String.match?(~r/^local|^127.0.0.1/)
121
122 remote_ip_disabled = not Pleroma.Config.get([Pleroma.Plugs.RemoteIp, :enabled])
123
124 localhost_or_socket and remote_ip_disabled
125 end
126
127 def inspect_bucket(conn, name_root, settings) do
128 settings =
129 settings
130 |> incorporate_conn_info(conn)
131
132 bucket_name = make_bucket_name(%{settings | name: name_root})
133 key_name = make_key_name(settings)
134 limit = get_limits(settings)
135
136 case Cachex.get(bucket_name, key_name) do
137 {:error, :no_cache} ->
138 {:err, :not_found}
139
140 {:ok, nil} ->
141 {0, limit}
142
143 {:ok, value} ->
144 {value, limit - value}
145 end
146 end
147
148 defp check_rate(settings) do
149 bucket_name = make_bucket_name(settings)
150 key_name = make_key_name(settings)
151 limit = get_limits(settings)
152
153 case Cachex.get_and_update(bucket_name, key_name, &increment_value(&1, limit)) do
154 {:commit, value} ->
155 {:ok, value}
156
157 {:ignore, value} ->
158 {:error, value}
159
160 {:error, :no_cache} ->
161 initialize_buckets(settings)
162 check_rate(settings)
163 end
164 end
165
166 defp increment_value(nil, _limit), do: {:commit, 1}
167
168 defp increment_value(val, limit) when val >= limit, do: {:ignore, val}
169
170 defp increment_value(val, _limit), do: {:commit, val + 1}
171
172 defp incorporate_conn_info(settings, %{assigns: %{user: %User{id: user_id}}, params: params}) do
173 Map.merge(settings, %{
174 mode: :user,
175 conn_params: params,
176 conn_info: "#{user_id}"
177 })
178 end
179
180 defp incorporate_conn_info(settings, %{params: params} = conn) do
181 Map.merge(settings, %{
182 mode: :anon,
183 conn_params: params,
184 conn_info: "#{ip(conn)}"
185 })
186 end
187
188 defp ip(%{remote_ip: remote_ip}) do
189 remote_ip
190 |> Tuple.to_list()
191 |> Enum.join(".")
192 end
193
194 defp render_throttled_error(conn) do
195 conn
196 |> render_error(:too_many_requests, "Throttled")
197 |> halt()
198 end
199
200 defp make_key_name(settings) do
201 ""
202 |> attach_params(settings)
203 |> attach_identity(settings)
204 end
205
206 defp get_scale(_, {scale, _}), do: scale
207
208 defp get_scale(:anon, [{scale, _}, {_, _}]), do: scale
209
210 defp get_scale(:user, [{_, _}, {scale, _}]), do: scale
211
212 defp get_limits(%{limits: {_scale, limit}}), do: limit
213
214 defp get_limits(%{mode: :user, limits: [_, {_, limit}]}), do: limit
215
216 defp get_limits(%{limits: [{_, limit}, _]}), do: limit
217
218 defp make_bucket_name(%{mode: :user, name: name_root}),
219 do: user_bucket_name(name_root)
220
221 defp make_bucket_name(%{mode: :anon, name: name_root}),
222 do: anon_bucket_name(name_root)
223
224 defp attach_params(input, %{conn_params: conn_params, opts: opts}) do
225 param_string =
226 opts
227 |> Keyword.get(:params, [])
228 |> Enum.sort()
229 |> Enum.map(&Map.get(conn_params, &1, ""))
230 |> Enum.join(":")
231
232 "#{input}#{param_string}"
233 end
234
235 defp initialize_buckets(%{name: _name, limits: nil}), do: :ok
236
237 defp initialize_buckets(%{name: name, limits: limits}) do
238 LimiterSupervisor.add_limiter(anon_bucket_name(name), get_scale(:anon, limits))
239 LimiterSupervisor.add_limiter(user_bucket_name(name), get_scale(:user, limits))
240 end
241
242 defp attach_identity(base, %{mode: :user, conn_info: conn_info}),
243 do: "user:#{base}:#{conn_info}"
244
245 defp attach_identity(base, %{mode: :anon, conn_info: conn_info}),
246 do: "ip:#{base}:#{conn_info}"
247
248 defp user_bucket_name(name_root), do: "user:#{name_root}" |> String.to_atom()
249 defp anon_bucket_name(name_root), do: "anon:#{name_root}" |> String.to_atom()
250 end