Rate Limits Phoenix

vkxni

vKxni

Posted on September 22, 2022

Rate Limits Phoenix

Ever wondered how to protect your APi/Backend from Spam? Here is how.

Requirements:

  • Elixir v13.3.2 +
  • Phoenix v1.6.10+
  • Basic Knowledge of Elixir
$ elixir -v
Elixir 1.13.2 (compiled with Erlang/OTP 24)

$ mix phx.new --version
Phoenix installer v1.6.10
Enter fullscreen mode Exit fullscreen mode

Getting started

Create a new Phoenix Project

$ mix phx.new ratelimit --no-mailer --no-assets --no-html --no-ecto --no-dashboard
Enter fullscreen mode Exit fullscreen mode

If you will be asked to install some dependencies, answer with y (yes).

Adding Dependencies

Lets add some dependencies that will help us doing all of this
mix.exs

defp deps do
    [
      # here is some other stuff

      # <-- add the stuff below here -->

      # http
      {:httpoison, "~> 1.8"},
      # rate limit
      {:hammer, "~> 6.1"},
    ]
  end
Enter fullscreen mode Exit fullscreen mode

For the rate limits, we will use the following library
https://github.com/ExHammer/hammer

Install dependencies

$ mix deps.get
Enter fullscreen mode Exit fullscreen mode

Project files

Now lets get our hands dirty by creating some helper functions.

lib/ratelimit/base.ex

defmodule Ratelimit.Base do
  use HTTPoison.Base

  @moduledoc """
  This handles HTTP requests without api key (basic requests).
  """

  def process_request_headers(headers) do
    [{"Content-Type", "application/json"} | headers]
  end
end
Enter fullscreen mode Exit fullscreen mode

Now lets add a function that gets the IP of the user visiting our website.
lib/ratelimit/helper/getip.ex

defmodule Ratelimit.IP do
  @doc """
  Get the IP address of the current user visiting the route.
  Formatted as a string: "123.456.78.9"
  """
  # {:ok, String.t()} | {:error, :api_down}
  @spec getIP() :: {String.t() | :api_down}
  def getIP() do
    ip_url = "https://api.ipify.org/"

    case Ratelimit.Base.get!(ip_url) do
      %HTTPoison.Response{body: body, status_code: 200} ->
        body

      %HTTPoison.Response{status_code: status_code} when status_code > 399 ->
        IO.inspect(status_code, label: "STATUS_CODE")
        :error

      _ ->
        raise "APi down"
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Awesome, now lets create a file that handles our ratelimits
lib/ratelimit/util/ratelimit.ex

defmodule RatelimitWeb.Plugs.RateLimiter do
  import Plug.Conn
  use RatelimitWeb, :controller

  alias Ratelimit.IP
  require Logger

  # two request / minute are allowed
  @limit 2

  def init(options), do: options

  def call(conn, _opts) do
    # call the ip function
    ip = IP.getIP()

    case Hammer.check_rate(ip, 60_000, @limit) do
      {:allow, count} ->
        assign(conn, :requests_count, count)

      {:deny, _limit} ->
        # Beep Boop, remove this in production
        Logger.debug("Rate limit exceeded for #{inspect(ip)}")
        error_response(conn)
    end
  end

  defp error_response(conn) do
    conn
    |> put_status(:service_unavailable) # set the json status
    |> json(%{message: "Please wait before sending another request."}) # return an error message
    |> halt() # stop the process
  end
end
Enter fullscreen mode Exit fullscreen mode

Now we have to configure Hammer in our config files.
For that, open config/config.exs and add this line here:

# Config the rate limiter
config :hammer,
  backend: {Hammer.Backend.ETS, [expiry_ms: 60_000 * 60 * 4, cleanup_interval_ms: 60_000 * 10]}
Enter fullscreen mode Exit fullscreen mode

Adding the Controller

Now we have to create a simple controller for our website.
lib/ratelimit_web/controllers/page_controller.ex

defmodule RatelimitWeb.PageController do
  use RatelimitWeb, :controller

  def index(conn, _params) do
    send_resp(conn, 200, "Hello there!")
  end
end
Enter fullscreen mode Exit fullscreen mode

and we have to edit our lib/ratelimit_web/router.ex to the following

pipeline :api do
    # add the rate limit plug here
    plug RatelimitWeb.Plugs.RateLimiter
    plug :accepts, ["json"]
  end

scope "/api", RatelimitWeb do
    pipe_through :api

    # add this here
    get "/test", PageController, :index
  end
Enter fullscreen mode Exit fullscreen mode

Start the Server

Now lets try to start our APi with the following

$ mix phx.server
Enter fullscreen mode Exit fullscreen mode

After that, navigate to the following URL:
http://localhost:4000/api/test.
You should see the following:


showcase

Sending requests

Now to test our rate limits, send multiple request to the same URL, by just refreshing the page more than 2 times.

You will see something changing suddenly, like this:




šŸŽ‰šŸŽ‰šŸŽ‰ You are awesome!

Additional Things

If you want to change the message, you can easily do this in the lib/ratelimit/util/ratelimit.ex file.

In production, remove the IP inspect at the Logger.debug.


The code can be found here: https://github.com/vKxni/ratelimit

šŸ’– šŸ’Ŗ šŸ™… šŸš©
vkxni
vKxni

Posted on September 22, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Rate Limits Phoenix
elixir Rate Limits Phoenix

September 22, 2022