Add RateLimiter
This commit is contained in:
parent
62cdf701f4
commit
2e5affce61
|
@ -616,3 +616,14 @@ To enable them, both the `rum_enabled` flag has to be set and the following spec
|
||||||
`mix ecto.migrate --migrations-path priv/repo/optional_migrations/rum_indexing/`
|
`mix ecto.migrate --migrations-path priv/repo/optional_migrations/rum_indexing/`
|
||||||
|
|
||||||
This will probably take a long time.
|
This will probably take a long time.
|
||||||
|
|
||||||
|
## :rate_limit
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
See [`Pleroma.Plugs.RateLimiter`](Pleroma.Plugs.RateLimiter.html) documentation for examples.
|
||||||
|
|
87
lib/pleroma/plugs/rate_limiter.ex
Normal file
87
lib/pleroma/plugs/rate_limiter.ex
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
config :pleroma, :rate_limit,
|
||||||
|
one: {1000, 10},
|
||||||
|
two: [{10_000, 10}, {10_000, 50}]
|
||||||
|
|
||||||
|
Here we have two limiters: `one` which is not over 10req/1s and `two` which has two limits 10req/10s for unauthenticated users and 50req/10s for authenticated users.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Inside a controller:
|
||||||
|
|
||||||
|
plug(Pleroma.Plugs.RateLimiter, :one when action == :one)
|
||||||
|
plug(Pleroma.Plugs.RateLimiter, :two when action in [:two, :three])
|
||||||
|
|
||||||
|
or inside a router pipiline:
|
||||||
|
|
||||||
|
pipeline :api do
|
||||||
|
...
|
||||||
|
plug(Pleroma.Plugs.RateLimiter, :one)
|
||||||
|
...
|
||||||
|
end
|
||||||
|
"""
|
||||||
|
|
||||||
|
import Phoenix.Controller, only: [json: 2]
|
||||||
|
import Plug.Conn
|
||||||
|
|
||||||
|
alias Pleroma.User
|
||||||
|
|
||||||
|
def init(limiter_name) do
|
||||||
|
case Pleroma.Config.get([:rate_limit, limiter_name]) do
|
||||||
|
nil -> nil
|
||||||
|
config -> {limiter_name, config}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# do not limit if there is no limiter configuration
|
||||||
|
def call(conn, nil), do: conn
|
||||||
|
|
||||||
|
def call(conn, opts) do
|
||||||
|
case check_rate(conn, opts) do
|
||||||
|
{:ok, _count} -> conn
|
||||||
|
{:error, _count} -> render_error(conn)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp check_rate(%{assigns: %{user: %User{id: user_id}}}, {limiter_name, [_, {scale, limit}]}) do
|
||||||
|
ExRated.check_rate("#{limiter_name}:#{user_id}", scale, limit)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp check_rate(conn, {limiter_name, [{scale, limit} | _]}) do
|
||||||
|
ExRated.check_rate("#{limiter_name}:#{ip(conn)}", scale, limit)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp check_rate(conn, {limiter_name, {scale, limit}}) do
|
||||||
|
check_rate(conn, {limiter_name, [{scale, limit}]})
|
||||||
|
end
|
||||||
|
|
||||||
|
def ip(%{remote_ip: remote_ip}) do
|
||||||
|
remote_ip
|
||||||
|
|> Tuple.to_list()
|
||||||
|
|> Enum.join(".")
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_error(conn) do
|
||||||
|
conn
|
||||||
|
|> put_status(:too_many_requests)
|
||||||
|
|> json(%{error: "Throttled"})
|
||||||
|
|> halt()
|
||||||
|
end
|
||||||
|
end
|
2
mix.exs
2
mix.exs
|
@ -129,7 +129,7 @@ defp deps do
|
||||||
{:quack, "~> 0.1.1"},
|
{:quack, "~> 0.1.1"},
|
||||||
{: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.2"},
|
{:ex_rated, "~> 1.3"},
|
||||||
{: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}
|
||||||
] ++ oauth_deps()
|
] ++ oauth_deps()
|
||||||
|
|
108
test/plugs/rate_limiter_test.exs
Normal file
108
test/plugs/rate_limiter_test.exs
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
defmodule Pleroma.Plugs.RateLimiterTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
use Plug.Test
|
||||||
|
|
||||||
|
alias Pleroma.Plugs.RateLimiter
|
||||||
|
|
||||||
|
import Pleroma.Factory
|
||||||
|
|
||||||
|
@limiter_name :testing
|
||||||
|
|
||||||
|
test "init/1" do
|
||||||
|
Pleroma.Config.put([:rate_limit, @limiter_name], {1, 1})
|
||||||
|
|
||||||
|
assert {@limiter_name, {1, 1}} == RateLimiter.init(@limiter_name)
|
||||||
|
assert nil == RateLimiter.init(:foo)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "ip/1" do
|
||||||
|
assert "127.0.0.1" == RateLimiter.ip(%{remote_ip: {127, 0, 0, 1}})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it restricts by opts" do
|
||||||
|
scale = 100
|
||||||
|
limit = 5
|
||||||
|
|
||||||
|
Pleroma.Config.put([:rate_limit, @limiter_name], {scale, limit})
|
||||||
|
|
||||||
|
opts = RateLimiter.init(@limiter_name)
|
||||||
|
conn = conn(:get, "/")
|
||||||
|
bucket_name = "#{@limiter_name}:#{RateLimiter.ip(conn)}"
|
||||||
|
|
||||||
|
conn = RateLimiter.call(conn, opts)
|
||||||
|
assert {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
|
||||||
|
|
||||||
|
conn = RateLimiter.call(conn, opts)
|
||||||
|
assert {2, 3, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
|
||||||
|
|
||||||
|
conn = RateLimiter.call(conn, opts)
|
||||||
|
assert {3, 2, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
|
||||||
|
|
||||||
|
conn = RateLimiter.call(conn, opts)
|
||||||
|
assert {4, 1, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
|
||||||
|
|
||||||
|
conn = RateLimiter.call(conn, opts)
|
||||||
|
assert {5, 0, to_reset, _, _} = 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
|
||||||
|
|
||||||
|
Process.sleep(to_reset)
|
||||||
|
|
||||||
|
conn = conn(:get, "/")
|
||||||
|
|
||||||
|
conn = RateLimiter.call(conn, opts)
|
||||||
|
assert {1, 4, _, _, _} = 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
|
||||||
|
|
||||||
|
test "optional limits for authenticated users" do
|
||||||
|
Ecto.Adapters.SQL.Sandbox.checkout(Pleroma.Repo)
|
||||||
|
|
||||||
|
scale = 100
|
||||||
|
limit = 5
|
||||||
|
Pleroma.Config.put([:rate_limit, @limiter_name], [{1, 10}, {scale, limit}])
|
||||||
|
|
||||||
|
opts = RateLimiter.init(@limiter_name)
|
||||||
|
|
||||||
|
user = insert(:user)
|
||||||
|
conn = conn(:get, "/") |> assign(:user, user)
|
||||||
|
bucket_name = "#{@limiter_name}:#{user.id}"
|
||||||
|
|
||||||
|
conn = RateLimiter.call(conn, opts)
|
||||||
|
assert {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
|
||||||
|
|
||||||
|
conn = RateLimiter.call(conn, opts)
|
||||||
|
assert {2, 3, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
|
||||||
|
|
||||||
|
conn = RateLimiter.call(conn, opts)
|
||||||
|
assert {3, 2, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
|
||||||
|
|
||||||
|
conn = RateLimiter.call(conn, opts)
|
||||||
|
assert {4, 1, _, _, _} = ExRated.inspect_bucket(bucket_name, scale, limit)
|
||||||
|
|
||||||
|
conn = RateLimiter.call(conn, opts)
|
||||||
|
assert {5, 0, to_reset, _, _} = 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
|
||||||
|
|
||||||
|
Process.sleep(to_reset)
|
||||||
|
|
||||||
|
conn = conn(:get, "/") |> assign(:user, user)
|
||||||
|
|
||||||
|
conn = RateLimiter.call(conn, opts)
|
||||||
|
assert {1, 4, _, _, _} = 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
|
||||||
|
end
|
Loading…
Reference in a new issue