Browse Source

New rate limiter

environments/review-fix-dokku-evrk7p/deployments/949
Steven Fuchs lain 4 years ago
parent
commit
94627baa5c
16 changed files with 450 additions and 260 deletions
  1. +2
    -1
      lib/pleroma/application.ex
  2. +0
    -131
      lib/pleroma/plugs/rate_limiter.ex
  3. +44
    -0
      lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex
  4. +227
    -0
      lib/pleroma/plugs/rate_limiter/rate_limiter.ex
  5. +16
    -0
      lib/pleroma/plugs/rate_limiter/supervisor.ex
  6. +3
    -3
      lib/pleroma/web/mastodon_api/controllers/account_controller.ex
  7. +1
    -1
      lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
  8. +1
    -1
      lib/pleroma/web/mastodon_api/controllers/search_controller.ex
  9. +3
    -3
      lib/pleroma/web/mastodon_api/controllers/status_controller.ex
  10. +2
    -2
      lib/pleroma/web/mongooseim/mongoose_im_controller.ex
  11. +2
    -1
      lib/pleroma/web/oauth/oauth_controller.ex
  12. +3
    -2
      lib/pleroma/web/ostatus/ostatus_controller.ex
  13. +1
    -1
      lib/pleroma/web/pleroma_api/controllers/account_controller.ex
  14. +0
    -1
      mix.exs
  15. +0
    -1
      mix.lock
  16. +145
    -112
      test/plugs/rate_limiter_test.exs

+ 2
- 1
lib/pleroma/application.ex View 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() ++


+ 0
- 131
lib/pleroma/plugs/rate_limiter.ex View File

@@ -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

+ 44
- 0
lib/pleroma/plugs/rate_limiter/limiter_supervisor.ex View File

@@ -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

+ 227
- 0
lib/pleroma/plugs/rate_limiter/rate_limiter.ex View File

@@ -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

+ 16
- 0
lib/pleroma/plugs/rate_limiter/supervisor.ex View File

@@ -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

+ 3
- 3
lib/pleroma/web/mastodon_api/controllers/account_controller.ex View 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)


+ 1
- 1
lib/pleroma/web/mastodon_api/controllers/auth_controller.ex View 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


+ 1
- 1
lib/pleroma/web/mastodon_api/controllers/search_controller.ex View 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))


+ 3
- 3
lib/pleroma/web/mastodon_api/controllers/status_controller.ex View 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)



+ 2
- 2
lib/pleroma/web/mongooseim/mongoose_im_controller.ex View 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


+ 2
- 1
lib/pleroma/web/oauth/oauth_controller.ex View 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)



+ 3
- 2
lib/pleroma/web/ostatus/ostatus_controller.ex View 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(


+ 1
- 1
lib/pleroma/web/pleroma_api/controllers/account_controller.ex View 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)



+ 0
- 1
mix.exs View File

@@ -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},


+ 0
- 1
mix.lock View File

@@ -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"},


+ 145
- 112
test/plugs/rate_limiter_test.exs View 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

Loading…
Cancel
Save