diff options
| author | 2025-05-28 22:04:14 +0100 | |
|---|---|---|
| committer | 2025-05-28 22:04:14 +0100 | |
| commit | bf13b9e54860e9de964760193fef39bc9167dca6 (patch) | |
| tree | 217ce018c2dfa593a061f7c8ca55ae09e8fa9940 /lib/util/public_suffix.ex | |
| 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/util/public_suffix.ex')
| -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 | 
