summaryrefslogtreecommitdiff
path: root/lib/pleroma/plugs/rate_limiter/rate_limiter.ex
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pleroma/plugs/rate_limiter/rate_limiter.ex')
-rw-r--r--lib/pleroma/plugs/rate_limiter/rate_limiter.ex267
1 files changed, 0 insertions, 267 deletions
diff --git a/lib/pleroma/plugs/rate_limiter/rate_limiter.ex b/lib/pleroma/plugs/rate_limiter/rate_limiter.ex
deleted file mode 100644
index c51e2c634..000000000
--- a/lib/pleroma/plugs/rate_limiter/rate_limiter.ex
+++ /dev/null
@@ -1,267 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 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.Config
- alias Pleroma.Plugs.RateLimiter.LimiterSupervisor
- alias Pleroma.User
-
- require Logger
-
- @doc false
- def init(plug_opts) do
- plug_opts
- end
-
- def call(conn, plug_opts) do
- if disabled?(conn) do
- handle_disabled(conn)
- else
- action_settings = action_settings(plug_opts)
- handle(conn, action_settings)
- end
- end
-
- defp handle_disabled(conn) do
- Logger.warn(
- "Rate limiter disabled due to forwarded IP not being found. Please ensure your reverse proxy is providing the X-Forwarded-For header or disable the RemoteIP plug/rate limiter."
- )
-
- conn
- end
-
- defp handle(conn, nil), do: conn
-
- defp handle(conn, action_settings) do
- action_settings
- |> incorporate_conn_info(conn)
- |> check_rate()
- |> case do
- {:ok, _count} ->
- conn
-
- {:error, _count} ->
- render_throttled_error(conn)
- end
- end
-
- def disabled?(conn) do
- if Map.has_key?(conn.assigns, :remote_ip_found),
- do: !conn.assigns.remote_ip_found,
- else: false
- end
-
- @inspect_bucket_not_found {:error, :not_found}
-
- def inspect_bucket(conn, bucket_name_root, plug_opts) do
- with %{name: _} = action_settings <- action_settings(plug_opts) do
- action_settings = incorporate_conn_info(action_settings, conn)
- bucket_name = make_bucket_name(%{action_settings | name: bucket_name_root})
- key_name = make_key_name(action_settings)
- limit = get_limits(action_settings)
-
- case Cachex.get(bucket_name, key_name) do
- {:error, :no_cache} ->
- @inspect_bucket_not_found
-
- {:ok, nil} ->
- {0, limit}
-
- {:ok, value} ->
- {value, limit - value}
- end
- else
- _ -> @inspect_bucket_not_found
- end
- end
-
- def action_settings(plug_opts) do
- with limiter_name when is_atom(limiter_name) <- plug_opts[:name],
- limits when not is_nil(limits) <- Config.get([:rate_limit, limiter_name]) do
- bucket_name_root = Keyword.get(plug_opts, :bucket_name, limiter_name)
-
- %{
- name: bucket_name_root,
- limits: limits,
- opts: plug_opts
- }
- end
- end
-
- defp check_rate(action_settings) do
- bucket_name = make_bucket_name(action_settings)
- key_name = make_key_name(action_settings)
- limit = get_limits(action_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!(action_settings)
- check_rate(action_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(action_settings, %{
- assigns: %{user: %User{id: user_id}},
- params: params
- }) do
- Map.merge(action_settings, %{
- mode: :user,
- conn_params: params,
- conn_info: "#{user_id}"
- })
- end
-
- defp incorporate_conn_info(action_settings, %{params: params} = conn) do
- Map.merge(action_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(action_settings) do
- ""
- |> attach_selected_params(action_settings)
- |> attach_identity(action_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: bucket_name_root}),
- do: user_bucket_name(bucket_name_root)
-
- defp make_bucket_name(%{mode: :anon, name: bucket_name_root}),
- do: anon_bucket_name(bucket_name_root)
-
- defp attach_selected_params(input, %{conn_params: conn_params, opts: plug_opts}) do
- params_string =
- plug_opts
- |> Keyword.get(:params, [])
- |> Enum.sort()
- |> Enum.map(&Map.get(conn_params, &1, ""))
- |> Enum.join(":")
-
- [input, params_string]
- |> Enum.join(":")
- |> String.replace_leading(":", "")
- end
-
- defp initialize_buckets!(%{name: _name, limits: nil}), do: :ok
-
- defp initialize_buckets!(%{name: name, limits: limits}) do
- {:ok, _pid} =
- LimiterSupervisor.add_or_return_limiter(anon_bucket_name(name), get_scale(:anon, limits))
-
- {:ok, _pid} =
- LimiterSupervisor.add_or_return_limiter(user_bucket_name(name), get_scale(:user, limits))
-
- :ok
- 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(bucket_name_root), do: "user:#{bucket_name_root}" |> String.to_atom()
- defp anon_bucket_name(bucket_name_root), do: "anon:#{bucket_name_root}" |> String.to_atom()
-end