defmodule Paraxial do
@moduledoc """
Paraxial.io functions for use by users.
"""
alias Paraxial.Helpers
alias Paraxial.RateLimit
require Logger
@doc """
Rate limiter that will also ban the relevant IP address via Paraxial.io.
Returns `{:allow, n} or {:deny, n}`
- `key: String to rate limit on, ex: "login-96.56.162.210", "send-email-michael@paraxial.io"`
- `seconds: Length of the rate limit rule`
- `count: Number of times the action can be performed in the seconds time limit`
- `ban_length: Valid strings are "alert_only", "hour", "day", "week", "infinity"`
- `ip: Tuple, you can pass `conn.remote_ip` directly here`
- `msg: Human-readable string, ex: "> 5 requests in 10 seconds to blackcatprojects.xyz/users/log_in from \#{ip}"`
```
ip_string = conn.remote_ip |> :inet.ntoa() |> to_string()
key = "user-register-get-\#{ip_string}"
seconds = 5
count = 5
ban_length = "hour"
ip = conn.remote_ip
msg = "> 5 requests in 10 seconds to \#{conn.host}/users/log_in from \#{ip_string}"
case Paraxial.check_rate(key, seconds, count, ban_length, ip, msg) do
{:allow, _} ->
# Allow code here
{:deny, _} ->
conn
|> put_resp_content_type("text/html")
|> send_resp(401, "Banned")
end
"""
def check_rate(key, seconds, count, ban_length, ip, msg) do
ms = seconds * 1000
result = RateLimit.check_rate(key, ms, count)
# {:allow, 3}
# {:allow, 4}
# {:allow, 5} <- this call is where the json is POSTed to Paraxial.io
# {:deny, 5}
# {:deny, 5}
if result == {:allow, count} do
# Send JSON
post_rule_event(key, seconds, count, ban_length, ip, msg)
result
else
result
end
end
defp post_rule_event(key, seconds, count, ban_length, ip, msg) do
# Send JSON for the rule event to the Paraxial.io backend
# key: "login-96.56.162.210", "send-email-michael@paraxial.io", etc
# seconds: the length of the rate limit rule
# count: number of times the key can be performed in the seconds time limit
# ban_length: "alert_only", "hour", "day", "week", "infinity"
# ip: tuple
# msg: "> 5 requests in 10 seconds to blackcatprojects.xyz/users/log_in from #{ip}"
api_key = Helpers.get_api_key()
m = %{
"key" => key,
"time_period" => seconds,
"n_requests" => count,
"on_trigger" => ban_length,
"ip_address" => Tuple.to_list(ip),
"msg" => msg,
"api_key" => api_key
}
url = Helpers.get_post_rule_event_url()
json = Jason.encode!(m)
case HTTPoison.post(url, json, [{"Content-Type", "application/json"}]) do
{:ok, %{status_code: 200}} ->
Logger.info("[Paraxial] Post rule event upload success")
_ ->
Logger.info("[Paraxial] Post rule event upload failed")
end
end
@doc """
Given an email, bulk action (such as :email), and count, return true or fase.any()
Example config:
```elixir
config :paraxial,
# ...
bulk: %{email: %{trusted: 100, untrusted: 3}},
trusted_domains: MapSet.new(["paraxial.io", "blackcatprojects.xyz"])
```
## Examples
iex> Paraxial.bulk_allowed?("mike@blackcatprojects.xyz", :email, 3)
true
iex> Paraxial.bulk_allowed?("mike@blackcatprojects.xyz", :email, 100)
true
iex> Paraxial.bulk_allowed?("mike@test.xyz", :email, 4)
false
"""
def bulk_allowed?(email, bulk_action, count) do
# bulk map: bulk: %{emails: %{trusted: 100, untrusted: 5}}
# trusted domains: trusted_domains: ["blackcatprojects.xyz", "paraxial.io"]
bulk_map = Helpers.get_bulk_map()
trusted_domains = Helpers.get_trusted_domains()
limits = bulk_map[bulk_action]
if email_trusted?(email, trusted_domains) do
count <= limits[:trusted]
else
count <= limits[:untrusted]
end
end
def email_trusted?(email, trusted_domains) do
[_h, domain] = String.split(email, "@")
domain in trusted_domains
end
end