New rate limiter
authorSteven Fuchs <steven.fuchs@dockyard.com>
Mon, 11 Nov 2019 12:13:06 +0000 (12:13 +0000)
committerlain <lain@soykaf.club>
Mon, 11 Nov 2019 12:13:06 +0000 (12:13 +0000)
16 files changed:
lib/pleroma/application.ex
lib/pleroma/plugs/rate_limiter.ex [deleted file]
lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex [new file with mode: 0644]
lib/pleroma/plugs/rate_limiter/rate_limiter.ex [new file with mode: 0644]
lib/pleroma/plugs/rate_limiter/supervisor.ex [new file with mode: 0644]
lib/pleroma/web/mastodon_api/controllers/account_controller.ex
lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
lib/pleroma/web/mastodon_api/controllers/search_controller.ex
lib/pleroma/web/mastodon_api/controllers/status_controller.ex
lib/pleroma/web/mongooseim/mongoose_im_controller.ex
lib/pleroma/web/oauth/oauth_controller.ex
lib/pleroma/web/ostatus/ostatus_controller.ex
lib/pleroma/web/pleroma_api/controllers/account_controller.ex
mix.exs
mix.lock
test/plugs/rate_limiter_test.exs

index d681eecc80f95920577e6c81263800c6e3d89d53..2b6a55f98fda23c120db6e170374a633fa92b1c3 100644 (file)
@@ -36,7 +36,8 @@ defmodule Pleroma.Application do
         Pleroma.Emoji,
         Pleroma.Captcha,
         Pleroma.Daemons.ScheduledActivityDaemon,
-        Pleroma.Daemons.ActivityExpirationDaemon
+        Pleroma.Daemons.ActivityExpirationDaemon,
+        Pleroma.Plugs.RateLimiter.Supervisor
       ] ++
         cachex_children() ++
         hackney_pool_children() ++
diff --git a/lib/pleroma/plugs/rate_limiter.ex b/lib/pleroma/plugs/rate_limiter.ex
deleted file mode 100644 (file)
index 31388f5..0000000
+++ /dev/null
@@ -1,131 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Plugs.RateLimiter do
-  @moduledoc """
-
-  ## Configuration
-
-  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:
-
-  * The first element: `scale` (Integer). The time scale in milliseconds.
-  * The second element: `limit` (Integer). How many requests to limit in the time scale provided.
-
-  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.
-
-  To disable a limiter set its value to `nil`.
-
-  ### Example
-
-      config :pleroma, :rate_limit,
-        one: {1000, 10},
-        two: [{10_000, 10}, {10_000, 50}],
-        foobar: nil
-
-  Here we have three limiters:
-
-  * `one` which is not over 10req/1s
-  * `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users
-  * `foobar` which is disabled
-
-  ## Usage
-
-  AllowedSyntax:
-
-      plug(Pleroma.Plugs.RateLimiter, :limiter_name)
-      plug(Pleroma.Plugs.RateLimiter, {:limiter_name, options})
-
-  Allowed options:
-
-      * `bucket_name` overrides bucket name (e.g. to have a separate limit for a set of actions)
-      * `params` appends values of specified request params (e.g. ["id"]) to bucket name
-
-  Inside a controller:
-
-      plug(Pleroma.Plugs.RateLimiter, :one when action == :one)
-      plug(Pleroma.Plugs.RateLimiter, :two when action in [:two, :three])
-
-      plug(
-        Pleroma.Plugs.RateLimiter,
-        {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
-        when action in ~w(fav_status unfav_status)a
-      )
-
-  or inside a router pipeline:
-
-      pipeline :api do
-        ...
-        plug(Pleroma.Plugs.RateLimiter, :one)
-        ...
-      end
-  """
-  import Pleroma.Web.TranslationHelpers
-  import Plug.Conn
-
-  alias Pleroma.User
-
-  def init(limiter_name) when is_atom(limiter_name) do
-    init({limiter_name, []})
-  end
-
-  def init({limiter_name, opts}) do
-    case Pleroma.Config.get([:rate_limit, limiter_name]) do
-      nil -> nil
-      config -> {limiter_name, config, opts}
-    end
-  end
-
-  # Do not limit if there is no limiter configuration
-  def call(conn, nil), do: conn
-
-  def call(conn, settings) do
-    case check_rate(conn, settings) do
-      {:ok, _count} ->
-        conn
-
-      {:error, _count} ->
-        render_throttled_error(conn)
-    end
-  end
-
-  defp bucket_name(conn, limiter_name, opts) do
-    bucket_name = opts[:bucket_name] || limiter_name
-
-    if params_names = opts[:params] do
-      params_values = for p <- Enum.sort(params_names), do: conn.params[p]
-      Enum.join([bucket_name] ++ params_values, ":")
-    else
-      bucket_name
-    end
-  end
-
-  defp check_rate(
-         %{assigns: %{user: %User{id: user_id}}} = conn,
-         {limiter_name, [_, {scale, limit}], opts}
-       ) do
-    bucket_name = bucket_name(conn, limiter_name, opts)
-    ExRated.check_rate("#{bucket_name}:#{user_id}", scale, limit)
-  end
-
-  defp check_rate(conn, {limiter_name, [{scale, limit} | _], opts}) do
-    bucket_name = bucket_name(conn, limiter_name, opts)
-    ExRated.check_rate("#{bucket_name}:#{ip(conn)}", scale, limit)
-  end
-
-  defp check_rate(conn, {limiter_name, {scale, limit}, opts}) do
-    check_rate(conn, {limiter_name, [{scale, limit}, {scale, limit}], opts})
-  end
-
-  def ip(%{remote_ip: remote_ip}) do
-    remote_ip
-    |> Tuple.to_list()
-    |> Enum.join(".")
-  end
-
-  defp render_throttled_error(conn) do
-    conn
-    |> render_error(:too_many_requests, "Throttled")
-    |> halt()
-  end
-end
diff --git a/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex b/lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex
new file mode 100644 (file)
index 0000000..187582e
--- /dev/null
@@ -0,0 +1,44 @@
+defmodule Pleroma.Plugs.RateLimiter.LimiterSupervisor do
+  use DynamicSupervisor
+
+  import Cachex.Spec
+
+  def start_link(init_arg) do
+    DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
+  end
+
+  def add_limiter(limiter_name, expiration) do
+    {:ok, _pid} =
+      DynamicSupervisor.start_child(
+        __MODULE__,
+        %{
+          id: String.to_atom("rl_#{limiter_name}"),
+          start:
+            {Cachex, :start_link,
+             [
+               limiter_name,
+               [
+                 expiration:
+                   expiration(
+                     default: expiration,
+                     interval: check_interval(expiration),
+                     lazy: true
+                   )
+               ]
+             ]}
+        }
+      )
+  end
+
+  @impl true
+  def init(_init_arg) do
+    DynamicSupervisor.init(strategy: :one_for_one)
+  end
+
+  defp check_interval(exp) do
+    (exp / 2)
+    |> Kernel.trunc()
+    |> Kernel.min(5000)
+    |> Kernel.max(1)
+  end
+end
diff --git a/lib/pleroma/plugs/rate_limiter/rate_limiter.ex b/lib/pleroma/plugs/rate_limiter/rate_limiter.ex
new file mode 100644 (file)
index 0000000..d720508
--- /dev/null
@@ -0,0 +1,227 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Plugs.RateLimiter do
+  @moduledoc """
+
+  ## Configuration
+
+  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:
+
+  * The first element: `scale` (Integer). The time scale in milliseconds.
+  * The second element: `limit` (Integer). How many requests to limit in the time scale provided.
+
+  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.
+
+  To disable a limiter set its value to `nil`.
+
+  ### Example
+
+      config :pleroma, :rate_limit,
+        one: {1000, 10},
+        two: [{10_000, 10}, {10_000, 50}],
+        foobar: nil
+
+  Here we have three limiters:
+
+  * `one` which is not over 10req/1s
+  * `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users
+  * `foobar` which is disabled
+
+  ## Usage
+
+  AllowedSyntax:
+
+      plug(Pleroma.Plugs.RateLimiter, name: :limiter_name)
+      plug(Pleroma.Plugs.RateLimiter, options)   # :name is a required option
+
+  Allowed options:
+
+      * `name` required, always used to fetch the limit values from the config
+      * `bucket_name` overrides name for counting purposes (e.g. to have a separate limit for a set of actions)
+      * `params` appends values of specified request params (e.g. ["id"]) to bucket name
+
+  Inside a controller:
+
+      plug(Pleroma.Plugs.RateLimiter, [name: :one] when action == :one)
+      plug(Pleroma.Plugs.RateLimiter, [name: :two] when action in [:two, :three])
+
+      plug(
+        Pleroma.Plugs.RateLimiter,
+        [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]]
+        when action in ~w(fav_status unfav_status)a
+      )
+
+  or inside a router pipeline:
+
+      pipeline :api do
+        ...
+        plug(Pleroma.Plugs.RateLimiter, name: :one)
+        ...
+      end
+  """
+  import Pleroma.Web.TranslationHelpers
+  import Plug.Conn
+
+  alias Pleroma.Plugs.RateLimiter.LimiterSupervisor
+  alias Pleroma.User
+
+  def init(opts) do
+    limiter_name = Keyword.get(opts, :name)
+
+    case Pleroma.Config.get([:rate_limit, limiter_name]) do
+      nil ->
+        nil
+
+      config ->
+        name_root = Keyword.get(opts, :bucket_name, limiter_name)
+
+        %{
+          name: name_root,
+          limits: config,
+          opts: opts
+        }
+    end
+  end
+
+  # Do not limit if there is no limiter configuration
+  def call(conn, nil), do: conn
+
+  def call(conn, settings) do
+    settings
+    |> incorporate_conn_info(conn)
+    |> check_rate()
+    |> case do
+      {:ok, _count} ->
+        conn
+
+      {:error, _count} ->
+        render_throttled_error(conn)
+    end
+  end
+
+  def inspect_bucket(conn, name_root, settings) do
+    settings =
+      settings
+      |> incorporate_conn_info(conn)
+
+    bucket_name = make_bucket_name(%{settings | name: name_root})
+    key_name = make_key_name(settings)
+    limit = get_limits(settings)
+
+    case Cachex.get(bucket_name, key_name) do
+      {:error, :no_cache} ->
+        {:err, :not_found}
+
+      {:ok, nil} ->
+        {0, limit}
+
+      {:ok, value} ->
+        {value, limit - value}
+    end
+  end
+
+  defp check_rate(settings) do
+    bucket_name = make_bucket_name(settings)
+    key_name = make_key_name(settings)
+    limit = get_limits(settings)
+
+    case Cachex.get_and_update(bucket_name, key_name, &increment_value(&1, limit)) do
+      {:commit, value} ->
+        {:ok, value}
+
+      {:ignore, value} ->
+        {:error, value}
+
+      {:error, :no_cache} ->
+        initialize_buckets(settings)
+        check_rate(settings)
+    end
+  end
+
+  defp increment_value(nil, _limit), do: {:commit, 1}
+
+  defp increment_value(val, limit) when val >= limit, do: {:ignore, val}
+
+  defp increment_value(val, _limit), do: {:commit, val + 1}
+
+  defp incorporate_conn_info(settings, %{assigns: %{user: %User{id: user_id}}, params: params}) do
+    Map.merge(settings, %{
+      mode: :user,
+      conn_params: params,
+      conn_info: "#{user_id}"
+    })
+  end
+
+  defp incorporate_conn_info(settings, %{params: params} = conn) do
+    Map.merge(settings, %{
+      mode: :anon,
+      conn_params: params,
+      conn_info: "#{ip(conn)}"
+    })
+  end
+
+  defp ip(%{remote_ip: remote_ip}) do
+    remote_ip
+    |> Tuple.to_list()
+    |> Enum.join(".")
+  end
+
+  defp render_throttled_error(conn) do
+    conn
+    |> render_error(:too_many_requests, "Throttled")
+    |> halt()
+  end
+
+  defp make_key_name(settings) do
+    ""
+    |> attach_params(settings)
+    |> attach_identity(settings)
+  end
+
+  defp get_scale(_, {scale, _}), do: scale
+
+  defp get_scale(:anon, [{scale, _}, {_, _}]), do: scale
+
+  defp get_scale(:user, [{_, _}, {scale, _}]), do: scale
+
+  defp get_limits(%{limits: {_scale, limit}}), do: limit
+
+  defp get_limits(%{mode: :user, limits: [_, {_, limit}]}), do: limit
+
+  defp get_limits(%{limits: [{_, limit}, _]}), do: limit
+
+  defp make_bucket_name(%{mode: :user, name: name_root}),
+    do: user_bucket_name(name_root)
+
+  defp make_bucket_name(%{mode: :anon, name: name_root}),
+    do: anon_bucket_name(name_root)
+
+  defp attach_params(input, %{conn_params: conn_params, opts: opts}) do
+    param_string =
+      opts
+      |> Keyword.get(:params, [])
+      |> Enum.sort()
+      |> Enum.map(&Map.get(conn_params, &1, ""))
+      |> Enum.join(":")
+
+    "#{input}#{param_string}"
+  end
+
+  defp initialize_buckets(%{name: _name, limits: nil}), do: :ok
+
+  defp initialize_buckets(%{name: name, limits: limits}) do
+    LimiterSupervisor.add_limiter(anon_bucket_name(name), get_scale(:anon, limits))
+    LimiterSupervisor.add_limiter(user_bucket_name(name), get_scale(:user, limits))
+  end
+
+  defp attach_identity(base, %{mode: :user, conn_info: conn_info}),
+    do: "user:#{base}:#{conn_info}"
+
+  defp attach_identity(base, %{mode: :anon, conn_info: conn_info}),
+    do: "ip:#{base}:#{conn_info}"
+
+  defp user_bucket_name(name_root), do: "user:#{name_root}" |> String.to_atom()
+  defp anon_bucket_name(name_root), do: "anon:#{name_root}" |> String.to_atom()
+end
diff --git a/lib/pleroma/plugs/rate_limiter/supervisor.ex b/lib/pleroma/plugs/rate_limiter/supervisor.ex
new file mode 100644 (file)
index 0000000..9672f78
--- /dev/null
@@ -0,0 +1,16 @@
+defmodule Pleroma.Plugs.RateLimiter.Supervisor do
+  use Supervisor
+
+  def start_link(opts) do
+    Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
+  end
+
+  def init(_args) do
+    children = [
+      Pleroma.Plugs.RateLimiter.LimiterSupervisor
+    ]
+
+    opts = [strategy: :one_for_one, name: Pleroma.Web.Streamer.Supervisor]
+    Supervisor.init(children, opts)
+  end
+end
index 73fad519ecbe1259f3ab0782beaddbf818943bcc..5b01b964b8a52fb85aa05a60de590df627a9f87a 100644 (file)
@@ -66,9 +66,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
   @relations [:follow, :unfollow]
   @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
 
-  plug(RateLimiter, {:relations_id_action, params: ["id", "uri"]} when action in @relations)
-  plug(RateLimiter, :relations_actions when action in @relations)
-  plug(RateLimiter, :app_account_creation when action == :create)
+  plug(RateLimiter, [name: :relations_id_action, params: ["id", "uri"]] when action in @relations)
+  plug(RateLimiter, [name: :relations_actions] when action in @relations)
+  plug(RateLimiter, [name: :app_account_creation] when action == :create)
   plug(:assign_account_by_id when action in @needs_account)
 
   action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
index bfd5120ba4bfb2ffa38cf2827c3ade35d5cced20..d9e51de7f27757c54d2ea2701669bec692052517 100644 (file)
@@ -15,7 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
 
   @local_mastodon_name "Mastodon-Local"
 
-  plug(Pleroma.Plugs.RateLimiter, :password_reset when action == :password_reset)
+  plug(Pleroma.Plugs.RateLimiter, [name: :password_reset] when action == :password_reset)
 
   @doc "GET /web/login"
   def login(%{assigns: %{user: %User{}}} = conn, _params) do
index 6cfd68a84b0d60e16fd2c3bbb6a0b42ee7e5f0ff..0a929f55b9137aa87ff57fdbfab750a68cec7c59 100644 (file)
@@ -22,7 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
 
   plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
 
-  plug(RateLimiter, :search when action in [:search, :search2, :account_search])
+  plug(RateLimiter, [name: :search] when action in [:search, :search2, :account_search])
 
   def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
     accounts = User.search(query, search_options(params, user))
index e5d016f63711dc0f899172448ddbef138a5f4cbb..74b223cf4efcfa1771c999c364bf80bcc808aae0 100644 (file)
@@ -82,17 +82,17 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
 
   plug(
     RateLimiter,
-    {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
+    [name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]]
     when action in ~w(reblog unreblog)a
   )
 
   plug(
     RateLimiter,
-    {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
+    [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]]
     when action in ~w(favourite unfavourite)a
   )
 
-  plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
+  plug(RateLimiter, [name: :statuses_actions] when action in @rate_limited_status_actions)
 
   action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
 
index 6ed181cffb78e43db51ba626837e482a086b10f0..358600e7d1ff34c0bc308fb80f4c831b6e10a459 100644 (file)
@@ -10,8 +10,8 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do
   alias Pleroma.Repo
   alias Pleroma.User
 
-  plug(RateLimiter, :authentication when action in [:user_exists, :check_password])
-  plug(RateLimiter, {:authentication, params: ["user"]} when action == :check_password)
+  plug(RateLimiter, [name: :authentication] when action in [:user_exists, :check_password])
+  plug(RateLimiter, [name: :authentication, params: ["user"]] when action == :check_password)
 
   def user_exists(conn, %{"user" => username}) do
     with %User{} <- Repo.get_by(User, nickname: username, local: true) do
index fe71aca8cea95f26f5523a439674ce620aa30cbd..1b1394787fdcf1be51e30aecf194e759f20abc3c 100644 (file)
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
   use Pleroma.Web, :controller
 
   alias Pleroma.Helpers.UriHelper
+  alias Pleroma.Plugs.RateLimiter
   alias Pleroma.Registration
   alias Pleroma.Repo
   alias Pleroma.User
@@ -24,7 +25,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
 
   plug(:fetch_session)
   plug(:fetch_flash)
-  plug(Pleroma.Plugs.RateLimiter, :authentication when action == :create_authorization)
+  plug(RateLimiter, [name: :authentication] when action == :create_authorization)
 
   action_fallback(Pleroma.Web.OAuth.FallbackController)
 
index 6958519de05d95acfadeecd749af7a52ec82d778..12a7c2365446e8ba67754b3de17af8a25bc14cd7 100644 (file)
@@ -8,6 +8,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
   alias Fallback.RedirectController
   alias Pleroma.Activity
   alias Pleroma.Object
+  alias Pleroma.Plugs.RateLimiter
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.ActivityPubController
   alias Pleroma.Web.ActivityPub.ObjectView
@@ -17,8 +18,8 @@ defmodule Pleroma.Web.OStatus.OStatusController do
   alias Pleroma.Web.Router
 
   plug(
-    Pleroma.Plugs.RateLimiter,
-    {:ap_routes, params: ["uuid"]} when action in [:object, :activity]
+    RateLimiter,
+    [name: :ap_routes, params: ["uuid"]] when action in [:object, :activity]
   )
 
   plug(
index db6faac835d2c4166e362cb1289db91127b59b98..bc2f1017c83fb52f9a3b5e1e025f835d9c2fcc99 100644 (file)
@@ -42,7 +42,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
     when action != :confirmation_resend
   )
 
-  plug(RateLimiter, :account_confirmation_resend when action == :confirmation_resend)
+  plug(RateLimiter, [name: :account_confirmation_resend] when action == :confirmation_resend)
   plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe])
   plug(:put_view, Pleroma.Web.MastodonAPI.AccountView)
 
diff --git a/mix.exs b/mix.exs
index dd7c7e979b3e03fa4376fb440b81b9437d808482..81ce4f25cef74d619776b7a7b6b18a569ff014f8 100644 (file)
--- a/mix.exs
+++ b/mix.exs
@@ -155,7 +155,6 @@ defmodule Pleroma.Mixfile do
       {:joken, "~> 2.0"},
       {:benchee, "~> 1.0"},
       {:esshd, "~> 0.1.0", runtime: Application.get_env(:esshd, :enabled, false)},
-      {:ex_rated, "~> 1.3"},
       {:ex_const, "~> 0.2"},
       {:plug_static_index_html, "~> 1.0.0"},
       {:excoveralls, "~> 0.11.1", only: :test},
index 5b471fe3dbfdb9f1d17807269224da2c35ec3b85..d4a80df77ccce408db16e0806333bfb24258bb6f 100644 (file)
--- a/mix.lock
+++ b/mix.lock
@@ -33,7 +33,6 @@
   "ex_const": {:hex, :ex_const, "0.2.4", "d06e540c9d834865b012a17407761455efa71d0ce91e5831e86881b9c9d82448", [:mix], [], "hexpm"},
   "ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
   "ex_machina": {:hex, :ex_machina, "2.3.0", "92a5ad0a8b10ea6314b876a99c8c9e3f25f4dde71a2a835845b136b9adaf199a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm"},
-  "ex_rated": {:hex, :ex_rated, "1.3.3", "30ecbdabe91f7eaa9d37fa4e81c85ba420f371babeb9d1910adbcd79ec798d27", [:mix], [{:ex2ms, "~> 1.5", [hex: :ex2ms, repo: "hexpm", optional: false]}], "hexpm"},
   "ex_syslogger": {:git, "https://github.com/slashmili/ex_syslogger.git", "f3963399047af17e038897c69e20d552e6899e1d", [tag: "1.4.0"]},
   "excoveralls": {:hex, :excoveralls, "0.11.2", "0c6f2c8db7683b0caa9d490fb8125709c54580b4255ffa7ad35f3264b075a643", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
   "fast_html": {:hex, :fast_html, "0.99.3", "e7ce6245fed0635f4719a31cc409091ed17b2091165a4a1cffbf2ceac77abbf4", [:make, :mix], [], "hexpm"},
index 395095079f1346b377af6e251a2ebab54cb3fdea..bacd621e18db0924d52416fca01878b501b2a120 100644 (file)
@@ -12,163 +12,196 @@ defmodule Pleroma.Plugs.RateLimiterTest do
 
   # Note: each example must work with separate buckets in order to prevent concurrency issues
 
-  test "init/1" do
-    limiter_name = :test_init
-    Pleroma.Config.put([:rate_limit, limiter_name], {1, 1})
+  describe "config" do
+    test "config is required for plug to work" do
+      limiter_name = :test_init
+      Pleroma.Config.put([:rate_limit, limiter_name], {1, 1})
 
-    assert {limiter_name, {1, 1}, []} == RateLimiter.init(limiter_name)
-    assert nil == RateLimiter.init(:foo)
-  end
+      assert %{limits: {1, 1}, name: :test_init, opts: [name: :test_init]} ==
+               RateLimiter.init(name: limiter_name)
 
-  test "ip/1" do
-    assert "127.0.0.1" == RateLimiter.ip(%{remote_ip: {127, 0, 0, 1}})
-  end
+      assert nil == RateLimiter.init(name: :foo)
+    end
 
-  test "it restricts by opts" do
-    limiter_name = :test_opts
-    scale = 1000
-    limit = 5
+    test "it restricts based on config values" do
+      limiter_name = :test_opts
+      scale = 60
+      limit = 5
 
-    Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})
+      Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})
 
-    opts = RateLimiter.init(limiter_name)
-    conn = conn(:get, "/")
-    bucket_name = "#{limiter_name}:#{RateLimiter.ip(conn)}"
+      opts = RateLimiter.init(name: limiter_name)
+      conn = conn(:get, "/")
 
-    conn = RateLimiter.call(conn, opts)
-    assert {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+      for i <- 1..5 do
+        conn = RateLimiter.call(conn, opts)
+        assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
+        Process.sleep(10)
+      end
 
-    conn = RateLimiter.call(conn, opts)
-    assert {2, 3, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+      conn = RateLimiter.call(conn, opts)
+      assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
+      assert conn.halted
 
-    conn = RateLimiter.call(conn, opts)
-    assert {3, 2, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+      Process.sleep(50)
 
-    conn = RateLimiter.call(conn, opts)
-    assert {4, 1, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+      conn = conn(:get, "/")
 
-    conn = RateLimiter.call(conn, opts)
-    assert {5, 0, to_reset, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+      conn = RateLimiter.call(conn, opts)
+      assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
 
-    conn = RateLimiter.call(conn, opts)
+      refute conn.status == Plug.Conn.Status.code(:too_many_requests)
+      refute conn.resp_body
+      refute conn.halted
+    end
+  end
 
-    assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
-    assert conn.halted
+  describe "options" do
+    test "`bucket_name` option overrides default bucket name" do
+      limiter_name = :test_bucket_name
 
-    Process.sleep(to_reset)
+      Pleroma.Config.put([:rate_limit, limiter_name], {1000, 5})
 
-    conn = conn(:get, "/")
+      base_bucket_name = "#{limiter_name}:group1"
+      opts = RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name)
 
-    conn = RateLimiter.call(conn, opts)
-    assert {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+      conn = conn(:get, "/")
 
-    refute conn.status == Plug.Conn.Status.code(:too_many_requests)
-    refute conn.resp_body
-    refute conn.halted
-  end
+      RateLimiter.call(conn, opts)
+      assert {1, 4} = RateLimiter.inspect_bucket(conn, base_bucket_name, opts)
+      assert {:err, :not_found} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
+    end
 
-  test "`bucket_name` option overrides default bucket name" do
-    limiter_name = :test_bucket_name
-    scale = 1000
-    limit = 5
+    test "`params` option allows different queries to be tracked independently" do
+      limiter_name = :test_params
+      Pleroma.Config.put([:rate_limit, limiter_name], {1000, 5})
 
-    Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})
-    base_bucket_name = "#{limiter_name}:group1"
-    opts = RateLimiter.init({limiter_name, bucket_name: base_bucket_name})
+      opts = RateLimiter.init(name: limiter_name, params: ["id"])
 
-    conn = conn(:get, "/")
-    default_bucket_name = "#{limiter_name}:#{RateLimiter.ip(conn)}"
-    customized_bucket_name = "#{base_bucket_name}:#{RateLimiter.ip(conn)}"
+      conn = conn(:get, "/?id=1")
+      conn = Plug.Conn.fetch_query_params(conn)
+      conn_2 = conn(:get, "/?id=2")
 
-    RateLimiter.call(conn, opts)
-    assert {1, 4, _, _, _} = ExRated.inspect_bucket(customized_bucket_name, scale, limit)
-    assert {0, 5, _, _, _} = ExRated.inspect_bucket(default_bucket_name, scale, limit)
-  end
-
-  test "`params` option appends specified params' values to bucket name" do
-    limiter_name = :test_params
-    scale = 1000
-    limit = 5
+      RateLimiter.call(conn, opts)
+      assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
+      assert {0, 5} = RateLimiter.inspect_bucket(conn_2, limiter_name, opts)
+    end
 
-    Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})
-    opts = RateLimiter.init({limiter_name, params: ["id"]})
-    id = "1"
+    test "it supports combination of options modifying bucket name" do
+      limiter_name = :test_options_combo
+      Pleroma.Config.put([:rate_limit, limiter_name], {1000, 5})
 
-    conn = conn(:get, "/?id=#{id}")
-    conn = Plug.Conn.fetch_query_params(conn)
+      base_bucket_name = "#{limiter_name}:group1"
+      opts = RateLimiter.init(name: limiter_name, bucket_name: base_bucket_name, params: ["id"])
+      id = "100"
 
-    default_bucket_name = "#{limiter_name}:#{RateLimiter.ip(conn)}"
-    parametrized_bucket_name = "#{limiter_name}:#{id}:#{RateLimiter.ip(conn)}"
+      conn = conn(:get, "/?id=#{id}")
+      conn = Plug.Conn.fetch_query_params(conn)
+      conn_2 = conn(:get, "/?id=#{101}")
 
-    RateLimiter.call(conn, opts)
-    assert {1, 4, _, _, _} = ExRated.inspect_bucket(parametrized_bucket_name, scale, limit)
-    assert {0, 5, _, _, _} = ExRated.inspect_bucket(default_bucket_name, scale, limit)
+      RateLimiter.call(conn, opts)
+      assert {1, 4} = RateLimiter.inspect_bucket(conn, base_bucket_name, opts)
+      assert {0, 5} = RateLimiter.inspect_bucket(conn_2, base_bucket_name, opts)
+    end
   end
 
-  test "it supports combination of options modifying bucket name" do
-    limiter_name = :test_options_combo
-    scale = 1000
-    limit = 5
+  describe "unauthenticated users" do
+    test "are restricted based on remote IP" do
+      limiter_name = :test_unauthenticated
+      Pleroma.Config.put([:rate_limit, limiter_name], [{1000, 5}, {1, 10}])
+
+      opts = RateLimiter.init(name: limiter_name)
 
-    Pleroma.Config.put([:rate_limit, limiter_name], {scale, limit})
-    base_bucket_name = "#{limiter_name}:group1"
-    opts = RateLimiter.init({limiter_name, bucket_name: base_bucket_name, params: ["id"]})
-    id = "100"
+      conn = %{conn(:get, "/") | remote_ip: {127, 0, 0, 2}}
+      conn_2 = %{conn(:get, "/") | remote_ip: {127, 0, 0, 3}}
 
-    conn = conn(:get, "/?id=#{id}")
-    conn = Plug.Conn.fetch_query_params(conn)
+      for i <- 1..5 do
+        conn = RateLimiter.call(conn, opts)
+        assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
+        refute conn.halted
+      end
 
-    default_bucket_name = "#{limiter_name}:#{RateLimiter.ip(conn)}"
-    parametrized_bucket_name = "#{base_bucket_name}:#{id}:#{RateLimiter.ip(conn)}"
+      conn = RateLimiter.call(conn, opts)
 
-    RateLimiter.call(conn, opts)
-    assert {1, 4, _, _, _} = ExRated.inspect_bucket(parametrized_bucket_name, scale, limit)
-    assert {0, 5, _, _, _} = ExRated.inspect_bucket(default_bucket_name, scale, limit)
+      assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
+      assert conn.halted
+
+      conn_2 = RateLimiter.call(conn_2, opts)
+      assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, opts)
+
+      refute conn_2.status == Plug.Conn.Status.code(:too_many_requests)
+      refute conn_2.resp_body
+      refute conn_2.halted
+    end
   end
 
-  test "optional limits for authenticated users" do
-    limiter_name = :test_authenticated
-    Ecto.Adapters.SQL.Sandbox.checkout(Pleroma.Repo)
+  describe "authenticated users" do
+    setup do
+      Ecto.Adapters.SQL.Sandbox.checkout(Pleroma.Repo)
+
+      :ok
+    end
+
+    test "can have limits seperate from unauthenticated connections" do
+      limiter_name = :test_authenticated
+
+      scale = 1000
+      limit = 5
+      Pleroma.Config.put([:rate_limit, limiter_name], [{1, 10}, {scale, limit}])
+
+      opts = RateLimiter.init(name: limiter_name)
 
-    scale = 1000
-    limit = 5
-    Pleroma.Config.put([:rate_limit, limiter_name], [{1, 10}, {scale, limit}])
+      user = insert(:user)
+      conn = conn(:get, "/") |> assign(:user, user)
 
-    opts = RateLimiter.init(limiter_name)
+      for i <- 1..5 do
+        conn = RateLimiter.call(conn, opts)
+        assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
+        refute conn.halted
+      end
 
-    user = insert(:user)
-    conn = conn(:get, "/") |> assign(:user, user)
-    bucket_name = "#{limiter_name}:#{user.id}"
+      conn = RateLimiter.call(conn, opts)
 
-    conn = RateLimiter.call(conn, opts)
-    assert {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+      assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
+      assert conn.halted
 
-    conn = RateLimiter.call(conn, opts)
-    assert {2, 3, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+      Process.sleep(1550)
 
-    conn = RateLimiter.call(conn, opts)
-    assert {3, 2, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+      conn = conn(:get, "/") |> assign(:user, user)
+      conn = RateLimiter.call(conn, opts)
+      assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
 
-    conn = RateLimiter.call(conn, opts)
-    assert {4, 1, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+      refute conn.status == Plug.Conn.Status.code(:too_many_requests)
+      refute conn.resp_body
+      refute conn.halted
+    end
 
-    conn = RateLimiter.call(conn, opts)
-    assert {5, 0, to_reset, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+    test "diffrerent users are counted independently" do
+      limiter_name = :test_authenticated
+      Pleroma.Config.put([:rate_limit, limiter_name], [{1, 10}, {1000, 5}])
 
-    conn = RateLimiter.call(conn, opts)
+      opts = RateLimiter.init(name: limiter_name)
 
-    assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
-    assert conn.halted
+      user = insert(:user)
+      conn = conn(:get, "/") |> assign(:user, user)
 
-    Process.sleep(to_reset)
+      user_2 = insert(:user)
+      conn_2 = conn(:get, "/") |> assign(:user, user_2)
 
-    conn = conn(:get, "/") |> assign(:user, user)
+      for i <- 1..5 do
+        conn = RateLimiter.call(conn, opts)
+        assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, opts)
+      end
 
-    conn = RateLimiter.call(conn, opts)
-    assert {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
+      conn = RateLimiter.call(conn, opts)
+      assert %{"error" => "Throttled"} = Phoenix.ConnTest.json_response(conn, :too_many_requests)
+      assert conn.halted
 
-    refute conn.status == Plug.Conn.Status.code(:too_many_requests)
-    refute conn.resp_body
-    refute conn.halted
+      conn_2 = RateLimiter.call(conn_2, opts)
+      assert {1, 4} = RateLimiter.inspect_bucket(conn_2, limiter_name, opts)
+      refute conn_2.status == Plug.Conn.Status.code(:too_many_requests)
+      refute conn_2.resp_body
+      refute conn_2.halted
+    end
   end
 end