1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.Plugs.RateLimiterTest do
6 use Pleroma.Web.ConnCase
9 alias Pleroma.Web.Plugs.RateLimiter
12 import Pleroma.Factory
13 import Pleroma.Tests.Helpers, only: [clear_config: 1, clear_config: 2]
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)
20 @limiter_name :test_init
21 setup do: clear_config([Pleroma.Web.Plugs.RemoteIp, :enabled])
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})
27 assert %{limits: {1, 1}, name: :test_init, opts: [name: :test_init]} ==
30 |> RateLimiter.action_settings()
33 [name: :nonexisting_limiter]
35 |> RateLimiter.action_settings()
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))
43 test "it is enabled if remote ip found" do
44 refute RateLimiter.disabled?(Conn.assign(build_conn(), :remote_ip_found, true))
47 test "it is enabled if remote_ip_found flag doesn't exist" do
48 refute RateLimiter.disabled?(build_conn())
51 test "it restricts based on config values" do
52 limiter_name = :test_plug_opts
56 clear_config([Pleroma.Web.Endpoint, :http, :ip], {127, 0, 0, 1})
57 clear_config([:rate_limit, limiter_name], {scale, limit})
59 plug_opts = RateLimiter.init(name: limiter_name)
60 conn = build_conn(:get, "/")
63 conn_limited = RateLimiter.call(conn, plug_opts)
65 refute conn_limited.status == Conn.Status.code(:too_many_requests)
66 refute conn_limited.resp_body
67 refute conn_limited.halted
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
74 expire_ttl(conn, limiter_name)
77 conn_limited = RateLimiter.call(conn, plug_opts)
79 refute conn_limited.status == Conn.Status.code(:too_many_requests)
80 refute conn_limited.resp_body
81 refute conn_limited.halted
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
90 test "`bucket_name` option overrides default bucket name" do
91 limiter_name = :test_bucket_name
93 clear_config([:rate_limit, limiter_name], {1000, 5})
94 clear_config([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8})
96 base_bucket_name = "#{limiter_name}:group1"
97 plug_opts = RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name)
99 conn = build_conn(:get, "/")
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)
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})
111 plug_opts = RateLimiter.init(name: limiter_name, params: ["id"])
113 conn = build_conn(:get, "/?id=1")
114 conn = Conn.fetch_query_params(conn)
115 conn_2 = build_conn(:get, "/?id=2")
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)
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})
127 base_bucket_name = "#{limiter_name}:group1"
130 RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name, params: ["id"])
134 conn = build_conn(:get, "/?id=#{id}")
135 conn = Conn.fetch_query_params(conn)
136 conn_2 = build_conn(:get, "/?id=#{101}")
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)
144 describe "unauthenticated users" do
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})
151 plug_opts = RateLimiter.init(name: limiter_name)
153 conn = %{build_conn(:get, "/") | remote_ip: {127, 0, 0, 2}}
154 conn_2 = %{build_conn(:get, "/") | remote_ip: {127, 0, 0, 3}}
157 conn = RateLimiter.call(conn, plug_opts)
158 assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts)
162 conn = RateLimiter.call(conn, plug_opts)
164 assert %{"error" => "Throttled"} = ConnTest.json_response(conn, :too_many_requests)
167 conn_2 = RateLimiter.call(conn_2, plug_opts)
168 assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, plug_opts)
170 refute conn_2.status == Conn.Status.code(:too_many_requests)
171 refute conn_2.resp_body
176 describe "authenticated users" do
178 Ecto.Adapters.SQL.Sandbox.checkout(Pleroma.Repo)
184 test "can have limits separate from unauthenticated connections" do
185 limiter_name = :test_authenticated1
189 clear_config([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8})
190 clear_config([:rate_limit, limiter_name], [{1000, 1}, {scale, limit}])
192 plug_opts = RateLimiter.init(name: limiter_name)
195 conn = build_conn(:get, "/") |> assign(:user, user)
198 conn = RateLimiter.call(conn, plug_opts)
199 assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts)
203 conn = RateLimiter.call(conn, plug_opts)
205 assert %{"error" => "Throttled"} = ConnTest.json_response(conn, :too_many_requests)
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})
215 plug_opts = RateLimiter.init(name: limiter_name)
218 conn = build_conn(:get, "/") |> assign(:user, user)
220 user_2 = insert(:user)
221 conn_2 = build_conn(:get, "/") |> assign(:user, user_2)
224 conn = RateLimiter.call(conn, plug_opts)
225 assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts)
228 conn = RateLimiter.call(conn, plug_opts)
229 assert %{"error" => "Throttled"} = ConnTest.json_response(conn, :too_many_requests)
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
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})
245 opts = RateLimiter.init(name: limiter_name)
247 conn = build_conn(:get, "/")
248 conn_2 = build_conn(:get, "/")
255 RateLimiter.call(conn, opts)
261 send(pid1, :process2_up)
262 RateLimiter.call(conn_2, opts)
268 refute {:err, :not_found} == RateLimiter.inspect_bucket(conn, limiter_name, opts)
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(".")}"
275 {:ok, bucket_value} = Cachex.get(bucket_name, key_name)
276 Cachex.put(bucket_name, key_name, bucket_value, ttl: -1)