logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma git clone https://hacktivis.me/git/pleroma.git

rate_limiter.ex (7673B)


  1. # Pleroma: A lightweight social networking server
  2. # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
  3. # SPDX-License-Identifier: AGPL-3.0-only
  4. defmodule Pleroma.Web.Plugs.RateLimiter do
  5. @moduledoc """
  6. ## Configuration
  7. A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration.
  8. The basic configuration is a tuple where:
  9. * The first element: `scale` (Integer). The time scale in milliseconds.
  10. * The second element: `limit` (Integer). How many requests to limit in the time scale provided.
  11. It is also possible to have different limits for unauthenticated and authenticated users: the keyword value must be a
  12. list of two tuples where the first one is a config for unauthenticated users and the second one is for authenticated.
  13. To disable a limiter set its value to `nil`.
  14. ### Example
  15. config :pleroma, :rate_limit,
  16. one: {1000, 10},
  17. two: [{10_000, 10}, {10_000, 50}],
  18. foobar: nil
  19. Here we have three limiters:
  20. * `one` which is not over 10req/1s
  21. * `two` which has two limits: 10req/10s for unauthenticated users and 50req/10s for authenticated users
  22. * `foobar` which is disabled
  23. ## Usage
  24. AllowedSyntax:
  25. plug(Pleroma.Web.Plugs.RateLimiter, name: :limiter_name)
  26. plug(Pleroma.Web.Plugs.RateLimiter, options) # :name is a required option
  27. Allowed options:
  28. * `name` required, always used to fetch the limit values from the config
  29. * `bucket_name` overrides name for counting purposes (e.g. to have a separate limit for a set of actions)
  30. * `params` appends values of specified request params (e.g. ["id"]) to bucket name
  31. Inside a controller:
  32. plug(Pleroma.Web.Plugs.RateLimiter, [name: :one] when action == :one)
  33. plug(Pleroma.Web.Plugs.RateLimiter, [name: :two] when action in [:two, :three])
  34. plug(
  35. Pleroma.Web.Plugs.RateLimiter,
  36. [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]]
  37. when action in ~w(fav_status unfav_status)a
  38. )
  39. or inside a router pipeline:
  40. pipeline :api do
  41. ...
  42. plug(Pleroma.Web.Plugs.RateLimiter, name: :one)
  43. ...
  44. end
  45. """
  46. import Pleroma.Web.TranslationHelpers
  47. import Plug.Conn
  48. alias Pleroma.Config
  49. alias Pleroma.User
  50. alias Pleroma.Web.Plugs.RateLimiter.LimiterSupervisor
  51. require Logger
  52. @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
  53. @doc false
  54. def init(plug_opts) do
  55. plug_opts
  56. end
  57. def call(conn, plug_opts) do
  58. if disabled?(conn) do
  59. handle_disabled(conn)
  60. else
  61. action_settings = action_settings(plug_opts)
  62. handle(conn, action_settings)
  63. end
  64. end
  65. defp handle_disabled(conn) do
  66. Logger.warning(
  67. "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."
  68. )
  69. conn
  70. end
  71. defp handle(conn, nil), do: conn
  72. defp handle(conn, action_settings) do
  73. action_settings
  74. |> incorporate_conn_info(conn)
  75. |> check_rate()
  76. |> case do
  77. {:ok, _count} ->
  78. conn
  79. {:error, _count} ->
  80. render_throttled_error(conn)
  81. end
  82. end
  83. def disabled?(conn) do
  84. if Map.has_key?(conn.assigns, :remote_ip_found),
  85. do: !conn.assigns.remote_ip_found,
  86. else: false
  87. end
  88. @inspect_bucket_not_found {:error, :not_found}
  89. def inspect_bucket(conn, bucket_name_root, plug_opts) do
  90. with %{name: _} = action_settings <- action_settings(plug_opts) do
  91. action_settings = incorporate_conn_info(action_settings, conn)
  92. bucket_name = make_bucket_name(%{action_settings | name: bucket_name_root})
  93. key_name = make_key_name(action_settings)
  94. limit = get_limits(action_settings)
  95. case @cachex.get(bucket_name, key_name) do
  96. {:error, :no_cache} ->
  97. @inspect_bucket_not_found
  98. {:ok, nil} ->
  99. {0, limit}
  100. {:ok, value} ->
  101. {value, limit - value}
  102. end
  103. else
  104. _ -> @inspect_bucket_not_found
  105. end
  106. end
  107. def action_settings(plug_opts) do
  108. with limiter_name when is_atom(limiter_name) <- plug_opts[:name],
  109. limits when not is_nil(limits) <- Config.get([:rate_limit, limiter_name]) do
  110. bucket_name_root = Keyword.get(plug_opts, :bucket_name, limiter_name)
  111. %{
  112. name: bucket_name_root,
  113. limits: limits,
  114. opts: plug_opts
  115. }
  116. end
  117. end
  118. defp check_rate(action_settings) do
  119. bucket_name = make_bucket_name(action_settings)
  120. key_name = make_key_name(action_settings)
  121. limit = get_limits(action_settings)
  122. case @cachex.get_and_update(bucket_name, key_name, &increment_value(&1, limit)) do
  123. {:commit, value} ->
  124. {:ok, value}
  125. {:ignore, value} ->
  126. {:error, value}
  127. {:error, :no_cache} ->
  128. initialize_buckets!(action_settings)
  129. check_rate(action_settings)
  130. end
  131. end
  132. defp increment_value(nil, _limit), do: {:commit, 1}
  133. defp increment_value(val, limit) when val >= limit, do: {:ignore, val}
  134. defp increment_value(val, _limit), do: {:commit, val + 1}
  135. defp incorporate_conn_info(action_settings, %{
  136. assigns: %{user: %User{id: user_id}},
  137. params: params
  138. }) do
  139. Map.merge(action_settings, %{
  140. mode: :user,
  141. conn_params: params,
  142. conn_info: "#{user_id}"
  143. })
  144. end
  145. defp incorporate_conn_info(action_settings, %{params: params} = conn) do
  146. Map.merge(action_settings, %{
  147. mode: :anon,
  148. conn_params: params,
  149. conn_info: "#{ip(conn)}"
  150. })
  151. end
  152. defp ip(%{remote_ip: remote_ip}) do
  153. remote_ip
  154. |> Tuple.to_list()
  155. |> Enum.join(".")
  156. end
  157. defp render_throttled_error(conn) do
  158. conn
  159. |> render_error(:too_many_requests, "Throttled")
  160. |> halt()
  161. end
  162. defp make_key_name(action_settings) do
  163. ""
  164. |> attach_selected_params(action_settings)
  165. |> attach_identity(action_settings)
  166. end
  167. defp get_scale(_, {scale, _}), do: scale
  168. defp get_scale(:anon, [{scale, _}, {_, _}]), do: scale
  169. defp get_scale(:user, [{_, _}, {scale, _}]), do: scale
  170. defp get_limits(%{limits: {_scale, limit}}), do: limit
  171. defp get_limits(%{mode: :user, limits: [_, {_, limit}]}), do: limit
  172. defp get_limits(%{limits: [{_, limit}, _]}), do: limit
  173. defp make_bucket_name(%{mode: :user, name: bucket_name_root}),
  174. do: user_bucket_name(bucket_name_root)
  175. defp make_bucket_name(%{mode: :anon, name: bucket_name_root}),
  176. do: anon_bucket_name(bucket_name_root)
  177. defp attach_selected_params(input, %{conn_params: conn_params, opts: plug_opts}) do
  178. params_string =
  179. plug_opts
  180. |> Keyword.get(:params, [])
  181. |> Enum.sort()
  182. |> Enum.map(&Map.get(conn_params, &1, ""))
  183. |> Enum.join(":")
  184. [input, params_string]
  185. |> Enum.join(":")
  186. |> String.replace_leading(":", "")
  187. end
  188. defp initialize_buckets!(%{name: _name, limits: nil}), do: :ok
  189. defp initialize_buckets!(%{name: name, limits: limits}) do
  190. {:ok, _pid} =
  191. LimiterSupervisor.add_or_return_limiter(anon_bucket_name(name), get_scale(:anon, limits))
  192. {:ok, _pid} =
  193. LimiterSupervisor.add_or_return_limiter(user_bucket_name(name), get_scale(:user, limits))
  194. :ok
  195. end
  196. defp attach_identity(base, %{mode: :user, conn_info: conn_info}),
  197. do: "user:#{base}:#{conn_info}"
  198. defp attach_identity(base, %{mode: :anon, conn_info: conn_info}),
  199. do: "ip:#{base}:#{conn_info}"
  200. defp user_bucket_name(bucket_name_root), do: "user:#{bucket_name_root}" |> String.to_atom()
  201. defp anon_bucket_name(bucket_name_root), do: "anon:#{bucket_name_root}" |> String.to_atom()
  202. end