--- /dev/null
+# 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
--- /dev/null
+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