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
Getting started
Create a new Phoenix Project
$ mix phx.new ratelimit --no-mailer --no-assets --no-html --no-ecto --no-dashboard
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
For the rate limits, we will use the following library
https://github.com/ExHammer/hammer
Install dependencies
$ mix deps.get
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
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
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
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]}
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
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
Start the Server
Now lets try to start our APi with the following
$ mix phx.server
After that, navigate to the following URL:
http://localhost:4000/api/test
.
You should see the following:
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
Top comments (0)
Some comments have been hidden by the post's author - find out more