rate limiter: disable based on if remote ip was found, not on if the plug was enabled
authorrinpatch <rinpatch@sdf.org>
Fri, 13 Mar 2020 18:15:42 +0000 (21:15 +0300)
committerrinpatch <rinpatch@sdf.org>
Sun, 15 Mar 2020 21:15:21 +0000 (00:15 +0300)
The current rate limiter disable logic won't trigger when the remote ip
is not forwarded, only when the remoteip plug is not enabled, which is
not the case on most instances since it's enabled by default. This
changes the behavior to warn and disable  when the remote ip was not forwarded,
even if the RemoteIP plug is enabled.

Also closes #1620

config/test.exs
lib/pleroma/plugs/rate_limiter/rate_limiter.ex
lib/pleroma/plugs/remote_ip.ex
test/plugs/rate_limiter_test.exs
test/web/mastodon_api/controllers/account_controller_test.exs

index a17886265e44de5d938115601a906d825c9e832b..b8ea63c9435f785321e943e47ff455a587f43287 100644 (file)
@@ -92,6 +92,8 @@ config :pleroma, :modules, runtime_dir: "test/fixtures/modules"
 
 config :pleroma, Pleroma.Emails.NewUsersDigestEmail, enabled: true
 
+config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false
+
 if File.exists?("./config/test.secret.exs") do
   import_config "test.secret.exs"
 else
index c3f6351c87cafe5ae60dfc7278a259b9274f5eaf..1529da7174a4cec57d3ab32b28a2641bf53acb7a 100644 (file)
@@ -78,7 +78,7 @@ defmodule Pleroma.Plugs.RateLimiter do
   end
 
   def call(conn, plug_opts) do
-    if disabled?() do
+    if disabled?(conn) do
       handle_disabled(conn)
     else
       action_settings = action_settings(plug_opts)
@@ -87,9 +87,9 @@ defmodule Pleroma.Plugs.RateLimiter do
   end
 
   defp handle_disabled(conn) do
-    if Config.get(:env) == :prod do
-      Logger.warn("Rate limiter is disabled for localhost/socket")
-    end
+    Logger.warn(
+      "Rate limiter disabled due to forwarded IP not being found. Please ensure your reverse proxy is providing the X-Forwarded-For header or disable the RemoteIP plug/rate limiter."
+    )
 
     conn
   end
@@ -109,16 +109,21 @@ defmodule Pleroma.Plugs.RateLimiter do
     end
   end
 
-  def disabled? do
+  def disabled?(conn) do
     localhost_or_socket =
-      Config.get([Pleroma.Web.Endpoint, :http, :ip])
-      |> Tuple.to_list()
-      |> Enum.join(".")
-      |> String.match?(~r/^local|^127.0.0.1/)
+      case Config.get([Pleroma.Web.Endpoint, :http, :ip]) do
+        {127, 0, 0, 1} -> true
+        {0, 0, 0, 0, 0, 0, 0, 1} -> true
+        {:local, _} -> true
+        _ -> false
+      end
 
-    remote_ip_disabled = not Config.get([Pleroma.Plugs.RemoteIp, :enabled])
+    remote_ip_not_found =
+      if Map.has_key?(conn.assigns, :remote_ip_found),
+        do: !conn.assigns.remote_ip_found,
+        else: false
 
-    localhost_or_socket and remote_ip_disabled
+    localhost_or_socket and remote_ip_not_found
   end
 
   @inspect_bucket_not_found {:error, :not_found}
index 2eca4f8f669789491870a8d473e79edfabb10a1f..0ac9050d0764149e2361811016179f1282b20c82 100644 (file)
@@ -7,6 +7,8 @@ defmodule Pleroma.Plugs.RemoteIp do
   This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.
   """
 
+  import Plug.Conn
+
   @behaviour Plug
 
   @headers ~w[
@@ -26,11 +28,12 @@ defmodule Pleroma.Plugs.RemoteIp do
 
   def init(_), do: nil
 
-  def call(conn, _) do
+  def call(%{remote_ip: original_remote_ip} = conn, _) do
     config = Pleroma.Config.get(__MODULE__, [])
 
     if Keyword.get(config, :enabled, false) do
-      RemoteIp.call(conn, remote_ip_opts(config))
+      %{remote_ip: new_remote_ip} = conn = RemoteIp.call(conn, remote_ip_opts(config))
+      assign(conn, :remote_ip_found, original_remote_ip != new_remote_ip)
     else
       conn
     end
index 8023271e468cc4d3854acd02f2baf66d27b402ce..81e2009c8f4490c96062af3230c4e19a1aa34ad4 100644 (file)
@@ -3,8 +3,7 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.Plugs.RateLimiterTest do
-  use ExUnit.Case, async: true
-  use Plug.Test
+  use Pleroma.Web.ConnCase
 
   alias Pleroma.Config
   alias Pleroma.Plugs.RateLimiter
@@ -36,63 +35,44 @@ defmodule Pleroma.Plugs.RateLimiterTest do
                |> RateLimiter.init()
                |> RateLimiter.action_settings()
     end
+  end
 
-    test "it is disabled for localhost" do
-      Config.put([:rate_limit, @limiter_name], {1, 1})
-      Config.put([Pleroma.Web.Endpoint, :http, :ip], {127, 0, 0, 1})
-      Config.put([Pleroma.Plugs.RemoteIp, :enabled], false)
-
-      assert RateLimiter.disabled?() == true
-    end
+  test "it is disabled if it remote ip plug is enabled but no remote ip is found" do
+    Config.put([Pleroma.Web.Endpoint, :http, :ip], {127, 0, 0, 1})
+    assert RateLimiter.disabled?(Plug.Conn.assign(build_conn(), :remote_ip_found, false))
+  end
 
-    test "it is disabled for socket" do
-      Config.put([:rate_limit, @limiter_name], {1, 1})
-      Config.put([Pleroma.Web.Endpoint, :http, :ip], {:local, "/path/to/pleroma.sock"})
-      Config.put([Pleroma.Plugs.RemoteIp, :enabled], false)
+  test "it restricts based on config values" do
+    limiter_name = :test_plug_opts
+    scale = 80
+    limit = 5
 
-      assert RateLimiter.disabled?() == true
-    end
+    Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8})
+    Config.put([:rate_limit, limiter_name], {scale, limit})
 
-    test "it is enabled for socket when remote ip is enabled" do
-      Config.put([:rate_limit, @limiter_name], {1, 1})
-      Config.put([Pleroma.Web.Endpoint, :http, :ip], {:local, "/path/to/pleroma.sock"})
-      Config.put([Pleroma.Plugs.RemoteIp, :enabled], true)
+    plug_opts = RateLimiter.init(name: limiter_name)
+    conn = conn(:get, "/")
 
-      assert RateLimiter.disabled?() == false
+    for i <- 1..5 do
+      conn = RateLimiter.call(conn, plug_opts)
+      assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts)
+      Process.sleep(10)
     end
 
-    test "it restricts based on config values" do
-      limiter_name = :test_plug_opts
-      scale = 80
-      limit = 5
-
-      Config.put([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8})
-      Config.put([:rate_limit, limiter_name], {scale, limit})
-
-      plug_opts = RateLimiter.init(name: limiter_name)
-      conn = conn(:get, "/")
-
-      for i <- 1..5 do
-        conn = RateLimiter.call(conn, plug_opts)
-        assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts)
-        Process.sleep(10)
-      end
+    conn = RateLimiter.call(conn, plug_opts)
+    assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
+    assert conn.halted
 
-      conn = RateLimiter.call(conn, plug_opts)
-      assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
-      assert conn.halted
+    Process.sleep(50)
 
-      Process.sleep(50)
+    conn = conn(:get, "/")
 
-      conn = conn(:get, "/")
+    conn = RateLimiter.call(conn, plug_opts)
+    assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts)
 
-      conn = RateLimiter.call(conn, plug_opts)
-      assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts)
-
-      refute conn.status == Plug.Conn.Status.code(:too_many_requests)
-      refute conn.resp_body
-      refute conn.halted
-    end
+    refute conn.status == Plug.Conn.Status.code(:too_many_requests)
+    refute conn.resp_body
+    refute conn.halted
   end
 
   describe "options" do
index 7f7d8cea383a11dab20ddfe5cd8713b4e793a622..7efccd9c4faca58f9c248d60374fcad8c8ec5283 100644 (file)
@@ -756,10 +756,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
   end
 
   describe "create account by app / rate limit" do
-    clear_config([Pleroma.Plugs.RemoteIp, :enabled]) do
-      Pleroma.Config.put([Pleroma.Plugs.RemoteIp, :enabled], true)
-    end
-
     clear_config([:rate_limit, :app_account_creation]) do
       Pleroma.Config.put([:rate_limit, :app_account_creation], {10_000, 2})
     end