move to 2.5.5
[anni] / test / pleroma / web / plugs / rate_limiter_test.exs
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 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