Skip to content

Instantly share code, notes, and snippets.

@yordis
Created August 16, 2019 09:50
Show Gist options
  • Save yordis/6a26459f21ea249dfcb6d2af0c633a52 to your computer and use it in GitHub Desktop.
Save yordis/6a26459f21ea249dfcb6d2af0c633a52 to your computer and use it in GitHub Desktop.
alias API.{ErrorView, Redis}
defmodule API.RateLimitPlug do
@moduledoc """
A plug that rate limits authenticated requests by the minute.
"""
import Plug.Conn
import Phoenix.Controller
@max_per_minute 120
@doc """
Initialize the plug.
"""
@spec init(Plug.opts) :: Plug.opts
def init(opts), do: opts
@doc """
Perform rate limiting on a connection if it is authenticated.
If it is not authenticated, allow it to pass through.
"""
@spec call(Plug.Conn.t, Plug.opts) :: Plug.Conn.t
def call(conn, opts) do
if conn.private.current_account do
do_call(conn, opts)
else
conn
end
end
# Perform rate limiting on an authenticated conn.
@spec do_call(Plug.Conn.t, Plug.opts) :: Plug.Conn.t
defp do_call(conn, _opts) do
cache_key = conn |> cache_key
count =
cache_key
|> Redis.get!
|> Kernel.||("0")
|> String.to_integer
|> Kernel.+(1)
cache_key |> Redis.setex!(70, count)
if count <= @max_per_minute do
conn
else
conn |> rate_limit_exceeded
end
end
# Get the cache key for a given connection.
@spec cache_key(Plug.Conn.t) :: String.t
defp cache_key(conn) do
"#{conn.private.current_account.id}:#{current_minute_ts}"
end
# Get a timestamp representing the current minute.
@spec current_minute_ts :: String.t
defp current_minute_ts do
now = DateTime.utc_now
"#{now.year}-#{now.month}-#{now.day}T#{now.hour}:#{now.minute}"
end
# Halt the connection and send an error.
@spec rate_limit_exceeded(Plug.Conn.t) :: Plug.Conn.t
defp rate_limit_exceeded(conn) do
conn
|> halt
|> put_status(:too_many_requests)
|> render(ErrorView, "429.json", detail: """
Rate limit exceeded. Maximum number of requests per minute may not exceed \
#{@max_per_minute}.\
""")
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment