diff --git a/lib/pleroma/config_db.ex b/lib/pleroma/config_db.ex index e9990fa35..2c3df773b 100644 --- a/lib/pleroma/config_db.ex +++ b/lib/pleroma/config_db.ex @@ -10,6 +10,7 @@ defmodule Pleroma.ConfigDB do import Pleroma.Web.Gettext alias __MODULE__ + alias Pleroma.EctoType.Config.RateLimit alias Pleroma.Repo @type t :: %__MODULE__{} @@ -60,8 +61,59 @@ defmodule Pleroma.ConfigDB do |> cast(params, [:key, :group, :value]) |> validate_required([:key, :group, :value]) |> unique_constraint(:key, name: :config_group_key_index) + |> validate_rate_limit() end + defp validate_rate_limit(changeset) do + group = get_field(changeset, :group) + key = get_field(changeset, :key) + + if group == :pleroma and key == :rate_limit do + value = get_field(changeset, :value) + + case normalize_rate_limit(value) do + {:ok, normalized_value} -> + put_change(changeset, :value, normalized_value) + + {:error, {limiter_name, reason}} -> + add_error( + changeset, + :value, + "invalid :rate_limit value for #{inspect(limiter_name)}: #{reason}" + ) + end + else + changeset + end + end + + defp normalize_rate_limit(nil), do: {:ok, nil} + + defp normalize_rate_limit(%{} = value), do: normalize_rate_limit(Map.to_list(value)) + + defp normalize_rate_limit(value) when is_list(value) do + if Keyword.keyword?(value) do + value + |> Enum.reduce_while({:ok, []}, fn {limiter_name, limiter_value}, {:ok, acc} -> + case RateLimit.cast_with_error(limiter_value) do + {:ok, normalized_limiter_value} -> + {:cont, {:ok, [{limiter_name, normalized_limiter_value} | acc]}} + + {:error, reason} -> + {:halt, {:error, {limiter_name, reason}}} + end + end) + |> case do + {:ok, acc} -> {:ok, Enum.reverse(acc)} + {:error, _} = error -> error + end + else + {:error, {:rate_limit, "must be a keyword list"}} + end + end + + defp normalize_rate_limit(_), do: {:error, {:rate_limit, "must be a keyword list"}} + defp create(params) do %ConfigDB{} |> changeset(params) diff --git a/lib/pleroma/ecto_type/config/rate_limit.ex b/lib/pleroma/ecto_type/config/rate_limit.ex new file mode 100644 index 000000000..0518ffd7e --- /dev/null +++ b/lib/pleroma/ecto_type/config/rate_limit.ex @@ -0,0 +1,71 @@ +# Pleroma: A lightweight social networking server +# Copyright © Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.EctoType.Config.RateLimit do + @moduledoc false + + use Ecto.Type + + @type t :: + nil + | {non_neg_integer(), non_neg_integer()} + | [{non_neg_integer(), non_neg_integer()}] + + @impl true + def type, do: :term + + @impl true + def cast(value) do + case cast_with_error(value) do + {:ok, normalized} -> {:ok, normalized} + {:error, _reason} -> :error + end + end + + @impl true + def load(value), do: cast(value) + + @impl true + def dump(value), do: cast(value) + + @spec cast_with_error(term()) :: {:ok, t()} | {:error, String.t()} + def cast_with_error(nil), do: {:ok, nil} + + def cast_with_error({scale, limit}) do + with {:ok, scale} <- parse_integer(scale, "scale"), + {:ok, limit} <- parse_integer(limit, "limit"), + true <- scale >= 1 and limit >= 1 do + {:ok, {scale, limit}} + else + false -> {:error, "scale and limit must be >= 1"} + {:error, reason} -> {:error, reason} + end + end + + def cast_with_error([{_, _} = unauth, {_, _} = auth]) do + with {:ok, unauth} <- cast_with_error(unauth), + {:ok, auth} <- cast_with_error(auth) do + {:ok, [unauth, auth]} + else + {:error, reason} -> {:error, reason} + end + end + + def cast_with_error(_), + do: + {:error, "must be a {scale, limit} tuple, a [{scale, limit}, {scale, limit}] list, or nil"} + + defp parse_integer(value, _label) when is_integer(value), do: {:ok, value} + + defp parse_integer(value, label) when is_binary(value) do + value = String.trim(value) + + case Integer.parse(value) do + {number, ""} -> {:ok, number} + _ -> {:error, "#{label} must be an integer"} + end + end + + defp parse_integer(_value, label), do: {:error, "#{label} must be an integer"} +end diff --git a/lib/pleroma/web/plugs/rate_limiter.ex b/lib/pleroma/web/plugs/rate_limiter.ex index 22c145514..0ebb73bbc 100644 --- a/lib/pleroma/web/plugs/rate_limiter.ex +++ b/lib/pleroma/web/plugs/rate_limiter.ex @@ -68,6 +68,7 @@ defmodule Pleroma.Web.Plugs.RateLimiter do alias Pleroma.Config alias Pleroma.Config.Holder + alias Pleroma.EctoType.Config.RateLimit alias Pleroma.User alias Pleroma.Web.Plugs.RateLimiter.LimiterSupervisor @@ -214,40 +215,13 @@ defmodule Pleroma.Web.Plugs.RateLimiter do defp normalize_limits(nil), do: :disabled - defp normalize_limits({scale, limit}) do - with {:ok, scale} <- normalize_integer(scale), - {:ok, limit} <- normalize_integer(limit), - true <- scale >= 1 and limit >= 1 do - {:ok, {scale, limit}} - else - _ -> :error + defp normalize_limits(limits) do + case RateLimit.cast(limits) do + {:ok, normalized_limits} -> {:ok, normalized_limits} + :error -> :error end end - defp normalize_limits([{_, _} = first, {_, _} = second]) do - with {:ok, first} <- normalize_limits(first), - {:ok, second} <- normalize_limits(second) do - {:ok, [first, second]} - else - _ -> :error - end - end - - defp normalize_limits(_), do: :error - - defp normalize_integer(value) when is_integer(value), do: {:ok, value} - - defp normalize_integer(value) when is_binary(value) do - value = String.trim(value) - - case Integer.parse(value) do - {number, ""} -> {:ok, number} - _ -> :error - end - end - - defp normalize_integer(_), do: :error - defp check_rate(action_settings) do bucket_name = make_bucket_name(action_settings) key_name = make_key_name(action_settings)