Merge pull request 'metrics' (#375) from stats into develop
[akkoma] / test / pleroma / web / plugs / rate_limiter_test.exs
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.Plugs.RateLimiterTest do
6 use Pleroma.Web.ConnCase
7
8 alias Phoenix.ConnTest
9 alias Pleroma.Web.Plugs.RateLimiter
10 alias Plug.Conn
11
12 import Pleroma.Factory
13 import Pleroma.Tests.Helpers, only: [clear_config: 1, clear_config: 2]
14
15 # Note: each example must work with separate buckets in order to prevent concurrency issues
16 setup do: clear_config([Pleroma.Web.Endpoint, :http, :ip])
17 setup do: clear_config(:rate_limit)
18
19 describe "config" do
20 @limiter_name :test_init
21 setup do: clear_config([Pleroma.Web.Plugs.RemoteIp, :enabled])
22
23 test "config is required for plug to work" do
24 clear_config([:rate_limit, @limiter_name], {1, 1})
25 clear_config([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8})
26
27 assert %{limits: {1, 1}, name: :test_init, opts: [name: :test_init]} ==
28 [name: @limiter_name]
29 |> RateLimiter.init()
30 |> RateLimiter.action_settings()
31
32 assert nil ==
33 [name: :nonexisting_limiter]
34 |> RateLimiter.init()
35 |> RateLimiter.action_settings()
36 end
37 end
38
39 test "it is disabled if it remote ip plug is enabled but no remote ip is found" do
40 assert RateLimiter.disabled?(Conn.assign(build_conn(), :remote_ip_found, false))
41 end
42
43 test "it is enabled if remote ip found" do
44 refute RateLimiter.disabled?(Conn.assign(build_conn(), :remote_ip_found, true))
45 end
46
47 test "it is enabled if remote_ip_found flag doesn't exist" do
48 refute RateLimiter.disabled?(build_conn())
49 end
50
51 test "it restricts based on config values" do
52 limiter_name = :test_plug_opts
53 scale = 80
54 limit = 5
55
56 clear_config([Pleroma.Web.Endpoint, :http, :ip], {127, 0, 0, 1})
57 clear_config([:rate_limit, limiter_name], {scale, limit})
58
59 plug_opts = RateLimiter.init(name: limiter_name)
60 conn = build_conn(:get, "/")
61
62 for _ <- 1..5 do
63 conn_limited = RateLimiter.call(conn, plug_opts)
64
65 refute conn_limited.status == Conn.Status.code(:too_many_requests)
66 refute conn_limited.resp_body
67 refute conn_limited.halted
68 end
69
70 conn_limited = RateLimiter.call(conn, plug_opts)
71 assert %{"error" => "Throttled"} = ConnTest.json_response(conn_limited, :too_many_requests)
72 assert conn_limited.halted
73
74 expire_ttl(conn, limiter_name)
75
76 for _ <- 1..5 do
77 conn_limited = RateLimiter.call(conn, plug_opts)
78
79 refute conn_limited.status == Conn.Status.code(:too_many_requests)
80 refute conn_limited.resp_body
81 refute conn_limited.halted
82 end
83
84 conn_limited = RateLimiter.call(conn, plug_opts)
85 assert %{"error" => "Throttled"} = ConnTest.json_response(conn_limited, :too_many_requests)
86 assert conn_limited.halted
87 end
88
89 describe "options" do
90 test "`bucket_name` option overrides default bucket name" do
91 limiter_name = :test_bucket_name
92
93 clear_config([:rate_limit, limiter_name], {1000, 5})
94 clear_config([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8})
95
96 base_bucket_name = "#{limiter_name}:group1"
97 plug_opts = RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name)
98
99 conn = build_conn(:get, "/")
100
101 RateLimiter.call(conn, plug_opts)
102 assert {1, 4} = RateLimiter.inspect_bucket(conn, base_bucket_name, plug_opts)
103 assert {:error, :not_found} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts)
104 end
105
106 test "`params` option allows different queries to be tracked independently" do
107 limiter_name = :test_params
108 clear_config([:rate_limit, limiter_name], {1000, 5})
109 clear_config([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8})
110
111 plug_opts = RateLimiter.init(name: limiter_name, params: ["id"])
112
113 conn = build_conn(:get, "/?id=1")
114 conn = Conn.fetch_query_params(conn)
115 conn_2 = build_conn(:get, "/?id=2")
116
117 RateLimiter.call(conn, plug_opts)
118 assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts)
119 assert {0, 5} = RateLimiter.inspect_bucket(conn_2, limiter_name, plug_opts)
120 end
121
122 test "it supports combination of options modifying bucket name" do
123 limiter_name = :test_options_combo
124 clear_config([:rate_limit, limiter_name], {1000, 5})
125 clear_config([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8})
126
127 base_bucket_name = "#{limiter_name}:group1"
128
129 plug_opts =
130 RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name, params: ["id"])
131
132 id = "100"
133
134 conn = build_conn(:get, "/?id=#{id}")
135 conn = Conn.fetch_query_params(conn)
136 conn_2 = build_conn(:get, "/?id=#{101}")
137
138 RateLimiter.call(conn, plug_opts)
139 assert {1, 4} = RateLimiter.inspect_bucket(conn, base_bucket_name, plug_opts)
140 assert {0, 5} = RateLimiter.inspect_bucket(conn_2, base_bucket_name, plug_opts)
141 end
142 end
143
144 describe "unauthenticated users" do
145 @tag :erratic
146 test "are restricted based on remote IP" do
147 limiter_name = :test_unauthenticated
148 clear_config([:rate_limit, limiter_name], [{1000, 5}, {1, 10}])
149 clear_config([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8})
150
151 plug_opts = RateLimiter.init(name: limiter_name)
152
153 conn = %{build_conn(:get, "/") | remote_ip: {127, 0, 0, 2}}
154 conn_2 = %{build_conn(:get, "/") | remote_ip: {127, 0, 0, 3}}
155
156 for i <- 1..5 do
157 conn = RateLimiter.call(conn, plug_opts)
158 assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts)
159 refute conn.halted
160 end
161
162 conn = RateLimiter.call(conn, plug_opts)
163
164 assert %{"error" => "Throttled"} = ConnTest.json_response(conn, :too_many_requests)
165 assert conn.halted
166
167 conn_2 = RateLimiter.call(conn_2, plug_opts)
168 assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, plug_opts)
169
170 refute conn_2.status == Conn.Status.code(:too_many_requests)
171 refute conn_2.resp_body
172 refute conn_2.halted
173 end
174 end
175
176 describe "authenticated users" do
177 setup do
178 Ecto.Adapters.SQL.Sandbox.checkout(Pleroma.Repo)
179
180 :ok
181 end
182
183 @tag :erratic
184 test "can have limits separate from unauthenticated connections" do
185 limiter_name = :test_authenticated1
186
187 scale = 50
188 limit = 5
189 clear_config([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8})
190 clear_config([:rate_limit, limiter_name], [{1000, 1}, {scale, limit}])
191
192 plug_opts = RateLimiter.init(name: limiter_name)
193
194 user = insert(:user)
195 conn = build_conn(:get, "/") |> assign(:user, user)
196
197 for i <- 1..5 do
198 conn = RateLimiter.call(conn, plug_opts)
199 assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts)
200 refute conn.halted
201 end
202
203 conn = RateLimiter.call(conn, plug_opts)
204
205 assert %{"error" => "Throttled"} = ConnTest.json_response(conn, :too_many_requests)
206 assert conn.halted
207 end
208
209 @tag :erratic
210 test "different users are counted independently" do
211 limiter_name = :test_authenticated2
212 clear_config([:rate_limit, limiter_name], [{1, 10}, {1000, 5}])
213 clear_config([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8})
214
215 plug_opts = RateLimiter.init(name: limiter_name)
216
217 user = insert(:user)
218 conn = build_conn(:get, "/") |> assign(:user, user)
219
220 user_2 = insert(:user)
221 conn_2 = build_conn(:get, "/") |> assign(:user, user_2)
222
223 for i <- 1..5 do
224 conn = RateLimiter.call(conn, plug_opts)
225 assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts)
226 end
227
228 conn = RateLimiter.call(conn, plug_opts)
229 assert %{"error" => "Throttled"} = ConnTest.json_response(conn, :too_many_requests)
230 assert conn.halted
231
232 conn_2 = RateLimiter.call(conn_2, plug_opts)
233 assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, plug_opts)
234 refute conn_2.status == Conn.Status.code(:too_many_requests)
235 refute conn_2.resp_body
236 refute conn_2.halted
237 end
238 end
239
240 test "doesn't crash due to a race condition when multiple requests are made at the same time and the bucket is not yet initialized" do
241 limiter_name = :test_race_condition
242 clear_config([:rate_limit, limiter_name], {1000, 5})
243 clear_config([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8})
244
245 opts = RateLimiter.init(name: limiter_name)
246
247 conn = build_conn(:get, "/")
248 conn_2 = build_conn(:get, "/")
249
250 %Task{pid: pid1} =
251 task1 =
252 Task.async(fn ->
253 receive do
254 :process2_up ->
255 RateLimiter.call(conn, opts)
256 end
257 end)
258
259 task2 =
260 Task.async(fn ->
261 send(pid1, :process2_up)
262 RateLimiter.call(conn_2, opts)
263 end)
264
265 Task.await(task1)
266 Task.await(task2)
267
268 refute {:err, :not_found} == RateLimiter.inspect_bucket(conn, limiter_name, opts)
269 end
270
271 def expire_ttl(%{remote_ip: remote_ip} = _conn, bucket_name_root) do
272 bucket_name = "anon:#{bucket_name_root}" |> String.to_atom()
273 key_name = "ip::#{remote_ip |> Tuple.to_list() |> Enum.join(".")}"
274
275 {:ok, bucket_value} = Cachex.get(bucket_name, key_name)
276 Cachex.put(bucket_name, key_name, bucket_value, ttl: -1)
277 end
278 end