49 lines
1.4 KiB
Elixir
49 lines
1.4 KiB
Elixir
|
defmodule BackendWeb.RateLimiter do
|
||
|
@moduledoc """
|
||
|
Functions used to rate limit:
|
||
|
* all endpoints by IP/endpoint
|
||
|
* authentication endpoints by domain
|
||
|
"""
|
||
|
|
||
|
import Phoenix.Controller, only: [json: 2]
|
||
|
import Plug.Conn, only: [put_status: 2]
|
||
|
use Plug.Builder
|
||
|
|
||
|
def rate_limit(conn, options \\ []) do
|
||
|
case check_rate(conn, options) do
|
||
|
{:ok, _count} -> conn # Do nothing, allow execution to continue
|
||
|
{:error, _count} -> render_error(conn)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def rate_limit_authentication(conn, options \\ []) do
|
||
|
%{"id" => domain} = conn.params
|
||
|
options = Keyword.put(options, :bucket_name, "authorization: #{domain}")
|
||
|
rate_limit(conn, options)
|
||
|
end
|
||
|
|
||
|
defp check_rate(conn, options) do
|
||
|
interval_milliseconds = options[:interval_seconds] * 1000
|
||
|
max_requests = options[:max_requests]
|
||
|
bucket_name = options[:bucket_name] || bucket_name(conn)
|
||
|
|
||
|
ExRated.check_rate(bucket_name, interval_milliseconds, max_requests)
|
||
|
end
|
||
|
|
||
|
# Bucket name should be a combination of ip address and request path, like so:
|
||
|
#
|
||
|
# "127.0.0.1:/api/v1/authorizations"
|
||
|
defp bucket_name(conn) do
|
||
|
path = Enum.join(conn.path_info, "/")
|
||
|
ip = conn.remote_ip |> Tuple.to_list |> Enum.join(".")
|
||
|
"#{ip}:#{path}"
|
||
|
end
|
||
|
|
||
|
defp render_error(conn) do
|
||
|
conn
|
||
|
|> put_status(:forbidden)
|
||
|
|> json(%{error: "Rate limit exceeded."})
|
||
|
|> halt # Stop execution of further plugs, return response now
|
||
|
end
|
||
|
end
|