@@ -36,7 +36,8 @@ defmodule Pleroma.Application do | |||||
Pleroma.Emoji, | Pleroma.Emoji, | ||||
Pleroma.Captcha, | Pleroma.Captcha, | ||||
Pleroma.Daemons.ScheduledActivityDaemon, | Pleroma.Daemons.ScheduledActivityDaemon, | ||||
Pleroma.Daemons.ActivityExpirationDaemon | |||||
Pleroma.Daemons.ActivityExpirationDaemon, | |||||
Pleroma.Plugs.RateLimiter.Supervisor | |||||
] ++ | ] ++ | ||||
cachex_children() ++ | cachex_children() ++ | ||||
hackney_pool_children() ++ | hackney_pool_children() ++ | ||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -66,9 +66,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do | |||||
@relations [:follow, :unfollow] | @relations [:follow, :unfollow] | ||||
@needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a | @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) | plug(:assign_account_by_id when action in @needs_account) | ||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController) | action_fallback(Pleroma.Web.MastodonAPI.FallbackController) | ||||
@@ -15,7 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do | |||||
@local_mastodon_name "Mastodon-Local" | @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" | @doc "GET /web/login" | ||||
def login(%{assigns: %{user: %User{}}} = conn, _params) do | def login(%{assigns: %{user: %User{}}} = conn, _params) do | ||||
@@ -22,7 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do | |||||
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug) | 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 | def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do | ||||
accounts = User.search(query, search_options(params, user)) | accounts = User.search(query, search_options(params, user)) | ||||
@@ -82,17 +82,17 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do | |||||
plug( | plug( | ||||
RateLimiter, | 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 | when action in ~w(reblog unreblog)a | ||||
) | ) | ||||
plug( | plug( | ||||
RateLimiter, | 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 | 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) | action_fallback(Pleroma.Web.MastodonAPI.FallbackController) | ||||
@@ -10,8 +10,8 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do | |||||
alias Pleroma.Repo | alias Pleroma.Repo | ||||
alias Pleroma.User | 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 | def user_exists(conn, %{"user" => username}) do | ||||
with %User{} <- Repo.get_by(User, nickname: username, local: true) do | with %User{} <- Repo.get_by(User, nickname: username, local: true) do | ||||
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||||
use Pleroma.Web, :controller | use Pleroma.Web, :controller | ||||
alias Pleroma.Helpers.UriHelper | alias Pleroma.Helpers.UriHelper | ||||
alias Pleroma.Plugs.RateLimiter | |||||
alias Pleroma.Registration | alias Pleroma.Registration | ||||
alias Pleroma.Repo | alias Pleroma.Repo | ||||
alias Pleroma.User | alias Pleroma.User | ||||
@@ -24,7 +25,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||||
plug(:fetch_session) | plug(:fetch_session) | ||||
plug(:fetch_flash) | 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) | action_fallback(Pleroma.Web.OAuth.FallbackController) | ||||
@@ -8,6 +8,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do | |||||
alias Fallback.RedirectController | alias Fallback.RedirectController | ||||
alias Pleroma.Activity | alias Pleroma.Activity | ||||
alias Pleroma.Object | alias Pleroma.Object | ||||
alias Pleroma.Plugs.RateLimiter | |||||
alias Pleroma.User | alias Pleroma.User | ||||
alias Pleroma.Web.ActivityPub.ActivityPubController | alias Pleroma.Web.ActivityPub.ActivityPubController | ||||
alias Pleroma.Web.ActivityPub.ObjectView | alias Pleroma.Web.ActivityPub.ObjectView | ||||
@@ -17,8 +18,8 @@ defmodule Pleroma.Web.OStatus.OStatusController do | |||||
alias Pleroma.Web.Router | alias Pleroma.Web.Router | ||||
plug( | 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( | plug( | ||||
@@ -42,7 +42,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do | |||||
when action != :confirmation_resend | 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(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe]) | ||||
plug(:put_view, Pleroma.Web.MastodonAPI.AccountView) | plug(:put_view, Pleroma.Web.MastodonAPI.AccountView) | ||||
@@ -155,7 +155,6 @@ defmodule Pleroma.Mixfile do | |||||
{:joken, "~> 2.0"}, | {:joken, "~> 2.0"}, | ||||
{:benchee, "~> 1.0"}, | {:benchee, "~> 1.0"}, | ||||
{:esshd, "~> 0.1.0", runtime: Application.get_env(:esshd, :enabled, false)}, | {:esshd, "~> 0.1.0", runtime: Application.get_env(:esshd, :enabled, false)}, | ||||
{:ex_rated, "~> 1.3"}, | |||||
{:ex_const, "~> 0.2"}, | {:ex_const, "~> 0.2"}, | ||||
{:plug_static_index_html, "~> 1.0.0"}, | {:plug_static_index_html, "~> 1.0.0"}, | ||||
{:excoveralls, "~> 0.11.1", only: :test}, | {:excoveralls, "~> 0.11.1", only: :test}, | ||||
@@ -33,7 +33,6 @@ | |||||
"ex_const": {:hex, :ex_const, "0.2.4", "d06e540c9d834865b012a17407761455efa71d0ce91e5831e86881b9c9d82448", [:mix], [], "hexpm"}, | "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_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_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"]}, | "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"}, | "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"}, | "fast_html": {:hex, :fast_html, "0.99.3", "e7ce6245fed0635f4719a31cc409091ed17b2091165a4a1cffbf2ceac77abbf4", [:make, :mix], [], "hexpm"}, | ||||
@@ -12,163 +12,196 @@ defmodule Pleroma.Plugs.RateLimiterTest do | |||||
# Note: each example must work with separate buckets in order to prevent concurrency issues | # 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 | 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 | 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 | ||||
end | end |