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