defmodule Mobilizon.Web.Plugs.PlugAttack do @moduledoc false use PlugAttack import Plug.Conn @alg :sha512 rule "allow local", conn do allow(conn.remote_ip == {127, 0, 0, 1}) end # rule "throttle by ip", conn do # throttle hash_ip(@alg, convert_ip(conn.remote_ip)), # period: 60_000, limit: 5, # storage: {PlugAttack.Storage.Ets, Mobilizon.Web.Plugs.PlugAttack.Storage} # end rule "fail2ban by ip", conn do fail2ban(hash_ip(@alg, convert_ip(conn.remote_ip)), period: 6_000, limit: 5, ban_for: 90_000, storage: {PlugAttack.Storage.Ets, Mobilizon.Web.Plugs.PlugAttack.Storage} ) end def allow_action(conn, {:throttle, data}, opts) do conn |> add_throttling_headers(data) |> allow_action(true, opts) end def allow_action(conn, _data, _opts) do conn end def block_action(conn, {:throttle, data}, opts) do conn |> add_throttling_headers(data) |> block_action(false, opts) end def block_action(conn, _data, _opts) do conn |> send_resp(:forbidden, "Forbidden\n") # It's important to halt connection once we send a response early |> halt end defp add_throttling_headers(conn, data) do # The expires_at value is a unix time in milliseconds, we want to return one # in seconds reset = div(data[:expires_at], 1_000) conn |> put_resp_header("x-ratelimit-limit", to_string(data[:limit])) |> put_resp_header("x-ratelimit-remaining", to_string(data[:remaining])) |> put_resp_header("x-ratelimit-reset", to_string(reset)) end defp hash_ip(alg, ip) do ip_hash_secret = :mobilizon |> Application.get_env(Mobilizon.Web.Endpoint) |> Keyword.get(:secret_key_base, "HASH_IP_SECRET") :crypto.mac(:hmac, alg, ip_hash_secret, ip) end defp convert_ip(ip) do ip |> Tuple.to_list() |> List.to_charlist() |> IO.chardata_to_string() end end