philomena/lib/philomena_web/plugs/limit_plug.ex

102 lines
2.4 KiB
Elixir
Raw Normal View History

2020-07-21 16:50:33 +02:00
defmodule PhilomenaWeb.LimitPlug do
@moduledoc """
This plug automatically limits requests which are submitted faster
than should be allowed for a given client.
## Example
plug PhilomenaWeb.LimitPlug, [time: 30, error: "Too fast! Slow down."]
"""
alias Plug.Conn
alias Phoenix.Controller
2020-07-24 14:51:23 +02:00
alias Philomena.Users.User
2020-07-21 16:50:33 +02:00
@doc false
@spec init(any()) :: any()
def init(opts), do: opts
@doc false
@spec call(Conn.t(), any()) :: Conn.t()
def call(conn, opts) do
limit = Keyword.get(opts, :limit, 1)
time = Keyword.get(opts, :time, 5)
error = Keyword.get(opts, :error)
skip_staff = Keyword.get(opts, :skip_staff, true)
2020-07-21 16:50:33 +02:00
data = [
current_user_id(conn.assigns.current_user),
:inet_parse.ntoa(conn.remote_ip),
conn.private.phoenix_action,
conn.private.phoenix_controller
]
key = "rl-#{Enum.join(data, "")}"
amt = Redix.command!(:redix, ["GET", key]) || 0
2020-07-21 16:50:33 +02:00
conn = increment_after_post(conn, key, time)
2020-07-21 16:50:33 +02:00
cond do
amt <= limit ->
conn
is_staff(conn.assigns.current_user) and skip_staff ->
2020-07-24 14:51:23 +02:00
conn
ajax?(conn) ->
conn
|> Controller.put_flash(:error, error)
|> Conn.send_resp(:multiple_choices, "")
|> Conn.halt()
2020-08-22 18:11:50 +02:00
api?(conn) ->
conn
|> Conn.put_status(:too_many_requests)
|> Controller.text("")
|> Conn.halt()
2020-07-21 16:50:33 +02:00
true ->
conn
|> Controller.put_flash(:error, error)
|> Controller.redirect(external: conn.assigns.referrer)
|> Conn.halt()
end
end
2020-07-24 14:51:23 +02:00
defp is_staff(%User{role: "admin"}), do: true
defp is_staff(%User{role: "moderator"}), do: true
defp is_staff(%User{role: "assistant"}), do: true
defp is_staff(_), do: false
2020-07-21 16:50:33 +02:00
defp current_user_id(%{id: id}), do: id
defp current_user_id(_), do: nil
2020-08-22 18:11:50 +02:00
defp api?(conn) do
case conn.path_info do
["api" | _] -> true
_ -> false
end
end
defp ajax?(conn) do
case Conn.get_req_header(conn, "x-requested-with") do
[value] -> String.downcase(value) == "xmlhttprequest"
_ -> false
end
end
defp increment_after_post(conn, key, time) do
Conn.register_before_send(conn, fn conn ->
# Phoenix status returns 200 for form validation errors
if conn.status != 200 do
Redix.pipeline!(:redix, [
["INCR", key],
["EXPIRE", key, time]
])
end
conn
end)
end
2020-07-21 16:50:33 +02:00
end