diff options
author | 2025-05-28 22:04:14 +0100 | |
---|---|---|
committer | 2025-05-28 22:04:14 +0100 | |
commit | bf13b9e54860e9de964760193fef39bc9167dca6 (patch) | |
tree | 217ce018c2dfa593a061f7c8ca55ae09e8fa9940 /lib | |
parent | Add priv directory (diff) |
Add PSL GenServer
I appreciate this is probably a nasty way to do this and we
should just have this be part of the startup, it was easier to bodge it in this way
Diffstat (limited to 'lib')
-rw-r--r-- | lib/util/public_suffix.ex | 104 |
1 files changed, 104 insertions, 0 deletions
diff --git a/lib/util/public_suffix.ex b/lib/util/public_suffix.ex new file mode 100644 index 0000000..e34994a --- /dev/null +++ b/lib/util/public_suffix.ex @@ -0,0 +1,104 @@ +defmodule Lithium.Util.PublicSuffix do + use GenServer + + require Logger + + @moduledoc """ + A GenServer that loads the public suffix list from a file and provides + functionality to find the organisational domain of a given domain name. + """ + + @public_suffix_list_path Application.app_dir(:lithium, "priv/public_suffix_list.dat") + @public_suffix_list_url "https://publicsuffix.org/list/public_suffix_list.dat" + @public_suffix_max_age :timer.hours(7 * 24) + + def start_link(_) do + GenServer.start_link(__MODULE__, :ok, name: __MODULE__) + end + + @impl true + def init(:ok) do + if File.exists?(@public_suffix_list_path) do + {:ok, load_public_suffix_list()} + else + {:ok, []} + end + end + + @impl true + def handle_info(:update_public_suffix_list, state) do + case fetch_public_suffix_list() do + {:ok, new_list} -> + {:noreply, new_list} + + {:error, reason} -> + Logger.error("Failed to update public suffix list: #{reason}") + {:noreply, state} + end + end + + @impl true + def handle_call({:get_domain, domain}, _from, state) do + {:reply, find_organizational_domain(domain, state), state} + end + + def load_public_suffix_list do + case File.read(@public_suffix_list_path) do + {:ok, content} -> + content + |> String.split("\n", trim: true) + |> Enum.reject(&String.starts_with?(&1, "//")) + |> Enum.map(&String.trim/1) + |> Enum.map(&String.downcase/1) + |> Enum.map(&String.replace(&1, "*.", "")) + + {:error, reason} -> + Logger.error("Failed to load public suffix list: #{reason}") + [] + end + end + + def fetch_public_suffix_list do + request = :httpc.request(:get, {@public_suffix_list_url, []}, [], []) + + case request do + {:ok, {{_, 200, _}, _, body}} -> + File.write(@public_suffix_list_path, body) + {:ok, load_public_suffix_list()} + + {:error, reason} -> + {:error, reason} + end + end + + def find_organizational_domain(domain, public_suffix_list) do + # x.y.z.jb3.dev -> jb3.dev + + # ["com", "jb3", "example"] + domain_parts = String.split(domain, ".") |> Enum.reverse() + + # [["com", "public-suffix"], ...] + suffix_parts = + Enum.map(public_suffix_list, &String.split(&1, ".")) |> Enum.map(&Enum.reverse/1) + + # Add domain parts together until it is no longer in the suffix_parts list + + Enum.reduce_while(domain_parts, [], fn part, acc -> + new_acc = acc ++ [part] + + if Enum.member?(suffix_parts, new_acc) do + {:cont, new_acc} + else + {:halt, new_acc} + end + end) + |> Enum.reverse() + |> Enum.join(".") + |> String.downcase() + |> String.trim() + end + + def get_domain(domain) do + GenServer.call(__MODULE__, {:get_domain, domain}) + end +end |